From 0f3e5e4ad26dbec8be3c678fce98d1fc0c98a288 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Fri, 21 Aug 2026 13:53:54 +0200 Subject: [PATCH 1/8] aambox: RAMTOP variable for testing low memory Added RAMTOP optional define to aambox, default is now $c000 to stress test page eviction. --- src/6502/Makefile | 2 +- src/6502/aambox_frontend.s | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/6502/Makefile b/src/6502/Makefile index 4cf0d83..6f9dbe7 100644 --- a/src/6502/Makefile +++ b/src/6502/Makefile @@ -9,7 +9,7 @@ clean: aambox6502: aambox6502.c fake6502.c aambox_frontend.bin: aambox_frontend.s engine.s - xa -o $@ $< + xa -l aambox.labels -o $@ $< -DRAMTOP=49152 c64_frontend.bin: c64_frontend.s engine.s font.bin xa -l labels -o $@ $< -DVERSION=\"$(VERSION)\" diff --git a/src/6502/aambox_frontend.s b/src/6502/aambox_frontend.s index b0e5a63..1f2f355 100644 --- a/src/6502/aambox_frontend.s +++ b/src/6502/aambox_frontend.s @@ -502,6 +502,10 @@ io_readpage #include "engine.s" SAFEPG = (* + $ff) >> 8 +#ifdef RAMTOP +RAMEND = RAMTOP +#else RAMEND = $10000 +#endif SAVEADDR = SAFEPG << 8 From 3e783c8bacc9b2fdefdfc9005a5b23adec67e80f Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 10 Aug 2026 22:20:48 +0200 Subject: [PATCH 2/8] 6502 engine fix: page-cache window overlapped the heap during init The page cache sits directly below the heap, but during init its upper bound is a fixed guess. initsegment sets firstpg = SAFEPG and endpg = SAFEPG + 48 so the init code can page in story data before the heap layout is known; endpg is only tightened to the real heap bottom much later, in initengine5. Everything the init code allocates in between grows the heap downwards: the virtual-address table (freeptr -= vtsize), every allocwords call, and every loadchunk. If freeptr+1 drops below SAFEPG+48 the two regions overlap and corrupt one another. To prevent that, we call 'shrinkcache' when changing heap allocation. It lowers endpg to freeptr+1 and evicts every physical page that falls outside the new window, so nothing stale stays mapped over heap. Two details: - The window is clamped to at least one page (firstpg+1). - If the round-robin cursor itself ended up outside the window, it is pulled back to firstpg. evict doubles as a flag: initsegment zeroes it and skips the eviction pass while it is zero, since at that point the page table does not exist yet and only endpg needs lowering. --- src/6502/engine.s | 79 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/6502/engine.s b/src/6502/engine.s index 0f9eb47..9ef68e0 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -9353,6 +9353,7 @@ loop lda #0 sta ioparam sta ioparam+1 + sta evict ; no page table yet ldx #SAFEPG stx firstpg lda #SAFEPG+48 @@ -9437,6 +9438,8 @@ szloop sta freeptr+1 sta vtmsb + jsr shrinkcache + .( sta vtptr+1 ldy #0 @@ -10312,6 +10315,81 @@ allocwords sbc phydata+1 sta freeptr+1 tay + + jsr shrinkcache + rts + .) + +shrinkcache + ; The page cache lives directly + ; below the heap, so whenever the + ; heap has grown downwards the + ; cache window has to shrink to + ; match. Any page that ends up + ; outside it is marked + ; out-of-core, because the heap + ; is about to be written over it. + ; + ; preserves a, x, y + + .( + pha + lda freeptr+1 + cmp endpg + bcs done ; cache still fits + + txa + pha + tya + pha + + ; keep at least one page, or the + ; round robin in fault would run + ; off the end of the window + + lda freeptr+1 + cmp firstpg + bcs haveroom + + lda firstpg +haveroom + cmp firstpg + bne notlast + + adc #0 ; carry is set +notlast + ldx endpg ; old end + sta endpg + + ldy evict + beq notyet ; no page table yet + + .( +loop + dex + cpx endpg + bcc evdone + + jsr evictx ; preserves x, y + jmp loop +evdone + .) + + ; keep the round robin inside + ; the window + + cpy endpg + bcc notyet + + lda firstpg + sta evict +notyet + pla + tay + pla + tax +done + pla rts .) @@ -10344,6 +10422,7 @@ loadchunk sta freeptr+1 sta phydata+1 + jsr shrinkcache jsr readdatato ldx freeptr From 8d35fe2f8c73275b95c5ed27385b07bbd2e18cfe Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 11 Aug 2026 19:38:42 +0200 Subject: [PATCH 3/8] 6502: keep the savefile image inside the reserved page-cache window savegame builds the savefile image in RAM at SAVEADDR, which lies inside the page cache. It reserves that region by raising firstpg above it, so the round robin is temporarily prevented from handing out the reserved pages. Fixes: * savegame now compares the top of the reserved region against endpg before touching anything and takes the normal failure path; a machine too small to hold both simply cannot save. * We now set evict = firstpg so that the evict cursor does not fall out of the reserved region. * putsavebyte had no upper bound. It walks phytmp forward a page at a time, evicting as it goes, so an image larger than the reserved region ran off the end of the cache and into the heap. It now tests phytmp against endpg, and if out of room it unwinds everything and fails. --- src/6502/engine.s | 49 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/6502/engine.s b/src/6502/engine.s index 9ef68e0..8923db8 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -6120,12 +6120,36 @@ savegame ; buffers. Avoid putting the ; initial state in the part ; that we will overwrite. + ; + ; The cache has to reach past + ; the reserved part, or the + ; round robin in swapin would + ; be left without a window to + ; run in. A machine too small + ; for that cannot save. + + lda #>(SAVEADDR+8192) + cmp endpg + bcc roomok + jmp failure +roomok lda firstpg pha - lda #>(SAVEADDR+4096) + lda #>(SAVEADDR+8192) sta firstpg + ; swapin only consults firstpg + ; where the round robin wraps, + ; so the cursor has to be + ; moved into the new window as + ; well. Otherwise pages would + ; still be handed out below + ; it, in the part we are about + ; to overwrite. + + sta evict + jsr cleanupmem jsr xorinit @@ -6409,11 +6433,31 @@ ok jmp ldyfetchnext .) +savefull + ; The image outgrew the page cache. + ; Drop the return address into savegame, + ; put firstpg back from where savegame + ; pushed it, and unxor the heap before + ; failing. failure resets the stack, so + ; whatever is left below does not matter. + + .( + pla + pla + + pla + sta firstpg + jsr xorinit + jmp failure + .) + putsavebyte ; input a = byte ; input/output phytmp ; (incremented) ; preserves y + ; fails the save if the image would + ; run past the end of the page cache .( ldx #0 @@ -6426,6 +6470,9 @@ wrap inc phytmp+1 ldx phytmp+1 + cpx endpg + bcs savefull + ;jmp evictx .) From 69fae5133dc96c62910b19a65dba5dadb8ca7fd0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 11 Aug 2026 19:58:30 +0200 Subject: [PATCH 4/8] engine: UNDO and SAVERESTORE defines to save interpreter space The undo and save/restore features cost a lot of bytes of engine code, so add conditional defines UNDO and SAVERESTORE to remove them from the engine if needed. The frontend assembler file should define these as 0 (excluded) or 1 (included). This might come in handy if someone wants to try to squeeze an interpreter into 48 kB. --- src/6502/aambox_frontend.s | 2 ++ src/6502/c64_frontend.s | 2 ++ src/6502/engine.s | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/6502/aambox_frontend.s b/src/6502/aambox_frontend.s index 1f2f355..449a9ae 100644 --- a/src/6502/aambox_frontend.s +++ b/src/6502/aambox_frontend.s @@ -10,6 +10,8 @@ TRACE_INST = 0 TRACE_STORE = 0 +UNDO = 1 +SAVERESTORE = 1 DEFWIDTH = 80 PREXTRA = 2 diff --git a/src/6502/c64_frontend.s b/src/6502/c64_frontend.s index 77ada99..d0cf424 100644 --- a/src/6502/c64_frontend.s +++ b/src/6502/c64_frontend.s @@ -20,6 +20,8 @@ TRACE_INST = 0 TRACE_STORE = 0 MEASURE_TIME = 0 +UNDO = 1 +SAVERESTORE = 1 DEFWIDTH = 40 PREXTRA = 8 diff --git a/src/6502/engine.s b/src/6502/engine.s index 8923db8..161487f 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -6111,6 +6111,7 @@ err lda #7 jmp error savegame +#if SAVERESTORE .( sty `pclsb @@ -6394,7 +6395,12 @@ ok jmp refetchnext .) .) +#else // SAVERESTORE + jmp failure +#endif + saveundo +#if UNDO jsr fetchcode sty `pclsb @@ -6431,8 +6437,12 @@ copy jmp failure ok jmp ldyfetchnext +#else // UNDO + jmp failure +#endif .) +#if SAVERESTORE savefull ; The image outgrew the page cache. ; Drop the return address into savegame, @@ -6475,6 +6485,7 @@ wrap ;jmp evictx .) +#endif evictx ; input x = physical page @@ -6500,6 +6511,7 @@ evictx rts .) +#if SAVERESTORE cleanupmem .( lda rtop+0 @@ -6628,6 +6640,7 @@ no4 done3 rts .) +#endif xorinit .( @@ -8048,7 +8061,11 @@ defnull cmp #$40 ; Undo bne cdone +#if UNDO jsr io_undosupp +#else + clc +#endif bcc cdone yes inc result+1 @@ -8111,6 +8128,7 @@ ext0_restart jmp fetchinst ext0_restore +#if SAVERESTORE .( jsr io_load bcc err @@ -8328,9 +8346,13 @@ notregs txt_wronggame .asc "Savefile doesn't match story.",0 .) +#else // SAVERESTORE + jmp failure +#endif ext0_undo .( +#if UNDO lda inbase sta ioparam lda inbase+1 @@ -8356,6 +8378,7 @@ copy err jmp ldyfetchnext fail +#endif // UNDO jmp failure .) From a17d3e501e49093da0e3fc0e467f837f9894cee0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Wed, 12 Aug 2026 12:24:14 +0200 Subject: [PATCH 5/8] engine: puts_xy saves a few bytes Convenience routine to print a string with address in X/Y. --- src/6502/engine.s | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/6502/engine.s b/src/6502/engine.s index 161487f..d287ec5 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -329,6 +329,12 @@ wrap2 jmp postwrap2 .) +puts_xy + .( + stx phydata + sty phydata+1 + ;jmp puts + .) puts ; input phydata = string in ram @@ -3134,11 +3140,9 @@ op_bad ror jsr puthex - lda #text - sta phydata+1 - jsr puts + ldx #text + jsr puts_xy lda `pclsb clc @@ -8187,11 +8191,9 @@ chunkloop jmp fetchinst wronggame - lda #txt_wronggame - sta phydata+1 - jsr puts + ldx #txt_wronggame + jsr puts_xy lda #SPC_AUTO sta rspc jsr vio_line From c099f06c63ba115066bdbad26c3fe971a780405b Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Wed, 12 Aug 2026 12:31:11 +0200 Subject: [PATCH 6/8] engine: define SAVEMAXBYTES for maximum size of savegame file (4096 bytes) The Apple II port needs a fixed-size save game file, so this is no longer an implementation detail. --- src/6502/engine.s | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/6502/engine.s b/src/6502/engine.s index d287ec5..aa8eb69 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -113,6 +113,8 @@ bits = $ff initdata = SAFEPG << 8 +SAVEMAXBYTES = $1000 + HEAPEND = RAMEND-$300 regs = HEAPEND+$000 ; 64 words, b-e @@ -6133,7 +6135,7 @@ savegame ; run in. A machine too small ; for that cannot save. - lda #>(SAVEADDR+8192) + lda #>(SAVEADDR+SAVEMAXBYTES) cmp endpg bcc roomok @@ -6141,7 +6143,7 @@ savegame roomok lda firstpg pha - lda #>(SAVEADDR+8192) + lda #>(SAVEADDR+SAVEMAXBYTES) sta firstpg ; swapin only consults firstpg From ca1a569646c73829aa25d66677576f0236ca45eb Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Mon, 17 Aug 2026 15:38:08 +0200 Subject: [PATCH 7/8] 6502: fix save/restore corrupting the page cache The phypc instruction pointer points to a physical page, and is only protected by the round robin function. If it happened to sit inside of the save game buffer being built, it was left untouched, and so the next instruction read would likely be garbage data. Fixed by moving the PC's page up into the reserved window -- evictx then swapin -- before any of the image is written. --- src/6502/engine.s | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/6502/engine.s b/src/6502/engine.s index aa8eb69..9c03cb5 100644 --- a/src/6502/engine.s +++ b/src/6502/engine.s @@ -6157,6 +6157,38 @@ roomok sta evict + ; The image is about to be written over + ; the bottom of the cache, but phypc is a + ; physical page held outside the page + ; table -- the round robin protects it, + ; nothing else does. fetchcode further + ; down still reads the current instruction + ; through it, so if it sits in the part we + ; overwrite it has to be moved up into the + ; reserved window first. Otherwise the + ; instruction pointer written into the + ; savefile is whatever the image happened + ; to leave behind, and restoring it later + ; runs the engine off into nothing. + + .( + lda `phypc+1 + cmp #>SAVEADDR + bcc safe + + cmp firstpg + bcs safe + + tax + jsr evictx + + ldy pcmsb + ldx pcbank + jsr swapin + sta `phypc+1 +safe + .) + jsr cleanupmem jsr xorinit From 3fd0f0d3f7e2026a0210218820361391b0b951e0 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Sat, 22 Aug 2026 22:21:51 +0200 Subject: [PATCH 8/8] updated readme --- readme.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/readme.txt b/readme.txt index 81bb92b..c4b4996 100644 --- a/readme.txt +++ b/readme.txt @@ -122,6 +122,11 @@ Project website: Release notes: + 1.0.3: + + 6502 engine: Fixed some memory corruption issues, especially + involving save/restore/undo operations. + 1.0.2: Cleaned up the specification and some dev tools.