6.9 KiB
FS2 C Port - Status vs Original
How the C port in port/ relates to the original Apple II FS2 (the
disassembly in src/). Updated 2026-09-06.
What the port is
fs2port is a plain C translation of the original program. It keeps a
64K Apple II RAM image (fs2Ram in machine.c), loads the four game
chunks and the loading panel into it at their original addresses,
mounts the ProDOS disk image and reads the FS2.1 file's blocks with the
same loader logic the game uses, and then runs C functions that are
transliterations of the 6502 routines. Every routine reads and writes
the same RAM cells as the original (zero page, the $08xx/$09xx/
$0Axx state, the hires pages, the scenery buffers), so the RAM image
can be compared byte for byte against a 6502 run of the real binary.
There is no 6502 emulator in the game. The only emulator in the tree
is port/tools/fs2trace.c, the offline oracle the regression compares
against.
Verification
port/tools/verify/regress.sh boots both the oracle (fs2trace) and
the port from the real boot entry, feeds each the same key script (one
key per ProcessInputTick), snapshots the whole 64K RAM at every tick
on both sides, and reports the first tick where any non-scratch byte
differs (snapDiff.py; the scratch list is the handful of zero-page
temporaries the original leaves in different states between routines).
Silence means identical.
| Script | Ticks | Covers |
|---|---|---|
| boot | 30 | colour prompt, first frames at Meigs |
| flight | 1200 | full throttle, rotation, climb, trim, flaps |
| modes | 800 | radar view and zoom, pause, war/bomb/guns keys, course plotter menu, magnetos, demo exit, mode library save/load |
| edit | 400 | the edit-mode overlay (text page editor), digits, next/previous field, slew keys |
| ww1 | 700 | Europe 1917 via the editor, war declared, guns, bomb, radar/3D view with the WW1 overlay resident |
| radios | 250 | NAV1/NAV2/COM/ADF/transponder/altimeter/heading/VOR digit entry, fuel, carb heat, lights, magnetos, pause |
| slew | 500 | slew mode via the editor: pitch/yoke/throttle/reset/digits keys |
| reality | 1000 | reality mode via the editor, then a take-off |
| night | 500 | hour 22 via the environmental page (dotted night lines, lights), then a take-off |
| course | 400 | course plotter record / display / precision / off |
| logdisk | 100 | Ctrl+E scenery disk log overlay |
| demo | 800 | no keys: the demo mode after the prompt times out |
All of these are byte-identical for their whole length as of
2026-09-06. Draw lists (every line, span and pixel with its
coordinates) and the frame work counter $32/$33 were compared the
same way while the pipeline was being brought up.
Source to port map
| Original | Port |
|---|---|
chunk4 math (MultiplyAXByC2, DivideSigned16, MultiplyXY, ScaleC2ByAX, sin/cos) |
chunk4.c |
| chunk4 indicators, needles, pixel lists, messages, characters | chunk4.c |
chunk4 disk loader (FetchSectorFromDisk, ReadBlocks, screen-hole stash swaps) |
chunk4.c + machine.c (SmartPort ReadBlock) |
| chunk5 boot, main loop, keys, input tick, pause | chunk5Main.c |
| chunk5 flight model, slew, envelope, engine | chunk5Flight.c |
| chunk5 panel: headings, radios, VOR/DME/OMI, needles | chunk5Panel.c |
| chunk5 page flip, sky/ground fill, artificial horizon, turn coordinator | chunk5Screen.c |
chunk5 view matrix (SetupViewProjection) |
chunk5Setup.c |
| chunk5 vertex transforms A/B | chunk5Transform.c |
| chunk5 classify/clip/project/curve | chunk5Vertex.c |
| chunk5 polygon clip passes and scan fill | chunk5Polygon.c |
| chunk5 scenery interpreter (all opcodes) | sceneryVm.c |
| chunk5 scenery file loading, dispatcher pointer, patch slots | chunk5Loader.c |
chunk5 hires kernels (PlotColorPixel, DrawColorLine, DrawColorSpan, pens) |
hires.c |
| chunk3 (64K hooks: ADF, reality mode, overlays, lights, engine, ATIS, war report, loader entries, rotated template) | chunk3.c |
| chunk2 (course plotter, wind, altimeter 10K hand, demo mode) | chunk2.c |
FS2.1 overlay: edit mode / mode library (src/ovl25.s) |
ovlEdit.c |
FS2.1 overlay: WW1 Ace (src/ovl2d.s) |
ovlWw1.c |
FS2.1 overlay: Ctrl+E disk log (src/ovl42.s) |
ovlLogDisk.c |
| Apple II hardware: soft switches, keyboard, paddles, speaker, language card, text page | machine.c, textScreen.c, main.c (SDL2 host) |
Symbols for every label in the disassembly come from
port/include/fs2Symbols.h, generated by port/tools/genSymbols.py
from the ca65 debug file, so the C names the same addresses the source
does.
Overlays
The four PatchSlot_* slots at $A7E8..$A7FF are rewritten by the
scenery file: each LoadSceneryFile* call reads its first page over
$A7E0, and the loaded page turns the slot into jmp <entry> (or
rts). The edit-mode editor (6K, mostly 6502 code at $B04B..$BA16),
the WW1 mode (3K, code at $AA03..$B1D6) and the Ctrl+E log page live
in the FS2.1 file, not in the resident chunks. They are disassembled in
src/ovl25.s, src/ovl2d.s, src/ovl42.s and src/ovl2b.s (the
boot page), each routine labelled and commented, validated byte for
byte by make validate, and translated in port/src/ovl*.c. The port walks the slot bytes the way the 6502
does (chunk5CallPatchSlot) and dispatches the overlay entry only when
the expected code is resident in RAM.
Deliberate differences
- Timing.
ProcessInputTickis paced to eight per second of wall time; the original's rate depends on the frame's cycle count. The work counter$32/$33that decides when a tick fires is charged with the same constants as the original, so the tick sequence is the same. - Reset key. The original's reset vector reloads the game from disk
(
LoadSceneryFile0+InitFromReset). The port restarts by reloading the chunk images fromorig/andres/. - Failure paths. Where the original drops into the monitor or hangs
(
BRKpads,LoadDispatcherPointerhang, loader errors), the port prints a message and restarts. - Text font. The 40-column text page is rendered with a built-in 5x7 font that approximates the Apple II character ROM.
- Sound. Speaker toggles are turned into clicks by the SDL host; the engine sound's pitch is not cycle-accurate.
- Disk writes. The mode library lives in RAM (
$D000..$D3CF) as in the original; the dead code in the editor that would write it to disk is not reachable in this build and is not implemented.
Not translated (unreachable in the shipped binary)
- The in-place ATIS pipeline at
$B148..$B337and the WW1 helpers at$B000..$B04Aof the chunk5 image are overwritten by scenery data on the first frame; the shipped code never runs them. - The residue at
$ABCC..$ABEB(the file's bytes differ from the RAM image there) is skipped over byMainGameEntry. - The editor's disk load/save branches (
cmp #$53three times in a row) can never be taken.