338 lines
17 KiB
Markdown
338 lines
17 KiB
Markdown
# 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.
|
||
|
||
Legend: ✅ done · 🟡 partial / approximation · ❌ missing
|
||
|
||
---
|
||
|
||
## Flight model
|
||
|
||
| 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 |
|
||
|
||
## 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) |
|
||
|
||
## 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 | ✅ |
|
||
|
||
## 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) |
|
||
|
||
## 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 |
|
||
|
||
## 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` |
|
||
|
||
## 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 | ❌ |
|
||
|
||
## 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 |
|
||
|
||
## Persisted state
|
||
|
||
| Feature | Original | Port |
|
||
|---|---|---|
|
||
| Edit mode revert (instrument save buffer) | $FC00+ | ❌ |
|
||
| Saved instrument state for crash recovery | yes | ❌ |
|
||
|
||
## 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.
|
||
|
||
## Visible scenery — UNBLOCKED 2026-05-06
|
||
|
||
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.
|
||
|
||
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.
|
||
|
||
## 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
|
||
`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
|
||
`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).
|