Docs updated.
This commit is contained in:
parent
a9561702fc
commit
b4ddd15710
4 changed files with 271 additions and 931 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <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`.
|
||||
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`.
|
||||
|
|
|
|||
|
|
@ -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 "<station> 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.
|
||||
|
|
|
|||
|
|
@ -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<n>` | 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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue