17 KiB
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
$07SceneryOpEnterLocalFrame: 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
math6502SinQ1.15 phase.
Visible scenery — UNBLOCKED 2026-05-06
After tracing the unit/sign mismatch:
pipe.proj.camX/camZwas being set to metres while the bytecode stream encodes scenery units (= metres × 3). Fixed insceneryAttachCamerato scale byAC_SCENERY_UNITS_PER_METRE(= 3) before storing.cameraGet2x3Matrixwas producing matRow2 with FS2's right-handed Z (forward = +world-Z) but the bytecode expects FS2's left-handed convention (Z increases southward). Negatedcam->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:
- 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 toLoadSceneryFile1..4at$A674/$A67D/$A686/$A68F. - Each
LoadSceneryFile*reads the city's dispatcher intoLA7E0+(256 bytes-1.5 KB, depending on descriptor). - The first
MainLoopiteration callsLoadDispatcherPointer($A61B) →ProcessScenery(via$L6006). This walks the city's dispatcher and fires$0DHEADER 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
- Vertex op
$40/$41/$42mismapping: port had them as draw/silent/draw, but chunk5'sSceneryOpcodeTablesays$40 = SceneryOpEmitV1Xform7EBC(silent V1 emit),$41 = SceneryOpEmitV2Xform7EBC(V2 emit + line draw v1->v2),$42 = SceneryOpRefreshCachedXform7EBC(cache refresh, advance 1). doEmitV2drew prev-V2 → new-V2; chunk5'sEmitClippedLinedraws current-V1 → new-V2. Fixed.- fs2trace block-list cap was 200 entries;
ComputeBlockFromSectorfor 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
DemoMode64Kflies a programmed circuit. Port'sautopilotDemois a simple altitude/ throttle hold. - Stuck-key magneto auto-alternation — chunk5
MagnetosLeft/Righthandle key-held edge cases. - ATIS chunked text scroll — chunk5
UpdateCOMMessageChunkscycles 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
DrawHorizonDiscetc 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).