fs2port/port/PORT_64K_AUDIT.md
2026-05-14 10:13:07 -05:00

14 KiB

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.