From b4ddd157105c336932f75bf0a3230b60bc2f9e41 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Thu, 14 May 2026 10:13:07 -0500 Subject: [PATCH] Docs updated. --- README.md | 4 +- SESSION_RECOVERY.md | 711 ++++------------------------------------- port/PORT_64K_AUDIT.md | 26 +- port/PORT_STATUS.md | 461 +++++++++++--------------- 4 files changed, 271 insertions(+), 931 deletions(-) diff --git a/README.md b/README.md index 9379f2d..7bcb1df 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Disassembly and analysis of SubLOGIC's Flight Simulator II (FS2) for the Apple II platform, circa 1984. -Work in progress. The effort is just getting started. +Two halves: +1. **Disassembly** of the original 6502 binary under `src/` (chunks `chunk2.s` .. `chunk5.s`). `make validate` enforces byte-identity against the original FS2 chunks. +2. **C port** in `port/` (SDL2). A from-scratch reimplementation that boots into the Meigs Field scenery, flies the FS2 flight model, renders scenery via a faithful translation of chunk5's bytecode VM, and adds modern affordances (config menu, in-flight editor, joystick, audio). See [`port/PORT_STATUS.md`](port/PORT_STATUS.md) for the per-feature comparison and [`ARCHITECTURE.md`](ARCHITECTURE.md) for the design notes. # Disassembly diff --git a/SESSION_RECOVERY.md b/SESSION_RECOVERY.md index a0bcdc6..eee865e 100644 --- a/SESSION_RECOVERY.md +++ b/SESSION_RECOVERY.md @@ -1,660 +1,81 @@ # FS2 Port Session Recovery -This file tracks active work so the session survives PNG-API context corruption. Update it as work progresses. +This file is the entry point for resuming work after a session break. +Updated 2026-05-14. ## How to recover -1. Read this file (covers current state). -2. Read `~/.claude/projects/-home-scott-claude-flight/memory/MEMORY.md` and the indexed entries. -3. Check `TaskList` for active tasks. -4. Read `port/PORT_STATUS.md` for the broader port state. -## Active tasks (as of last update) - -| ID | Status | Subject | -|----|--------|---------| -| #9 | in_progress | Fix port matrix construction to match MAME's $78..$89 | -| #10 | pending | Investigate missing Sears Tower in port Meigs render | -| #11 | in_progress | Make port's chunk5 dispatcher reach the records MAME renders | - -## Latest session changes (2026-05-07) - -### $42 RefreshCachedXform7EBC + $04 cull now active -- `port/src/sceneryVm.c`: `doRefreshCachedXform` populates the vertex - cache pool ($0140 + idx*8) by transforming the 4-byte stream packet - via `chunk5TransformVertex7EBC`, classifying for outcode (byte 6), - storing $FF in byte 7 (chunk5's $DB-marker so $04 enters the AND - path). $D3..$DB snapshot/restore around the call mirrors chunk5's - L695D/L697D so V2 isn't perturbed for in-flight polygons. -- `doCullByOutcodeList` now actually culls: walks listed indices, - checks cache[idx][7] high bit (chunk5's "vertex behind camera" - flag) and ANDs cache[idx][6] outcode bits. If accumulator stays - non-zero with no on-screen vertex, jump to the cull target. - Otherwise fall through. -- Build confirmed; visual output unchanged because port's dispatcher - doesn't currently reach any $42 or $04 ops at the default Meigs - position. - -### Why the $04 fix didn't change the rendered image -- Port reaches $A800-$B4FE area, hits 128 ops, makes 12 draws. -- MAME's $04 ops live at $B17B / $B1AF / $B1E3. -- Port's dispatcher passes $B171 ($13) but JUMPS to $B18E because - port's $13 cull rejects on Z axis (camera Z=804, ref Z=596, - bound=75 -> |delta|=208 > 75). -- MAME presumably reaches $B17B via a different path -- the - cursor trace at frame 11500 only had 14 entries (too short to see - the dispatcher reach $B17B). -- Port draws and MAME draws have totally different 3D coords, which - means port and MAME enter different polygon records. The cull - decisions diverge somewhere upstream. - -### Screenshot physics-step was drifting camera off Meigs (CRITICAL FIX) -- `runScreenshot` ran 90 physics steps after positioning the aircraft - at Meigs (worldX=96, Y=25, Z=268). With throttle=60% the aircraft - drifted forward ~58m, leaving worldZ=326 by the time - `sceneryAttachCamera` wrote $5C/$64. -- All chunk5 cull tests at $A800 use $5C/$64 (cam X/Z); with the - drifted Z=326 (= scenery units 978), the very first $21 cull at - $AA4D rejected (range [785,825], value 978 = OUTSIDE) -- so port's - dispatcher took a wrong branch and never reached the polygon-draw - ops MAME hits. -- Fix: snapshot worldX/Y/Z before the physics loop, restore after. -- `ac.pitch = 0` (was 256-8 = -8). The -8 default produced a heavily - tilted matrix; MAME's Meigs boot has $6C/$6D=-109 (~-0.6 deg) so - level is closer. - -### $07 SceneryOpEnterLocalFrame variant 2 fixed -- chunk5 L6C6E does not byte-swap scratch[i]; it combines the HIGH - byte of scratch[i-1] with the LOW byte of scratch[i+1] (chunk5: - `ldx $19; ldy $66; stx $66; sty $67`). -- Port's old logic byte-swapped scratch[i], producing wrong $68/$69 - scale -> base Y stayed at 0 -> all polygons drew at the horizon. -- After fix: $68/$69 = 3 at $07 records, base Y = -3. - -### $23 SceneryOpJumpIfBitsClear was a no-op -- chunk5: jump if (mask2 & *(ptr+1) == 0) AND (mask1 & *(ptr+0) == 0). -- Port advance(7) ignored the test, falling through every time -> at - $AB10 port took the no-jump path while MAME jumped to $AB1A. -- Fix: new doJumpIfBitsClear that reads ptr/masks and follows - chunk5's truth table. With this in place port matches MAME's first - 131 dispatch fetches 1:1. - -### MAME logger pollution discovered + lua tap alternative -- Earlier MAME draw-list captures (`tmp/mame_drawlist_long.txt`) - used a 6502-side logger writing to `$B500-$BFFF`, which OVERLAPS - chunk5's bytecode area. Each DrawColorLine clobbered the next - bytecode bytes the dispatcher would read, causing the dispatcher - to terminate early and skewing the captured draw count. -- `tmp/mame_drawlist_clean.lua` and `tmp/mame_drawlist_tap.lua` - attempt to capture via lua-side hooks (debugger breakpoint, read - tap) so RAM stays untouched. The breakpoint approach needs - `-debug` which fails in headless MAME; the read-tap fires - successfully but every entry shows identical V1/V2 values -- - suggesting MAME's FS2 boot is stuck on a single draw early in - the dispatch (= splash/menu, not Meigs flight mode yet). -- Conclusion: the captured 89-entry MAME draw list was an artefact - of the logger pollution; clean captures are blocked by the boot - state never reaching the live Meigs-flight render. Port's actual - 82 unique draws (Hancock antennas + body + ground polygons) is - closer to the true MAME render than the buggy 89-entry capture - suggested. - -### 64K feature audit + draw-list comparison -- **64K patch table audit**: walked chunk5.s line 10159+ (PatchTable - entries). Most hooks are present in port (LookupADFStation, - ApplyWind, ComputeWindComponents, ComputeDayPhase, - HandleCrashOrSplash, RealityModeHook, DrawSlewOverlays, - CoursePlottingMenu, DemoMode64K, altimeter 10K hand, magneto - state, radar view, SceneryLoaderEntry1-7). Missing ones - (ADFKeyboardHook, DrawViewOverlays, UpdateInstrumentLights, - DrawATISMessage, UpdateCOMMessageChunks, etc.) are minor UI - features that don't affect 3D scenery rendering. See - `port/PORT_64K_AUDIT.md` for the full table. -- **MAME draw list at port-equivalent state**: captured 89 total / - 48 unique polygons from MAME (`tmp/mame_drawlist_long.txt`, - via `tmp/mame_drawlist_long.lua`). Port produces 82 unique - draws. MAME's captured state shows ALL polygons at native row 48+ - (= ground polygons), no above-horizon building polygons; port - draws Hancock antennas + body in rows 28-46. The captured MAME - 4-second window may miss the Hancock-rendering frames; the - reference image (`tmp/mame_meigs_ref.png`) may have been taken - during a different frame. - -### Closing parity to MAME (this session) -- Added `pitchFine`/`bankFine`/`yawFine` 8-bit fields to `CameraT` - so chunk5SetupViewProjection sees full 16-bit angle precision - (e.g. -109 in $6C/$6D = -0.6 deg). With these `cam->pitch=$FF`, - `pitchFine=$93` → 16-bit yaw input -109 (matches MAME). -- Added `viewDirection` field to `CameraT` for chunk5's $0A70 input; - default 0. -- `runScreenshot` now sets the camera matrix DIRECTLY to MAME's - captured boot values: row0=(16382,0,0), row1=(0,32760,100), - row2=(0,-401,8190). The patched chunk5 (Apply64KPatchTable + - runtime $25/$1A modifications) produces these slightly different - values from what the source-faithful chunk5SetupViewProjection - computes (32761/85/-339). Port's transliteration matches the - ORIGINAL chunk5 binary (verified via FS2TRACE_USE_ORIG=1 on - fs2trace), so the override is the simplest fix without porting - the entire 64K patch table. -- Final state: port draws 82 unique polygons spanning native rows - 31-55, MAME draws 89 total / 48 unique (= ~2x double-buffer - redraws). Hancock antennas (rows 28-32), tower body zigzag (rows - 33-46), ground polygons (rows 49-55). - -### Per-frame draw list comparison (this session) -- `tmp/mame_drawlist_long.lua`: extended capture script (logger at - $7800, 16-byte entries, indirect-Y store via $FE/$FF, buffer - $B500-$BFFF for 176 entries, resets on $8B==LA7E0). Dumps - `tmp/mame_drawlist_long.txt` (89 draws across one full $A800 - dispatch iteration) and `tmp/capture_drawlist_long.bin` (RAM at - end of iteration). -- Port draws (with all current fixes): 82 draws via SCENERY_DRAW_LIST=1. -- Counts within ~8% (port 82 vs MAME 89). Visible structure now - spans rows 28-75 with Hancock antennas + tower body. -- Direct draw-by-draw comparison is misleading because each side - logs slightly different coordinate spaces: - * MAME's $E9-$EC screen coords use chunk5's full 192-row hires - output (so e.g. row 126 is meaningful below port's - viewport-bottom of 99). - * Port's logger writes Q-format projected screen coords through a - 280x99 viewport with horizon at native row 49. - * MAME's V1 capture is a snapshot of $CB-$D0 at the moment of - DrawColorLine, which often holds the *previous* polyline's clip - state (not the polygon being drawn now). - -### Outstanding matrix discrepancy ($82, $86) -- MAME runtime matrix: $82=100, $86=-401 (= small yaw rotation - encoded by chunk5SetupViewProjection from $6C/$6D=-109). -- Port runtime: $82=0, $86=0 (port's cam->pitch is uint8_t with - resolution 1/256 of a circle; MAME's $6C is 1/65536, finer than - port can represent. cam->pitch=0 -> port's matrix has no yaw - contribution). -- Effect: port's polygons project to native row 49 (horizon), - MAME's to row ~53 (about 4 rows below horizon). Same TOPOLOGY, - different absolute screen-Y. -- To close: change cam->pitch / cam->bank / cam->yaw to int16_t - (= 1/65536 resolution, full 16-bit pitch precision) so cameraUpdate - passes the exact MAME-equivalent angles to chunk5SetupViewProjection. - Big-ish refactor (cam->pitch is read in many places). - -### Cached vertex outcode read was wrong (= bogus polygon culls) -- Port's `$32`/`$33`/`$35` (cached vertex emit ops) loaded - `v.outcode = cv[7]`, but my `$31`/`$42` cache writes set - `cache[7] = $FF` as the chunk5 "outcode-bytes-valid" marker. So - every cached vertex came back with outcode = $FF (= all clip - planes violated), and `(prev.outcode & v2.outcode) != 0` rejected - every $33 line draw. -- chunk5 L68C7 only treats `cv[6]` as the outcode when `cv[7]`'s - high bit is set (flag valid); otherwise the cached vertex is - on-screen and outcode = 0. Fixed all three handlers to use - `(cv[7] & 0x80) ? cv[6] : 0`. -- After fix: 82 draws (was 66). Hancock building body now visible - (draws 69-81 form a zigzag from antenna-top Y=416 down to Y=66). - -### Off-by-one camera X conversion was the actual visual culprit -- `sceneryAttachCamera` converted aircraft worldX (Q16.16 metres) to - scenery units via `wxUnits = (worldX >> 16) * 3` -- truncates to - integer metres before multiplying. With ac.worldX = 96m exact this - produced wxUnits = 288, but MAME's captured Meigs ZP has $5C = 287. -- One unit off cascaded: every $13/$21/$22 cull at the top of the - $A800 chain rejected on a different boundary, port's dispatcher - walked an entirely different code path, and the rendered scene - collapsed to a single horizon line. -- Two-part fix: - 1. Use Q16.16-precision conversion: `wxUnits = (int64)worldX * 3 >> 16`. - 2. Set ac.worldX so the conversion produces exactly 287: - `ac.worldX = ((287 << 16) + 2) / 3` (= ~95.667m). -- After fix: dispatcher reaches 501 ops (was 466), draws 66 polygons - (was 30), and viewport ink now spans native rows 28..75 -- with - visible structure above the horizon (buildings). - -### $31 advance length (8 bytes, not 6) -- chunk5 $31 = SceneryOpRefreshCachedXform80C5 uses xform-A's 6-byte - vertex stream + 1 idx + 1 opcode = 8 bytes total. Earlier I had - $31 sharing $42's 6-byte advance. Fixed: doRefreshCachedXform - takes a `xformA` flag, $31 dispatches with xformA=true and chunk5 - TransformVertex80C5; $42 stays at xformA=false (6-byte record). - -### TransformVertex80C5 now ported (was identical to 7EBC) -- `port/src/chunk5Transform.c`: replaced the bogus - `chunk5TransformVertex80C5 = transformVertexCommon` (= 7EBC) stub - with a real port of chunk5.s line 4576-4707. Reads 6 stream bytes - (XYZ pairs), subtracts `$66/$68/$6A`, auto-scales when |delta_hi| - >= $40, runs all 9 matrix coefficients through `chunk5ScaleC2ByC4` - (chunk4 ZPScale's signed 16-bit multiply), and applies the L8234 - range-check halve. Returns advance count 7 (vs 7EBC's 5). -- `port/src/sceneryVm.c`: `doEmitV1` and `doEmitV2` take a `xformA` - bool. $00/$01/$02 dispatch with `xformA=true` (7-byte record, - TransformVertex80C5 path); $40/$41 with `xformA=false` (5-byte - record, TransformVertex7EBC path). Without this, $00/$01/$02 read - 4 bytes instead of 6 and lost the per-vertex Y entirely -- the - single $01 in port's trace at $B50B mis-advanced. - -### $07/$24 frame setup now mirrors L6BB0 + variant dispatch -- The previous port did C-level int subtraction across all 6 axes - and treated variants 0/2/4 with simplified bit-shuffles. chunk5's - L6BB0 actually uses an 8-bit SBC chain with carry propagating - across all axis pairs, then dispatches on `variant - 2` to - L6C53/L6CCE/L6C6E/L6C89/L6D28. chunk5 also retains scratch slots - ($18/$19, $1B/$1C, $1E/$1F) across calls -- $07 (no stash) writes - them, $24 (with $AD set) leaves them alone. -- Port's new `doFrameSetup` does byte-for-byte `scenerySbc8` with a - carry chain matching chunk5's `sec`-at-top-of-axis-group pattern, - uses real ZP slots in the RAM image so cross-call state survives, - and implements variants 0 (L6CCE 4x asl/rol cascade), 2 (L6C6E - byte combine), and 6 (L6C89 cascade with scratch hi byte). - $07/$24 are thin wrappers on top. -- After fix: port's $07/$24 produce non-zero Y bases. Polygons now - emit with V.Y in [-254, 0] (was always 0). Visible polygon ink - now spans 3 native rows (49, 50, 51) -- still a horizon smear, - but no longer a single line. - -### Why polygons still cluster near the horizon -- chunk5's vertex stream encodes only X/Z for $40/$41 (xform-B); - port's path is dominated by $40/$41. Y comes solely from the - section base set by $07/$24, which for the records port reaches - has very small Y2 anchors (`cursor[8..9]=$00 $00` on every $07 - port hits). With pitch=0 and small altitude delta the L631D - output base_Y is in the single digits. -- For Sears Tower / Hancock to render, the dispatcher needs to - reach $07 records with significantly non-zero altitude anchors, - OR $00/$01/$02 emits where Y comes from the stream. Port's - current path through ~330 ops doesn't hit either. -- MAME's RAM dump at frame 12500 has $B500-$B5FF rewritten with a - table of 16-bit cursor addresses that port's static RAM doesn't - contain. Some opcode in MAME's dispatch is mutating $B500+; we - haven't found which yet. - -### $24 PushOriginWithStash now updates frame state (FIXED) -- chunk5 $24 calls L6BB0 with $AD set, reading 6 stream bytes - (cam_X - sX, cam_Y - sY, cam_Z - sZ via $5C/$60/$64) into $66/$68/$6A - and dispatches on the variant byte before falling through to L631D - (recompute base). -- Port's $24 was `advance(state, 8)` -- correct length but no frame - setup, so subsequent vertex transforms used the previous frame's - $66-$6B / $4A-$52. -- Fix: new `doPushOriginWithStash` reads 7 stream bytes (variant + - 3x16-bit anchors), computes deltas vs cam, applies variant 0/2/4 - scaling, writes $66-$6B, calls `sceneryComputeBaseL631D`. Variant 6 - (the only one observed in current data) takes the default path. -- After fix: port produces 13 draws (was 12) at default Meigs. - -### $31 advance was 2 bytes; should be 6 (FIXED) -- chunk5 $31 = SceneryOpRefreshCachedXform80C5 (same shape as $42: - 6-byte record = opcode + idx + 4-byte vertex packet). -- Port's enum mis-named it SCENERY_OP_L6947 with `advance(state, 2)`. - Fix: renamed to SCENERY_OP_REFRESH_LO and dispatch via - `doRefreshCachedXform` (same handler as $42). -- After fix: port's dispatcher advances correctly past $B4FC ($31) - to $B502 ($2B) -> $B50B ($01) -> $B510 (terminator $AA). -- Without the fix: port advanced 2 bytes from $B4FC to $B4FE, found - the $F5 byte (= part of the $31 record's payload), interpreted it - as a stream-end terminator. Lost the next two ops ($2B and $01) - and any subsequent reachable polygons. - -### Why the visible output still doesn't match MAME -- Port and MAME use different RAM dumps: - * `port/sceneryRam_FS2.1.bin` = clean boot state (matches - `tmp/capture_boot.bin` byte-for-byte at $A800-$BFFF). - * `tmp/capture_drawlist.bin` = mid-flight state with 365 byte - differences in $A800-$BFFF (chunk5's $25/$1A writes during - earlier frames mutated the bytecode). -- Port starts dispatch at LA7E0 = $A800; MAME's frame-11500 dispatch - began with $8B = $BC55 (mid-stream from previous frames). -- $BC55 polygons are reached from $A442 ($20 cull-jump), $A442 itself - from $A43C ($31 fall-through). Port's dispatcher path through - $A800-$B510 never reaches $A4XX. -- Substituting `capture_drawlist.bin` for port's RAM produces 0 draws - (24 vertices behind camera) -- the matrix/base differs from what - the mutated bytecode expects. - -### Next investigation step -- Capture a deeper MAME cursor trace across multiple frames to see - the FULL dispatcher walk from `$A800` reset onward. Frame 11500 - had only 14 fetches because chunk5 was already mid-stream. -- Run port's dispatcher with op-trace and compare opcode-by-opcode - against the MAME trace, finding the first cursor divergence. -- Likely candidates: a $13/$20/$21/$22 cull where port reads a - different value from $5C-$65 than MAME, or an opcode whose advance - count is still wrong. - - -Closed in this session: -- #1 Compare port pipeline vs MAME without RAM cheat (verified: port runs without cheat env vars) -- #2 Diff port-computed rotation matrix vs MAME $79..$8A (matrix matches when using MAME's via USE_RAM_STATE; port's own diverges) -- #3 Diff port L631D base vs MAME (port impl byte-faithful to chunk5 L6363; runtime $4A clobbered before snapshot) -- #4 HEADER demand-load section payload from .SD (added zero-skip guard) -- #5 Port matrix L6301 col shifts (applied to both pipeline + RAM mirror) -- #6 All-vertices-collapsed regression (was correct interpretation of zero-byte garbage; resolved by #4) -- #7 Render Meigs Field via FS2.1_chicago (initial wrong claim; corrected via #8) -- #8 Capture MAME Meigs state for port comparison (working pipeline produced) - -## Key facts established this session - -### FS2 boot view IS Meigs Field (not WW1) -- MAME at boot frame 13000 (`tmp/capture_boot.bin`) shows Meigs: Sears Tower visible, water/ground horizon. -- ZP state: `$5C/$5D=287` (camX east), `$64/$65=804` (camY north), `$60/$61=0` (alt), `$6C/$6D=-109` (yaw $FF93). -- Default `aircraftInit` worldX=96m, worldZ=268m matches via *3 scenery-units conversion. -- Prior memory's "WW1 training field" claim was wrong; corrected in `project_fs2port_radios.md`. - -### MAME capture pipeline (working) -- Script: `tmp/mame_capture.lua` -- boots FS2, optionally pokes ZP, dumps RAM/ZP/screenshot. -- Critical: must use `-video none` (not `-window`) for headless. With `-window` and no DISPLAY, MAME runs at <10fps. -- Disk: `downloads/scenery/fs2.dsk` (140KB 5.25" floppy). The 2MB san-inc `.po` needs a smartport HD card MAME lacks firmware for. -- Working invocation: - ``` - cd /home/scott/claude/flight/port && \ - MAME_TAG=boot MAME_OUT_DIR=$PWD/../tmp \ - timeout 90 mame apple2gs \ - -flop1 ../downloads/scenery/fs2.dsk \ - -nat -nothrottle -sound none -video none \ - -autoboot_script ../tmp/mame_capture.lua \ - -seconds_to_run 220 - ``` -- Snapshots land in `~/.mame/snap/apple2gs/NNNN.png`. - -### Port-vs-MAME comparison @ Meigs boot -- MAME: `tmp/mame_boot.png` (= `tmp/mame_meigs_ref.png`). -- Port without RAM cheat: 51-56 draws, completely different geometry from MAME. -- Port with `SCENERY_USE_RAM_STATE` (= using MAME's matrix/base verbatim): 93 draws, ground structures appear -- but **Sears Tower still missing**. -- Side-by-side: `tmp/compare_mame_vs_port_ramstate.png`. -- Port command for the comparison run: - ``` - cd /home/scott/claude/flight/port - cp sceneryRam_FS2.1.bin sceneryRam_FS2.1.bin.bak - cp ../tmp/capture_boot.bin sceneryRam_FS2.1.bin - SCENERY_STATS=1 SCENERY_USE_RAM_STATE=1 \ - SCENERY_FORCE_X=96 SCENERY_FORCE_Y=0 SCENERY_FORCE_Z=268 SCENERY_FORCE_YAW=245 \ - bin/fs2port --screenshot screenshots/match_mame_ramstate.ppm - cp sceneryRam_FS2.1.bin.bak sceneryRam_FS2.1.bin - rm sceneryRam_FS2.1.bin.bak - ``` - -### MAME ground-truth state (frame 13000, Meigs view) -From `tmp/capture_boot.zp`: -- LA7E0 dispatcher entry: $A800 -- camX = 287 ($011F), camY (north) = 804 ($0324), camAlt = 0 -- yaw = -109 ($FF93), pitch = 0, bank = 0 -- Matrix at $78..$89 (post-L6301): - - row 0: (16382, 0, 0) - - row 1: (0, 32760, 100) - - row 2: (0, -401, 8190) -- Section base at $4A..$52 (24-bit signed): - - base[0] = -257793 - - base[1] = -138241 - - base[2] = 1396736 -- $66/$67=0, $68/$69=48, $6A/$6B=-3819 (camera-relative section origin) -- **ViewDirection ($0A70) = $0F = 15** at boot (NOT 0 -- earlier recovery - text was wrong). chunk5 SetupViewProjection scales it x16 into a - byte-angle ($3E=$F0=-22.5deg) and feeds it into the yaw/pitch/bank - cascade via L6155. The port's `sceneryAttachCamera` ignores - ViewDirection entirely -- this is the most likely root cause of the - port-vs-MAME matrix mismatch. - -## Code changes landed this session - -### `port/src/sceneryVm.c` - -1. **`sceneryAttachCamera` matrix block** (around line 1255-1322): refactored so chunk5 L6301 column shifts (col 0 >>= 1, col 2 >>= 2) apply to BOTH the int8 pipeline matRow1/matRow2 AND the int16 writableRam mirror at $78..$89, in lockstep. Single source of truth. - -2. **`doHeader` zero-skip guard** (around line 354-380): when `state->sceneryFile` source range is entirely zero (= unused file block in .blocks indirection), skip the copy. Prevents clobbering destination $A84E+ with zero-byte garbage that the interpreter would mistake for $00 vertex_emit ops. - -## Active investigation: #9 — port matrix construction - -### Tooling - -- `port/bin/matrixProbe [wx wy wz]` - runs the port's `sceneryAttachCamera` and dumps `$78..$89`. Build - with `make -C port bin/matrixProbe`. -- `port/bin/fs2trace --matrix ` - runs the original chunk5 `SetupViewProjection` on the in-project - 6502 emulator (the same fs2trace already used for loader tracing), - using `tmp/capture_boot.bin` as the RAM image. Byte-perfect - ground-truth oracle for any (yaw, pitch, bank, VD) input. Build with - `make -C port bin/fs2trace`. -- `tmp/mame_capture.lua` accepts `MAME_POKE_VD` and `MAME_POKE_YPR` - for pinning ViewDirection / attitude angles continuously when - capturing fresh references. - -### Findings (2026-05-07 second session) - -1. **VD doesn't matter at boot.** Re-captured MAME with VD pinned to 0 - (`tmp/capture_boot_vd0.bin`). Matrix at $78..$89 is IDENTICAL to - the VD=15 capture: `[16382,0,0; 0,32760,100; 0,-401,8190]`. The - small off-diagonal terms in MAME do NOT come from ViewDirection. - -2. **chunk5 and port use DIFFERENT Euler conventions.** Verified with - the oracle by sweeping each input to 90 degrees while the others - are zero: - - | ZP slot | chunk5 axis | Port `cam->` field | - |------------------|-----------------------|---------------------| - | $6C/$6D "yaw" | rotation around X | `cam->pitch` | - | $6E/$6F "pitch" | rotation around Z | `cam->bank` | - | $70/$71 "bank" | rotation around Y | `cam->yaw` | - - The disassembly's labels are misleading. chunk5's "yaw" really - tilts up/down (X-axis = standard pitch); chunk5's "bank" really - spins about world up (Y-axis = standard yaw); chunk5's "pitch" - really rolls (Z-axis = standard bank). - -3. **The boot M12=100 / M21=-401 is from chunk5 yaw=$FF93 (-109/16b).** - That value is a tiny X-axis rotation (~0.7deg upward tilt). chunk5 - places the small term in M12/M21 (Y-Z plane). The port treats yaw - as Y-axis rotation and would place the same magnitude in M02/M20 - (X-Z plane). Both matrices are CORRECT for their convention -- - just expressed in different coordinate frames. - -4. **At zero angles** (yaw=pitch=bank=0, VD=0) the oracle and port - matrices match within rounding (essentially identity with the - col 0 >>= 1, col 2 >>= 2 shifts). They diverge only when angles - are non-zero AND map to different axes. - -### MAME draw-list capture findings (2026-05-07 evening session) - -Used MAME lua hooks to install a 6502 logger that JMP-traps -`DrawColorLine` ($795A in patched chunk5) and records each call's -$E9-$EC (screen coords) and $CB-$D9 (V1/V2 3D coords) into a -buffer at $B500. lua dumps the buffer per frame to -`tmp/mame_drawlist.txt`. Same for cursor trajectory hook at $6772 -in `tmp/mame_cursor_trace.txt`. - -What we learned: - -1. **MAME absolutely DOES draw chunk5 polygon scenery at boot.** - Hires page in `tmp/capture_boot.bin` rows 101-130 are rich with - line-pattern bytes — that's the actual scenery. The "MAME doesn't - draw" conclusion from earlier `fs2trace --scenery` was a dead end - caused by fs2trace not emulating Apple IIgs language-card bank - switching for $05 ADF -> chunk3 LookupADFStation calls. - -2. **At Meigs, MAME walks the dispatcher into a section at ~$B294 - and emits ~75 line draws per frame.** Cursor trajectory: - $B294 -> $B504 (one section, lots of $40/$41 vertex emits with - intermixed $13 culls). - -3. **Port wasn't chaining V1 from V2** after $41 emits, so polylines - degenerated into fans. chunk5's `EmitClippedLine` cleanup at - L6B2F overwrites V1 ($C9..$D2 / port: $CB..$D0) with V2's shadow - so the next emit chains correctly. Fixed in `doEmitV2`. - -4. **Port and MAME enter DIFFERENT sections from the outer - dispatcher.** Port hits a $0B JumpRelative at $AB17 -> $BADA; - MAME ends up at $B294. With USE_RAM_STATE (= MAME's exact - matrix + base + camera origin) the 3D vertex coords still don't - match -- port produces values ~5x MAME's magnitudes, suggesting - `chunk5TransformVertex7EBC` (port's C transliteration of the - $7EBC asm) has bugs. - -5. **The captured chunk5 RAM at $7EBC differs from the assembled - source.** Earlier hypothesis: "Apply64KPatchTable relocates - TransformVertex7EBC" -- VERIFIED FALSE. The 64K patch table - has no entry targeting $7EBC or $80C5. The runtime divergence - must come from something else -- likely a `$25 SceneryOpStoreImmWord` - or `$1A SceneryOpWriteWord` in early-boot scenery writing into - the chunk5 code area, OR the captured RAM image was taken from - a savefile / mid-run state where chunk5 had been mutated. - We've since built a bit-perfect `--xform` oracle running the - *source* chunk5 binary via FS2TRACE_USE_ORIG=1; that's the - correct reference for byte-level verification. - -### Remaining work - -- Compare port's vertex transform output to MAME's by feeding both - the SAME vertex bytes + state, then diff intermediate accumulator - values. Use `tmp/mame_drawlist.lua` (V1/V2 capture) as the - reference; instrument port's `chunk5TransformVertex7EBC` to dump - pre/post-multiply state for the same input. -- The discrepancy between port and MAME entry sections probably has - the same root cause -- the port walks a different dispatcher path - because some opcode handler (cull, sub-invoke, or store-imm-word) - diverges from the asm's behavior. - -### Concrete bug reproducer for chunk5TransformVertex7EBC - -`fs2trace --xform [ram.bin]` runs the asm $7EBC routine -on the unpatched chunk5 binary using captured RAM state (everything -except the chunk5 code regions that contain the routine). It overlays -the original chunk5 binary at $6000-$B27F so the asm executes -source-faithfully against MAME's matrix/base/camera. Inputs: - -- vertex bytes at $B28F: `40 B0 08 23 FD` (op $40 + xLo $B0 + xHi $08 - + zLo $23 + zHi $FD) -- state from `tmp/capture_drawlist.bin` (frame 11500 dump): - - matrix: `(16382,0,0 / 0,32760,100 / 0,-401,8190)` - - base ($4A..$4C MID/HI/LO): `D0 CC FF` - - base ($4D..$4F): `90 00 00` - - base ($50..$52): `60 F4 FF` - - camera ($66..$6B): `00 00 04 00 21 01` - -**Bug found and fixed (2026-05-07 night):** `op_l1818` in -`chunk5Transform.c` had **7 shift-add iterations in its main loop**; -chunk4.s `L1818` has only **6** (between labels L183A and L185D), -plus one final lsr+ror at L1864. The extra iteration shifted every -multiply result right by one bit, halving it. After the fix port and -asm produce bit-identical output for the matched test case: -V=(-12033, 160, -3224) for both. Verified by adding step-by-step -intermediate trace to both port and `fs2trace --xform` and walking -through one call. - -**Status post-fix:** chunk5TransformVertex7EBC now byte-identical to -asm for at least one test case. 42 chunk5 line draws produced at -boot Meigs (vs 51 with the bug, but those were wrong-positioned). -Visible scenery still doesn't match MAME because port's chunk5 -dispatcher walks INTO different sections than MAME's -- port enters -$BADA via $0B JumpRelative; MAME enters $B294. Same bytecode, -different cull-test outcomes upstream. That's the next bug to find, -not a multiplier issue. - -### Tooling now available - -- `port/bin/fs2trace --xform [ram.bin]` — runs asm $7EBC - oracle. Use frame-matched RAM state from - `tmp/capture_drawlist.bin` (= dumped by mame_drawlist.lua at the - same frame as the draw list). -- `port/bin/fs2trace --scenery [ram.bin]` — counts DrawColorSpan - calls across one chunk5 ProcessScenery pass. -- `port/bin/fs2trace --matrix yaw pitch bank vd` — already validated - bit-perfect. -- `port/bin/fs2trace --zpscale a b` — already validated bit-perfect. -- `port/bin/fs2trace --l177b a x` — already validated bit-perfect. -- `tmp/mame_drawlist.lua` — captures MAME line draws + V1/V2 3D - coords; also dumps RAM at end of capture frame. Run via - `mame apple2gs ... -autoboot_script tmp/mame_drawlist.lua`. -- `tmp/mame_cursor_trace.lua` — captures dispatcher cursor - trajectory. - -### B1 status (landed 2026-05-07) - -The actual divergence wasn't an Euler-order issue, it was a transpose -convention. chunk5 stores R (camera-to-world) at $78..$89; the port's -`cam->rot` stores R^T (world-to-camera) so its `cameraTransform` can -multiply (dx,dy,dz) directly. Same data, transposed access. - -**Implementation:** -- `CameraT` now carries a sibling `int16_t rotChunk5[3][3]` (R, no - transpose). `cameraUpdate` writes both -- one assignment block per - shape, no extra trig. -- `sceneryAttachCamera` mirrors `cam->rotChunk5` (NOT `cam->rot`) - into `writableRam[$78..$89]`. The renderer's int8 projection rows - (`matRow1/matRow2`) keep coming from `cam->rot` so projection math - is unchanged. -- `cameraTransform` is untouched -- still reads `cam->rot`. - -**Verification (port `matrixProbe` vs chunk5 `fs2trace --matrix`, -clean RAM, all-zero baseline):** - - | Test | Port matrix | chunk5 matrix | Match? | - |------------------------|-------------------------|--------------------------|--------| - | yaw=64 (Y+90 deg) | (0,0,8191/0,32766,0/-16383,0,0) | (0,0,8191/0,32765,0/-16383,0,0) | yes (+/-1) | - | pitch=64 (X+90 deg) | (M11=0, M12=-8192, M21=32767) | (M11=401, M12=-8191, M21=32758, M22=100) | shape yes, residual no | - | bank=64 (Z+90 deg) | (M00=0, M01=-32766, M10=16383) | (M01=-32765, M10=16380, M12=100, M20=-201) | shape yes, residual no | - -**Open sub-issue resolved: bit-perfect chunk5 transliteration landed.** - -The residual was an artifact of comparing port output to a CAPTURED MAME -RAM dump (frame 13000) where chunk5 has been heavily patched at runtime -by Apply64KPatchTable. The patched routine differs from the -chunk5.s source. Source-faithful comparison (port vs unpatched chunk5 -binary running on fs2trace's 6502 sim) is now bit-perfect. - -### Bit-perfect chunk5 SetupViewProjection in C - -`port/src/chunk5Setup.c` is a transliteration of: -- chunk5.s `SetupViewProjection` (lines 203-432) -- the full cascade. -- chunk4.s `ScaleC2ByC4` / `ZPScale` (lines 1565-1744) -- 16-bit - shift-and-add multiply. Bit-perfect against `fs2trace --zpscale` - for arbitrary inputs. -- chunk4.s `L177B` / `L1778` / `L17BC` / `L17DA` / `L17E1` (lines - 1900-2007) -- cos/sin lookup with sub-byte interpolation, including - the special X=$80 midpoint-average path. Bit-perfect against - `fs2trace --l177b` over a 256-case sweep. -- chunk4 cos table (132 bytes from offset $141A in - `out/4_0200-25ff`). - -Validation: `make -C port bin/chunk5SetupTest && bin/chunk5SetupTest`. -All test cases pass. The test driver shells out to `fs2trace` for -oracle values; running `fs2trace --matrix` with `FS2TRACE_USE_ORIG=1` -(load unpatched chunks, not the captured RAM) gives the source- -faithful reference. - -`cameraUpdate` now calls `chunk5SetupViewProjection` to populate -`cam->rotChunk5`; `sceneryAttachCamera` mirrors that into -`writableRam[$78..$89]`. The renderer pipeline still uses the -existing `cam->rot` (= R^T, world-to-camera) for vertex projection. - -The captured-RAM comparison is no longer the right reference -- use -the unpatched chunk5 binary via `FS2TRACE_USE_ORIG=1`. +1. Read this file (current state + ground rules). +2. Read `~/.claude/projects/-home-scott-claude-flight/memory/MEMORY.md` + and follow the indexed entries that are relevant to the current task. +3. Read `port/PORT_STATUS.md` for the per-feature port-vs-original table. +4. Read `port/PORT_64K_AUDIT.md` if working on 64K-mode patch hooks or + the scenery VM's `$03` / `$0E` opcodes. + +## Current state (2026-05-14) + +The port is feature-complete against FS2 for most subsystems. See +`port/PORT_STATUS.md` for the detailed comparison. Highlights: + +- **Rendering**: 36/36 pixel-exact line matches vs MAME at boot Meigs + via a 5-plane frustum line clipper with chunk5ScaleC2ByC4 plane-snap + (see memory `project_fs2port_frustum_clip.md`). Palette-buffer + rendering by default; `SCENERY_NTSC=1` reverts to true HIRES decode. +- **Scenery demand-load**: default-on; `port/src/sceneryVm.c::doHeader` + pulls section bytes from the .SD file on every HEADER opcode. All + FS2.1 region variants share the `FS2.1` .SD source via `sdSourceFile` + in `RegionMetaT`. Set `SCENERY_DEMAND_TRACE=1` to log fires. +- **Instruments**: all FS2 gauges drawn except oil temp/pressure. + VOR2/ADF gauge mode-gating on `ac->adfMode` matches FS2 behaviour. +- **WW1 ace mode**: bullets, bombs, AI fire, damage, drop-with-velocity. +- **Modes**: full edit mode (F7) via `editor.c`; title/config screen + via `title.c`; crash recovery snapshot (Space when crashed). +- **Recent cleanup pass**: dead code removed (`ww1aceDropBomb`, + `Coord16T/VertexT`, `DEG2RAD`, `rendererSwapFillColors`); shared + helpers consolidated into `camera.h` / `font.h` / `fs2math.h`; + 19-branch SCENERY_REGION strcmp chain replaced with table-driven + `sceneryDataRegionFromName`. ## Files NOT to delete -- `tmp/mame_capture.lua` — capture script -- `tmp/capture_boot.bin` / `.zp` — MAME ground-truth state -- `tmp/mame_boot.png` / `mame_meigs_ref.png` — MAME ground-truth screenshot -- `tmp/compare_mame_vs_port_ramstate.png` — side-by-side comparison -- `port/screenshots/match_mame_ramstate.png` — port's best-effort match -- `port/sceneryRam_FS2.1.bin` — original port-side FS2.1 RAM dump (NOT MAME's; do not overwrite) -- `port/sceneryRam_FS2.1_chicago.bin` — original port-side chicago RAM dump +- `tmp/mame_capture.lua` - MAME capture script +- `tmp/capture_boot.bin` / `.zp` - MAME ground-truth state +- `tmp/mame_boot.png` / `mame_meigs_ref.png` - MAME ground-truth screenshot +- `tmp/compare_mame_vs_port_ramstate.png` - side-by-side comparison +- `port/screenshots/match_mame_ramstate.png` - port's best-effort match +- `port/sceneryRam_FS2.1.bin` - original port-side FS2.1 RAM dump (NOT MAME's; do not overwrite) +- `port/sceneryRam_FS2.1_chicago.bin` - original port-side chicago RAM dump -## Remember -- Port lives outside git; don't run git on it. -- Scratch files go in `./tmp/`, not `/tmp/`. -- Screenshots go in `port/screenshots/`. -- The port uses fixed-point math; don't introduce float reinterpretations. +## Ground rules -## NEVER `Read` PNGs (avoids the API context corruption) +- **Port lives outside git** - don't run git on `port/`. The + disassembly side (`src/`, `out/`) IS tracked; `make validate` enforces + byte-identity there. +- **Scratch files go in `./tmp/`** inside the project, NOT `/tmp/`. +- **Screenshots go in `port/screenshots/`**, never `port/` or `/tmp/`. +- **Port uses fixed-point math** - don't introduce float + reinterpretations of 6502 fixed-point values; translate the math + directly. See memory `feedback_port_fixed_point.md`. +- **Byte-identical validation discipline** applies to the disassembly + side: every change to `src/chunk*.s` must keep `make validate` green. + See memory `feedback_byte_identical.md`. -The user views PNGs directly. Claude must NOT use the Read tool on PNGs -- -each multimodal image upload bloats the request and has tripped a recurring -"PNG-API context corruption" failure that nukes the session. +## NEVER `Read` PNGs (avoids API context corruption) + +The user views PNGs directly. Claude must NOT use the Read tool on PNGs; +each multimodal image upload bloats the request and has tripped a +recurring failure that nukes the session. Workflow: -- Compare two images (text report, ASCII heatmap, auto-resizes mismatched scales): +- Compare two images (text report, ASCII heatmap, auto-resizes + mismatched scales): ``` cd /home/scott/claude/flight port/tools/imgDiagnose.sh diff tmp/mame_boot.png port/screenshots/match_mame_ramstate.png --ascii ``` -- Single-image summary (non-black coverage, luminance histogram, horizon-row guess): +- Single-image summary (non-black coverage, luminance histogram, + horizon-row guess): ``` port/tools/imgDiagnose.sh stats tmp/mame_boot.png ``` @@ -662,5 +83,15 @@ Workflow: ImageMagick into a temp PPM in `tmp/` that the C tools read. Tools live at `port/tools/imgDiff.c` / `imgStats.c` and build into `port/bin/` via `make -C port tools` (auto-built on first wrapper run). -- The port already writes PPMs from `--screenshot` -- prefer those over +- The port already writes PPMs from `--screenshot`; prefer those over re-encoding to PNG when possible. + +## Historical archive + +Pre-2026-05-13 session journals documenting the scenery-rendering +breakthroughs (matrix construction, $07 EnterLocalFrame variants, +$23 SceneryOpJumpIfBitsClear, $42 RefreshCachedXform, $04 +CullByOutcodeList, frustum clip, etc.) are now indexed in the memory +system rather than journaled here. Each significant finding has a +dedicated memory note under +`~/.claude/projects/-home-scott-claude-flight/memory/project_fs2port_*.md`. diff --git a/port/PORT_64K_AUDIT.md b/port/PORT_64K_AUDIT.md index 14f029d..35f6444 100644 --- a/port/PORT_64K_AUDIT.md +++ b/port/PORT_64K_AUDIT.md @@ -51,7 +51,7 @@ that are listed in the patch table. None affect 3D scenery rendering. | `ADFKeyboardHook` | missing | Keyboard ADF tuning hotkeys; port handles ADF via in-app UI. | | `RequestADFStationLookup` | missing | Trigger to re-lookup ADF after freq change; port re-resolves on every radiosUpdate. | | `UpdateADFIndicator` | missing | ADF needle update; port already redraws needle each frame from radios state. | -| `DrawViewOverlays` | missing | View-mode text labels ("RIGHT VIEW" etc.); port shows view via gauge changes. | +| `DrawViewOverlays` | ok | `rendererDrawWingOverlay` paints wing-strut / fin / wheel-well shapes for non-forward views. | | `UpdateInstrumentLights` | missing | Night-time gauge backlighting; port renders day-mode gauges only. | | `UpdateEngineWithMagneto` | partial | Engine reacts to magneto in aircraftStep; chunk3's full coupling not modelled. | | `DrawMagnetoStateHook` | partial | Magneto state shown via instruments.c indicator; chunk3's specific draw path absent. | @@ -59,9 +59,9 @@ that are listed in the patch table. None affect 3D scenery rendering. | `SelectRadarViewPatch` / `Select3DViewPatch` | missing | View-mode keyboard handlers; port toggles via menu. | | `HideOrShowInstruments` | missing | Toggle instrument panel; port always shows panel. | | `UpdateCoursePlotter` | partial | Course plotter has data; live frame update not wired. | -| `DrawATISMessage` | missing | ATIS text bulletin overlay. | -| `UpdateCOMMessageChunks` | missing | Scrolling COM radio text. | -| `KeyDecreasePatch` / `KeyIncreasePatch` | missing | 64K-only key behavior tweaks. | +| `DrawATISMessage` | ok | `panelDigits.c` scrolls a synthesized " WIND 270/12 ALT 30.05 RWY 18L TIME hh:00" through an 8-char window beside COM freq. | +| `UpdateCOMMessageChunks` | ok | Same scrolling-window implementation drives both. | +| `KeyDecreasePatch` / `KeyIncreasePatch` | ok | `radiosEnterDigit()` mirrors FS2's per-digit BCD entry; armed via Ctrl+1..4 for NAV1/NAV2/COM1/ADF. | ## What 64K patches DO NOT cover @@ -244,12 +244,14 @@ SDS1 SF Bay) DO contain explicit `$12 02` water-colour polygons and will render proper water through the existing port path once those regions become reachable. -## Demand-load (`SCENERY_DEMAND_LOAD` env var) +## Demand-load (default-on) -`port/src/sceneryVm.c::doHeader` supports loading section bytecode -fresh from the `.SD` file via the ASM-faithful formula -`((sid>>2)+1)*4096 + (sid&3)*256 + skip`, where `skip` comes from -`SCENERY_DEMAND_LOAD` (default off; values 0..64 produce different -source-byte alignment). Disabled by default because the captured RAM -dump's leftover state is currently the only known-working render -path. +`port/src/sceneryVm.c::doHeader` loads section bytecode fresh from the +`.SD` file every HEADER opcode via the ASM-faithful formula +`((sid>>2)+1)*4096 + (sid&3)*256 + skip`. The `SCENERY_DEMAND_LOAD` env +var now controls only an optional source-byte SKIP for offline +experiments (default 0 = natural ASM behaviour). All four FS2.1 region +variants share the same `FS2.1` .SD source via the `sdSourceFile` field +in `RegionMetaT` (`sceneryData.c`); when adding new SCENERY_* regions, +populate that field. Run with `SCENERY_DEMAND_TRACE=1` to log every +fired load. diff --git a/port/PORT_STATUS.md b/port/PORT_STATUS.md index 7d9fd13..a29e4e1 100644 --- a/port/PORT_STATUS.md +++ b/port/PORT_STATUS.md @@ -1,9 +1,9 @@ -# FS2 C Port — Status vs Original +# FS2 C Port - Status vs Original Comparison of the original Apple II FS2 (disassembled in `src/chunk*.s`) -against the C port in `port/`. Generated 2026-05-06. +against the C port in `port/`. Updated 2026-05-14. -Legend: ✅ done · 🟡 partial / approximation · ❌ missing +Legend: ok = done; partial = approximation or limited; missing = not implemented --- @@ -11,328 +11,233 @@ Legend: ✅ done · 🟡 partial / approximation · ❌ missing | Feature | Original | Port | |---|---|---| -| Position integrator (24-bit XYZ) | `IntegratePhysicsStep` | ✅ `aircraftStep` (Q16.16) | -| Pitch / bank / yaw rates | `UpdateAutoTrimAndYaw` etc | ✅ `stepFlight` | -| Auto-coordination (bank → yaw) | `ApplyAutoCoordination` | ✅ `stepFlight` | -| Wind / turbulence | chunk2 `ApplyWind` (64K) | ✅ `windCompute` / `windApply` | -| Stall detection & break | per-instrument check | 🟡 stalled flag, no spin | -| Spin recovery | implicit in stall handling | ❌ | -| High-G / VNE bleed | `CheckFlightEnvelope` | 🟡 `envelopeWarning` flag, no bleed | -| Flap/gear speed effects | `RefreshElevatorIndicator` | ❌ | -| Mixture too-lean = engine cut | implicit | ❌ | -| Carb heat icing | chunk5 `CarbHeat` | 🟡 audio penalty added, no icing model | -| Magneto on/off effect on engine | `UpdateEngineWithMagneto` | 🟡 audio penalty added (no full model) | -| Engine fault dispatch | `FailureProcTable` | ✅ `aircraftStep` reality dispatch | -| Engine knock audio | per-fault sound | ✅ `audioUpdate` wobble | -| Crash detection | `HandleCrashOrSplash` | ✅ `aircraftStep` ground/water | -| Splash detection (water) | `CheckSplash` | 🟡 land-only crash | -| Building / mountain crash | `crash_msg_table` | 🟡 type set but no scenery-aware test | +| Position integrator (24-bit XYZ) | `IntegratePhysicsStep` | ok `aircraftStep` (Q16.16) | +| Pitch / bank / yaw rates | `UpdateAutoTrimAndYaw` etc | ok `stepFlight` | +| Auto-coordination (bank to yaw) | `ApplyAutoCoordination` | ok `stepFlight` | +| Wind / turbulence | chunk2 `ApplyWind` (64K) | ok `windCompute` / `windApply` | +| Stall detection and break | per-instrument check | ok `stalled` flag | +| Spin entry from stall | implicit in stall handling | ok yawRate-driven `spinning` state | +| Spin recovery | opposite rudder + nose-down | ok 30-frame recovery condition | +| High-G / VNE damage | `CheckFlightEnvelope` | ok `loadFactor_q88` + `airframeDamage` accumulator | +| Flap speed drag | `RefreshElevatorIndicator` | ok `forwardSpeed *= (256 - flaps>>3) / 256` | +| Mixture too-lean = engine cut | implicit | partial audio penalty, no full model | +| Carb heat icing | chunk5 `CarbHeat` | partial audio penalty, no icing model | +| Magneto on/off effect on engine | `UpdateEngineWithMagneto` | partial audio penalty | +| Engine fault dispatch | `FailureProcTable` | ok reality-mode dispatch | +| Engine knock audio | per-fault sound | ok `audioUpdate` wobble | +| Crash detection (ground) | `HandleCrashOrSplash` | ok | +| Splash detection (water) | `CheckSplash` | ok via `worldZ` water range | +| Building / mountain crash | `crash_msg_table` | partial type set, no scenery-aware test | ## Modes | Feature | Original | Port | |---|---|---| -| Free flight | default | ✅ | -| Slew mode | `SlewMode` (chunk5) | ✅ `aircraftToggleSlew` | -| Slew digit overlay | `DrawSlewOverlays` | ✅ `instruments.c` north/east/alt | -| Demo mode | `DemoMode64K` | 🟡 `autopilotDemo` (basic) | -| Demo waypoint sequence | per-mode auto-flight | ❌ | -| Edit mode | `EditModeFlag` | 🟡 toggleable, no state save/restore | -| Reality mode | `RealityMode` | ✅ instrument & engine failures | -| Radar view | `RadarView` | ✅ `worldRenderRadar` | -| WW1 ace combat | `WW1AceMode` | ✅ `ww1ace.c` | -| Course Plotter | chunk2 `CoursePlottingMenu` | ✅ `coursePlotter.c` (record/display) | -| Pause | `TogglePause` | ✅ P key | -| Boot DOS | `BootDOS` | ❌ (no DOS to boot) | +| Free flight | default | ok | +| Slew mode | `SlewMode` (chunk5) | ok `aircraftToggleSlew` | +| Slew digit overlay | `DrawSlewOverlays` | ok | +| Demo mode | `DemoMode64K` | ok `autopilotDemo` with 4-waypoint sequence | +| Edit mode | `EditModeFlag` | ok F7 toggle, full field editor (`editor.c`) | +| Reality mode | `RealityMode` | ok instrument and engine failures | +| Radar view | `RadarView` | ok `worldRenderRadar` | +| WW1 ace combat | `WW1AceMode` | ok bullets, bombs, AI fire, damage | +| Course Plotter | chunk2 `CoursePlottingMenu` | ok `coursePlotter.c` (record/display) | +| Pause | `TogglePause` | ok P key | +| Title / config screen | boot menu sequence | ok `title.c` (MODE/REGION/DISPLAY/TIME/REALITY/START/QUIT) | +| Boot DOS | `BootDOS` | missing (no DOS to boot) | ## Instruments | Feature | Original | Port | |---|---|---| -| Airspeed needle | `UpdateAirspeedIndicator` | ✅ `instruments.c::airspeedGauge` | -| Altimeter main hand | `UpdateAltimeterIndicator` | ✅ | -| Altimeter 10K hand | `UpdateAltimeter10K` (64K) | ✅ | -| Attitude indicator | tilted disc | ✅ `drawHorizonDisc` | -| Heading bug | `DrawHeading` | ✅ digit readout | -| Magnetic compass | `DrawMagCompass` | 🟡 digit only, no rotating compass card | -| Vertical speed | `UpdateVerticalSpeedIndicator` | ✅ | -| Turn coordinator | `UpdateTurnCoordinator` | ✅ | -| Slip/skid ball | `PLSlipSkidIndicator` | ✅ | -| Throttle position | `UpdateThrottleIndicator` | ❌ (no graphical needle) | -| Mixture position | `UpdateMixtureControlIndicator` | ❌ | -| Flap position | `UpdateFlapsIndicator` | ❌ | -| Trim position | implicit in auto-trim | ❌ (no key bindings) | -| Fuel tank L/R | `UpdateFuelTankGauges` | ❌ | -| Oil temp/pressure | `UpdateOilTempAndPressureGauges` | ❌ | -| RPM display | `DrawRPM` | ✅ digit | -| Magneto state visual | `DrawMagnetoState` | ✅ MAG OFF/L/R/START indicator | -| Carb heat state | switch position | ✅ "CARB HEAT" indicator | -| Lights state | switch position | ✅ "LIGHTS ON" indicator | -| Failure indicator (X over gauge) | `DrawX` per gauge | ✅ `drawFailX` | -| Stall warning | `STALL` text | ✅ | -| VNE warning | `VNE` text | ✅ | +| Airspeed needle | `UpdateAirspeedIndicator` | ok | +| Altimeter main hand | `UpdateAltimeterIndicator` | ok | +| Altimeter 10K hand | `UpdateAltimeter10K` (64K) | ok | +| Attitude indicator | tilted disc | ok `drawHorizonDisc` | +| Heading bug | `DrawHeading` | ok digit readout | +| Magnetic compass | `DrawMagCompass` | partial digit only | +| Vertical speed | `UpdateVerticalSpeedIndicator` | ok | +| Turn coordinator | `UpdateTurnCoordinator` | ok | +| Slip/skid ball | `PLSlipSkidIndicator` | ok reads `sideslip_q88` | +| Throttle position | `UpdateThrottleIndicator` | ok bar indicator | +| Mixture position | `UpdateMixtureControlIndicator` | ok bar indicator | +| Flap position | `UpdateFlapsIndicator` | ok bar indicator | +| Trim position | implicit in auto-trim | ok HOME/END keys + indicator | +| Fuel tank L/R | `UpdateFuelTankGauges` | ok per-frame burn, alternating tanks | +| Oil temp/pressure | `UpdateOilTempAndPressureGauges` | missing | +| RPM display | `DrawRPM` | ok digit | +| Magneto state visual | `DrawMagnetoState` | ok MAG OFF/L/R/START indicator | +| Carb heat state | switch position | ok "C.H." / "HEAT" indicator | +| Lights state | switch position | ok "1" / "O" indicator | +| Failure indicator (X over gauge) | `DrawX` per gauge | ok `drawFailX` | +| Stall / VNE warning | `STALL` / `VNE` text | ok | +| VOR2 / ADF gauge mode-gating | chunk4 ADFMode flag | ok `ac->adfMode` switches needle/digits | ## Radios / Navigation | Feature | Original | Port | |---|---|---| -| NAV1 frequency | `NAV1` ($08F7) | ✅ `radios.c` | -| NAV2 frequency | `NAV2` ($08F5) | ✅ | -| ADF frequency | `ADFFreq*` | ✅ | -| COM1 frequency | str_com1 | ✅ | -| Station database (deduped) | per-region | ✅ 695 entries from extractstations | -| BCD frequency increment | step keys | ✅ shift+digit / digit | -| BCD per-digit entry | `KeyDecreasePatch` | ❌ | -| OBS course knob | OBS-related | ✅ `,`/`.` keys | -| VOR CDI needle | `DrawVOR1IndicatorChanges` | ✅ | -| VOR TO/FROM flag | `msg_vor_flags` | ✅ "TO"/"FR"/"OFF" | -| ILS glide slope | not in original | ❌ | -| DME readout | `DrawATISMessage` ATIS bound | ✅ | -| ADF needle | `DrawADFPanel` | ✅ | -| ADF heading digits | `DrawADFHeadingDigits` | ✅ | -| ATIS message | `UpdateCOMMessageChunks` | 🟡 freq shown, no chunked text | -| Tune-to-nearest button | not in original | ✅ T key (port-only) | +| NAV1 frequency | `NAV1` ($08F7) | ok `radios.c` | +| NAV2 frequency | `NAV2` ($08F5) | ok | +| ADF frequency | `ADFFreq*` | ok | +| COM1 frequency | `str_com1` | ok | +| Station database (deduped) | per-region | ok 695 entries from extractstations | +| BCD frequency increment | step keys | ok shift+digit / digit | +| BCD per-digit entry | `KeyDecreasePatch` | ok Ctrl+1..4 arms input, digit keys append | +| OBS course knob | OBS-related | ok `,` `.` keys | +| VOR CDI needle | `DrawVOR1IndicatorChanges` | ok | +| VOR TO/FROM flag | `msg_vor_flags` | ok "TO" / "FR" / "OFF" | +| ILS glide slope | not in original | missing | +| DME readout | `DrawATISMessage` ATIS bound | ok | +| ADF needle | `DrawADFPanel` | ok gated on `ac->adfMode` | +| ADF heading digits | `DrawADFHeadingDigits` | ok | +| ATIS chunked text | `UpdateCOMMessageChunks` | ok 8-char scrolling window | +| Tune-to-nearest button | not in original | ok T key (port-only) | ## Scenery system | Feature | Original | Port | |---|---|---| -| Disk loader (`SceneryReadUntilC0`) | chunk3 `SceneryLoaderEntry1` | 🟡 RAM dump pre-load + .SD demand-load | -| Block-list indirection | chunk4 `ComputeBlockFromSector` | ✅ `doHeader` correct mapping | -| Nibble decode | `SceneryNibbleDecode` | ❌ (only used by Entry4 path) | -| HEADER opcode ($0D) | `SceneryOpHeader` + `LA63A` | ✅ `doHeader` (with cache) | -| L631D section base | `L631D` | ✅ `sceneryComputeBaseL631D` | -| EnterLocalFrame ($07) | `SceneryOpEnterLocalFrame` | 🟡 simplified passthrough | -| Vertex emit + transform ($00-$02, $40-$42) | `SceneryOpEmitV*` | ✅ | -| Cull ($20/$21/$22) | `SceneryOpCullIfOutside*` | ✅ `doCullN` | -| Cull by outcode list ($04) | `SceneryOpCullByOutcodeList` | 🟡 walks list, no actual cull | -| Jump-if-beyond-XY/XYZ ($13/$14) | `SceneryOpJumpIfBeyondXY*` | ✅ | -| REL_JUMP ($0B) | `SceneryOpJumpRelative` | ✅ | -| SUB_INVOKE ($18) / RETURN ($19) | `SceneryOpSubInvoke` | ✅ | -| RESET_STATE ($2F) | `SceneryOpResetState` | ✅ | -| MODE_WHITE ($1B) | line-kernel patch | ✅ semantic equivalent | -| DAY_ONLY ($1C) | line-kernel patch | ✅ skip-on-night flag | -| WriteWord ($1A) / StoreImmWord ($25) | self-mod patches | ✅ | -| Vertex-cache ops ($31/$32/$33/$35/$42) | cached vertex pool at $0140 | ✅ pool reads, no full $31/$42 transform | -| ADF/NAV/COM record ($05/$1D/$1E) | station records | ✅ | -| SET_COLOR ($12) | `SceneryOpSetColor` | ✅ | -| Polygon edge emit | `EmitClippedLine` | 🟡 line draw only, no polygon close | -| Polygon scanline fill | `DrawColorSpan` etc | 🟡 2D scanline edge-intercept (`rendererFillPolygon`); not chunk5's 3D-clipped scanline emitter | -| Polygon 4-pass 3D clipper | `PolygonScanFillSetup` + Top/Right/Bottom passes | ✅ `sceneryClipPolygon3D` (source-faithful Sutherland-Hodgman against Z-X / Z-Y / X+Z / Y+Z planes; ping-pongs PrimVerts↔SecVerts; produces expanded wedge polygons that span the frustum). `PORT_LEGACY_POLY_FILL=1` reverts to 2D-only fill. | -| Sky/ground tilted fill | `FlipPagesFillViewport` | ✅ `rendererFillTiltedSkyGround` | -| Frustum clipping (3D) | `ClipBothVerticesToFrustum` | ✅ outcode + perspective divide | -| Vertex pool / EmitPrimaryVertex | $0AB8 column array | 🟡 small pool, no polygon closure | +| Disk loader (`SceneryReadUntilC0`) | chunk3 `SceneryLoaderEntry1` | ok RAM dump + .SD demand-load (default-on) | +| Block-list indirection | chunk4 `ComputeBlockFromSector` | ok `doHeader` correct mapping | +| Nibble decode | `SceneryNibbleDecode` | missing (only used by Entry4 path) | +| HEADER opcode ($0D) | `SceneryOpHeader` + `LA63A` | ok `doHeader` with cache, default-on demand-load | +| L631D section base | `L631D` | ok `sceneryComputeBaseL631D` | +| EnterLocalFrame ($07) | `SceneryOpEnterLocalFrame` | partial simplified passthrough | +| Vertex emit + transform ($00-$02, $40-$42) | `SceneryOpEmitV*` | ok | +| Cull ($20/$21/$22) | `SceneryOpCullIfOutside*` | ok `doCullN` | +| Cull by outcode list ($04) | `SceneryOpCullByOutcodeList` | partial walks list, no actual cull | +| Jump-if-beyond-XY/XYZ ($13/$14) | `SceneryOpJumpIfBeyondXY*` | ok | +| REL_JUMP ($0B) | `SceneryOpJumpRelative` | ok | +| SUB_INVOKE ($18) / RETURN ($19) | `SceneryOpSubInvoke` | ok | +| RESET_STATE ($2F) | `SceneryOpResetState` | ok | +| MODE_WHITE ($1B) | line-kernel patch | ok semantic equivalent | +| DAY_ONLY ($1C) | line-kernel patch | ok skip-on-night flag | +| WriteWord ($1A) / StoreImmWord ($25) | self-mod patches | ok | +| Curve emit ($2B) | `SceneryOpEmitCurve` | ok via 6502 interpreter on MAME-patched RAM | +| Vertex-cache ops ($31/$32/$33/$35/$42) | cached vertex pool at $0140 | ok pool reads | +| ADF/NAV/COM record ($05/$1D/$1E) | station records | ok | +| SET_COLOR ($12) | `SceneryOpSetColor` | ok | +| Polygon edge emit | `EmitClippedLine` | partial line draw only, no polygon close | +| Polygon scanline fill | `DrawColorSpan` etc | partial 2D scanline edge-intercept; not chunk5's 3D-clipped emitter (FS2 boot Meigs is line-only) | +| Polygon 4-pass 3D clipper | `PolygonScanFillSetup` | ok `sceneryClipPolygon3D` (Sutherland-Hodgman against Z-X / Z-Y / X+Z / Y+Z) | +| Sky/ground tilted fill | `FlipPagesFillViewport` | ok `rendererFillTiltedSkyGround` | +| Frustum line clip (5-plane) | `ClipBothVerticesToFrustum` | ok full-frustum clipper, plane-snap via `chunk5ScaleC2ByC4`; 36/36 pixel-exact vs MAME | +| Vertex pool / EmitPrimaryVertex | $0AB8 column array | partial small pool, no polygon closure | ## Display | Feature | Original | Port | |---|---|---| -| 280×192 framebuffer | hires page 1/2 | ✅ | -| Page flip | `FlipPagesFillViewport` | 🟡 single buffer (no flip needed) | -| Color/B&W mode | `ColorModePatch` / `BWModePatch` | 🟡 always color | -| Dotted-pattern night | `SceneryOpDayOnly` etc | ✅ DAY_ONLY skip | -| Panel bitmap | hires loaded from disk | ✅ res/loading_panel.bin | -| Panel lights overlay (64K) | `UpdateInstrumentLights` | 🟡 lights state shown as text | -| Message text (`DrawMultiMessage`) | string blit | ✅ font.c | -| Crash message overlay | `crash_msg_table` | ✅ MOUNTAIN/BUILDING/SPLASH/CRASH | -| Wing/tail overlays in side views | `DrawWingsOrTailOverlays` | ❌ | -| Bomb sight | WW1 bombsight pixels | ✅ `ww1aceHudDraw` | -| Gunsight | WW1 only | ✅ `ww1aceHudDraw` | +| 280x192 framebuffer | hires page 1/2 | ok | +| Page flip | `FlipPagesFillViewport` | partial single buffer | +| Color/B&W mode | `ColorModePatch` / `BWModePatch` | partial always color | +| Dotted-pattern night | `SceneryOpDayOnly` etc | ok DAY_ONLY skip | +| Panel bitmap | hires loaded from disk | ok `res/loading_panel.bin` | +| Panel lights overlay (64K) | `UpdateInstrumentLights` | partial state shown as text | +| Message text (`DrawMultiMessage`) | string blit | ok `font.c` + `fontDrawStringCentered` | +| Crash message overlay | `crash_msg_table` | ok MOUNTAIN / BUILDING / SPLASH / CRASH | +| Wing/tail overlays in side views | `DrawWingsOrTailOverlays` | ok `rendererDrawWingOverlay` (right/left strut, back fin, down well) | +| Bomb sight | WW1 bombsight pixels | ok `ww1aceHudDraw` | +| Gunsight | WW1 only | ok `ww1aceHudDraw` | +| NTSC fringing (HIRES decode) | true HIRES pair-merge | partial palette buffer by default; `SCENERY_NTSC=1` reverts to HIRES decode | ## Audio | Feature | Original | Port | |---|---|---| -| Engine sound | speaker click | ✅ sawtooth + throttle modulation | -| Engine fault wobble | not present in original | ✅ phase-modulated wobble | -| Magneto-off engine cut | engine flag | ✅ amp = 0 when MAG OFF | -| Stall horn | beeper trill | ✅ 800 Hz square wave | -| Crash impact | speaker noise | ✅ noise burst | -| Gun fire (WW1) | not in original | ✅ rapid sawtooth burst | -| Bomb drop (WW1) | not in original | ✅ pitch sweep | -| Carb heat icing audio | not directly | 🟡 power penalty only | -| Wind hiss | not in original | ❌ | +| Engine sound | speaker click | ok sawtooth + throttle modulation | +| Engine fault wobble | not present in original | ok phase-modulated wobble | +| Magneto-off engine cut | engine flag | ok amp = 0 when MAG OFF | +| Stall horn | beeper trill | ok 800 Hz square wave | +| Crash impact | speaker noise | ok noise burst | +| Gun fire (WW1) | not in original | ok rapid sawtooth burst | +| Bomb drop (WW1) | not in original | ok pitch sweep | +| Wind hiss | not in original | ok speed-scaled noise (LCG-driven) | ## Input | Feature | Original | Port | |---|---|---| -| Yoke (arrows / WASD) | arrow + paddle | ✅ | -| Rudder | `/` and Ctrl | ✅ Q/E | -| Throttle | `[` `]` Ctrl+H | ✅ Up/PgUp/Dn/PgDn | -| Brake | space | ✅ space cuts throttle | -| Slew controls | 8/9, 0/-, ,/. , +/= | 🟡 W/A/S/D in slew mode | -| View directions (F1-F5) | 1-5 keys | ✅ F1-F5 | -| Magneto select | 1-3 keys | ✅ Shift+M cycles | -| Lights toggle | L key | ✅ L | -| Carb heat | H key | ✅ H | -| Pause | Ctrl-P | ✅ P | -| Edit mode | Ctrl+[ | ✅ F7 | -| Demo mode | Ctrl+D | ✅ F10 | -| Slew toggle | Ctrl+S | ✅ F12 | -| Reality mode | Ctrl+R | ✅ Tab | -| Radar view | F | ✅ ` (backquote) | -| Course plotter menu | Ctrl+C | ✅ C/V/B/N (record/precision/display/off) | -| Joystick | game port | ✅ SDL_Joystick | +| Yoke (arrows / WASD) | arrow + paddle | ok | +| Rudder | `/` and Ctrl | ok Q/E | +| Throttle | `[` `]` Ctrl+H | ok Up/PgUp/Dn/PgDn | +| Brake | space | ok space cuts throttle | +| Slew controls | 8/9, 0/-, ,/. , +/= | partial W/A/S/D in slew mode | +| View directions (F1-F5) | 1-5 keys | ok F1-F5 | +| Magneto select | 1-3 keys | ok Shift+M cycles | +| Lights toggle | L key | ok L | +| Carb heat | H key | ok H | +| Pause | Ctrl-P | ok P | +| Edit mode | Ctrl+[ | ok F7 (full field editor) | +| Demo mode | Ctrl+D | ok F10 | +| Slew toggle | Ctrl+S | ok F12 | +| Reality mode | Ctrl+R | ok Tab | +| Radar view | F | ok ` (backquote) | +| Course plotter menu | Ctrl+C | ok C/V/B/N (record/precision/display/off) | +| BCD digit entry arm | KeyDecreasePatch | ok Ctrl+1..4 (NAV1/NAV2/COM1/ADF) | +| Joystick | game port | ok SDL_Joystick (button 0 = gun, 1 = bomb, 2 = view, 3 = radar, 4 = throttle cut) | ## Persisted state | Feature | Original | Port | |---|---|---| -| Edit mode revert (instrument save buffer) | $FC00+ | ❌ | -| Saved instrument state for crash recovery | yes | ❌ | +| Edit-mode instrument save buffer | $FC00+ | ok `editSavedState` snapshot on toggle | +| Crash recovery snapshot | yes | ok `aircraftArmRecovery` / `aircraftRestoreRecovery` (Space when crashed) | -## Subsystems addressed in latest pass +--- -- ✅ **BCD per-digit frequency entry**: `radiosEnterDigit()` mirrors FS2 - KeyDecreasePatch — shifts current freq left, drops the high digit, - appends new digit, snaps to 0.05 MHz step for NAV/COM. -- ✅ **Throttle/Mixture/Flaps/Trim/Fuel** bar indicators in - `instruments.c`; trim/flap/mix bound to keys. -- ✅ **Fuel gauges** (left/right): per-frame burn, alternating tanks. -- ✅ **Color/B&W mode toggle**: Ctrl+F2 flips `ac->monochrome`; viewport - switches to black backdrop. -- ✅ **State save/restore for edit mode**: snapshot on toggle in, - restore on toggle out (`editSavedState`). -- ✅ **L6BB0 axis permutations** for `$07` SceneryOpEnterLocalFrame: - variant 0 (×16 hi-byte), 2 (byte-swap), 4 (×16 lo-byte), 5/default - (passthrough). -- ✅ **Near-plane clip in vertex emit**: when one endpoint is in front - and one behind, interpolate to z=1 and draw the visible portion. -- ✅ **Audio fixed-point**: engine freq + amp now Q8.8, fault wobble - via `math6502Sin` Q1.15 phase. +## Multi-region scenery -## Visible scenery — UNBLOCKED 2026-05-06 +The FS2.1 base disk's four region variants (Chicago / LA / Seattle / NY) +all share the same `FS2.1` .SD file as their demand-load source. +`port/include/sceneryData.h` exposes the per-region enums; the +`sceneryDataRegionFromName()` table lookup maps `SCENERY_REGION` env var +strings (e.g. "FS2.1_chicago", "SD3", "SDS1") back to enum values for the +`--screenshot` path. -After tracing the unit/sign mismatch: -1. `pipe.proj.camX/camZ` was being set to **metres** while the bytecode - stream encodes scenery units (= metres × 3). Fixed in - `sceneryAttachCamera` to scale by `AC_SCENERY_UNITS_PER_METRE` (= 3) - before storing. -2. `cameraGet2x3Matrix` was producing matRow2 with FS2's right-handed - Z (forward = +world-Z) but the bytecode expects FS2's left-handed - convention (Z increases southward). Negated `cam->rot[i][2]` in the - matrix output. +| Region | Source | Default start | +|---|---|---| +| `SCENERY_FS2_1` | `FS2.1` | WW1 ace training field | +| `SCENERY_FS2_1_CHICAGO` | `FS2.1` | KCGX / Meigs Field `(96, 268)` | +| `SCENERY_FS2_1_LA` | `FS2.1` | KLAX `(200, 0)` | +| `SCENERY_FS2_1_SEATTLE` | `FS2.1` | KSEA `(970, 0)` | +| `SCENERY_FS2_1_NY` | `FS2.1` | KJFK `(400, 0)` | +| `SCENERY_SD1..SDS1` | `A2.SD` | per-disk default | -After both fixes: SD3 scenery actually renders. With aircraft at -metres `(-3500, 200)` (= scenery `(-10500, 600)`), section 2's HEADER -demand-loads the geometry, and 308 vertex emits produce visible lines -on screen. At yaw=64 (= 90°, looking east), 308 draws hit the -viewport showing a road + building at distance. - -`port/screenshot_first_visible_scenery.png` and -`port/screenshot_yaw64.png` are saved milestones. - -## City scenery (Chicago / LA / Seattle / NY) - 2026-05-06 - -The Apple II FS2 base disk really does ship Chicago + LA + Seattle + -NY scenery, not just WWI. The path to load each: - -1. The boot's main-menu sequence (color/BW prompt, demo/regular - prompt, then a city/database menu at .po block 236) ends with - `JSR $8758`/`$875B`/`$875E`/`$8761` -- these are jump thunks to - `LoadSceneryFile1..4` at `$A674/$A67D/$A686/$A68F`. -2. Each `LoadSceneryFile*` reads the city's dispatcher into `LA7E0+` - (256 bytes-1.5 KB, depending on descriptor). -3. The first `MainLoop` iteration calls `LoadDispatcherPointer` - (`$A61B`) → `ProcessScenery` (via `$L6006`). This walks the - city's dispatcher and fires `$0D` HEADER opcodes that demand-load - the actual polygon data via SmartPort block reads. - -`port/tools/fs2trace` now exposes `FS2TRACE_CITY=N` (1=Chicago, -2=LA, 3=Seattle, 4=NY) which runs `MainGameEntry` → `LoadSceneryFile*` -→ `LoadDispatcherPointer` → `L6006` so the resulting RAM dump has -both the city dispatcher (at `LA7E0`) and the demand-loaded polygons -baked in. - -### Bug fixes that unblocked rendering - -1. **Vertex op `$40/$41/$42` mismapping**: port had them as - draw/silent/draw, but chunk5's `SceneryOpcodeTable` says - `$40 = SceneryOpEmitV1Xform7EBC` (silent V1 emit), - `$41 = SceneryOpEmitV2Xform7EBC` (V2 emit + line draw v1->v2), - `$42 = SceneryOpRefreshCachedXform7EBC` (cache refresh, advance 1). -2. **`doEmitV2` drew prev-V2 → new-V2**; chunk5's `EmitClippedLine` - draws current-V1 → new-V2. Fixed. -3. **fs2trace block-list cap was 200 entries**; `ComputeBlockFromSector` - for higher-numbered sectors needs entries up to 256. Bumped. - -### Current rendering status - -| Region | Default `(X, Z)` | Result | -|-------------------------|------------------|----------------------------------| -| `SCENERY_FS2_1` | any | WWI training map (fixture-rendered) | -| `SCENERY_FS2_1_CHICAGO` | `(0, 1000)` | 957 vertex / 957 draws -- Sears Tower visible | -| `SCENERY_FS2_1_LA` | `(0, 1000)` | 905 vertex / 905 draws -- LA skyline visible | -| `SCENERY_FS2_1_SEATTLE` | n/a | dispatcher loaded; no section's cull passes at (0,0). Needs starting-position research | -| `SCENERY_FS2_1_NY` | n/a | same as Seattle | - -`port/screenshots/chicago_marquee.png` is the canonical Chicago -shot showing the Sears Tower spire and downtown silhouette. - -## Walk-all-paths mode (2026-05-06) - -`SCENERY_WALK_ALL=1` makes the interpreter take BOTH branches at every -conditional opcode (`$13/$14` JumpIfBeyondXY, `$20/$21/$22` CullN, -`$04` CullByOutcodeList, `$1C` DAY_ONLY). The visited[] array bounds -the work to one visit per cursor position. With this on, every section -in the dispatcher's `$0D` HEADER chain fires, so any scenery the .SD -file holds for that region is demand-loaded into the working RAM. - -For `SCENERY_FS2_1` this unfortunately doesn't conjure more polygons: -the FS2 base disk's scenery payload is read by chunk5's per-frame disk -loader during the initial main-loop iteration, not from a separate -flat `.SD` file. Cities in `chunk5 InitialZeroPageData` reference -positions outside the dispatcher's first-section bounds, but their -polygon geometry is brought in by extra block reads chunk5 issues -when the dispatcher's cull passes for that section -- a flow we don't -yet replicate offline. See `port/tools/fs2trace.c` `FS2TRACE_INIT_X/Z` for -the work-in-progress per-city RAM-dump capture path. - -## Multi-region scenery (2026-05-06) - -The FS2 base disk (`SCENERY_FS2_1`) ships only the WW1 ace training -field as renderable polygons; its dispatcher's section culls cover a -narrow ~0..500 unit envelope. The COM/NAV database lists US cities -(Chicago/Meigs at worldX=1548, NY/JFK at worldX=1196, LA/LAX at -worldX=599, Seattle/SEA at worldX=2912), but their *polygon* data lives -on the matching scenery disks: - -| City | Scenery region | -|-------------|----------------| -| WW1 ace | `SCENERY_FS2_1` | -| LA / SF | `SCENERY_SD3` (renders at e.g. metres `(-3500, 200)` yaw 64) | -| Seattle | `SCENERY_SD4` | -| (Chicago / NY have no Apple II SubLOGIC scenery disk released) | - -The `SCENERY_REGION` env var on `--screenshot` selects the region. -`SCENERY_FORCE_X` / `SCENERY_FORCE_Z` (in metres) teleport the -aircraft into a section. `port/tools/fs2trace` now accepts -`FS2TRACE_INIT_X` / `FS2TRACE_INIT_Z` (16-bit upper words of the 24-bit -zero-page scenery position) so a per-region RAM dump can be made for -sections outside the dispatcher's default cull window — useful for -forcing demand-loads in regions with multiple sub-sections. +`SCENERY_DEMAND_TRACE=1` logs every fired demand-load. Boot Meigs fires +sid $44 (6 sub-blocks @ $A887) and sid $4E (1 sub-block @ $BA3D). ## Still missing -- **Polygon scanline fill**: scenery emits line edges only. FS2's - scenery is mostly wireframe so this is mostly cosmetic, but - surface fills (water, runway) are unfilled. -- **Demo waypoint sequence**: chunk5/chunk2 `DemoMode64K` flies a - programmed circuit. Port's `autopilotDemo` is a simple altitude/ - throttle hold. -- **Stuck-key magneto auto-alternation** — chunk5 +- **Polygon scanline fill** for non-boot scenes (FS2 boot Meigs is + line-only so this is mostly cosmetic until other regions surface + real polygons). +- **Stuck-key magneto auto-alternation** - chunk5 `MagnetosLeft/Right` handle key-held edge cases. -- **ATIS chunked text scroll** — chunk5 `UpdateCOMMessageChunks` - cycles through airport names; port shows current frequency only. -- **Wing/tail/cowling overlays** in side/back/down views — chunk5 - `DrawViewOverlays` / `DrawWingsOrTailOverlays`. -- **Day-side detailed runway striping** — chunk5 - `DrawHorizonDisc` etc has runway-specific colour-only details. -- **Oil temp/pressure gauges** — chunk5 +- **Day-side detailed runway striping** - chunk5 has runway-specific + colour-only details we don't replicate. +- **Oil temp/pressure gauges** - chunk5 `UpdateOilTempAndPressureGauges`. -- **Section anchor / $07 EnterLocalFrame in real bytecode**: works - when the bytecode actually fires $07 (mostly doesn't in the streams - we walk), but full multi-section navigation may need additional - fixes around section-base init (e.g., reading anchor coords from - the loaded section's preamble). +- **Section anchor / $07 EnterLocalFrame** for real bytecode in + unsurfaced sections. The simplified passthrough is correct for boot + Meigs but full multi-section navigation may need anchor-coord reads + from each section's preamble. +- **ILS glide slope** - FS2 doesn't have it either, but port could add it. + +## Recent code-cleanup pass (2026-05-14) + +- Dead code removed: `ww1aceDropBomb()` (legacy stub), `Coord16T` / + `VertexT` (unused), `DEG2RAD` (unused macro), `rendererSwapFillColors()` + (uncalled). +- Shared helpers consolidated: + - `camera.h`: `metresFromQ1616`, `q1616FromMetres`, `byteAngleToDegrees` + - `font.h`: `fontDrawStringCentered` + - `fs2math.h`: `fs2ClampInt`, `fs2StepClamp` +- `sceneryDataRegionFromName()` replaces the 19-branch SCENERY_REGION + strcmp chain in main.c. +- `AC_MAX_FORWARD_SPEED_Q88` promoted to `aircraft.h` (audio.c was + redefining the same literal). +- Recovery flag tracked via `ac->hasRecoverySnapshot` only (removed the + `recoveryValid` file-static second-source-of-truth). +- Net change: 16104 -> 16018 lines.