From f872f7bef24c0d5dffc969a05f7c3116f922c14e Mon Sep 17 00:00:00 2001 From: DNSGeek Date: Wed, 12 Aug 2026 19:21:17 -0700 Subject: [PATCH 1/3] Make module zero page target-selectable for the C128 port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groundwork for a native 40-column C128 build. No behaviour change on the C64: all 11 binaries assemble byte-for-byte identical to before this commit, verified against a pre-change baseline. The module scratch pool ($3A-$3F, $F7-$FF) was hardcoded as literal equates in every module, so it could not be retargeted. It now resolves through zp.inc, which selects zp_c64.inc or zp_c128.inc (-D TARGET_C128). The pool is expressed as two contiguous blocks plus a flag rather than 15 loose constants, because that is the actual constraint: ZP_SCRATCH is indexed in the save/restore loops, and all four ZP_PTRS slots are dereferenced with (zp),y. Retargeting is now three constants. Modules alias their own local names onto pool slots (SRC_PTR = ZP_PTR2, TMP = ZP_SCRATCH+0, ...) so module code reads as it did before. Also replaces the magic "cpx #6" in the save loops with "cpx #ZP_SCRATCH_LEN", so the loop bound tracks the block. The C128 map is filled in and -D TARGET_C128 now assembles and links all 11 modules. Derived from the CBM archive's C128 RAM map: the C128 zero page is fully allocated apart from 5 bytes at $FA-$FE, so the port banks BASIC out and claims its $02-$8F region. Every IRQ-touched location sits at $90 or above, which matters because the editor issues no sei/cli of its own and depends on the default KERNAL IRQ throughout. The editor's own .zeropage block does not move, so petproject.cfg is already correct for both targets. BASIC and KERNAL ABI addresses (TXTTAB, VARTAB, MEMSIZ, VARPNT, TXTPTR, AYINT's $14/$15, JIFFY_LO, FA) are deliberately left as literals — they are fixed by the ROM being called, not ours to relocate. docs/c128-port-notes.md records the full audit and plan, including the finding that the C128 MMU covers $C000-$FFFF with a single 2-bit field, so RAM at $C000 and KERNAL ROM at $E000 are mutually exclusive. Seven of ten modules currently depend on $C000-$CFFF being RAM; rehoming that is the next phase and can be done and tested on the C64. Co-Authored-By: Claude Opus 5 --- docs/c128-port-notes.md | 591 ++++++++++++++++++++++++++++++++++++++++ editor.asm | 15 +- modasm.asm | 39 +-- moddet.asm | 19 +- moddis.asm | 39 +-- moddsk.asm | 25 +- modren.asm | 41 +-- modscr.asm | 7 +- modscrh.asm | 9 +- modsct.asm | 25 +- modtok.asm | 25 +- zp.inc | 72 +++++ zp_c128.inc | 84 ++++++ zp_c64.inc | 32 +++ 14 files changed, 921 insertions(+), 102 deletions(-) create mode 100644 docs/c128-port-notes.md create mode 100644 zp.inc create mode 100644 zp_c128.inc create mode 100644 zp_c64.inc diff --git a/docs/c128-port-notes.md b/docs/c128-port-notes.md new file mode 100644 index 0000000..3ecf597 --- /dev/null +++ b/docs/c128-port-notes.md @@ -0,0 +1,591 @@ +# PETProject — C128 Port Notes + +Working reference for porting PETProject to run natively on a Commodore 128. +Not a manual — this is an engineering audit and plan. Everything in the +"Current state" sections is derived from the source tree and verified against +`build/editor.dbg`; items needing external confirmation are flagged +explicitly under [Open items](#open-items). + +--- + +## Table of Contents + +- [Scope](#scope) +- [Zero-page audit](#zero-page-audit) +- [Banking](#banking) +- [What ports for free](#what-ports-for-free) +- [Component status](#component-status) +- [Relocation strategy](#relocation-strategy) +- [Phasing](#phasing) +- [Open items](#open-items) + +--- + +## Scope + +**Target: native C128, 40-column VIC-IIe.** + +A C128 already runs `petproject.d64` unchanged in C64 mode, so a native port +only earns its keep through things C64 mode cannot offer. At 40 columns those +are: bank-1 RAM (a bigger buffer, and REU-free scripting), burst-mode disk, +and 2 MHz bursts. + +80-column VDC output is **explicitly out of scope**. It was priced and +rejected: the VDC keeps screen and attribute RAM in its own 16K behind +`$D600`/`$D601`, which would require abstracting roughly 350 direct screen +writes across nine files, re-doing reverse video as an attribute rather than +screen-code bit 7, and making width a runtime property (81 hardcoded `40`s in +`moddsk.asm` alone). Choosing 40 columns removes all of that. + +### Note on 2 MHz + +The 40-column screen **cannot** be displayed at 2 MHz — the VIC-IIe can't +sustain its fetches, and the KERNAL's `FAST` blanks the display rather than +show garbage. The VDC is immune because it is an independent chip, which is +why 80-column mode runs at 2 MHz by default. + +So on this target 2 MHz is a *blank-screen burst* around compute-only work: +the assembler's passes, tokenize/detokenize, renumber, search/replace. Two +constraints: + +- It kills the progress spinner (`SPIN_CELL`, `modasm.asm:177`, + `moddis.asm:83`) — nothing in color RAM is visible while blanked. +- **Do not hold 2 MHz across KERNAL disk I/O.** CIA timers are fed by the + system clock and serial timing depends on them (see the note at + `modasm.asm:2588`). Bracket the 2 MHz window tightly around computation, + excluding file reads. + +A partial-credit alternative — toggling `$D030` to run 2 MHz only during +vertical blank and border — keeps the display up but nets only ~10–12%. +Not worth the complexity. + +--- + +## Zero-page audit + +PETProject uses **41 bytes** of zero page across two disjoint tiers, plus two +read-only system locations. + +### Tier 1 — editor-owned block, `$02–$1B` (26 bytes) + +Declared as a `.zeropage` segment at `editor.asm:126`, placed by the `ZP:` +entry in `petproject.cfg`. + +| Range | Sz | Symbol | Role | +| --- | --- | --- | --- | +| `$02-$03` | 2 | `GAP_START` | gap buffer | +| `$04-$05` | 2 | `GAP_END` | gap buffer | +| `$06-$07` | 2 | `BUF_PTR` | buffer walk | +| `$08-$09` | 2 | `SCREEN_PTR` | screen RAM pointer | +| `$0A-$0B` | 2 | `TOP_LINE` | viewport origin | +| `$0C` | 1 | `LEFT_COL` | horizontal scroll | +| `$0D-$0E` | 2 | `TMP` | scratch; aliased `CLR_SCOL` / `CLR_TMP` | +| `$0F` | 1 | `SAVED_X` | X preservation | +| `$10` | 1 | `CURSOR_ROW` | | +| `$11` | 1 | `CURSOR_COL` | | +| `$12` | 1 | `COL_SAVE` | aliased `CLR_LCOL` | +| `$13-$14` | 2 | `TXT_PTR` | text scratch pointer | +| `$15-$16` | 2 | `LPTR` | load/save + module trampoline | +| `$17-$18` | 2 | `CLR_PTR` | color RAM pointer | +| `$19` | 1 | `CLR_KWLEN` | | +| `$1A-$1B` | 2 | `CLR_CTMP` | colortab walker | + +**This block is already fully relocatable.** No code references these by +literal address; everything resolves through the ca65 zeropage segment. +Relocating it is a one-line change (`start = $02`) in a C128 linker config, +provided a 26-byte contiguous hole exists. ca65 keeps the segment in +declaration order, so all pointer pairs stay adjacent wherever the block +lands. + +### Tier 2 — module scratch, `$3A–$3F` and `$F7–$FF` (15 bytes) + +> **Status: converted.** These addresses now come from `zp.inc`, which +> selects `zp_c64.inc` or `zp_c128.inc` (`-D TARGET_C128`). The table below +> describes the C64 map, which is unchanged. See +> [Relocation strategy](#relocation-strategy) for what remains. + +| Range | Used by | As | +| --- | --- | --- | +| `$3A-$3B` | modtok, modsct, moddet, modasm, moddis, modren | `LINENO` / `KW_TOKEN`+`KW_XSAVE` / `TMP` | +| `$3C-$3D` | above + modscr, modscrh | `TMP16` / `TMP2` / `NZFLAG`+`KWTAB` / `HND_TMP` | +| `$3E-$3F` | modtok, modsct, moddet, modasm, moddis, modren | `IN_STRING`+`AFTER_REM` / `TMP3` / `OVFLAG` | +| `$F7-$F8` | modtok, modsct, moddet | `LINK_PTR` / `COPY_SRC` | +| `$F9-$FA` | modtok, modsct, moddet | `BASIC_ADDR` / `COPY_DST` | +| `$FB-$FC` | all seven compute modules | `SRC_PTR` | +| `$FD-$FE` | all seven compute modules | `DST_PTR` | +| `$FF` | modtok, modsct | `OVFLAG` | + +### Cross-tier alias + +`editor.asm:149` declares `KW_TOKEN = $3A` so colorize can reach into module +scratch. The comment justifies it — the editor never calls tokenizer code +directly — and it holds today. It is a coupling that must move in lockstep +with any relocation, and it should be retired if the two tiers become +contiguous. + +### Save/restore discipline — inconsistent, and not load-bearing + +| Module | Saves | Does not save | +| --- | --- | --- | +| modasm, moddis, modren | `$3A-$3F` + `$FB-$FE` (10 B) | `$F7-$FA`, `$FF` | +| moddsk | `$FB-$FE` (4 B) | — consistent, touches nothing else | +| moddet, modtok, modsct, modscrh, modsfr | **nothing** | everything they use | + +An earlier revision of this document called the inconsistency "the main hazard +in the whole port." **That was wrong.** On examination the saves protect +nothing that anything depends on: + +- **The editor holds no state in the pool.** Its only use is `KW_TOKEN`, and + every read is preceded by a write in the same routine + (`colorize.asm:552`→`581`, `colorize.asm:770`→`772`, + `editor.asm:3068`→`3072`). It is transient scratch, never live across a + module call. Nothing else in `editor.asm`, `colorize.asm`, `loadsave.asm` + or `modules.asm` touches the pool at all. +- **The saves carry no stated rationale** — the comments read only "Save ZP". +- **What they would nominally protect is moot.** `$3A-$3F` is BASIC's and + `$F7-$FE` is RS-232's, but the editor quits through BASIC *cold start* + (`editor.asm:542`), which reinitialises all of it. +- **The one real in-flight hazard is not addressed by saving.** The C64 KERNAL + IRQ using `$FB`/`$FC` for cursor blink (`modasm.asm:260`) is a collision + *during* execution; entry/exit save/restore does nothing for it. `sei` does, + which is why modasm holds one. + +So the requirement on any relocation is **disjointness alone** — the two tiers +must not overlap. Making saving universal is *not* a substitute for that, and +is not worth doing on its own merits: it would mean threading save/restore +through every exit path in five working modules (`modsfr` has 31 `rts`, +`modsct` 30, `modscrh` 18), risking a regression on some error path in +exchange for no identified benefit. + +**Enforce disjointness at build time instead.** `petproject.cfg` already sets +`define = yes` on both the `ZP` memory area and the `ZEROPAGE` segment, so +`editor.asm` can `.import __ZP_START__, __ZP_SIZE__` and assert non-overlap +against `ZP_SCRATCH` / `ZP_PTRS`. It needs the `lderror` action rather than +`error`, since those values resolve at link time. Zero runtime cost, zero +regression risk, and it fails the build the moment a C128 map puts the tiers +on top of each other — which is the only scenario that ever mattered. + +### System ZP — read, not owned + +| Addr | Symbol | Sites | C128 | +| --- | --- | --- | --- | +| `$A2` | `JIFFY_LO` | `editor.asm:32`, `moddsk.asm:110`, `modscrh.asm:441,444` | same address | +| `$BA` | `FA` (device) | `editor.asm:26` | same address | +| `$00`/`$01` | processor port | 6 banking sites | **see below** | + +### IRQ interaction + +The editor issues no `sei`/`cli` of its own — it relies on the default KERNAL +IRQ for keyboard scanning (all 20 `GETIN` sites) and reads `$A2` for cursor +blink. So the ZP set to avoid is defined by **what the KERNAL IRQ handler +touches 60 times a second**, not merely by the routines PETProject calls. +That is a strictly larger set. + +`modasm` is the exception: it holds `sei` across an *entire assembly*, on +explicitly C64-specific reasoning (`modasm.asm:260` — the C64 KERNAL IRQ uses +`$FB`/`$FC` as cursor-blink scratch, which would corrupt `SRC_PTR`). That +premise must be **re-derived** for the C128, not inherited. A multi-second +`sei` is riskier there, and the better answer may be to relocate `SRC_PTR` +and drop the `sei` entirely. + +### C128 zero-page map (resolved) + +Source: the CBM archive's C128 RAM map, +. + +The C128 zero page divides cleanly in two, with almost nothing spare: + +| Range | Owner | Size | +| --- | --- | --- | +| `$02-$8F` | BASIC 7.0 — tokens, SYS registers, program/variable/array pointers, FP accumulators, `DS$`, graphics work values | 142 B | +| `$90-$F9` | KERNAL + screen editor — `ST`, serial/tape/RS-232, jiffy clock, file tables, keyboard decode and buffer, all cursor/margin/color state | 106 B | +| `$FA-$FE` | **"Not used"** — the only officially spare zero page | 5 B | +| `$FF` | BASIC scratch | 1 B | + +Five spare bytes against a 41-byte requirement, so the +[relocation strategy](#relocation-strategy) is confirmed as the only viable +route: **bank BASIC out and claim `$02-$8F`.** 142 bytes for 41 leaves 101 +bytes of headroom. + +**Why that is safe, not merely convenient.** The binding constraint is the +KERNAL IRQ, which runs throughout (the editor issues no `sei`/`cli` of its +own). Every IRQ-touched location on the C128 sits at `$90` or above — jiffy +clock `$A0-$A2`, keyboard decode pointer `$CC-$CD`, key buffer and codes +`$D0-$D5`, the cassette switch at `$C0` which the map explicitly annotates +"Updated during IRQ", and the screen editor block `$E0-$F9`. Nothing below +`$90` is IRQ-touched. The KERNAL I/O we call sits above the line too: status +`$90`, file tables `$B7-$BC`, device `$BA`, bank registers `$C6-$C7`. + +**Adopted layout** — packed from the bottom of the BASIC region, implemented +in `zp_c128.inc`: + +| Range | Contents | Size | +| --- | --- | --- | +| `$02-$1B` | editor `.zeropage` segment (Tier 1) | 26 B | +| `$1C-$21` | `ZP_SCRATCH` | 6 B | +| `$22-$29` | `ZP_PTRS` (four pointer pairs) | 8 B | +| `$2A` | `ZP_OVFLAG` | 1 B | + +Two consequences: + +- **Tier 1 does not move.** `petproject.cfg`'s `ZP: start = $02, size = $1A` + is already correct for the C128, so a C128 linker config will differ only + where something actually changes — banking and load addresses, not zero + page. Strategy step 1 below is therefore a no-op. +- **The tiers are now contiguous as well as disjoint**, which was the stated + goal: non-overlap becomes obvious rather than incidental, and the + `KW_TOKEN` alias can be retired by giving colorize a byte of its own. + +**Residual empirical check.** The above comes from a static allocation map, +which documents who *owns* each byte rather than what the ROM demonstrably +writes. Before trusting it in anger, run a poisoned-pattern test on a real +C128 or in VICE: fill `$02-$2A` with a known pattern, exercise the IDE +including disk I/O, and confirm only PETProject's own writes appear. + +--- + +## Banking + +### `$01` does not bank memory on a C128 — and fails silently + +On the 8502, `$01` is the tape / caps-lock sense port. Memory banking is the +MMU's job. So every existing `lda #$36 / sta $01` will **not** error — it +does nothing useful while writing junk to the tape lines, leaving the wrong +memory visible. Silent wrong-memory is the worst failure mode to debug, so +these should be converted **first**, before anything else is tested. + +Sites: + +| File:line | Current | Purpose | +| --- | --- | --- | +| `modasm.asm:273` | `#$36` | page BASIC out to run from `$A000` | +| `moddsk.asm:214` | `#$37` | restore | +| `modscr.asm:207` | `#$37` | restore, from `$033C` trampoline | +| `modscrh.asm:1080` | `#$36` | page out for MODASM handoff | +| `modscrh.asm:1126` | `#$37` | restore | +| `modules.asm:724` | `#$36` | module execution banking | +| `modules.asm:735` | `#$37` | restore | + +Use the MMU's `$FF01–$FF04` preconfiguration registers so each switch stays a +single store, preserving the shape of the existing code. + +### MMU configuration register + +`$D500`, mirrored at `$FF00`, with preconfiguration registers `$FF01–$FF04` +that load a stored config in a single store. The `$FF00–$FF04` window is +visible from **every** bank, which is what makes it usable from switching +code. + +| Field | Controls | +| --- | --- | +| bit 0 | `$D000-$DFFF` — I/O vs ROM/RAM | +| bit 1 | `$4000-$7FFF` | +| bits 2-3 | `$8000-$BFFF` | +| **bits 4-5** | **`$C000-$FFFF`** | +| bits 6-7 | RAM bank select | + +Essentially **one config covers the whole session** — `$0E`: RAM from +`$0000-$BFFF`, I/O at `$D000`, KERNAL ROM above. That is strictly better than +the C64 arrangement, because `$A000-$BFFF` is simply RAM. The `#$36`/`#$37` +dance around the big modules does not get translated, it **disappears** — +along with the "do not restore `$01` while executing here" hazard at +`modasm.asm:449`. + +*(CR bit assignments and the `$0E` value want confirming against the C128 +Programmer's Reference — see [Open items](#open-items).)* + +### The `$C000` collision — the real work in this phase + +**Bits 4-5 cover `$C000-$FFFF` as a single unit.** On the C128 `$C000-$CFFF` +is the screen editor ROM, sharing that field with the KERNAL at `$E000`. So +**RAM at `$C000` and KERNAL ROM at `$E000` are mutually exclusive.** + +PETProject leans on `$C000-$CFFF` heavily: + +| What | Detail | +| --- | --- | +| 6 modules load there | moddet, modtok, moddsk, modren, modsfr, modscrh — largest is moddsk at 3,255 bytes | +| modasm's entire working state | 44 equates at `$C0xx` — symbol table, PC, pass counter, error state, output filename, `ZP_SAVE`, gap pointers (`modasm.asm:115-151`) | + +That is 7 of 10 modules. On the C64 it was the natural choice: `$C000-$CFFF` +is the permanent 4 K RAM gap between BASIC and I/O. On the C128 it is the one +region you cannot have while keeping the KERNAL. + +A `$C000`-resident module also cannot trampoline its way out — it would need +RAM at `$C000` to execute and ROM at `$E000` to call the KERNAL, and moddsk +is almost entirely KERNAL I/O. + +#### Resolution: rehome, don't trampoline + +Keep config `$0E` for the whole session and move everything off `$C000`. The +budget works: + +``` +editor BSS ends $8C1E +free below module area $8C1F-$9FFF 5,089 bytes +must be rehomed $C000-$CFFF 4,096 bytes + ------------ +spare 993 bytes +``` + +Two things to design around: + +- **That margin shrinks as the editor's BSS grows.** Wants a link-time + assertion — same technique as the ZP overlap check, fail the build rather + than discover it as corruption. +- **The modscrh/modasm choreography survives unchanged in shape.** modscrh + currently stashes itself out of `$C000-$CFFF` to REU `$013000` so modasm can + use that space as scratch (`modscrh.asm:102`). Relocating both is + mechanical — same dance, different address. + +The alternative — all-RAM bank 0 with KERNAL calls trampolined through common +RAM — preserves the memory map exactly but taxes every I/O call in the +project. Rejected for that reason. + +**Sequencing note:** the rehoming is the substantive work, it is independent +of the MMU stores, and it can be **done and tested entirely on the C64** — +moving modules from `$C000` to `$8C20` is a valid C64 layout too. That turns +the risky part into something verifiable on hardware you already have, before +any C128-specific code exists. Do it first; the MMU stores are small once +nothing depends on `$C000`. + +### Trampolines must move + +`modscr.asm:201` and `modscrh.asm:94` place bank-switch trampolines at `$033C` +(the C64 cassette buffer). The C128 cassette buffer is at `$0B00`, so `$033C` +is wrong regardless of banking — and page 3 is much more crowded on the C128 +than the C64, so there is no drop-in replacement. See +[Open items](#open-items) #2. + +Rehoming `$C000` *reduces* how much trampolining is needed, since one config +then covers the session. + +### `SETBNK` + +C128 KERNAL LOAD/SAVE take a bank argument via `SETBNK` (`$FF68`), which must +be called before each. Few sites, since everything funnels through wrappers: +`modules.asm:661` (module loader) and the `loadsave.asm` paths. + +--- + +## What ports for free + +Choosing 40 columns makes the largest category of work vanish: + +- **Screen and color RAM.** The C128's 40-column VIC screen is at `$0400` + with color at `$D800` — identical to the C64. All direct writes across + `editor.asm` (118 refs), `moddsk.asm` (97), `modules.asm` (50), + `loadsave.asm` (24), `modasm.asm` (23), and the rest work unchanged. +- **Layout.** `COLS = 40`, the `row40_lo`/`row40_hi` tables + (`editor.asm:1528`), `LEFT_COL` horizontal scrolling, popup geometry + (`POP_LEFT`/`POP_WIDTH`), and `ora #SCR_REVERSE` reverse video are all + still correct. +- **Instruction set.** The 8502 is instruction-compatible with the 6502, + illegal opcodes included. `modasm` and `moddis` need no ISA work. +- **Keyboard.** `GETIN` is compatible. The C128's TAB key already emits `$09`, + matching `KEY_CTRL_I`. +- **`$A000–$BFFF` modules get simpler.** That range is plain bank-0 RAM on the + C128, so the "don't restore `$01` while executing here" hazard + (`modasm.asm:449`) largely stops being a hazard. +- **Detokenizer tables.** `moddet` already uses an embedded keyword table + (`moddet.asm:436`) rather than reading BASIC ROM at `$A09E`, so it carries + no ROM dependency. + +--- + +## Component status + +| Component | C128 effort | Notes | +| --- | --- | --- | +| `editor.asm` | **Low** | ZP relocation via cfg; one line for the quit path | +| `colorize.asm` | **None** | aliases editor ZP only; moves with it | +| `loadsave.asm` | **Low** | add `SETBNK` | +| `modules.asm` | **Low** | MMU conversion + `SETBNK` | +| `modsfr.asm` | **None** | no banking, no ROM calls | +| `moddet.asm` | **Low** | ZP scratch equates only | +| `modtok.asm` | **Low** | ZP scratch equates only | +| `modren.asm` | **Low** | ZP scratch + save loop | +| `moddis.asm` | **Low** | ZP scratch + save loop | +| `modasm.asm` | **Medium** | MMU + ZP + re-derive the `sei` decision | +| `moddsk.asm` | **Medium** | MMU + ZP + `SETBNK` | +| `modsct.asm` | **High** | REU staging at `$B000`; BASIC ABI | +| `modscr.asm` | **High** | BASIC ROM entry points; `$B000` staging | +| `modscrh.asm` | **Highest** | 12 BASIC ROM entry points; BASIC 2.0 ZP ABI | + +### The script runner is a BASIC-ABI problem, not a ZP problem + +This is the single most-affected subsystem, and it is worse than it first +appears. It hardcodes **12 C64 BASIC ROM entry points**, none of which exist +at those addresses in BASIC 7.0: + +| Addr | Symbol | Site | +| --- | --- | --- | +| `$A7AE` | `NEWSTT` / `BASIC_RUNC` | `modscr.asm:78`, `modscrh.asm:47,58` | +| `$A871` | `RUNC` | `modscr.asm:79` | +| `$A659` | `CLR` | `modscr.asm:80`, `modscrh.asm:57` | +| `$A533` | `RELINK` | `modscrh.asm:56` | +| `$A437` | `ERROR` | `modscrh.asm:75` | +| `$A7E7` | `GONE_ORIG` | `modscrh.asm:48` | +| `$B08B` | `PTRGET` | `modscrh.asm:73` | +| `$B79E` | `GETBYT` | `modscrh.asm:50` | +| `$AD9E` | `FRMNUM` | `modscrh.asm:535` | +| `$B1AA` | `AYINT` | `modscrh.asm:536` | +| `$E544` | `CLRSCR` (KERNAL) | `modscr.asm:82` | +| `$E394` | BASIC cold start | `editor.asm:542` | + +Plus the BASIC 2.0 zero-page ABI: `$2B`/`$2D` (TXTTAB/VARTAB), `$37` (MEMSIZ), +`$47` (VARPNT), `$7A`/`$7B` (TXTPTR), and `$14`/`$15` as AYINT's big-endian +output (`modscrh.asm:540`). It also stages into `$B000` (`modscr.asm:96`, +`modsct.asm:149`), which is BASIC HI ROM territory on the C128. + +**Recommendation: cut the script runner from the first C128 release.** The +editor, assembler, disassembler, renumber, disk utility and search modules +have no BASIC ROM dependency at all and can ship without it. + +Note that `editor.asm:542` (`jmp $E394`) needs replacing regardless — it is +the normal quit path, not part of the script runner. + +--- + +## Relocation strategy + +**Claim BASIC 7.0's zero page wholesale.** + +PETProject banks BASIC out for its entire session and — apart from the script +runner, which is being cut from release 1 — never calls it. The KERNAL's +reserved ZP set is the smaller and better-documented half; BASIC 7.0's is the +large low region. Bank BASIC out at startup, stay clear of the KERNAL set, +and there is room for **both** tiers in one contiguous region. + +That is worth more than just fitting: contiguity makes the two tiers trivially +disjoint — the one property the port actually requires (see +[Save/restore discipline](#saverestore-discipline--inconsistent-and-not-load-bearing)) +— and it allows the `KW_TOKEN` alias to be retired. + +Mechanically: + +1. **Tier 1** — ✅ **no change needed.** The adopted C128 map leaves the + editor block at `$02-$1B`, so `petproject.cfg`'s existing `ZP:` entry is + already correct for both targets. +2. **Tier 2** — ✅ **done.** The literal equates and the raw `lda $3A,x` / + `sta $FB` save loops are now symbolic, resolving through `zp.inc`. + Verified twice over: the C64 build is byte-for-byte identical to the + pre-conversion output, and a `-D TARGET_C128` build relocates cleanly + (every `lda (SRC_PTR),y` moved `$FB`→`$26`, same counts, no stale + references, identical binary sizes). +3. **Assert non-overlap at link time** via `__ZP_START__` / `__ZP_SIZE__` and + `lderror`, as described above. *(Not yet done. Small, safe, independent of + the C128 map — can land at any point.)* + + This replaces an earlier plan to make ZP saving universal across all + modules. That plan was dropped: it guarded against nothing real and would + have meant editing every exit path in five working modules. + +#### Files + +| File | Role | +| --- | --- | +| `zp.inc` | target dispatch, derived `ZP_PTR0..3`, contract + assertions | +| `zp_c64.inc` | the historical C64 map — `$3A`, `$F7`, `$FF` | +| `zp_c128.inc` | the C128 map — `$1C`, `$22`, `$2A`, with the derivation and the residual check | + +Each module now aliases its own local names onto pool slots +(`SRC_PTR = ZP_PTR2`, `TMP = ZP_SCRATCH+0`, …), so module code reads exactly +as before and the diff stays small. Converted: `editor.asm` (the `KW_TOKEN` +alias), `modasm`, `moddet`, `moddis`, `modren`, `moddsk`, `modscr`, +`modscrh`, `modsct`, `modtok`. `modsfr` needed no change — it uses no pool +scratch. + +BASIC/KERNAL ABI addresses (`TXTTAB`, `VARTAB`, `MEMSIZ`, `VARPNT`, `TXTPTR`, +AYINT's `$14`/`$15`, `JIFFY_LO`, `FA`) were deliberately **left as literals**. +They are fixed by the ROM being called, not ours to relocate, and folding them +into the pool would have obscured that distinction. + +Single source tree throughout — `.if TARGET_C128` conditionals plus parallel +`.cfg` files, not a fork. The module configs barely change; `$A000` and +`$C000` remain valid load addresses. + +--- + +## Phasing + +**Phase 0 — zero page.** ✅ **Done.** Tier 2 include-file conversion, C128 map +resolved and adopted, Tier 1 confirmed to need no change. C64 build verified +byte-identical throughout; `-D TARGET_C128` assembles and links. + +**Phase 1 — rehome `$C000`.** Move the six `$C000` modules and modasm's 4 K of +working state below `$A000`, per +[The `$C000` collision](#the-c000-collision--the-real-work-in-this-phase). +Deliberately first, and deliberately **done on the C64**: the new layout is +valid there, so it is testable on real hardware before any C128-specific code +exists. Add the link-time assertion guarding the 993-byte margin. + +**Phase 2 — banking.** MMU conversion (7 sites), trampoline relocation, +`SETBNK`, quit path. Small once nothing depends on `$C000`. Ends with a native +C128 build that boots. + +**Phase 3 — build and packaging.** `TARGET_C128` conditionals, parallel +`.cfg` set, C128 autoboot sector in `make_disk.py`. One disk can carry both +builds — the C64 BASIC stub and the C128 boot sector select between them, so +no runtime machine detection is needed. + +**Phase 4 — the payoff.** In value-per-effort order: + +1. **REU-free scripting.** `modscr.asm` stashes `$0801–$9FFF` to REU `$009000` + purely to make room. Bank 1 does that job, which would remove the one + hardware requirement in the README. (Gated on the script runner rework.) +2. **Burst mode** with a 1571/1581 — retires the "use a fastloader cartridge" + advice. +3. **2 MHz bursts** around the assembler's passes, screen blanked, disk I/O + excluded. +4. **Bigger buffer** via bank 1. Real, but the most invasive: the gap buffer + needs a windowing scheme rather than per-byte far access through + `INDFET`/`INDSTA` (`$FF74`/`$FF77`), which are correct but slow. + +BASIC 7.0 token support — extending `modtok`, `moddet`, `colorize` and +`modren` to the two-byte `$CE xx` / `$FE xx` tokens, including which new +keywords take line-number arguments (`TRAP`, `RESUME`, the `GO` forms) — is a +separate content project, not part of the port. + +--- + +## Open items + +Claims not derived from the source tree. Reference for C128 hardware +allocation throughout: the CBM archive's C128 RAM map, +. + +1. ~~**C128 KERNAL zero-page reservation list.**~~ ✅ **Resolved** — see + [C128 zero-page map](#c128-zero-page-map-resolved). One residual empirical + check is noted there. +2. **Trampoline placement — reopened, and harder than first thought.** An + earlier revision of this document suggested putting the relocated + trampolines somewhere in page 3, on the reasoning that the C128's default + bottom common RAM is `$0000–$03FF` so anything below `$0400` is visible + from every bank. The visibility argument holds, but **page 3 is far more + crowded on the C128 than on the C64** — `$0334-$0349` editor indirect + vectors, `$034A-$034D` IRQ keyboard buffer, `$0354-$035D` tab-stop + bitmaps, `$0362-$0376` file tables, `$0380-$03BF` CHRGET, `$03C0-$03FF` + bank-relative fetch/poke. Only small gaps remain, and the C64's `$033C` + cassette buffer does not exist there as free space. + + The likelier answer is the C128 cassette buffer at **`$0B00-$0BFF`** (256 + bytes, free when tape is unused) — but that is outside the default 1 K + bottom common RAM, so it needs the MMU's RCR (`$D506`) set to a 4 K bottom + common region (`$0000-$0FFF`) to be bank-visible. Both halves of that need + confirming: that `$0B00` is genuinely free in our configuration, and the + RCR setting. +3. **MMU configuration register details** — the CR bit assignments tabulated + under [Banking](#mmu-configuration-register), and that `$0E` is the right + whole-session config value (RAM `$0000-$BFFF`, I/O, KERNAL ROM). The + consequence that matters — bits 4-5 covering `$C000-$FFFF` as one unit, and + therefore the `$C000` collision — follows directly from these, so confirm + before committing to the rehoming layout. +4. **`RPTFLG` at `$028A`** (`editor.asm:33`) — confirm the address and that + `$80` still means "all keys repeat" on the C128. +5. **The `$0200–$0222` boot / module parameter block** (`modules.asm:63`) + overlaps the C128 KERNAL input buffer. Interactive input uses `GETIN` so it + is likely safe, but `moddsk`'s drive-status reads go through `CHRIN`, which + does use that buffer. +6. **C128 BASIC cold-start entry** to replace `jmp $E394`. `jmp ($FFFC)` is + the safe fallback. diff --git a/editor.asm b/editor.asm index ce17376..4b45822 100644 --- a/editor.asm +++ b/editor.asm @@ -142,11 +142,18 @@ CLR_PTR: .res 2 ; color RAM row pointer CLR_KWLEN: .res 1 ; keyword length returned by col_try_keyword CLR_CTMP: .res 2 ; colortab walk pointer (col_try_keyword internal) -; col_try_keyword uses ZP scratch at $3A for the matched token byte. -; This byte is above the editor's reserved ZP block ($02-$1B) and is -; the same address used by modtok.asm — safe to alias here since the +; col_try_keyword needs one byte of scratch for the matched token byte. +; It borrows the first byte of the *module* scratch pool rather than +; spending one of the editor's own reserved bytes — safe only because the ; editor never calls tokenizer code directly. -KW_TOKEN = $3A ; token byte from col_try_keyword (colorize scratch) +; +; This is the one place the editor reaches across into module zero page, so +; it must track any relocation of that pool: hence zp.inc rather than a +; literal. See docs/c128-port-notes.md ("Cross-tier alias") — the intent is +; to retire this alias once the C128 map puts both tiers in one region. +.include "zp.inc" + +KW_TOKEN = ZP_SCRATCH+0 ; token byte from col_try_keyword (colorize scratch) .segment "LOADADDR" diff --git a/modasm.asm b/modasm.asm index 7e8b21c..c7b18de 100644 --- a/modasm.asm +++ b/modasm.asm @@ -87,10 +87,13 @@ COLS = 40 DEFAULT_COLOR = 14 ; light blue ; ---- ZP ---- -SRC_PTR = $FB ; lo (hi=$FC) - source walker, gap-aware -TMP = $3A ; general scratch (hi=$3B) -TMP2 = $3C ; (hi=$3D) -TMP3 = $3E ; (hi=$3F) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +SRC_PTR = ZP_PTR2 ; lo (hi=+1) - source walker, gap-aware +TMP = ZP_SCRATCH+0 ; general scratch (hi=+1) +TMP2 = ZP_SCRATCH+2 ; (hi=+3) +TMP3 = ZP_SCRATCH+4 ; (hi=+5) ; ---- Mode constants ---- MODE_IMP = 0 @@ -127,7 +130,9 @@ ASM_FNAME_LEN = $C021 ; output filename length ASM_FNAME = $C022 ; output filename (16 bytes) ; ---- ZP save area ---- -ZP_SAVE = $C03A ; 10 bytes: saves $3A-$3F, $FB-$FE +; 10 bytes: saves the 6-byte ZP_SCRATCH block plus ZP_PTR2/ZP_PTR3. +; (Not zero page itself — module scratch RAM above the $A000 image.) +ZP_SAVE = $C03A ; ---- Gap pointers (copied from params) ---- ASM_GAP_S_LO = $C032 @@ -276,18 +281,18 @@ assemble: ; Save ZP ldx #0 @zpsave: - lda $3A,x + lda ZP_SCRATCH,x sta ZP_SAVE,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zpsave - lda $FB + lda ZP_PTR2 sta ZP_SAVE+6 - lda $FC + lda ZP_PTR2+1 sta ZP_SAVE+7 - lda $FD + lda ZP_PTR3 sta ZP_SAVE+8 - lda $FE + lda ZP_PTR3+1 sta ZP_SAVE+9 ; Copy gap/buffer params @@ -453,18 +458,18 @@ assemble: ldx #0 @zprest: lda ZP_SAVE,x - sta $3A,x + sta ZP_SCRATCH,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zprest lda ZP_SAVE+6 - sta $FB + sta ZP_PTR2 lda ZP_SAVE+7 - sta $FC + sta ZP_PTR2+1 lda ZP_SAVE+8 - sta $FD + sta ZP_PTR3 lda ZP_SAVE+9 - sta $FE + sta ZP_PTR3+1 cli ; re-enable IRQ before returning rts diff --git a/moddet.asm b/moddet.asm index ddbee7c..5f17768 100644 --- a/moddet.asm +++ b/moddet.asm @@ -60,14 +60,17 @@ MOD_NEW_END_HI = $0220 MOD_MAGIC_VAL = $4D ; ---- Zero page ------------------------------------------------------------- -SRC_PTR = $FB ; source walk pointer (lo/hi) -DST_PTR = $FD ; output write pointer (lo/hi) -COPY_SRC = $F7 ; copy-back source (lo/hi) -COPY_DST = $F9 ; copy-back dest (lo/hi) -LINENO = $3A ; 16-bit line number scratch (lo/hi) -NZFLAG = $3C ; non-zero digit seen (decimal output) -KWTAB = $3D ; keyword table walker (lo/hi) -OVFLAG = $3F ; output-overflow flag ($FF = won't fit) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +SRC_PTR = ZP_PTR2 ; source walk pointer (lo/hi) +DST_PTR = ZP_PTR3 ; output write pointer (lo/hi) +COPY_SRC = ZP_PTR0 ; copy-back source (lo/hi) +COPY_DST = ZP_PTR1 ; copy-back dest (lo/hi) +LINENO = ZP_SCRATCH+0 ; 16-bit line number scratch (lo/hi) +NZFLAG = ZP_SCRATCH+2 ; non-zero digit seen (decimal output) +KWTAB = ZP_SCRATCH+3 ; keyword table walker (lo/hi) +OVFLAG = ZP_SCRATCH+5 ; output-overflow flag ($FF = won't fit) diff --git a/moddis.asm b/moddis.asm index 722f22a..a6caef0 100644 --- a/moddis.asm +++ b/moddis.asm @@ -67,11 +67,14 @@ MOD_NEW_END_HI = $0220 CHROUT = $FFD2 ; ---- ZP ---- -SRC_PTR = $FB ; lo (hi=$FC) - source walker -DST_PTR = $FD ; output pointer lo (hi at $FE); consecutive pair for (addr),y -TMP = $3A ; scratch (hi=$3B) -TMP2 = $3C ; scratch (hi=$3D) -TMP3 = $3E ; scratch (hi=$3F) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +SRC_PTR = ZP_PTR2 ; lo (hi=+1) - source walker +DST_PTR = ZP_PTR3 ; output pointer lo (hi at +1); consecutive pair for (addr),y +TMP = ZP_SCRATCH+0 ; scratch (hi=+1) +TMP2 = ZP_SCRATCH+2 ; scratch (hi=+3) +TMP3 = ZP_SCRATCH+4 ; scratch (hi=+5) ; ---- ZP save area and disassembler state ---- ; Declared at the end of this file (after all code/tables) as plain labels @@ -128,18 +131,18 @@ disassemble: ; Save ZP ldx #0 @zpsave: - lda $3A,x + lda ZP_SCRATCH,x sta ZP_SAVE,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zpsave - lda $FB + lda ZP_PTR2 sta ZP_SAVE+6 - lda $FC + lda ZP_PTR2+1 sta ZP_SAVE+7 - lda $FD + lda ZP_PTR3 sta ZP_SAVE+8 - lda $FE + lda ZP_PTR3+1 sta ZP_SAVE+9 ; Copy params @@ -321,18 +324,18 @@ disassemble: ldx #0 @zprestore: lda ZP_SAVE,x - sta $3A,x + sta ZP_SCRATCH,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zprestore lda ZP_SAVE+6 - sta $FB + sta ZP_PTR2 lda ZP_SAVE+7 - sta $FC + sta ZP_PTR2+1 lda ZP_SAVE+8 - sta $FD + sta ZP_PTR3 lda ZP_SAVE+9 - sta $FE + sta ZP_PTR3+1 ; Report success lda #$02 @@ -1122,7 +1125,7 @@ mnem_strs: ; TXS/TYA corruption bug this file used to have. ; ============================================================================ -ZP_SAVE: .res 10 ; saves $3A-$3F, $FB-$FE +ZP_SAVE: .res 10 ; saves ZP_SCRATCH (6) + ZP_PTR2/ZP_PTR3 (4) DIS_PC_LO: .res 1 ; current PC lo DIS_PC_HI: .res 1 ; current PC hi DIS_SRC_END_LO: .res 1 ; end of source binary lo (= GAP_START) diff --git a/moddsk.asm b/moddsk.asm index fa04e5e..57a3322 100644 --- a/moddsk.asm +++ b/moddsk.asm @@ -167,7 +167,7 @@ DSK_FREE_LO: .res 1 ; blocks free lo DSK_FREE_HI: .res 1 ; blocks free hi DSK_DISKNAME: .res 16 ; disk name, 16 bytes PETSCII DSK_DISKID: .res 2 ; disk id, 2 bytes -DSK_ZP_SAVE: .res 4 ; saved ZP $FB-$FE +DSK_ZP_SAVE: .res 4 ; saved ZP_PTR2 / ZP_PTR3 DSK_TMP: .res 1 ; general scratch byte DSK_TMP2: .res 1 ; second scratch byte DSK_NAMELEN_TMP: .res 1 ; scratch for rename/format input length @@ -186,8 +186,11 @@ DSK_CACHE: .res 680 ; directory cache: 34 entries × 20 bytes ; throughout the code will resolve correctly via the linker. ; Zero page pointers (saved/restored) -DSK_PTR = $FB ; lo (hi = $FC) -DSK_PTR2 = $FD ; lo (hi = $FE) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +DSK_PTR = ZP_PTR2 ; lo (hi = +1) +DSK_PTR2 = ZP_PTR3 ; lo (hi = +1) ; ============================================================================ ; Module entry point @@ -215,13 +218,13 @@ disk_main: sta $01 ; Save ZP - lda $FB + lda ZP_PTR2 sta DSK_ZP_SAVE+0 - lda $FC + lda ZP_PTR2+1 sta DSK_ZP_SAVE+1 - lda $FD + lda ZP_PTR3 sta DSK_ZP_SAVE+2 - lda $FE + lda ZP_PTR3+1 sta DSK_ZP_SAVE+3 ; Copy drive from parameter block @@ -328,13 +331,13 @@ disk_main: dsk_exit: ; Restore ZP lda DSK_ZP_SAVE+0 - sta $FB + sta ZP_PTR2 lda DSK_ZP_SAVE+1 - sta $FC + sta ZP_PTR2+1 lda DSK_ZP_SAVE+2 - sta $FD + sta ZP_PTR3 lda DSK_ZP_SAVE+3 - sta $FE + sta ZP_PTR3+1 rts ; ============================================================================ diff --git a/modren.asm b/modren.asm index 090d7bf..ccbdcde 100644 --- a/modren.asm +++ b/modren.asm @@ -53,11 +53,14 @@ SPIN_COLOR_A = $01 SPIN_COLOR_B = $00 ; ---- Zero page (saved / restored around module call) ---- -SRC_PTR = $FB ; 16-bit source walker ($FC = hi) -DST_PTR = $FD ; 16-bit destination ptr ($FE = hi) -TMP = $3A ; scratch lo ($3B = hi) -TMP2 = $3C ; number lo ($3D = TMP2+1 = hi) -TMP3 = $3E ; table ptr lo ($3F = TMP3+1 = hi) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +SRC_PTR = ZP_PTR2 ; 16-bit source walker (+1 = hi) +DST_PTR = ZP_PTR3 ; 16-bit destination ptr (+1 = hi) +TMP = ZP_SCRATCH+0 ; scratch lo (+1 = hi) +TMP2 = ZP_SCRATCH+2 ; number lo (TMP2+1 = hi) +TMP3 = ZP_SCRATCH+4 ; table ptr lo (TMP3+1 = hi) ; ============================================================================ .segment "LOADADDR" @@ -78,22 +81,22 @@ modren_entry: cli rts : - ; Save ZP $3A-$3F + ; Save the ZP_SCRATCH block ldx #0 @zpsave: - lda $3A,x + lda ZP_SCRATCH,x sta ZP_SAVE,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zpsave - ; Save $FB-$FE - lda $FB + ; Save ZP_PTR2 / ZP_PTR3 + lda ZP_PTR2 sta ZP_SAVE+6 - lda $FC + lda ZP_PTR2+1 sta ZP_SAVE+7 - lda $FD + lda ZP_PTR3 sta ZP_SAVE+8 - lda $FE + lda ZP_PTR3+1 sta ZP_SAVE+9 ; Copy MOD parameters into local state @@ -164,18 +167,18 @@ modren_entry: ldx #0 @zprestore: lda ZP_SAVE,x - sta $3A,x + sta ZP_SCRATCH,x inx - cpx #6 + cpx #ZP_SCRATCH_LEN bne @zprestore lda ZP_SAVE+6 - sta $FB + sta ZP_PTR2 lda ZP_SAVE+7 - sta $FC + sta ZP_PTR2+1 lda ZP_SAVE+8 - sta $FD + sta ZP_PTR3 lda ZP_SAVE+9 - sta $FE + sta ZP_PTR3+1 cli rts diff --git a/modscr.asm b/modscr.asm index 3895d1f..915ce9a 100644 --- a/modscr.asm +++ b/modscr.asm @@ -90,7 +90,12 @@ IDE_LEN_LO = <($9FFF - $0801 + 1) IDE_LEN_HI = >($9FFF - $0801 + 1) ; ---- Scratch ---- -TMP16 = $3C ; lo (hi=$3D) +; Address comes from zp.inc (see docs/c128-port-notes.md). The BASIC ABI +; locations above (TXTTAB, VARTAB, MEMSIZ) are fixed by the ROM and are +; deliberately NOT part of the relocatable pool. +.include "zp.inc" + +TMP16 = ZP_SCRATCH+2 ; lo (hi=+3) ; Metadata fetch buffer (module RAM, reused after verify) META_BUF = $B000 diff --git a/modscrh.asm b/modscrh.asm index 891ec6f..1fa62e8 100644 --- a/modscrh.asm +++ b/modscrh.asm @@ -137,8 +137,13 @@ VEC_IERROR = $0300 VEC_IMAIN = $0302 VEC_IGONE = $0308 -; ZP scratch -HND_TMP = $3C ; lo (hi=$3D) — scratch pointer (no conflict during script) +; ZP scratch — address comes from zp.inc (see docs/c128-port-notes.md). +; NOTE: the BASIC ABI locations this module also uses (TXTTAB, VARTAB, +; VARPNT, BASIC_TXTPTR, and AYINT's $14/$15 output) are fixed by the ROM and +; are deliberately NOT part of the relocatable pool. +.include "zp.inc" + +HND_TMP = ZP_SCRATCH+2 ; lo (hi=+3) — scratch pointer (no conflict during script) ; Module status MOD_STATUS = $021E diff --git a/modsct.asm b/modsct.asm index 38df8ef..e6b8b1e 100644 --- a/modsct.asm +++ b/modsct.asm @@ -110,17 +110,20 @@ META_VERSION = 5 ; $01 META_SIZE = 6 ; ---- ZP — same layout as modtok (no conflict between phases) ---- -LINENO = $3A ; line number lo (hi=$3B); = KW_TOKEN in try_keyword -KW_TOKEN = $3A -KW_XSAVE = $3B -TMP16 = $3C ; general 16-bit scratch lo (hi=$3D) -IN_STRING = $3E -AFTER_REM = $3F -LINK_PTR = $F7 ; lo (hi=$F8) — link word back-patch pointer / scan pointer -BASIC_ADDR = $F9 ; lo (hi=$FA) — running C64 address tracker -SRC_PTR = $FB ; lo (hi=$FC) — source walker -DST_PTR = $FD ; lo (hi=$FE) — staging output pointer -OVFLAG = $FF ; staging overflow flag ($FF = overflowed, output invalid) +; Addresses come from zp.inc (see docs/c128-port-notes.md). +.include "zp.inc" + +LINENO = ZP_SCRATCH+0 ; line number lo (hi=+1); = KW_TOKEN in try_keyword +KW_TOKEN = ZP_SCRATCH+0 +KW_XSAVE = ZP_SCRATCH+1 +TMP16 = ZP_SCRATCH+2 ; general 16-bit scratch lo (hi=+3) +IN_STRING = ZP_SCRATCH+4 +AFTER_REM = ZP_SCRATCH+5 +LINK_PTR = ZP_PTR0 ; lo (hi=+1) — link word back-patch pointer / scan pointer +BASIC_ADDR = ZP_PTR1 ; lo (hi=+1) — running C64 address tracker +SRC_PTR = ZP_PTR2 ; lo (hi=+1) — source walker +DST_PTR = ZP_PTR3 ; lo (hi=+1) — staging output pointer +OVFLAG = ZP_OVFLAG ; staging overflow flag ($FF = overflowed, output invalid) ; ---- Module scratch RAM ---- ; The REU parameter block and include table are RESERVED INSIDE THE IMAGE diff --git a/modtok.asm b/modtok.asm index 38a21a4..9be58fd 100644 --- a/modtok.asm +++ b/modtok.asm @@ -42,17 +42,20 @@ MOD_NEW_END_HI = $0220 MOD_GAP_START_LO = $0216 MOD_GAP_START_HI = $0217 -LINENO = $3A ; line number lo (hi at $3B); also KW_TOKEN in try_keyword -KW_TOKEN = $3A ; token byte (try_keyword) — same ZP, different phase -KW_XSAVE = $3B ; saved source index (try_keyword) -TMP16 = $3C ; 16-bit scratch lo (hi at $3D) -IN_STRING = $3E -AFTER_REM = $3F -LINK_PTR = $F7 ; lo (hi at $F8) -BASIC_ADDR = $F9 ; lo (hi at $FA) -SRC_PTR = $FB ; lo (hi at $FC) -DST_PTR = $FD ; lo (hi at $FE) -OVFLAG = $FF ; staging overflow flag ($FF = overflowed) +; ---- ZP scratch — addresses come from zp.inc (see docs/c128-port-notes.md) -- +.include "zp.inc" + +LINENO = ZP_SCRATCH+0 ; line number lo (hi at +1); also KW_TOKEN +KW_TOKEN = ZP_SCRATCH+0 ; token byte (try_keyword) — same ZP, different phase +KW_XSAVE = ZP_SCRATCH+1 ; saved source index (try_keyword) +TMP16 = ZP_SCRATCH+2 ; 16-bit scratch lo (hi at +3) +IN_STRING = ZP_SCRATCH+4 +AFTER_REM = ZP_SCRATCH+5 +LINK_PTR = ZP_PTR0 ; lo (hi at +1) +BASIC_ADDR = ZP_PTR1 ; lo (hi at +1) +SRC_PTR = ZP_PTR2 ; lo (hi at +1) +DST_PTR = ZP_PTR3 ; lo (hi at +1) +OVFLAG = ZP_OVFLAG ; staging overflow flag ($FF = overflowed) BASIC_START = $0801 ; STREAMING MODEL (no staging buffer): the source text is first relocated diff --git a/zp.inc b/zp.inc new file mode 100644 index 0000000..8543449 --- /dev/null +++ b/zp.inc @@ -0,0 +1,72 @@ +; ============================================================================ +; zp.inc — zero-page scratch pool for PETProject loadable modules +; +; Include this at the top of every module that uses zero-page scratch. +; Selects the per-target map; assemble with `-D TARGET_C128` for the C128. +; +; --------------------------------------------------------------------------- +; WHAT THIS COVERS +; --------------------------------------------------------------------------- +; The *module* scratch tier only. Two other tiers exist and are NOT defined +; here: +; +; 1. The editor's own block, declared as a `.zeropage` segment in +; editor.asm and placed by the `ZP:` entry in petproject.cfg. It is +; already relocatable by changing one line in the linker config. +; +; 2. BASIC and KERNAL ABI addresses — TXTTAB ($2B), VARTAB ($2D), +; MEMSIZ ($37), VARPNT ($47), TXTPTR ($7A), AYINT's output ($14/$15), +; JIFFY_LO ($A2), FA ($BA). These are *not ours to relocate*; they are +; fixed by the ROM we are calling. They stay as literals at their use +; sites in modscr.asm / modscrh.asm / editor.asm / moddsk.asm. +; +; See docs/c128-port-notes.md for the full audit. +; +; --------------------------------------------------------------------------- +; CONTRACT — any target map must satisfy all of these +; --------------------------------------------------------------------------- +; ZP_SCRATCH 6 CONTIGUOUS bytes. Modules address it as ZP_SCRATCH+0..+5 +; and index it (`lda ZP_SCRATCH,x`) in save/restore loops. +; Sub-slots are used as 16-bit values, so +0/+1, +2/+3 and +; +4/+5 must each be an adjacent pair. +; +; ZP_PTRS 8 CONTIGUOUS bytes = four 2-byte pointers at +0, +2, +4, +6. +; Every one of these is dereferenced with (zp),y, so each pair +; must be adjacent and must not straddle the end of page zero. +; +; ZP_OVFLAG 1 byte. Standalone flag; no adjacency requirement. +; +; All 15 bytes must be disjoint from the editor's `.zeropage` block, and +; disjoint from anything the target KERNAL touches — including its IRQ +; handler, which runs throughout (the editor issues no sei/cli of its own). +; ============================================================================ + +.ifdef TARGET_C128 + .include "zp_c128.inc" +.else + .include "zp_c64.inc" +.endif + +; --------------------------------------------------------------------------- +; Derived pointer slots — same on every target, computed from ZP_PTRS. +; --------------------------------------------------------------------------- +ZP_PTR0 = ZP_PTRS + 0 +ZP_PTR1 = ZP_PTRS + 2 +ZP_PTR2 = ZP_PTRS + 4 +ZP_PTR3 = ZP_PTRS + 6 + +; --------------------------------------------------------------------------- +; Contract assertions — a bad target map fails the build. +; +; ca65 evaluates .assert at the END of assembly and skips it if the assembly +; already produced errors. So a map whose ADDRESSES are out of page zero +; usually surfaces first as "Range error (256 not in [0..255])" at the use +; site, and these messages never print. That is fine — the build still fails +; — but don't expect the friendly message in that case. A map that is merely +; the wrong SHAPE (bad length) does report through here. +; --------------------------------------------------------------------------- +.assert ZP_SCRATCH >= 2 && ZP_SCRATCH + ZP_SCRATCH_LEN <= $100, error, "zp.inc: ZP_SCRATCH block does not fit in page zero" +.assert ZP_PTRS >= 2 && ZP_PTRS + ZP_PTRS_LEN <= $100, error, "zp.inc: ZP_PTRS block does not fit in page zero" +.assert ZP_OVFLAG >= 2 && ZP_OVFLAG <= $FF, error, "zp.inc: ZP_OVFLAG is not in page zero" +.assert ZP_SCRATCH_LEN = 6, error, "zp.inc: ZP_SCRATCH must be 6 bytes" +.assert ZP_PTRS_LEN = 8, error, "zp.inc: ZP_PTRS must be 8 bytes" diff --git a/zp_c128.inc b/zp_c128.inc new file mode 100644 index 0000000..ae9bfda --- /dev/null +++ b/zp_c128.inc @@ -0,0 +1,84 @@ +; ============================================================================ +; zp_c128.inc — C128 module scratch map +; +; Do not include this directly — include zp.inc, which selects the target. +; +; Source: C128 RAM memory map, https://www.zimmers.net/anonftp/pub/cbm/maps/ +; C128ram.txt (the CBM archive's zero page / page 3 allocation list). +; +; --------------------------------------------------------------------------- +; HOW THE C128 ZERO PAGE DIVIDES +; --------------------------------------------------------------------------- +; $02-$8F BASIC 7.0 142 bytes. Tokens, SYS registers, program/variable/ +; array pointers, FP accumulators, DS$ descriptor, +; graphics work values. +; $90-$F9 KERNAL + 106 bytes. ST, serial/tape/RS-232, jiffy clock, +; screen editor file tables, keyboard decode and buffer, all the +; screen editor state (cursor, margins, colors). +; $FA-$FE "Not used" 5 bytes. The only officially spare zero page. +; $FF BASIC scratch +; +; Five free bytes is nowhere near the 41 PETProject needs, so the port takes +; the other route: BASIC is banked out for the whole session and never called, +; which makes the entire $02-$8F region available. 142 bytes for a 41-byte +; requirement — 101 bytes of headroom. +; +; The reason this is safe rather than merely convenient: the editor issues no +; sei/cli of its own and depends on the default KERNAL IRQ throughout (all 20 +; GETIN sites, plus the jiffy clock at $A0-$A2 for cursor blink). So what the +; IRQ handler touches matters more than what our own calls touch. Every +; IRQ-updated location on the C128 sits at $90 or above — jiffy clock +; $A0-$A2, keyboard decode pointer $CC-$CD, key buffer and codes $D0-$D5, +; the cassette switch at $C0 (which the map explicitly annotates "Updated +; during IRQ"), and the screen editor block $E0-$F9. Nothing below $90 is +; IRQ-touched. The same holds for the KERNAL I/O we call: status $90, file +; tables $B7-$BC, device $BA, bank registers $C6-$C7 — all above the line. +; +; --------------------------------------------------------------------------- +; LAYOUT — packed from the bottom of the BASIC region +; --------------------------------------------------------------------------- +; $02-$1B editor .zeropage segment 26 B (Tier 1 — UNCHANGED from C64) +; $1C-$21 ZP_SCRATCH 6 B +; $22-$29 ZP_PTRS 8 B (four pointer pairs) +; $2A ZP_OVFLAG 1 B +; ---- +; 41 B, occupying $02-$2A +; +; Two consequences worth knowing: +; +; * Tier 1 does not move. petproject.cfg's `ZP: start = $02, size = $1A` +; is already correct for the C128, so a C128 linker config differs from +; the C64 one only in the areas that actually change (banking, load +; addresses) — not in zero page. +; +; * The two tiers are now CONTIGUOUS as well as disjoint. That was the goal +; stated in docs/c128-port-notes.md: it makes non-overlap obvious rather +; than incidental, and it clears the way to retire the KW_TOKEN alias in +; editor.asm by giving colorize a byte of its own. +; +; --------------------------------------------------------------------------- +; STILL TO CONFIRM ON HARDWARE / IN VICE +; --------------------------------------------------------------------------- +; The reasoning above comes from a static allocation map, which documents who +; OWNS each byte, not empirically what the ROM writes. Before trusting this in +; anger, verify on a running C128 that nothing disturbs $02-$2A across a long +; editing session with disk I/O — the cheap check is a poisoned-pattern test: +; fill $02-$2A with a known pattern, run the IDE, and confirm only PETProject's +; own writes appear. +; ============================================================================ + +ZP_SCRATCH = $1C +ZP_SCRATCH_LEN = 6 + +ZP_PTRS = $22 +ZP_PTRS_LEN = 8 + +ZP_OVFLAG = $2A + +; Guard against Tier 2 colliding with the editor's Tier 1 block. Tier 1 is +; placed by the linker, so this is the assembly-time half of that check: it +; pins the assumption that Tier 1 ends at $1B. The link-time half (importing +; __ZP_START__ / __ZP_SIZE__ and asserting with lderror) is described in +; docs/c128-port-notes.md and is still to be added. +C128_TIER1_END = $1B +.assert ZP_SCRATCH > C128_TIER1_END, error, "zp_c128.inc: ZP_SCRATCH overlaps the editor's zeropage block" diff --git a/zp_c64.inc b/zp_c64.inc new file mode 100644 index 0000000..aae7dd6 --- /dev/null +++ b/zp_c64.inc @@ -0,0 +1,32 @@ +; ============================================================================ +; zp_c64.inc — C64 module scratch map (the historical layout) +; +; These are the exact addresses the modules used as literals before the +; include-file conversion, so a C64 build is byte-for-byte unchanged. +; Do not include this directly — include zp.inc, which selects the target. +; +; Layout: +; $3A-$3F ZP_SCRATCH general scratch (TMP/TMP2/TMP3, LINENO, KW_TOKEN, +; KW_XSAVE, TMP16, NZFLAG, KWTAB, IN_STRING, +; AFTER_REM, OVFLAG, HND_TMP — names vary by module) +; $F7-$FE ZP_PTRS four pointer pairs +; +0 = $F7 COPY_SRC / LINK_PTR +; +2 = $F9 COPY_DST / BASIC_ADDR +; +4 = $FB SRC_PTR / DSK_PTR +; +6 = $FD DST_PTR / DSK_PTR2 +; $FF ZP_OVFLAG staging overflow flag (modtok, modsct) +; +; Why these are free on a C64: $3A-$3F and $F7-$FF belong to BASIC and the +; RS-232 buffers, neither of which is live while a module runs. Note that +; the C64 KERNAL IRQ handler *does* use $FB/$FC as cursor-blink scratch — +; which is why modasm holds sei across an entire assembly (modasm.asm:260). +; That reasoning is C64-specific and must be re-derived for the C128. +; ============================================================================ + +ZP_SCRATCH = $3A +ZP_SCRATCH_LEN = 6 + +ZP_PTRS = $F7 +ZP_PTRS_LEN = 8 + +ZP_OVFLAG = $FF From 7daf605bd59652a62769ac9a49e43b037f119b9f Mon Sep 17 00:00:00 2001 From: DNSGeek Date: Wed, 12 Aug 2026 19:26:40 -0700 Subject: [PATCH 2/3] ci: mark make_disk.py and test_include.py executable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unblocks the formatting workflow. Unrelated to the C128 port work in the rest of this branch — kept as its own commit so it can be extracted. Both files carry a `#!/usr/bin/env python3` shebang but were committed mode 100644, which newer ruff flags as EXE001. Nothing regressed: the workflow installs ruff unpinned (`pip install black isort ssort ruff`), main last passed on 2026-07-11, and a ruff release since then activated the rule against a pre-existing condition. Mode change only, no content change. Verified against ruff 0.16.2: 2 errors before, "All checks passed" after, with isort and black clean both ways. Co-Authored-By: Claude Opus 5 --- make_disk.py | 0 tests/test_include.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 make_disk.py mode change 100644 => 100755 tests/test_include.py diff --git a/make_disk.py b/make_disk.py old mode 100644 new mode 100755 diff --git a/tests/test_include.py b/tests/test_include.py old mode 100644 new mode 100755 From 218aac9cede354c3c8816b0ea4747eda55f43a05 Mon Sep 17 00:00:00 2001 From: DNSGeek Date: Wed, 12 Aug 2026 19:30:18 -0700 Subject: [PATCH 3/3] style: prettier-normalize c128-port-notes.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Formatting only — no content change. Table column padding and *emphasis* -> _emphasis_, matching what `npx prettier --write "**/*.md"` produces with default settings. The formatting workflow runs prettier in --write mode and then commits the result, so an unformatted file makes its verify_diff step fire. On a pull_request event that step then dies in `git push` ("You are not currently on a branch" — the PR checkout is a detached HEAD), which is what turned the format check red. Pre-formatting means verify_diff stays false and the step is skipped. Verified the full CI formatter set is clean at this commit: prettier (md, and js/json — none present), black, isort, ssort, ruff, and shfmt with CI's exact flags (-i 2 -ci -bn). All 12 internal doc anchors still resolve. Note for later: the detached-HEAD push failure is a latent workflow bug, not specific to this branch. Any PR that touches a file the formatters want to change will hit it. Left alone here as out of scope. Co-Authored-By: Claude Opus 5 --- docs/c128-port-notes.md | 223 ++++++++++++++++++++-------------------- 1 file changed, 112 insertions(+), 111 deletions(-) diff --git a/docs/c128-port-notes.md b/docs/c128-port-notes.md index 3ecf597..e7ed865 100644 --- a/docs/c128-port-notes.md +++ b/docs/c128-port-notes.md @@ -44,7 +44,7 @@ sustain its fetches, and the KERNAL's `FAST` blanks the display rather than show garbage. The VDC is immune because it is an independent chip, which is why 80-column mode runs at 2 MHz by default. -So on this target 2 MHz is a *blank-screen burst* around compute-only work: +So on this target 2 MHz is a _blank-screen burst_ around compute-only work: the assembler's passes, tokenize/detokenize, renumber, search/replace. Two constraints: @@ -71,24 +71,24 @@ read-only system locations. Declared as a `.zeropage` segment at `editor.asm:126`, placed by the `ZP:` entry in `petproject.cfg`. -| Range | Sz | Symbol | Role | -| --- | --- | --- | --- | -| `$02-$03` | 2 | `GAP_START` | gap buffer | -| `$04-$05` | 2 | `GAP_END` | gap buffer | -| `$06-$07` | 2 | `BUF_PTR` | buffer walk | -| `$08-$09` | 2 | `SCREEN_PTR` | screen RAM pointer | -| `$0A-$0B` | 2 | `TOP_LINE` | viewport origin | -| `$0C` | 1 | `LEFT_COL` | horizontal scroll | -| `$0D-$0E` | 2 | `TMP` | scratch; aliased `CLR_SCOL` / `CLR_TMP` | -| `$0F` | 1 | `SAVED_X` | X preservation | -| `$10` | 1 | `CURSOR_ROW` | | -| `$11` | 1 | `CURSOR_COL` | | -| `$12` | 1 | `COL_SAVE` | aliased `CLR_LCOL` | -| `$13-$14` | 2 | `TXT_PTR` | text scratch pointer | -| `$15-$16` | 2 | `LPTR` | load/save + module trampoline | -| `$17-$18` | 2 | `CLR_PTR` | color RAM pointer | -| `$19` | 1 | `CLR_KWLEN` | | -| `$1A-$1B` | 2 | `CLR_CTMP` | colortab walker | +| Range | Sz | Symbol | Role | +| --------- | --- | ------------ | --------------------------------------- | +| `$02-$03` | 2 | `GAP_START` | gap buffer | +| `$04-$05` | 2 | `GAP_END` | gap buffer | +| `$06-$07` | 2 | `BUF_PTR` | buffer walk | +| `$08-$09` | 2 | `SCREEN_PTR` | screen RAM pointer | +| `$0A-$0B` | 2 | `TOP_LINE` | viewport origin | +| `$0C` | 1 | `LEFT_COL` | horizontal scroll | +| `$0D-$0E` | 2 | `TMP` | scratch; aliased `CLR_SCOL` / `CLR_TMP` | +| `$0F` | 1 | `SAVED_X` | X preservation | +| `$10` | 1 | `CURSOR_ROW` | | +| `$11` | 1 | `CURSOR_COL` | | +| `$12` | 1 | `COL_SAVE` | aliased `CLR_LCOL` | +| `$13-$14` | 2 | `TXT_PTR` | text scratch pointer | +| `$15-$16` | 2 | `LPTR` | load/save + module trampoline | +| `$17-$18` | 2 | `CLR_PTR` | color RAM pointer | +| `$19` | 1 | `CLR_KWLEN` | | +| `$1A-$1B` | 2 | `CLR_CTMP` | colortab walker | **This block is already fully relocatable.** No code references these by literal address; everything resolves through the ca65 zeropage segment. @@ -104,16 +104,16 @@ lands. > describes the C64 map, which is unchanged. See > [Relocation strategy](#relocation-strategy) for what remains. -| Range | Used by | As | -| --- | --- | --- | -| `$3A-$3B` | modtok, modsct, moddet, modasm, moddis, modren | `LINENO` / `KW_TOKEN`+`KW_XSAVE` / `TMP` | -| `$3C-$3D` | above + modscr, modscrh | `TMP16` / `TMP2` / `NZFLAG`+`KWTAB` / `HND_TMP` | -| `$3E-$3F` | modtok, modsct, moddet, modasm, moddis, modren | `IN_STRING`+`AFTER_REM` / `TMP3` / `OVFLAG` | -| `$F7-$F8` | modtok, modsct, moddet | `LINK_PTR` / `COPY_SRC` | -| `$F9-$FA` | modtok, modsct, moddet | `BASIC_ADDR` / `COPY_DST` | -| `$FB-$FC` | all seven compute modules | `SRC_PTR` | -| `$FD-$FE` | all seven compute modules | `DST_PTR` | -| `$FF` | modtok, modsct | `OVFLAG` | +| Range | Used by | As | +| --------- | ---------------------------------------------- | ----------------------------------------------- | +| `$3A-$3B` | modtok, modsct, moddet, modasm, moddis, modren | `LINENO` / `KW_TOKEN`+`KW_XSAVE` / `TMP` | +| `$3C-$3D` | above + modscr, modscrh | `TMP16` / `TMP2` / `NZFLAG`+`KWTAB` / `HND_TMP` | +| `$3E-$3F` | modtok, modsct, moddet, modasm, moddis, modren | `IN_STRING`+`AFTER_REM` / `TMP3` / `OVFLAG` | +| `$F7-$F8` | modtok, modsct, moddet | `LINK_PTR` / `COPY_SRC` | +| `$F9-$FA` | modtok, modsct, moddet | `BASIC_ADDR` / `COPY_DST` | +| `$FB-$FC` | all seven compute modules | `SRC_PTR` | +| `$FD-$FE` | all seven compute modules | `DST_PTR` | +| `$FF` | modtok, modsct | `OVFLAG` | ### Cross-tier alias @@ -125,11 +125,11 @@ contiguous. ### Save/restore discipline — inconsistent, and not load-bearing -| Module | Saves | Does not save | -| --- | --- | --- | -| modasm, moddis, modren | `$3A-$3F` + `$FB-$FE` (10 B) | `$F7-$FA`, `$FF` | -| moddsk | `$FB-$FE` (4 B) | — consistent, touches nothing else | -| moddet, modtok, modsct, modscrh, modsfr | **nothing** | everything they use | +| Module | Saves | Does not save | +| --------------------------------------- | ---------------------------- | ---------------------------------- | +| modasm, moddis, modren | `$3A-$3F` + `$FB-$FE` (10 B) | `$F7-$FA`, `$FF` | +| moddsk | `$FB-$FE` (4 B) | — consistent, touches nothing else | +| moddet, modtok, modsct, modscrh, modsfr | **nothing** | everything they use | An earlier revision of this document called the inconsistency "the main hazard in the whole port." **That was wrong.** On examination the saves protect @@ -143,15 +143,15 @@ nothing that anything depends on: or `modules.asm` touches the pool at all. - **The saves carry no stated rationale** — the comments read only "Save ZP". - **What they would nominally protect is moot.** `$3A-$3F` is BASIC's and - `$F7-$FE` is RS-232's, but the editor quits through BASIC *cold start* + `$F7-$FE` is RS-232's, but the editor quits through BASIC _cold start_ (`editor.asm:542`), which reinitialises all of it. - **The one real in-flight hazard is not addressed by saving.** The C64 KERNAL IRQ using `$FB`/`$FC` for cursor blink (`modasm.asm:260`) is a collision - *during* execution; entry/exit save/restore does nothing for it. `sei` does, + _during_ execution; entry/exit save/restore does nothing for it. `sei` does, which is why modasm holds one. So the requirement on any relocation is **disjointness alone** — the two tiers -must not overlap. Making saving universal is *not* a substitute for that, and +must not overlap. Making saving universal is _not_ a substitute for that, and is not worth doing on its own merits: it would mean threading save/restore through every exit path in five working modules (`modsfr` has 31 `rts`, `modsct` 30, `modscrh` 18), risking a regression on some error path in @@ -167,11 +167,11 @@ on top of each other — which is the only scenario that ever mattered. ### System ZP — read, not owned -| Addr | Symbol | Sites | C128 | -| --- | --- | --- | --- | -| `$A2` | `JIFFY_LO` | `editor.asm:32`, `moddsk.asm:110`, `modscrh.asm:441,444` | same address | -| `$BA` | `FA` (device) | `editor.asm:26` | same address | -| `$00`/`$01` | processor port | 6 banking sites | **see below** | +| Addr | Symbol | Sites | C128 | +| ----------- | -------------- | -------------------------------------------------------- | ------------- | +| `$A2` | `JIFFY_LO` | `editor.asm:32`, `moddsk.asm:110`, `modscrh.asm:441,444` | same address | +| `$BA` | `FA` (device) | `editor.asm:26` | same address | +| `$00`/`$01` | processor port | 6 banking sites | **see below** | ### IRQ interaction @@ -181,7 +181,7 @@ blink. So the ZP set to avoid is defined by **what the KERNAL IRQ handler touches 60 times a second**, not merely by the routines PETProject calls. That is a strictly larger set. -`modasm` is the exception: it holds `sei` across an *entire assembly*, on +`modasm` is the exception: it holds `sei` across an _entire assembly_, on explicitly C64-specific reasoning (`modasm.asm:260` — the C64 KERNAL IRQ uses `$FB`/`$FC` as cursor-blink scratch, which would corrupt `SRC_PTR`). That premise must be **re-derived** for the C128, not inherited. A multi-second @@ -195,12 +195,12 @@ Source: the CBM archive's C128 RAM map, The C128 zero page divides cleanly in two, with almost nothing spare: -| Range | Owner | Size | -| --- | --- | --- | -| `$02-$8F` | BASIC 7.0 — tokens, SYS registers, program/variable/array pointers, FP accumulators, `DS$`, graphics work values | 142 B | +| Range | Owner | Size | +| --------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| `$02-$8F` | BASIC 7.0 — tokens, SYS registers, program/variable/array pointers, FP accumulators, `DS$`, graphics work values | 142 B | | `$90-$F9` | KERNAL + screen editor — `ST`, serial/tape/RS-232, jiffy clock, file tables, keyboard decode and buffer, all cursor/margin/color state | 106 B | -| `$FA-$FE` | **"Not used"** — the only officially spare zero page | 5 B | -| `$FF` | BASIC scratch | 1 B | +| `$FA-$FE` | **"Not used"** — the only officially spare zero page | 5 B | +| `$FF` | BASIC scratch | 1 B | Five spare bytes against a 41-byte requirement, so the [relocation strategy](#relocation-strategy) is confirmed as the only viable @@ -219,12 +219,12 @@ clock `$A0-$A2`, keyboard decode pointer `$CC-$CD`, key buffer and codes **Adopted layout** — packed from the bottom of the BASIC region, implemented in `zp_c128.inc`: -| Range | Contents | Size | -| --- | --- | --- | +| Range | Contents | Size | +| --------- | ----------------------------------- | ---- | | `$02-$1B` | editor `.zeropage` segment (Tier 1) | 26 B | -| `$1C-$21` | `ZP_SCRATCH` | 6 B | -| `$22-$29` | `ZP_PTRS` (four pointer pairs) | 8 B | -| `$2A` | `ZP_OVFLAG` | 1 B | +| `$1C-$21` | `ZP_SCRATCH` | 6 B | +| `$22-$29` | `ZP_PTRS` (four pointer pairs) | 8 B | +| `$2A` | `ZP_OVFLAG` | 1 B | Two consequences: @@ -237,7 +237,7 @@ Two consequences: `KW_TOKEN` alias can be retired by giving colorize a byte of its own. **Residual empirical check.** The above comes from a static allocation map, -which documents who *owns* each byte rather than what the ROM demonstrably +which documents who _owns_ each byte rather than what the ROM demonstrably writes. Before trusting it in anger, run a poisoned-pattern test on a real C128 or in VICE: fill `$02-$2A` with a known pattern, exercise the IDE including disk I/O, and confirm only PETProject's own writes appear. @@ -256,15 +256,15 @@ these should be converted **first**, before anything else is tested. Sites: -| File:line | Current | Purpose | -| --- | --- | --- | -| `modasm.asm:273` | `#$36` | page BASIC out to run from `$A000` | -| `moddsk.asm:214` | `#$37` | restore | -| `modscr.asm:207` | `#$37` | restore, from `$033C` trampoline | -| `modscrh.asm:1080` | `#$36` | page out for MODASM handoff | -| `modscrh.asm:1126` | `#$37` | restore | -| `modules.asm:724` | `#$36` | module execution banking | -| `modules.asm:735` | `#$37` | restore | +| File:line | Current | Purpose | +| ------------------ | ------- | ---------------------------------- | +| `modasm.asm:273` | `#$36` | page BASIC out to run from `$A000` | +| `moddsk.asm:214` | `#$37` | restore | +| `modscr.asm:207` | `#$37` | restore, from `$033C` trampoline | +| `modscrh.asm:1080` | `#$36` | page out for MODASM handoff | +| `modscrh.asm:1126` | `#$37` | restore | +| `modules.asm:724` | `#$36` | module execution banking | +| `modules.asm:735` | `#$37` | restore | Use the MMU's `$FF01–$FF04` preconfiguration registers so each switch stays a single store, preserving the shape of the existing code. @@ -276,13 +276,13 @@ that load a stored config in a single store. The `$FF00–$FF04` window is visible from **every** bank, which is what makes it usable from switching code. -| Field | Controls | -| --- | --- | -| bit 0 | `$D000-$DFFF` — I/O vs ROM/RAM | -| bit 1 | `$4000-$7FFF` | -| bits 2-3 | `$8000-$BFFF` | -| **bits 4-5** | **`$C000-$FFFF`** | -| bits 6-7 | RAM bank select | +| Field | Controls | +| ------------ | ------------------------------ | +| bit 0 | `$D000-$DFFF` — I/O vs ROM/RAM | +| bit 1 | `$4000-$7FFF` | +| bits 2-3 | `$8000-$BFFF` | +| **bits 4-5** | **`$C000-$FFFF`** | +| bits 6-7 | RAM bank select | Essentially **one config covers the whole session** — `$0E`: RAM from `$0000-$BFFF`, I/O at `$D000`, KERNAL ROM above. That is strictly better than @@ -291,8 +291,8 @@ dance around the big modules does not get translated, it **disappears** — along with the "do not restore `$01` while executing here" hazard at `modasm.asm:449`. -*(CR bit assignments and the `$0E` value want confirming against the C128 -Programmer's Reference — see [Open items](#open-items).)* +_(CR bit assignments and the `$0E` value want confirming against the C128 +Programmer's Reference — see [Open items](#open-items).)_ ### The `$C000` collision — the real work in this phase @@ -302,9 +302,9 @@ is the screen editor ROM, sharing that field with the KERNAL at `$E000`. So PETProject leans on `$C000-$CFFF` heavily: -| What | Detail | -| --- | --- | -| 6 modules load there | moddet, modtok, moddsk, modren, modsfr, modscrh — largest is moddsk at 3,255 bytes | +| What | Detail | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| 6 modules load there | moddet, modtok, moddsk, modren, modsfr, modscrh — largest is moddsk at 3,255 bytes | | modasm's entire working state | 44 equates at `$C0xx` — symbol table, PC, pass counter, error state, output filename, `ZP_SAVE`, gap pointers (`modasm.asm:115-151`) | That is 7 of 10 modules. On the C64 it was the natural choice: `$C000-$CFFF` @@ -357,7 +357,7 @@ is wrong regardless of banking — and page 3 is much more crowded on the C128 than the C64, so there is no drop-in replacement. See [Open items](#open-items) #2. -Rehoming `$C000` *reduces* how much trampolining is needed, since one config +Rehoming `$C000` _reduces_ how much trampolining is needed, since one config then covers the session. ### `SETBNK` @@ -395,22 +395,22 @@ Choosing 40 columns makes the largest category of work vanish: ## Component status -| Component | C128 effort | Notes | -| --- | --- | --- | -| `editor.asm` | **Low** | ZP relocation via cfg; one line for the quit path | -| `colorize.asm` | **None** | aliases editor ZP only; moves with it | -| `loadsave.asm` | **Low** | add `SETBNK` | -| `modules.asm` | **Low** | MMU conversion + `SETBNK` | -| `modsfr.asm` | **None** | no banking, no ROM calls | -| `moddet.asm` | **Low** | ZP scratch equates only | -| `modtok.asm` | **Low** | ZP scratch equates only | -| `modren.asm` | **Low** | ZP scratch + save loop | -| `moddis.asm` | **Low** | ZP scratch + save loop | -| `modasm.asm` | **Medium** | MMU + ZP + re-derive the `sei` decision | -| `moddsk.asm` | **Medium** | MMU + ZP + `SETBNK` | -| `modsct.asm` | **High** | REU staging at `$B000`; BASIC ABI | -| `modscr.asm` | **High** | BASIC ROM entry points; `$B000` staging | -| `modscrh.asm` | **Highest** | 12 BASIC ROM entry points; BASIC 2.0 ZP ABI | +| Component | C128 effort | Notes | +| -------------- | ----------- | ------------------------------------------------- | +| `editor.asm` | **Low** | ZP relocation via cfg; one line for the quit path | +| `colorize.asm` | **None** | aliases editor ZP only; moves with it | +| `loadsave.asm` | **Low** | add `SETBNK` | +| `modules.asm` | **Low** | MMU conversion + `SETBNK` | +| `modsfr.asm` | **None** | no banking, no ROM calls | +| `moddet.asm` | **Low** | ZP scratch equates only | +| `modtok.asm` | **Low** | ZP scratch equates only | +| `modren.asm` | **Low** | ZP scratch + save loop | +| `moddis.asm` | **Low** | ZP scratch + save loop | +| `modasm.asm` | **Medium** | MMU + ZP + re-derive the `sei` decision | +| `moddsk.asm` | **Medium** | MMU + ZP + `SETBNK` | +| `modsct.asm` | **High** | REU staging at `$B000`; BASIC ABI | +| `modscr.asm` | **High** | BASIC ROM entry points; `$B000` staging | +| `modscrh.asm` | **Highest** | 12 BASIC ROM entry points; BASIC 2.0 ZP ABI | ### The script runner is a BASIC-ABI problem, not a ZP problem @@ -418,20 +418,20 @@ This is the single most-affected subsystem, and it is worse than it first appears. It hardcodes **12 C64 BASIC ROM entry points**, none of which exist at those addresses in BASIC 7.0: -| Addr | Symbol | Site | -| --- | --- | --- | +| Addr | Symbol | Site | +| ------- | ----------------------- | ------------------------------------ | | `$A7AE` | `NEWSTT` / `BASIC_RUNC` | `modscr.asm:78`, `modscrh.asm:47,58` | -| `$A871` | `RUNC` | `modscr.asm:79` | -| `$A659` | `CLR` | `modscr.asm:80`, `modscrh.asm:57` | -| `$A533` | `RELINK` | `modscrh.asm:56` | -| `$A437` | `ERROR` | `modscrh.asm:75` | -| `$A7E7` | `GONE_ORIG` | `modscrh.asm:48` | -| `$B08B` | `PTRGET` | `modscrh.asm:73` | -| `$B79E` | `GETBYT` | `modscrh.asm:50` | -| `$AD9E` | `FRMNUM` | `modscrh.asm:535` | -| `$B1AA` | `AYINT` | `modscrh.asm:536` | -| `$E544` | `CLRSCR` (KERNAL) | `modscr.asm:82` | -| `$E394` | BASIC cold start | `editor.asm:542` | +| `$A871` | `RUNC` | `modscr.asm:79` | +| `$A659` | `CLR` | `modscr.asm:80`, `modscrh.asm:57` | +| `$A533` | `RELINK` | `modscrh.asm:56` | +| `$A437` | `ERROR` | `modscrh.asm:75` | +| `$A7E7` | `GONE_ORIG` | `modscrh.asm:48` | +| `$B08B` | `PTRGET` | `modscrh.asm:73` | +| `$B79E` | `GETBYT` | `modscrh.asm:50` | +| `$AD9E` | `FRMNUM` | `modscrh.asm:535` | +| `$B1AA` | `AYINT` | `modscrh.asm:536` | +| `$E544` | `CLRSCR` (KERNAL) | `modscr.asm:82` | +| `$E394` | BASIC cold start | `editor.asm:542` | Plus the BASIC 2.0 zero-page ABI: `$2B`/`$2D` (TXTTAB/VARTAB), `$37` (MEMSIZ), `$47` (VARPNT), `$7A`/`$7B` (TXTPTR), and `$14`/`$15` as AYINT's big-endian @@ -474,8 +474,8 @@ Mechanically: (every `lda (SRC_PTR),y` moved `$FB`→`$26`, same counts, no stale references, identical binary sizes). 3. **Assert non-overlap at link time** via `__ZP_START__` / `__ZP_SIZE__` and - `lderror`, as described above. *(Not yet done. Small, safe, independent of - the C128 map — can land at any point.)* + `lderror`, as described above. _(Not yet done. Small, safe, independent of + the C128 map — can land at any point.)_ This replaces an earlier plan to make ZP saving universal across all modules. That plan was dropped: it guarded against nothing real and would @@ -483,10 +483,10 @@ Mechanically: #### Files -| File | Role | -| --- | --- | -| `zp.inc` | target dispatch, derived `ZP_PTR0..3`, contract + assertions | -| `zp_c64.inc` | the historical C64 map — `$3A`, `$F7`, `$FF` | +| File | Role | +| ------------- | ------------------------------------------------------------------------------ | +| `zp.inc` | target dispatch, derived `ZP_PTR0..3`, contract + assertions | +| `zp_c64.inc` | the historical C64 map — `$3A`, `$F7`, `$FF` | | `zp_c128.inc` | the C128 map — `$1C`, `$22`, `$2A`, with the derivation and the residual check | Each module now aliases its own local names onto pool slots @@ -575,6 +575,7 @@ allocation throughout: the CBM archive's C128 RAM map, common region (`$0000-$0FFF`) to be bank-visible. Both halves of that need confirming: that `$0B00` is genuinely free in our configuration, and the RCR setting. + 3. **MMU configuration register details** — the CR bit assignments tabulated under [Banking](#mmu-configuration-register), and that `$0E` is the right whole-session config value (RAM `$0000-$BFFF`, I/O, KERNAL ROM). The