# 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).