Running log of work on the Tetris (GB) → Watara Supervision port. Newest entries at the top. Every entry should be traceable to ROM bytes/code.
- Confirm ROM identity (header decode, checksums) — docs/01
- Disassembly toolchain (RGBDS 1.0.1 + cc65 2.18 + mgbdis 3.0, inherited from SML)
- Reassembling disassembly, byte-identical rebuild ✓ SHA1 74591cc9… — docs/02
- [~] Phase 2: jump tables recovered (state table + serial table); boot/menu/gameplay engine RE'd and capture-verified (docs/03); 2P link, rocket ending, B-type win, sound engine still to do
- Phase 3: extraction scripts (tiles, tilemaps, tables, music, sfx) — tools/extract_*.py
- Phase 4: Supervision hardware reference (docs/20, from the SML port)
- [~] Phase 5: 65C02 port — single-player game complete and verified against the GB (demo lock-step, forced endings/hi-score paths, pixel-exact VRAM, music A + title note-verified, SFX ids); music: all 17 tracks × 4 channels machine-verified, the wave channel on the DMA voice; runs on real hardware (2026-09-19); 2P link out of scope
- Port's own boot splash (ELEFAS-RETRODEV, as in the SML port) — docs/23
- User flashed
SUPERPICO-TETRIS-CHEAT.uf2(the 128 KB image, stock SuperPico firmware): graphics and controls fine; music and sound play, including the DMA bass voice (first time on a console) — "interesting" through the unit's old speaker; the owner's call: no further work. - State at close: single-player game complete and verified against the original (logic pass by pass, music on all four channels, effects, endings by playtest). Not ported: 2-player link. Known limits: docs/21 §4a and §7.
- User: A-type — all three rocket endings render fine; B-type — dancers fine, shuttle launch fine.
- User screenshots: some dancers' halves swapped. The blitter ignored the OAM flip bits while
the metasprite engine mirrors tile positions →
flip_tile(X/Y). The GB side-by-side then showed the jumper white-bodied on the GB: OBP1 ($C4: colour 2 → opaque white) →blit_obp1. svflick/svpix model both. Shuttle launch confirmed "almost flicker free".
- User played B-type 9-5 to the end: "dancers and the spaceship flicker".
tools/svflick.py(VRAM at frame end vs the state it was rendered from): dancers flickered on 70 % of frames and ran 25 % slow. Sprite pass rewritten (docs/21 §4a): incremental, GB priority order, per-sprite repaint → dancers 0.1 %, GB length; shuttle ascent ~20 % partial. - The side-by-side with the GB showed a REAL bug the memory comparisons had reported and I
had glossed over ("shadow map: 122 cells differ"): state
$22copied the dancers' stage from bank 2 withoutcold_on— the 9-5 victory scene kept the playfield as background. Fixed; audited every bank-2 copy site.passcmpover the whole 9-5 sequence (win → dancers → shuttle → ascent): 3170/3175 passes identical, the rest the random preview piece. make cheat: playtest ROM (paused + SELECT: B lines − 1 / A score + 10000).
- Pitch wobble (
$6D67) ported exactly: note table holds the GB word,gbx_to_fconverts at run time (all 2048 x tested), verifier square tolerance 0, zero same-frame pitch differences. verify_music.py --sfx: effects fired mid-track on both machines. Found: the stream player played a delay of d as d+1 frames (every effect slow, sweeps at half speed, since day one); the 4-line effect took the melody square (GB: ch1 then a wave sweep, ch2 untouched); interrupted notes revived at the effect's pitch (GB: silent until the next note-on); wave effects must gate all four music channels. All fixed; 9 effects over two tracks match.- Cursor blink: the one-frame offset was a sampling artefact — PyBoy ticks end mid-pass.
svcmpsamples the GB at the end of its pass now; every report is exactly OK (no more ±1 timer notes). A dense run (every frame, copyright → menus → game) then found one real difference: the menu exits (A/START/B) un-hide the cursor and redraw the sprites in the same pass on the GB; the port drew a frame late and, from the music screen, un-hid the wrong cursor. Fixed: 554/590 frames exact, the rest are screen-load frames (ambiguous sampling). tools/lrwav.c: headless libretro frontend → WAV (Gambatte reference captures).
- Tools: button chords in scripts (
DS4),tools/gbscript.py(GB-only state trace for writing scripts),svcmpapplies inputs/pokes by logical frame (the wall-clock script reached the port 1–2 frames off after screen loads: constantfall_ctroffsets), and compares the heart flag and the playing music track. - Verified in lock-step: Down+Start heart mode (flag
$88, heart tile by the level, gravity level+10, heart letter$27in name entry), game-type/music menu navigation with the track changes (A/B/C/off), music B through pause, top-out, game over jingle and back to the menu, B-type level/height selection, forced B-type win → dancers → tally → B hi-score entry (level 2/height 1 and level 6/height 5). - BUG 1 (B-type, any height): the garbage generator's end test compared the mirror pointer's
high nibble with the GB's (
$9Avs$10), so it never stopped at the bottom row and ran on through the$9C00map mirror and the hi-score tables. - BUG 2 (B-type garbage content): the DIV stand-in advanced by fixed steps, which froze the
parity that decides block/hole. Replaced by a virtual DIV advanced by the GB's exact
T-cycle cost between reads, in both generators (docs/21 §7).
tools/verify_rng.py: 12/12 GB garbage fields reproduced cell for cell, 45/45 piece candidates.
tools/verify_music.py: every track on both machines (PyBoy APU-write hooks vs the port under py65 with a DMA + done-IRQ model), per-frame(F, vol)per channel. Final: 17/17 tracks, 4/4 channels, 0 mismatch frames.- Found and fixed on the way (all traced to
$6C5E-$6D39, docs/13): the SFX hook-site finder skipped theNRx2 := $08silence writes (recursive-descent decoder now); ch3 level from the instrument's 3rd byte + the 6-ticks fade to 50 %; instruments persist across songs (dance4 inherits the B-win jingle's); only$A0-$AFare lengths; the byte after a length is always a note; notes are byte-addressed (misaligned words for odd bytes; 256-entry note table). - The GB wave channel now plays on the SV DMA-audio channel:
tools/extract_wave.pyrenders whole-period buffers of the game's own wave patterns (150 buffers, ~36 KB, banks 3–6 of a 128 KB image),mus_ch3ttriggers them and the DMA-done IRQ loops them. Proven on the Potator core (irq_count,svshotnow pulls audio per frame). - Layout: 128 KB, FIXED = last 16 KB (cfg, harnesses, firmware script updated).
src/splash.inc: the SML port's power-on splash, ported to the Tetris port's primitives (FIXED bank, no relocation needed; in-game font,-=$25). Called fromresetbeforesoft_reset, so power-on only. Verified: Potator frames (slide, meeting, settled tag), py65 trace of the square-1 writes (C-E-G-C at 4-frame spacing, silence, registers zeroed), andsvcmpon the demo identical to a no-splash baseline.svcmpport run extended by 130 frames. Shipped~/Desktop/tetris_sv3.sv+ rebuiltSUPERPICO-TETRIS.uf2.
svcmp --poke(RAM pokes applied to both machines at the same script frame) drives the paths a scripted demo never reaches: a top-out with a 120000 score (rows of 9 cells poked into the playfield) → GAME OVER → rocket ending → level select → name entry; a B-type win (lines := 1 during the B demo) → dancers → win screen → tally → wait. Both chains now match the GB state for state and the screens are pixel-exact (svpix on the dumps).- Bugs found and fixed on the way: the VBlank row chain's address loop never
decremented its counter (hang at task 3 after any line clear); the collapse
loop dropped the source pointer's high byte; two screen loads copied from the
wrong bank (win screen, GAME OVER box); the ending map fill loaded its value
before the pointer macro clobbered A; the tally's product digits were one cell
off (the GB keeps its cell pointer decremented); the hi-score write loop's
bpl :-bound to an inner anonymous label (only one byte stored); the pointer- decrement helper clobbered the blank tile; sprites 4-px-aligned but not 8-px aligned (the rockets) left a stale column; sprites above the GB's top edge are now clipped instead of painting the Supervision's extra band. - Anonymous-label discipline: a backward
:-must never have an inner:between it and the loop head; a scan in tools/ checks the sources.
src/audio.inc: the SML 65C02 sequencer + stream player adapted (per-frame ticks, one data blob with the drum table at MUS_DRUMS, TWO SFX slots because the GB drives $DFE0 and $DFF8 effects independently, the GB's SFX priority gates $6583..$6595, pause = driver freeze + the three pips). Mailboxes keep their GB addresses;sound_update= SoundUpdate_64D3's order.- Title music verified at the note level: py65 register log of the port vs a PyBoy hook capture of the GB — same pitches (SV F ↔ GB x), same 12-frame cadence, same rest/gap structure.
- extract_sfx: a PyBoy hook on a byte INSIDE a
jp zoperand ($6835) made the GB's wave-2 handler run forever; hook sites now skip operand bytes (all 14 effects capture cleanly, 1.4 KB). - ROM layout: [BANK0 = audio 7K + menu/game tiles + demo script][BANK1 = audio
- ending tiles][BANK2 = audio + cold tilemaps][FIXED = code + sprite/table
data]; screen loads map bank 2 (
cold_on) then the tile bank.
- ending tiles][BANK2 = audio + cold tilemaps][FIXED = code + sprite/table
data]; screen loads map bank 2 (
- svcmp/svpix/demo all still green after the re-layout.
src/: main.s (boot per SML real-HW laws, NMI frame loop, GB-layout input, state dispatch), render.inc (BG-map mirrors + dirty-cell blitter, masked sprites with OBJ priority, unrolled blits ~250 cyc/cell), sprites.inc (the $2A89 metasprite engine), states.inc/states2.inc/endings.inc (all 1P states), engine.inc (game init, gameplay, RNG, DAS, lock, lines, collapse, scoring, level-up), hiscore.inc, vblank.inc (row-copy chain side jobs, BCD drawing).- RAM map = GB addresses minus fixed offsets (docs/21 §2) so
tools/svcmp.pydiffs the two machines mechanically: same input script on PyBoy and on the real Potator core (build/svshot), aligned by logical frame (frame counter), comparing HRAM vars, objects, OAM, shadow map and the visible BG map. - Result: menus and the whole A-type DEMO (title → StartDemo → game init → ~1400 frames of scripted play) match cell-for-cell; the only residual is a one-frame snapshot-phase offset of the counters (PyBoy ticks land mid- iteration), verified with tools/svtrace.py (py65 tracer with injected NMIs).
- LCD-off screen loads: the GB loses iterations while the LCD is off; the port
idles the measured count (
lcd_stall: 3 for tile loads, 1 map-only). tools/svpix.py= pixel-exact check of the port's VRAM against its own map mirror + tile set + shadow OAM (composited in Python): menus and gameplay frames PIXEL-EXACT. It caught the renderer having drawn garbage since the soft-reset path started wiping the port's row/mask tables.- Bugs found by the harness, all fixed: BSS placed over the mirrored OAM,
an include that landed in the wrong bank, a
#<addr+nmacro precedence slip, a mis-readjr nzin state $06 (misaligned first disassembly), the DAS idle value ($17, not 0), a 255-iteration anonymous-label loop.
symbols/tetris.sym: main state table (55 states), serial table, ~100 named routines/tables; bank-0 data islands and the whole$2B64–$3FFF/ bank-1$4000–$64D1data marked. mgbdis lesson: block lengths are hex;.code:Nblocks that cut an instruction emitdb+ misaligned code — labels +.dataonly.- docs/03: boot, main loop, VBlank row-copy chain, state graph (captured:
$07→$08→$0E→$10→$11→$0A→$00→$01→$0D→$04), piece objects + metasprite renderer, RNG (rDIV-seeded, OR-test reject, 2-deep pipeline), gravity table, fall/soft-drop, rotate/move (DAS 23/9), collision via rendered sprite coords, lock, line check, clear animation, collapse, scoring, level-up, hi-scores, name entry, pause, demo record/playback, RAM/HRAM map. - PyBoy (
tools/gbharness.py): screens/tiles matched to ROM regions cell-for-cell; gravity 53 f/row @L0, DAS 23/9, soft drop 3 f, A/B rotation directions — all match the bytes. Save states inbuild/states/.
- ROM:
tetris.gb, 32 KB, ROM-only (no MBC, no RAM), version byte 1, header and global checksums recomputed and matching (unmodified image). - Repo skeleton mirrors the SML port:
tools/(mgbdis + regen.sh),symbols/(tetris.sym, tracked),docs/, gitignore per rule 5. tools/regen.sh→ OK, byte-identical.- First look at the vectors:
rst $28= jump-table dispatch (index in A, LE word table inline after the call); VBlank →$017E; STAT and Timer share$26BE; Serial →$005B(link play). mgbdis mis-decodes the inline tables as code — Phase 2 starts by marking them.