fs2port/port/PORT_64K_AUDIT.md
2026-05-13 21:32:05 -05:00

255 lines
14 KiB
Markdown

# 64K Feature Audit
This document maps each entry in chunk5.s `PatchTable` (line 10159+) to
its port equivalent. The 64K-mode chunk5 binary patches in JMP/JSR
redirects to chunk3 callbacks; the port re-implements those callbacks
in C, so the patch table itself doesn't run -- but every functional
hook should still be present.
## Scenery-VM 64K opcodes
In addition to the PatchTable redirects, two chunk5 scenery opcodes
behave differently in 64K mode (they're 1-byte / 6-byte no-ops in 48K
but call into chunk3 in 64K). These DO affect 3D scenery rendering.
| Opcode | 48K behaviour | 64K behaviour | Port status |
|--------|---------------|-----------------------------------------------------|-------------|
| `$03` | advance 6 | `chunk3 SceneryRotatedTransform` (chunk3.s:2662) -- builds 2D rotation matrix at `$F244..$F25F`, recursively runs the chunk3-resident scenery template at `$F240` (a 4-vertex quad + 8-segment EmitCurve) with that transform. Used to stamp repeating shapes. | Recognised + advance 6 (`doCall64KRotated` in sceneryVm.c). The chunk3-resident `$F240` template + matrix slot is **not yet** populated in port's writableRam, so the recursive template invocation is a no-op. |
| `$0E` | advance 1 | `chunk3 SceneryOp64KCallback` (chunk3.s:2821) -- reads a 2-byte ABSOLUTE address from cursor[1..2] and tail-jumps via SceneryJumpToFetched. Used for cross-region calls (e.g. into chunk3 RAM). | Recognised + advance 3, with in-stream-range targets followed (`doCall64K`). Out-of-range targets (e.g. into chunk3 RAM) are skipped because chunk3 isn't loaded. |
**Sid `$44` (Meigs) has zero `$03` and zero `$0E` ops**, so completing
the chunk3-template integration won't change the Meigs render. Sections
that DO use these (FS2.1 sid `$03`, `$3A`, `$74`, `$78`; many SD-disk
sections) will need the chunk3 template loaded into writableRam to
render correctly.
## Present in port
| Patch hook | Port location | Notes |
|-----------------------------|-------------------------|-------|
| `LookupADFStation` | sceneryVm.c (doAdfRecord) + radios.c | $05 ADF record handler registers station; radios resolves freq -> closest. |
| `ApplyWind` | wind.c (windApply) + aircraft.c | Per-frame wind applied to airspeed/heading. |
| `ComputeWindComponents` | wind.c (windInit, windApply) | Magnitude+direction -> XY components. |
| `ComputeDayPhase` | timeOfDay.c | Day/dusk/night phase from in-game time. |
| `HandleCrashOrSplash` | instruments.c (crash overlay) + aircraft.c (crashed flag) | |
| `RealityModeHook` | aircraft.c (realityMode + roll-out) | |
| `DrawSlewOverlays` | instruments.c | Slew-mode overlay text. |
| `CoursePlottingMenu` | main.c + coursePlotter.c | Menu wiring. |
| `DemoMode64K` | aircraft.c (demoMode flag) | |
| Altimeter (main + 10K hand) | instruments.c (altimeterGauge) | Main needle + 10K hand. |
| Magneto state | aircraft.c (ac->magnetos) + instruments.c (display) | |
| RadarView mode | aircraft.c (radarView) + chunk5Setup.c (radarView path) | |
| `SceneryLoaderEntry1-7` | sceneryData.c (sceneryDataLoad) | Direct .SD file load. |
## Stubbed / missing
These are minor UI features the port doesn't currently surface but
that are listed in the patch table. None affect 3D scenery rendering.
| Patch hook | Status | Impact |
|-----------------------------|----------|--------|
| `ADFKeyboardHook` | missing | Keyboard ADF tuning hotkeys; port handles ADF via in-app UI. |
| `RequestADFStationLookup` | missing | Trigger to re-lookup ADF after freq change; port re-resolves on every radiosUpdate. |
| `UpdateADFIndicator` | missing | ADF needle update; port already redraws needle each frame from radios state. |
| `DrawViewOverlays` | missing | View-mode text labels ("RIGHT VIEW" etc.); port shows view via gauge changes. |
| `UpdateInstrumentLights` | missing | Night-time gauge backlighting; port renders day-mode gauges only. |
| `UpdateEngineWithMagneto` | partial | Engine reacts to magneto in aircraftStep; chunk3's full coupling not modelled. |
| `DrawMagnetoStateHook` | partial | Magneto state shown via instruments.c indicator; chunk3's specific draw path absent. |
| `SetMagnetoFromA` | partial | Just a helper; magneto state set directly via ac->magnetos. |
| `SelectRadarViewPatch` / `Select3DViewPatch` | missing | View-mode keyboard handlers; port toggles via menu. |
| `HideOrShowInstruments` | missing | Toggle instrument panel; port always shows panel. |
| `UpdateCoursePlotter` | partial | Course plotter has data; live frame update not wired. |
| `DrawATISMessage` | missing | ATIS text bulletin overlay. |
| `UpdateCOMMessageChunks` | missing | Scrolling COM radio text. |
| `KeyDecreasePatch` / `KeyIncreasePatch` | missing | 64K-only key behavior tweaks. |
## What 64K patches DO NOT cover
The PatchTable itself only redirects PRE-EXISTING chunk5 NoOp/stub call
sites into chunk3-resident handlers. The patch list does not modify
SceneryOpcodeTable, dispatcher loop, vertex transforms, matrix setup,
or scenery data layout.
**However**, two chunk5 opcodes (`$03` and `$0E`) that exist in the
table have 48K-mode no-op semantics and 64K-mode chunk3-callback
semantics. They DO affect rendering for any scenery section that
contains them. See "Scenery-VM 64K opcodes" above.
## How FS2 decides what colors to render (the full graph)
FS2 doesn't have a "color per polygon" notion. The hires display
generates colors from the BIT PATTERN written to the framebuffer, and
chunk5 manipulates which BITS get set per pixel-plot/line-draw.
**Color ladder (chunk5.s:3800-3829):**
```
HIRES_BLACK1 = 0 HIRES_BLUE = 5
HIRES_VIOLET = 1 HIRES_ORANGE = 6
HIRES_GREEN = 2
HIRES_WHITE1 = 3
```
**The byte patterns** (when written to a hires page byte):
- `$00` = BLACK (palette 0, no bits set)
- `$80` = BLACK (palette 1)
- `$2A` = bits 1,3,5 set in palette 0 = **GREEN** (per FS2 convention)
- `$55` = bits 0,2,4,6 set in palette 0 = **VIOLET** (= magenta on TV)
- `$D5` = bits 0,2,4,6 set in palette 1 = **BLUE**
- `$AA` = bits 1,3,5 set in palette 1 = **ORANGE**
- `$7F` / `$FF` = all 7 bits set = **WHITE** (palette 0 / 1)
**Where bytes get written** (= the "color decision" entry points):
1. **Sky/ground fill** (`FlipPagesFillViewport` chunk5.s:480-689):
- `FillColor` (`$ED`) and `AltFillColor` (`$EE`) hold the byte values
- Set ONCE per frame from `$0882` (ground) and `$0880` (sky):
`$00` -> BLACK, `$FF` -> WHITE, anything else -> `$2A` (ground)
or `$D5` (sky). **Cannot become `$55` (violet)** -- chunk5 hard-
codes `$2A`/`$D5` literals there.
- `DrawSkyGroundRowUnrolled` writes byte then `eor #$7F` for the
next column, so adjacent columns alternate `$2A`/`$55`. That
ALTERNATION is what makes "ground" SOLID GREEN on TV (each pair
of adjacent same-color slots fills both green pixel positions).
2. **Polygon line draw** (`DrawColorLine` ~chunk5.s:3555):
- Uses self-modified opcodes patched by `SetPixelDrawMode`
(chunk5.s:3847-3927).
- `SetPixelDrawMode` selects `OrMaskTable1` (= bits 0,2,4,6 -> $55
pattern) or `OrMaskTable2` (= bits 1,3,5 -> $2A pattern), AND
similarly `AndMaskTable1`/`AndMaskTable2`, depending on whether
the requested HIRES_* color sets bits at even or odd positions.
- So a line drawn in HIRES_VIOLET sets `$55`-pattern bits;
HIRES_GREEN sets `$2A`-pattern bits; HIRES_WHITE sets both.
3. **Color selection routes** (= what calls `SetPixelDrawMode`):
- Boot init: HIRES_VIOLET as fallback.
- `SceneryOpDayOnly` (`$1C`) at NIGHT: HIRES_VIOLET (so any
un-patched draw stays sane).
- `SceneryOpSetColor` (`$12`): reads next byte, indexes into
`ToHiresColorTable[16]`, picks one of {BLACK1, GREEN, VIOLET,
WHITE1}.
- Panel HUD: `DrawTurnCoordinatorAtAngle` etc. use HIRES_BLACK1
directly.
- Chunk3 `DrawWingsOrTail` writes scenery code to `$0876`, then
calls `MapColorAndPrepRowRoutine` which reads `$0876`, looks up
`ToHiresColorTable[$0876 & $0F]`, and configures the masks the
same way.
4. **Color-clash suppression** (`TidySkyGroundEdgeInRow` chunk5.s:704):
- At each sky/ground transition column, OR's in `L149E`/`L14A5`
edge-mask bits to force WHITE pixels right at the edge. This
PREVENTS the color clash that would otherwise produce stray
violet/orange pixels at the horizon.
**So the visible color is fully determined by the BIT PATTERN in the
hires page byte.** The chunk5 "color code" only chooses WHICH BITS to
set; the Apple II display NTSC encoder then turns the bits into a
color based on (a) bit position in byte (= pixel column parity) and
(b) byte's high bit (= palette).
## Where Meigs's magenta comes from (analysis on captured RAM)
The captured RAM's hires page 1 (`$2000-$3FFF`) byte distribution:
```
$2A (green-pos): 826 bytes $55 (violet-pos): 827 bytes
$D5 (blue-pos): 1154 $AA (orange-pos): 1164
$7F/$FF (white): 188 $00/$80 (black): 1718
```
**59 isolated `$55` bytes** (= without a `$2A` left neighbour) =
"pure violet patches" not part of the alternating-green-fill pattern.
These are where MAME's magenta comes from. They're produced by
polygon line draws in HIRES_VIOLET color (= via `SetColor $02` /
`$04` / etc.) overwriting parts of the alternating ground pattern,
or by HIRES_VIOLET lines drawn into the sky region.
**The port now reproduces these** via a real Apple II hires bitplane
in `port/include/hires.h` + `port/src/hires.c`:
- `FramebufferT` carries an extra 7680-byte hires bitplane alongside
the legacy palette image.
- `rendererDrawLine` plots BITS into the bitplane via per-color
even-byte / odd-byte patterns (chunk5 ColorTableEven/Odd, taken
from the sky/ground fill values verified in chunk5.s:558-565). A
`$12 $0F` SetColor sets `hiresColor=HIRES_WHITE1`, drawing
white-pattern bits; `$12 $02` would set `hiresColor=HIRES_VIOLET`
drawing `$2A`/`$55` violet pattern bits, etc.
- `rendererFillTiltedSkyGround` writes the alternating `$D5/$AA`
(sky = palette-1 BLUE) and `$2A/$55` (ground = palette-0 GREEN)
byte patterns the way `DrawSkyGroundRowUnrolled` does.
- `framebufferBlitTo32` decodes the bitplane through `hiresDecodeToRgb`
using pair-based Apple II NTSC color rules:
- both bits of pair set -> WHITE
- first-of-pair only set -> VIOLET (palette 0) or BLUE (palette 1)
- second-of-pair only set -> GREEN (palette 0) or ORANGE (palette 1)
- neither set -> BLACK
- The chunk5 `$12` SetColor handler now drives `rendererSetHiresColor`
with the chunk5 `ToHiresColorTable[code & 0x0F]` value (BLACK1 /
VIOLET / GREEN / WHITE1) -- no more modern-palette guessing.
This is universal across all 14 scenery disks: any disk that emits
`$12 02` SetColor will render water in MAGENTA; any disk with `$12 06`
RUNWAY will render in WHITE; etc. All bit patterns the original chunk5
generates now end up in the right pixel slots. At Meigs the visible
result: BLUE sky + GREEN ground + WHITE polygon outlines, matching
the Apple II hires color set MAME displays.
## Where water comes from at Meigs
User-reported "no water at Meigs" investigation:
**Sid `$44` (the Meigs/Chicago section reachable at start position)**:
- ZERO SetColor for water (`$12 02` / `$12 04`) on any walk path the
chunk5 VM actually takes (verified via `SCENERY_OP_TRACE=1`).
- ZERO `$03` stamps and ZERO `$0E` cross-region jumps.
- 55 polygon emits, all drawn in default WHITE or `$0F` CITY tan.
**MAME's reference shows ~1500 magenta (HIRES_VIOLET) pixels** in
concentrated bands at rows `Y=73-77` (~862 px) and `Y=119` (~510 px)
plus a 40-row vertical at column 72-73 -- not random noise but
deliberate filled regions.
**Two `$12 $02` byte-aligned candidates exist in the captured RAM**
at `$B781` and `$B7A6`. They ARE valid SetColor opcodes if reached,
but the chunk5 walker's actual path through sid `$44`
(`...$B769 $07 EnterLocalFrame`(14b)`->$B777 $01 EmitV1Xform80C5`(7b)
`->$B77E $02`(7b)`->$B785 $01`...) jumps OVER them. No conditional
jump in the dispatcher region targets `$B770` or `$B771` (the only
entry points that would walk INTO `$B781`).
**Tried fresh `.SD` demand-load with various source-byte skips**
(`SCENERY_DEMAND_LOAD=0`, `=14`). Neither offset reveals a
reachable `$12 02` SetColor. With `skip=0` the port hits ONE
SetColor `$06` (RUNWAY!) but loses building outline; with
`skip=14` the building returns but no water/runway color appears.
**Loaded chunk3 binary into writableRam at `$D300`** so the `$03`
SceneryRotatedTransform stamp template (`$F240`) and `$0E`
SceneryOp64KCallback targets are now resolvable. Sid `$44` doesn't
use those opcodes so this is invisible at Meigs but completes the
64K infrastructure for other scenery files that do.
**Most likely actual mechanism**: MAME's Apple //e display emulator
applies NTSC color-artifact rules to the hires framebuffer. Chunk5
draws WHITE polygons whose pixel BITS happen to fall on odd-only
column positions; the Apple II hires display rules turn those bits
into VIOLET (= water-color magenta) at the monitor level. Our port
draws at native palette resolution and skips this artifacting layer.
Confirming this would require running MAME with a Lua tap that logs
every `DrawColorLine` call for one frame and verifying the FillColor
state at each call -- the previous capture got buffer-corrupted.
Other scenery files (SD7B Miami, SD11 Detroit, SD14B Channel/Germany,
SDS1 SF Bay) DO contain explicit `$12 02` water-colour polygons and
will render proper water through the existing port path once those
regions become reachable.
## Demand-load (`SCENERY_DEMAND_LOAD` env var)
`port/src/sceneryVm.c::doHeader` supports loading section bytecode
fresh from the `.SD` file via the ASM-faithful formula
`((sid>>2)+1)*4096 + (sid&3)*256 + skip`, where `skip` comes from
`SCENERY_DEMAND_LOAD` (default off; values 0..64 produce different
source-byte alignment). Disabled by default because the captured RAM
dump's leftover state is currently the only known-working render
path.