124 lines
6.9 KiB
Markdown
124 lines
6.9 KiB
Markdown
# 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.** `ProcessInputTick` is paced to eight per second of wall
|
|
time; the original's rate depends on the frame's cycle count. The
|
|
work counter `$32/$33` that 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 from `orig/` and `res/`.
|
|
* **Failure paths.** Where the original drops into the monitor or hangs
|
|
(`BRK` pads, `LoadDispatcherPointer` hang, 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..$B337` and the WW1 helpers at
|
|
`$B000..$B04A` of 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 by `MainGameEntry`.
|
|
* The editor's disk load/save branches (`cmp #$53` three times in a
|
|
row) can never be taken.
|