diff --git a/docs/c128-port-notes.md b/docs/c128-port-notes.md new file mode 100644 index 0000000..e7ed865 --- /dev/null +++ b/docs/c128-port-notes.md @@ -0,0 +1,592 @@ +# 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/make_disk.py b/make_disk.py old mode 100644 new mode 100755 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/tests/test_include.py b/tests/test_include.py old mode 100644 new mode 100755 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