Docs updated.

This commit is contained in:
Scott Duensing 2026-05-14 10:13:07 -05:00
parent a9561702fc
commit b4ddd15710
4 changed files with 271 additions and 931 deletions

View file

@ -1,6 +1,8 @@
Disassembly and analysis of SubLOGIC's Flight Simulator II (FS2) for the Apple II platform, circa 1984. 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 # Disassembly

View file

@ -1,660 +1,81 @@
# FS2 Port Session Recovery # 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 ## 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) 1. Read this file (current state + ground rules).
2. Read `~/.claude/projects/-home-scott-claude-flight/memory/MEMORY.md`
| ID | Status | Subject | 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.
| #9 | in_progress | Fix port matrix construction to match MAME's $78..$89 | 4. Read `port/PORT_64K_AUDIT.md` if working on 64K-mode patch hooks or
| #10 | pending | Investigate missing Sears Tower in port Meigs render | the scenery VM's `$03` / `$0E` opcodes.
| #11 | in_progress | Make port's chunk5 dispatcher reach the records MAME renders |
## Current state (2026-05-14)
## Latest session changes (2026-05-07)
The port is feature-complete against FS2 for most subsystems. See
### $42 RefreshCachedXform7EBC + $04 cull now active `port/PORT_STATUS.md` for the detailed comparison. Highlights:
- `port/src/sceneryVm.c`: `doRefreshCachedXform` populates the vertex
cache pool ($0140 + idx*8) by transforming the 4-byte stream packet - **Rendering**: 36/36 pixel-exact line matches vs MAME at boot Meigs
via `chunk5TransformVertex7EBC`, classifying for outcode (byte 6), via a 5-plane frustum line clipper with chunk5ScaleC2ByC4 plane-snap
storing $FF in byte 7 (chunk5's $DB-marker so $04 enters the AND (see memory `project_fs2port_frustum_clip.md`). Palette-buffer
path). $D3..$DB snapshot/restore around the call mirrors chunk5's rendering by default; `SCENERY_NTSC=1` reverts to true HIRES decode.
L695D/L697D so V2 isn't perturbed for in-flight polygons. - **Scenery demand-load**: default-on; `port/src/sceneryVm.c::doHeader`
- `doCullByOutcodeList` now actually culls: walks listed indices, pulls section bytes from the .SD file on every HEADER opcode. All
checks cache[idx][7] high bit (chunk5's "vertex behind camera" FS2.1 region variants share the `FS2.1` .SD source via `sdSourceFile`
flag) and ANDs cache[idx][6] outcode bits. If accumulator stays in `RegionMetaT`. Set `SCENERY_DEMAND_TRACE=1` to log fires.
non-zero with no on-screen vertex, jump to the cull target. - **Instruments**: all FS2 gauges drawn except oil temp/pressure.
Otherwise fall through. VOR2/ADF gauge mode-gating on `ac->adfMode` matches FS2 behaviour.
- Build confirmed; visual output unchanged because port's dispatcher - **WW1 ace mode**: bullets, bombs, AI fire, damage, drop-with-velocity.
doesn't currently reach any $42 or $04 ops at the default Meigs - **Modes**: full edit mode (F7) via `editor.c`; title/config screen
position. via `title.c`; crash recovery snapshot (Space when crashed).
- **Recent cleanup pass**: dead code removed (`ww1aceDropBomb`,
### Why the $04 fix didn't change the rendered image `Coord16T/VertexT`, `DEG2RAD`, `rendererSwapFillColors`); shared
- Port reaches $A800-$B4FE area, hits 128 ops, makes 12 draws. helpers consolidated into `camera.h` / `font.h` / `fs2math.h`;
- MAME's $04 ops live at $B17B / $B1AF / $B1E3. 19-branch SCENERY_REGION strcmp chain replaced with table-driven
- Port's dispatcher passes $B171 ($13) but JUMPS to $B18E because `sceneryDataRegionFromName`.
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 <yaw_byte> <pitch_byte> <bank_byte> [wx wy wz]`
runs the port's `sceneryAttachCamera` and dumps `$78..$89`. Build
with `make -C port bin/matrixProbe`.
- `port/bin/fs2trace --matrix <yaw_i16> <pitch_i16> <bank_i16> <vd_byte>`
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 <stream_addr> [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 <addr> [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`.
## Files NOT to delete ## Files NOT to delete
- `tmp/mame_capture.lua` capture script - `tmp/mame_capture.lua` - MAME capture script
- `tmp/capture_boot.bin` / `.zp` MAME ground-truth state - `tmp/capture_boot.bin` / `.zp` - MAME ground-truth state
- `tmp/mame_boot.png` / `mame_meigs_ref.png` MAME ground-truth screenshot - `tmp/mame_boot.png` / `mame_meigs_ref.png` - MAME ground-truth screenshot
- `tmp/compare_mame_vs_port_ramstate.png` side-by-side comparison - `tmp/compare_mame_vs_port_ramstate.png` - side-by-side comparison
- `port/screenshots/match_mame_ramstate.png` port's best-effort match - `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.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 - `port/sceneryRam_FS2.1_chicago.bin` - original port-side chicago RAM dump
## Remember ## Ground rules
- 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.
## 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 -- ## NEVER `Read` PNGs (avoids API context corruption)
each multimodal image upload bloats the request and has tripped a recurring
"PNG-API context corruption" failure that nukes the session. 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: 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 cd /home/scott/claude/flight
port/tools/imgDiagnose.sh diff tmp/mame_boot.png port/screenshots/match_mame_ramstate.png --ascii 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 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 ImageMagick into a temp PPM in `tmp/` that the C tools read. Tools
live at `port/tools/imgDiff.c` / `imgStats.c` and build into live at `port/tools/imgDiff.c` / `imgStats.c` and build into
`port/bin/` via `make -C port tools` (auto-built on first wrapper run). `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. 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`.

View file

@ -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. | | `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. | | `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. | | `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. | | `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. | | `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. | | `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. | | `SelectRadarViewPatch` / `Select3DViewPatch` | missing | View-mode keyboard handlers; port toggles via menu. |
| `HideOrShowInstruments` | missing | Toggle instrument panel; port always shows panel. | | `HideOrShowInstruments` | missing | Toggle instrument panel; port always shows panel. |
| `UpdateCoursePlotter` | partial | Course plotter has data; live frame update not wired. | | `UpdateCoursePlotter` | partial | Course plotter has data; live frame update not wired. |
| `DrawATISMessage` | missing | ATIS text bulletin overlay. | | `DrawATISMessage` | ok | `panelDigits.c` scrolls a synthesized "<station> WIND 270/12 ALT 30.05 RWY 18L TIME hh:00" through an 8-char window beside COM freq. |
| `UpdateCOMMessageChunks` | missing | Scrolling COM radio text. | | `UpdateCOMMessageChunks` | ok | Same scrolling-window implementation drives both. |
| `KeyDecreasePatch` / `KeyIncreasePatch` | missing | 64K-only key behavior tweaks. | | `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 ## 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 will render proper water through the existing port path once those
regions become reachable. regions become reachable.
## Demand-load (`SCENERY_DEMAND_LOAD` env var) ## Demand-load (default-on)
`port/src/sceneryVm.c::doHeader` supports loading section bytecode `port/src/sceneryVm.c::doHeader` loads section bytecode fresh from the
fresh from the `.SD` file via the ASM-faithful formula `.SD` file every HEADER opcode via the ASM-faithful formula
`((sid>>2)+1)*4096 + (sid&3)*256 + skip`, where `skip` comes from `((sid>>2)+1)*4096 + (sid&3)*256 + skip`. The `SCENERY_DEMAND_LOAD` env
`SCENERY_DEMAND_LOAD` (default off; values 0..64 produce different var now controls only an optional source-byte SKIP for offline
source-byte alignment). Disabled by default because the captured RAM experiments (default 0 = natural ASM behaviour). All four FS2.1 region
dump's leftover state is currently the only known-working render variants share the same `FS2.1` .SD source via the `sdSourceFile` field
path. in `RegionMetaT` (`sceneryData.c`); when adding new SCENERY_* regions,
populate that field. Run with `SCENERY_DEMAND_TRACE=1` to log every
fired load.

View file

@ -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`) 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 | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Position integrator (24-bit XYZ) | `IntegratePhysicsStep` | ✅ `aircraftStep` (Q16.16) | | Position integrator (24-bit XYZ) | `IntegratePhysicsStep` | ok `aircraftStep` (Q16.16) |
| Pitch / bank / yaw rates | `UpdateAutoTrimAndYaw` etc | ✅ `stepFlight` | | Pitch / bank / yaw rates | `UpdateAutoTrimAndYaw` etc | ok `stepFlight` |
| Auto-coordination (bank → yaw) | `ApplyAutoCoordination` | ✅ `stepFlight` | | Auto-coordination (bank to yaw) | `ApplyAutoCoordination` | ok `stepFlight` |
| Wind / turbulence | chunk2 `ApplyWind` (64K) | ✅ `windCompute` / `windApply` | | Wind / turbulence | chunk2 `ApplyWind` (64K) | ok `windCompute` / `windApply` |
| Stall detection & break | per-instrument check | 🟡 stalled flag, no spin | | Stall detection and break | per-instrument check | ok `stalled` flag |
| Spin recovery | implicit in stall handling | ❌ | | Spin entry from stall | implicit in stall handling | ok yawRate-driven `spinning` state |
| High-G / VNE bleed | `CheckFlightEnvelope` | 🟡 `envelopeWarning` flag, no bleed | | Spin recovery | opposite rudder + nose-down | ok 30-frame recovery condition |
| Flap/gear speed effects | `RefreshElevatorIndicator` | ❌ | | High-G / VNE damage | `CheckFlightEnvelope` | ok `loadFactor_q88` + `airframeDamage` accumulator |
| Mixture too-lean = engine cut | implicit | ❌ | | Flap speed drag | `RefreshElevatorIndicator` | ok `forwardSpeed *= (256 - flaps>>3) / 256` |
| Carb heat icing | chunk5 `CarbHeat` | 🟡 audio penalty added, no icing model | | Mixture too-lean = engine cut | implicit | partial audio penalty, no full model |
| Magneto on/off effect on engine | `UpdateEngineWithMagneto` | 🟡 audio penalty added (no full model) | | Carb heat icing | chunk5 `CarbHeat` | partial audio penalty, no icing model |
| Engine fault dispatch | `FailureProcTable` | ✅ `aircraftStep` reality dispatch | | Magneto on/off effect on engine | `UpdateEngineWithMagneto` | partial audio penalty |
| Engine knock audio | per-fault sound | ✅ `audioUpdate` wobble | | Engine fault dispatch | `FailureProcTable` | ok reality-mode dispatch |
| Crash detection | `HandleCrashOrSplash` | ✅ `aircraftStep` ground/water | | Engine knock audio | per-fault sound | ok `audioUpdate` wobble |
| Splash detection (water) | `CheckSplash` | 🟡 land-only crash | | Crash detection (ground) | `HandleCrashOrSplash` | ok |
| Building / mountain crash | `crash_msg_table` | 🟡 type set but no scenery-aware test | | Splash detection (water) | `CheckSplash` | ok via `worldZ` water range |
| Building / mountain crash | `crash_msg_table` | partial type set, no scenery-aware test |
## Modes ## Modes
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Free flight | default | | | Free flight | default | ok |
| Slew mode | `SlewMode` (chunk5) | `aircraftToggleSlew` | | Slew mode | `SlewMode` (chunk5) | ok `aircraftToggleSlew` |
| Slew digit overlay | `DrawSlewOverlays` | `instruments.c` north/east/alt | | Slew digit overlay | `DrawSlewOverlays` | ok |
| Demo mode | `DemoMode64K` | 🟡 `autopilotDemo` (basic) | | Demo mode | `DemoMode64K` | ok `autopilotDemo` with 4-waypoint sequence |
| Demo waypoint sequence | per-mode auto-flight | ❌ | | Edit mode | `EditModeFlag` | ok F7 toggle, full field editor (`editor.c`) |
| Edit mode | `EditModeFlag` | 🟡 toggleable, no state save/restore | | Reality mode | `RealityMode` | ok instrument and engine failures |
| Reality mode | `RealityMode` | ✅ instrument & engine failures | | Radar view | `RadarView` | ok `worldRenderRadar` |
| Radar view | `RadarView` | ✅ `worldRenderRadar` | | WW1 ace combat | `WW1AceMode` | ok bullets, bombs, AI fire, damage |
| WW1 ace combat | `WW1AceMode` | ✅ `ww1ace.c` | | Course Plotter | chunk2 `CoursePlottingMenu` | ok `coursePlotter.c` (record/display) |
| Course Plotter | chunk2 `CoursePlottingMenu` | ✅ `coursePlotter.c` (record/display) | | Pause | `TogglePause` | ok P key |
| Pause | `TogglePause` | ✅ P key | | Title / config screen | boot menu sequence | ok `title.c` (MODE/REGION/DISPLAY/TIME/REALITY/START/QUIT) |
| Boot DOS | `BootDOS` | (no DOS to boot) | | Boot DOS | `BootDOS` | missing (no DOS to boot) |
## Instruments ## Instruments
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Airspeed needle | `UpdateAirspeedIndicator` | `instruments.c::airspeedGauge` | | Airspeed needle | `UpdateAirspeedIndicator` | ok |
| Altimeter main hand | `UpdateAltimeterIndicator` | | | Altimeter main hand | `UpdateAltimeterIndicator` | ok |
| Altimeter 10K hand | `UpdateAltimeter10K` (64K) | | | Altimeter 10K hand | `UpdateAltimeter10K` (64K) | ok |
| Attitude indicator | tilted disc | `drawHorizonDisc` | | Attitude indicator | tilted disc | ok `drawHorizonDisc` |
| Heading bug | `DrawHeading` | digit readout | | Heading bug | `DrawHeading` | ok digit readout |
| Magnetic compass | `DrawMagCompass` | 🟡 digit only, no rotating compass card | | Magnetic compass | `DrawMagCompass` | partial digit only |
| Vertical speed | `UpdateVerticalSpeedIndicator` | | | Vertical speed | `UpdateVerticalSpeedIndicator` | ok |
| Turn coordinator | `UpdateTurnCoordinator` | | | Turn coordinator | `UpdateTurnCoordinator` | ok |
| Slip/skid ball | `PLSlipSkidIndicator` | | | Slip/skid ball | `PLSlipSkidIndicator` | ok reads `sideslip_q88` |
| Throttle position | `UpdateThrottleIndicator` | ❌ (no graphical needle) | | Throttle position | `UpdateThrottleIndicator` | ok bar indicator |
| Mixture position | `UpdateMixtureControlIndicator` | | | Mixture position | `UpdateMixtureControlIndicator` | ok bar indicator |
| Flap position | `UpdateFlapsIndicator` | | | Flap position | `UpdateFlapsIndicator` | ok bar indicator |
| Trim position | implicit in auto-trim | ❌ (no key bindings) | | Trim position | implicit in auto-trim | ok HOME/END keys + indicator |
| Fuel tank L/R | `UpdateFuelTankGauges` | | | Fuel tank L/R | `UpdateFuelTankGauges` | ok per-frame burn, alternating tanks |
| Oil temp/pressure | `UpdateOilTempAndPressureGauges` | | | Oil temp/pressure | `UpdateOilTempAndPressureGauges` | missing |
| RPM display | `DrawRPM` | digit | | RPM display | `DrawRPM` | ok digit |
| Magneto state visual | `DrawMagnetoState` | MAG OFF/L/R/START indicator | | Magneto state visual | `DrawMagnetoState` | ok MAG OFF/L/R/START indicator |
| Carb heat state | switch position | ✅ "CARB HEAT" indicator | | Carb heat state | switch position | ok "C.H." / "HEAT" indicator |
| Lights state | switch position | ✅ "LIGHTS ON" indicator | | Lights state | switch position | ok "1" / "O" indicator |
| Failure indicator (X over gauge) | `DrawX` per gauge | `drawFailX` | | Failure indicator (X over gauge) | `DrawX` per gauge | ok `drawFailX` |
| Stall warning | `STALL` text | ✅ | | Stall / VNE warning | `STALL` / `VNE` text | ok |
| VNE warning | `VNE` text | ✅ | | VOR2 / ADF gauge mode-gating | chunk4 ADFMode flag | ok `ac->adfMode` switches needle/digits |
## Radios / Navigation ## Radios / Navigation
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| NAV1 frequency | `NAV1` ($08F7) | `radios.c` | | NAV1 frequency | `NAV1` ($08F7) | ok `radios.c` |
| NAV2 frequency | `NAV2` ($08F5) | | | NAV2 frequency | `NAV2` ($08F5) | ok |
| ADF frequency | `ADFFreq*` | | | ADF frequency | `ADFFreq*` | ok |
| COM1 frequency | str_com1 | ✅ | | COM1 frequency | `str_com1` | ok |
| Station database (deduped) | per-region | 695 entries from extractstations | | Station database (deduped) | per-region | ok 695 entries from extractstations |
| BCD frequency increment | step keys | shift+digit / digit | | BCD frequency increment | step keys | ok shift+digit / digit |
| BCD per-digit entry | `KeyDecreasePatch` | | | BCD per-digit entry | `KeyDecreasePatch` | ok Ctrl+1..4 arms input, digit keys append |
| OBS course knob | OBS-related | `,`/`.` keys | | OBS course knob | OBS-related | ok `,` `.` keys |
| VOR CDI needle | `DrawVOR1IndicatorChanges` | | | VOR CDI needle | `DrawVOR1IndicatorChanges` | ok |
| VOR TO/FROM flag | `msg_vor_flags` | ✅ "TO"/"FR"/"OFF" | | VOR TO/FROM flag | `msg_vor_flags` | ok "TO" / "FR" / "OFF" |
| ILS glide slope | not in original | | | ILS glide slope | not in original | missing |
| DME readout | `DrawATISMessage` ATIS bound | | | DME readout | `DrawATISMessage` ATIS bound | ok |
| ADF needle | `DrawADFPanel` | | | ADF needle | `DrawADFPanel` | ok gated on `ac->adfMode` |
| ADF heading digits | `DrawADFHeadingDigits` | | | ADF heading digits | `DrawADFHeadingDigits` | ok |
| ATIS message | `UpdateCOMMessageChunks` | 🟡 freq shown, no chunked text | | ATIS chunked text | `UpdateCOMMessageChunks` | ok 8-char scrolling window |
| Tune-to-nearest button | not in original | T key (port-only) | | Tune-to-nearest button | not in original | ok T key (port-only) |
## Scenery system ## Scenery system
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Disk loader (`SceneryReadUntilC0`) | chunk3 `SceneryLoaderEntry1` | 🟡 RAM dump pre-load + .SD demand-load | | Disk loader (`SceneryReadUntilC0`) | chunk3 `SceneryLoaderEntry1` | ok RAM dump + .SD demand-load (default-on) |
| Block-list indirection | chunk4 `ComputeBlockFromSector` | ✅ `doHeader` correct mapping | | Block-list indirection | chunk4 `ComputeBlockFromSector` | ok `doHeader` correct mapping |
| Nibble decode | `SceneryNibbleDecode` | ❌ (only used by Entry4 path) | | Nibble decode | `SceneryNibbleDecode` | missing (only used by Entry4 path) |
| HEADER opcode ($0D) | `SceneryOpHeader` + `LA63A` | ✅ `doHeader` (with cache) | | HEADER opcode ($0D) | `SceneryOpHeader` + `LA63A` | ok `doHeader` with cache, default-on demand-load |
| L631D section base | `L631D` | ✅ `sceneryComputeBaseL631D` | | L631D section base | `L631D` | ok `sceneryComputeBaseL631D` |
| EnterLocalFrame ($07) | `SceneryOpEnterLocalFrame` | 🟡 simplified passthrough | | EnterLocalFrame ($07) | `SceneryOpEnterLocalFrame` | partial simplified passthrough |
| Vertex emit + transform ($00-$02, $40-$42) | `SceneryOpEmitV*` | ✅ | | Vertex emit + transform ($00-$02, $40-$42) | `SceneryOpEmitV*` | ok |
| Cull ($20/$21/$22) | `SceneryOpCullIfOutside*` | ✅ `doCullN` | | Cull ($20/$21/$22) | `SceneryOpCullIfOutside*` | ok `doCullN` |
| Cull by outcode list ($04) | `SceneryOpCullByOutcodeList` | 🟡 walks list, no actual cull | | Cull by outcode list ($04) | `SceneryOpCullByOutcodeList` | partial walks list, no actual cull |
| Jump-if-beyond-XY/XYZ ($13/$14) | `SceneryOpJumpIfBeyondXY*` | ✅ | | Jump-if-beyond-XY/XYZ ($13/$14) | `SceneryOpJumpIfBeyondXY*` | ok |
| REL_JUMP ($0B) | `SceneryOpJumpRelative` | ✅ | | REL_JUMP ($0B) | `SceneryOpJumpRelative` | ok |
| SUB_INVOKE ($18) / RETURN ($19) | `SceneryOpSubInvoke` | ✅ | | SUB_INVOKE ($18) / RETURN ($19) | `SceneryOpSubInvoke` | ok |
| RESET_STATE ($2F) | `SceneryOpResetState` | ✅ | | RESET_STATE ($2F) | `SceneryOpResetState` | ok |
| MODE_WHITE ($1B) | line-kernel patch | ✅ semantic equivalent | | MODE_WHITE ($1B) | line-kernel patch | ok semantic equivalent |
| DAY_ONLY ($1C) | line-kernel patch | ✅ skip-on-night flag | | DAY_ONLY ($1C) | line-kernel patch | ok skip-on-night flag |
| WriteWord ($1A) / StoreImmWord ($25) | self-mod patches | ✅ | | WriteWord ($1A) / StoreImmWord ($25) | self-mod patches | ok |
| Vertex-cache ops ($31/$32/$33/$35/$42) | cached vertex pool at $0140 | ✅ pool reads, no full $31/$42 transform | | Curve emit ($2B) | `SceneryOpEmitCurve` | ok via 6502 interpreter on MAME-patched RAM |
| ADF/NAV/COM record ($05/$1D/$1E) | station records | ✅ | | Vertex-cache ops ($31/$32/$33/$35/$42) | cached vertex pool at $0140 | ok pool reads |
| SET_COLOR ($12) | `SceneryOpSetColor` | ✅ | | ADF/NAV/COM record ($05/$1D/$1E) | station records | ok |
| Polygon edge emit | `EmitClippedLine` | 🟡 line draw only, no polygon close | | SET_COLOR ($12) | `SceneryOpSetColor` | ok |
| Polygon scanline fill | `DrawColorSpan` etc | 🟡 2D scanline edge-intercept (`rendererFillPolygon`); not chunk5's 3D-clipped scanline emitter | | Polygon edge emit | `EmitClippedLine` | partial line draw only, no polygon close |
| 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. | | Polygon scanline fill | `DrawColorSpan` etc | partial 2D scanline edge-intercept; not chunk5's 3D-clipped emitter (FS2 boot Meigs is line-only) |
| Sky/ground tilted fill | `FlipPagesFillViewport` | ✅ `rendererFillTiltedSkyGround` | | Polygon 4-pass 3D clipper | `PolygonScanFillSetup` | ok `sceneryClipPolygon3D` (Sutherland-Hodgman against Z-X / Z-Y / X+Z / Y+Z) |
| Frustum clipping (3D) | `ClipBothVerticesToFrustum` | ✅ outcode + perspective divide | | Sky/ground tilted fill | `FlipPagesFillViewport` | ok `rendererFillTiltedSkyGround` |
| Vertex pool / EmitPrimaryVertex | $0AB8 column array | 🟡 small pool, no polygon closure | | 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 ## Display
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| 280×192 framebuffer | hires page 1/2 | ✅ | | 280x192 framebuffer | hires page 1/2 | ok |
| Page flip | `FlipPagesFillViewport` | 🟡 single buffer (no flip needed) | | Page flip | `FlipPagesFillViewport` | partial single buffer |
| Color/B&W mode | `ColorModePatch` / `BWModePatch` | 🟡 always color | | Color/B&W mode | `ColorModePatch` / `BWModePatch` | partial always color |
| Dotted-pattern night | `SceneryOpDayOnly` etc | ✅ DAY_ONLY skip | | Dotted-pattern night | `SceneryOpDayOnly` etc | ok DAY_ONLY skip |
| Panel bitmap | hires loaded from disk | ✅ res/loading_panel.bin | | Panel bitmap | hires loaded from disk | ok `res/loading_panel.bin` |
| Panel lights overlay (64K) | `UpdateInstrumentLights` | 🟡 lights state shown as text | | Panel lights overlay (64K) | `UpdateInstrumentLights` | partial state shown as text |
| Message text (`DrawMultiMessage`) | string blit | ✅ font.c | | Message text (`DrawMultiMessage`) | string blit | ok `font.c` + `fontDrawStringCentered` |
| Crash message overlay | `crash_msg_table` | ✅ MOUNTAIN/BUILDING/SPLASH/CRASH | | Crash message overlay | `crash_msg_table` | ok MOUNTAIN / BUILDING / SPLASH / CRASH |
| Wing/tail overlays in side views | `DrawWingsOrTailOverlays` | ❌ | | Wing/tail overlays in side views | `DrawWingsOrTailOverlays` | ok `rendererDrawWingOverlay` (right/left strut, back fin, down well) |
| Bomb sight | WW1 bombsight pixels | ✅ `ww1aceHudDraw` | | Bomb sight | WW1 bombsight pixels | ok `ww1aceHudDraw` |
| Gunsight | WW1 only | ✅ `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 ## Audio
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Engine sound | speaker click | ✅ sawtooth + throttle modulation | | Engine sound | speaker click | ok sawtooth + throttle modulation |
| Engine fault wobble | not present in original | ✅ phase-modulated wobble | | Engine fault wobble | not present in original | ok phase-modulated wobble |
| Magneto-off engine cut | engine flag | ✅ amp = 0 when MAG OFF | | Magneto-off engine cut | engine flag | ok amp = 0 when MAG OFF |
| Stall horn | beeper trill | ✅ 800 Hz square wave | | Stall horn | beeper trill | ok 800 Hz square wave |
| Crash impact | speaker noise | ✅ noise burst | | Crash impact | speaker noise | ok noise burst |
| Gun fire (WW1) | not in original | ✅ rapid sawtooth burst | | Gun fire (WW1) | not in original | ok rapid sawtooth burst |
| Bomb drop (WW1) | not in original | ✅ pitch sweep | | Bomb drop (WW1) | not in original | ok pitch sweep |
| Carb heat icing audio | not directly | 🟡 power penalty only | | Wind hiss | not in original | ok speed-scaled noise (LCG-driven) |
| Wind hiss | not in original | ❌ |
## Input ## Input
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Yoke (arrows / WASD) | arrow + paddle | ✅ | | Yoke (arrows / WASD) | arrow + paddle | ok |
| Rudder | `/` and Ctrl | ✅ Q/E | | Rudder | `/` and Ctrl | ok Q/E |
| Throttle | `[` `]` Ctrl+H | ✅ Up/PgUp/Dn/PgDn | | Throttle | `[` `]` Ctrl+H | ok Up/PgUp/Dn/PgDn |
| Brake | space | ✅ space cuts throttle | | Brake | space | ok space cuts throttle |
| Slew controls | 8/9, 0/-, ,/. , +/= | 🟡 W/A/S/D in slew mode | | Slew controls | 8/9, 0/-, ,/. , +/= | partial W/A/S/D in slew mode |
| View directions (F1-F5) | 1-5 keys | ✅ F1-F5 | | View directions (F1-F5) | 1-5 keys | ok F1-F5 |
| Magneto select | 1-3 keys | ✅ Shift+M cycles | | Magneto select | 1-3 keys | ok Shift+M cycles |
| Lights toggle | L key | ✅ L | | Lights toggle | L key | ok L |
| Carb heat | H key | ✅ H | | Carb heat | H key | ok H |
| Pause | Ctrl-P | ✅ P | | Pause | Ctrl-P | ok P |
| Edit mode | Ctrl+[ | ✅ F7 | | Edit mode | Ctrl+[ | ok F7 (full field editor) |
| Demo mode | Ctrl+D | ✅ F10 | | Demo mode | Ctrl+D | ok F10 |
| Slew toggle | Ctrl+S | ✅ F12 | | Slew toggle | Ctrl+S | ok F12 |
| Reality mode | Ctrl+R | ✅ Tab | | Reality mode | Ctrl+R | ok Tab |
| Radar view | F | ✅ ` (backquote) | | Radar view | F | ok ` (backquote) |
| Course plotter menu | Ctrl+C | ✅ C/V/B/N (record/precision/display/off) | | Course plotter menu | Ctrl+C | ok C/V/B/N (record/precision/display/off) |
| Joystick | game port | ✅ SDL_Joystick | | 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 ## Persisted state
| Feature | Original | Port | | Feature | Original | Port |
|---|---|---| |---|---|---|
| Edit mode revert (instrument save buffer) | $FC00+ | ❌ | | Edit-mode instrument save buffer | $FC00+ | ok `editSavedState` snapshot on toggle |
| Saved instrument state for crash recovery | yes | ❌ | | Crash recovery snapshot | yes | ok `aircraftArmRecovery` / `aircraftRestoreRecovery` (Space when crashed) |
## Subsystems addressed in latest pass ---
- ✅ **BCD per-digit frequency entry**: `radiosEnterDigit()` mirrors FS2 ## Multi-region scenery
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.
## 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: | Region | Source | Default start |
1. `pipe.proj.camX/camZ` was being set to **metres** while the bytecode |---|---|---|
stream encodes scenery units (= metres × 3). Fixed in | `SCENERY_FS2_1` | `FS2.1` | WW1 ace training field |
`sceneryAttachCamera` to scale by `AC_SCENERY_UNITS_PER_METRE` (= 3) | `SCENERY_FS2_1_CHICAGO` | `FS2.1` | KCGX / Meigs Field `(96, 268)` |
before storing. | `SCENERY_FS2_1_LA` | `FS2.1` | KLAX `(200, 0)` |
2. `cameraGet2x3Matrix` was producing matRow2 with FS2's right-handed | `SCENERY_FS2_1_SEATTLE` | `FS2.1` | KSEA `(970, 0)` |
Z (forward = +world-Z) but the bytecode expects FS2's left-handed | `SCENERY_FS2_1_NY` | `FS2.1` | KJFK `(400, 0)` |
convention (Z increases southward). Negated `cam->rot[i][2]` in the | `SCENERY_SD1..SDS1` | `A2.SD<n>` | per-disk default |
matrix output.
After both fixes: SD3 scenery actually renders. With aircraft at `SCENERY_DEMAND_TRACE=1` logs every fired demand-load. Boot Meigs fires
metres `(-3500, 200)` (= scenery `(-10500, 600)`), section 2's HEADER sid $44 (6 sub-blocks @ $A887) and sid $4E (1 sub-block @ $BA3D).
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.
## Still missing ## Still missing
- **Polygon scanline fill**: scenery emits line edges only. FS2's - **Polygon scanline fill** for non-boot scenes (FS2 boot Meigs is
scenery is mostly wireframe so this is mostly cosmetic, but line-only so this is mostly cosmetic until other regions surface
surface fills (water, runway) are unfilled. real polygons).
- **Demo waypoint sequence**: chunk5/chunk2 `DemoMode64K` flies a - **Stuck-key magneto auto-alternation** - chunk5
programmed circuit. Port's `autopilotDemo` is a simple altitude/
throttle hold.
- **Stuck-key magneto auto-alternation** — chunk5
`MagnetosLeft/Right` handle key-held edge cases. `MagnetosLeft/Right` handle key-held edge cases.
- **ATIS chunked text scroll** — chunk5 `UpdateCOMMessageChunks` - **Day-side detailed runway striping** - chunk5 has runway-specific
cycles through airport names; port shows current frequency only. colour-only details we don't replicate.
- **Wing/tail/cowling overlays** in side/back/down views — chunk5 - **Oil temp/pressure gauges** - chunk5
`DrawViewOverlays` / `DrawWingsOrTailOverlays`.
- **Day-side detailed runway striping** — chunk5
`DrawHorizonDisc` etc has runway-specific colour-only details.
- **Oil temp/pressure gauges** — chunk5
`UpdateOilTempAndPressureGauges`. `UpdateOilTempAndPressureGauges`.
- **Section anchor / $07 EnterLocalFrame in real bytecode**: works - **Section anchor / $07 EnterLocalFrame** for real bytecode in
when the bytecode actually fires $07 (mostly doesn't in the streams unsurfaced sections. The simplified passthrough is correct for boot
we walk), but full multi-section navigation may need additional Meigs but full multi-section navigation may need anchor-coord reads
fixes around section-base init (e.g., reading anchor coords from from each section's preamble.
the loaded 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.