# 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` | ok | `rendererDrawWingOverlay` paints wing-strut / fin / wheel-well shapes for non-forward views. | | `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` | ok | `panelDigits.c` scrolls a synthesized " WIND 270/12 ALT 30.05 RWY 18L TIME hh:00" through an 8-char window beside COM freq. | | `UpdateCOMMessageChunks` | ok | Same scrolling-window implementation drives both. | | `KeyDecreasePatch` / `KeyIncreasePatch` | ok | `radiosEnterDigit()` mirrors FS2's per-digit BCD entry; armed via Ctrl+1..4 for NAV1/NAV2/COM1/ADF. | ## 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 (default-on) `port/src/sceneryVm.c::doHeader` loads section bytecode fresh from the `.SD` file every HEADER opcode via the ASM-faithful formula `((sid>>2)+1)*4096 + (sid&3)*256 + skip`. The `SCENERY_DEMAND_LOAD` env var now controls only an optional source-byte SKIP for offline experiments (default 0 = natural ASM behaviour). All four FS2.1 region variants share the same `FS2.1` .SD source via the `sdSourceFile` field in `RegionMetaT` (`sceneryData.c`); when adding new SCENERY_* regions, populate that field. Run with `SCENERY_DEMAND_TRACE=1` to log every fired load.