From 78936ffc22362ae5f598f8c64084d3b50ad7fa85 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Wed, 13 May 2026 20:57:40 -0500 Subject: [PATCH] Finally reasonable rendering. --- .gitattributes | 4 + .gitignore | 13 + ARCHITECTURE.md | 1450 ++ Makefile | 77 + README.md | 22 + SESSION_RECOVERY.md | 666 + orig/1_4000-5fff | 3 + orig/2_f600-fbff | 3 + orig/3_d300-f3ff | 3 + orig/4_0200-25ff | 3 + orig/5_6000-b3df | 3 + orig/FS2#062499 | 3 + ...or 2 with scenery PRODOS (san inc pack).po | 3 + port/Makefile | 73 + port/PORT_64K_AUDIT.md | 255 + port/PORT_STATUS.md | 338 + port/docs/scenery_opcodes.md | 1006 ++ port/include/aircraft.h | 238 + port/include/apple2hires.h | 38 + port/include/audio.h | 24 + port/include/camera.h | 81 + port/include/chunk5Setup.h | 59 + port/include/chunk5Transform.h | 33 + port/include/coursePlotter.h | 68 + port/include/cpu6502.h | 94 + port/include/fixture.h | 11 + port/include/font.h | 21 + port/include/framebuffer.h | 30 + port/include/fs2math.h | 64 + port/include/hires.h | 92 + port/include/hud.h | 13 + port/include/instruments.h | 14 + port/include/math6502.h | 28 + port/include/needleData.h | 22 + port/include/palette.h | 39 + port/include/panelDigits.h | 14 + port/include/projection.h | 39 + port/include/radios.h | 119 + port/include/renderer.h | 57 + port/include/sceneryData.h | 68 + port/include/sceneryProjection.h | 172 + port/include/sceneryStationsData.h | 719 + port/include/sceneryVm.h | 147 + port/include/timeOfDay.h | 53 + port/include/title.h | 43 + port/include/types.h | 46 + port/include/wind.h | 75 + port/include/world.h | 29 + port/include/ww1ace.h | 141 + port/sceneryRam_FS2.1.bin | 3 + port/sceneryRam_FS2.1_chicago.bin | 3 + port/sceneryRam_FS2.1_frozen.bin | 3 + port/sceneryRam_FS2.1_la.bin | 3 + port/sceneryRam_FS2.1_ny.bin | 3 + port/sceneryRam_FS2.1_seattle.bin | 3 + port/sceneryRam_SD1.bin | 3 + port/sceneryRam_SD11.bin | 3 + port/sceneryRam_SD13.bin | 3 + port/sceneryRam_SD14A.bin | 3 + port/sceneryRam_SD14B.bin | 3 + port/sceneryRam_SD2.bin | 3 + port/sceneryRam_SD3.bin | 3 + port/sceneryRam_SD4.bin | 3 + port/sceneryRam_SD5.bin | 3 + port/sceneryRam_SD6.bin | 3 + port/sceneryRam_SD7A.bin | 3 + port/sceneryRam_SD7B.bin | 3 + port/sceneryRam_SDS1.bin | 3 + port/src/aircraft.c | 681 + port/src/apple2hires.c | 83 + port/src/audio.c | 258 + port/src/camera.c | 176 + port/src/chunk5Setup.c | 556 + port/src/chunk5Transform.c | 785 + port/src/coursePlotter.c | 161 + port/src/cpu6502.c | 408 + port/src/fixture.c | 63 + port/src/font.c | 108 + port/src/framebuffer.c | 107 + port/src/fs2math.c | 238 + port/src/hires.c | 348 + port/src/hud.c | 137 + port/src/instruments.c | 622 + port/src/main.c | 1200 ++ port/src/math6502.c | 63 + port/src/needleData.c | 131 + port/src/palette.c | 55 + port/src/panelDigits.c | 174 + port/src/projection.c | 157 + port/src/radios.c | 397 + port/src/renderer.c | 393 + port/src/sceneryData.c | 342 + port/src/sceneryProjection.c | 441 + port/src/sceneryVm.c | 2837 ++++ port/src/timeOfDay.c | 103 + port/src/title.c | 86 + port/src/wind.c | 155 + port/src/world.c | 241 + port/src/ww1ace.c | 712 + port/tools/chunk5SetupTest.c | 209 + port/tools/dumpStations.c | 277 + port/tools/extractCityPolygons.c | 68 + port/tools/extractFS2Sections.c | 207 + port/tools/extractscenery.c | 103 + port/tools/extractstations.c | 519 + port/tools/findPolySections.c | 97 + port/tools/fs2trace.c | 2234 +++ port/tools/imgDiagnose.sh | 89 + port/tools/imgDiff.c | 271 + port/tools/imgStats.c | 261 + port/tools/matrixProbe.c | 148 + port/tools/nib2dsk.c | 236 + port/tools/prodosextract.c | 264 + port/tools/sampleRow.c | 107 + port/tools/sceneryExtract.py | 524 + port/tools/sceneryRender.py | 389 + res/README.md | 4 + res/loading_panel.bin | 3 + src/README.md | 68 + src/asm.cfg | 13 + src/chunk2.s | 679 + src/chunk3.s | 3178 ++++ src/chunk4.s | 3875 +++++ src/chunk5.s | 13320 ++++++++++++++++ src/complete.s | 136 + src/macros.inc | 156 + src/sceneryVm.c | 0 127 files changed, 46326 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 SESSION_RECOVERY.md create mode 100644 orig/1_4000-5fff create mode 100644 orig/2_f600-fbff create mode 100644 orig/3_d300-f3ff create mode 100644 orig/4_0200-25ff create mode 100644 orig/5_6000-b3df create mode 100644 orig/FS2#062499 create mode 100644 orig/flight simulator 2 with scenery PRODOS (san inc pack).po create mode 100644 port/Makefile create mode 100644 port/PORT_64K_AUDIT.md create mode 100644 port/PORT_STATUS.md create mode 100644 port/docs/scenery_opcodes.md create mode 100644 port/include/aircraft.h create mode 100644 port/include/apple2hires.h create mode 100644 port/include/audio.h create mode 100644 port/include/camera.h create mode 100644 port/include/chunk5Setup.h create mode 100644 port/include/chunk5Transform.h create mode 100644 port/include/coursePlotter.h create mode 100644 port/include/cpu6502.h create mode 100644 port/include/fixture.h create mode 100644 port/include/font.h create mode 100644 port/include/framebuffer.h create mode 100644 port/include/fs2math.h create mode 100644 port/include/hires.h create mode 100644 port/include/hud.h create mode 100644 port/include/instruments.h create mode 100644 port/include/math6502.h create mode 100644 port/include/needleData.h create mode 100644 port/include/palette.h create mode 100644 port/include/panelDigits.h create mode 100644 port/include/projection.h create mode 100644 port/include/radios.h create mode 100644 port/include/renderer.h create mode 100644 port/include/sceneryData.h create mode 100644 port/include/sceneryProjection.h create mode 100644 port/include/sceneryStationsData.h create mode 100644 port/include/sceneryVm.h create mode 100644 port/include/timeOfDay.h create mode 100644 port/include/title.h create mode 100644 port/include/types.h create mode 100644 port/include/wind.h create mode 100644 port/include/world.h create mode 100644 port/include/ww1ace.h create mode 100644 port/sceneryRam_FS2.1.bin create mode 100644 port/sceneryRam_FS2.1_chicago.bin create mode 100644 port/sceneryRam_FS2.1_frozen.bin create mode 100644 port/sceneryRam_FS2.1_la.bin create mode 100644 port/sceneryRam_FS2.1_ny.bin create mode 100644 port/sceneryRam_FS2.1_seattle.bin create mode 100644 port/sceneryRam_SD1.bin create mode 100644 port/sceneryRam_SD11.bin create mode 100644 port/sceneryRam_SD13.bin create mode 100644 port/sceneryRam_SD14A.bin create mode 100644 port/sceneryRam_SD14B.bin create mode 100644 port/sceneryRam_SD2.bin create mode 100644 port/sceneryRam_SD3.bin create mode 100644 port/sceneryRam_SD4.bin create mode 100644 port/sceneryRam_SD5.bin create mode 100644 port/sceneryRam_SD6.bin create mode 100644 port/sceneryRam_SD7A.bin create mode 100644 port/sceneryRam_SD7B.bin create mode 100644 port/sceneryRam_SDS1.bin create mode 100644 port/src/aircraft.c create mode 100644 port/src/apple2hires.c create mode 100644 port/src/audio.c create mode 100644 port/src/camera.c create mode 100644 port/src/chunk5Setup.c create mode 100644 port/src/chunk5Transform.c create mode 100644 port/src/coursePlotter.c create mode 100644 port/src/cpu6502.c create mode 100644 port/src/fixture.c create mode 100644 port/src/font.c create mode 100644 port/src/framebuffer.c create mode 100644 port/src/fs2math.c create mode 100644 port/src/hires.c create mode 100644 port/src/hud.c create mode 100644 port/src/instruments.c create mode 100644 port/src/main.c create mode 100644 port/src/math6502.c create mode 100644 port/src/needleData.c create mode 100644 port/src/palette.c create mode 100644 port/src/panelDigits.c create mode 100644 port/src/projection.c create mode 100644 port/src/radios.c create mode 100644 port/src/renderer.c create mode 100644 port/src/sceneryData.c create mode 100644 port/src/sceneryProjection.c create mode 100644 port/src/sceneryVm.c create mode 100644 port/src/timeOfDay.c create mode 100644 port/src/title.c create mode 100644 port/src/wind.c create mode 100644 port/src/world.c create mode 100644 port/src/ww1ace.c create mode 100644 port/tools/chunk5SetupTest.c create mode 100644 port/tools/dumpStations.c create mode 100644 port/tools/extractCityPolygons.c create mode 100644 port/tools/extractFS2Sections.c create mode 100644 port/tools/extractscenery.c create mode 100644 port/tools/extractstations.c create mode 100644 port/tools/findPolySections.c create mode 100644 port/tools/fs2trace.c create mode 100755 port/tools/imgDiagnose.sh create mode 100644 port/tools/imgDiff.c create mode 100644 port/tools/imgStats.c create mode 100644 port/tools/matrixProbe.c create mode 100644 port/tools/nib2dsk.c create mode 100644 port/tools/prodosextract.c create mode 100644 port/tools/sampleRow.c create mode 100755 port/tools/sceneryExtract.py create mode 100644 port/tools/sceneryRender.py create mode 100644 res/README.md create mode 100644 res/loading_panel.bin create mode 100644 src/README.md create mode 100644 src/asm.cfg create mode 100644 src/chunk2.s create mode 100644 src/chunk3.s create mode 100644 src/chunk4.s create mode 100644 src/chunk5.s create mode 100644 src/complete.s create mode 100644 src/macros.inc create mode 100644 src/sceneryVm.c diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d094e3f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.pdf filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +A2.* filter=lfs diff=lfs merge=lfs -text +orig/* filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..956dabd --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.claude/ +loader/ +out/ +port/bin/ +port/obj/ +port/screenshots/ +tmp/ +downloads/ +port/tools/__pycache__/ +extracted_db/ + +*.bin.* +*~ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..8cdcefa --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1450 @@ +# FS2 — Apple II Flight Simulator II Architecture + +This document describes the architecture of *Flight Simulator II* for the Apple II as +reconstructed from this disassembly. The original was written by Bruce Artwick / Sublogic +(1984). The build target here is the qkumba ProDOS port that reassembles byte-identical to +the shipped chunks via cc65. + +Conventions used below: + +* Hex addresses are written `$1234`; binary is `%01010101`. +* `Lxxxx` are raw labels from the disassembly; semantic names like `MainGameEntry` are + aliases we've added. Both resolve to the same byte address. +* Citations are `file:line` against `src/`. +* "ZP" = zero page (`$00..$FF`). + +--- + +## Table of Contents + +1. [System Overview](#1-system-overview) +2. [Boot & Initialization](#2-boot--initialization) +3. [Main Loop](#3-main-loop) +4. [Zero Page Atlas](#4-zero-page-atlas) +5. [Aircraft State Model](#5-aircraft-state-model) +6. [Physics](#6-physics) +7. [Scenery System](#7-scenery-system) +8. [3D Pipeline](#8-3d-pipeline) +9. [Rasterization](#9-rasterization) +10. [Instrument Panel](#10-instrument-panel) +11. [Engine Model](#11-engine-model) +12. [Input](#12-input) +13. [64K Patch Mechanism](#13-64k-patch-mechanism) +14. [WW1 Ace Mode](#14-ww1-ace-mode) +15. [Self-Modifying Tricks](#15-self-modifying-tricks) +16. [Mode Library](#16-mode-library) +17. [Known Unknowns](#17-known-unknowns) + +--- + +## 1. System Overview + +### 1.1 Memory Map + +``` +$0000-$00FF Zero page (control state + matrix + dispatcher cursor) +$0100-$01FF Stack +$0200-$25FF chunk4 (~9 KB: initial ZP data, instrument panel bitmaps, + pixel-list tables, message/digit renderer, + sin/cos tables, multiply primitives, + ProDOS read driver) +$2600-$3FFF Scratch / disk-load staging +$4000-$5FFF chunk1 (loading-panel hires page, displayed during boot) +$6000-$B3DF chunk5 (~21 KB: scenery interpreter, 3D pipeline, + rasterizer, instrument compositor, + key dispatch, main loop, boot init, + 64K patch table) +$B3E0-$BFFF chunk5 tail / scenery RAM +$A800-$BFFF Live scenery bytecode area (overwritten as the dispatcher + walks; loader streams sections in via L1E03 cursor) +$C000-$CFFF I/O soft switches & slot ROM +$D000-$F3FF chunk3 (~8.5 KB: ATIS/COM message scroller, ADF model, + reality-mode + crash handler, + engine model w/ magnetos (64K), + scenery sector loader, hide/show panel + instruments, war report) +$F400-$F5FF Free / data +$F600-$FBFF chunk2 (~1.5 KB: course plotter, altimeter 10K-foot + hand, wind/turbulence, demo-mode 64K) +$FC00-$FFFF ROM (Apple II monitor / ProDOS) + + language-card reset/IRQ vectors in 64K mode +``` + +The whole image links as a single segment in `src/asm.cfg` starting at `$4000` and is +sliced into the original disk chunks by `dd` in the Makefile. This matches the original +disk layout, where the loader pulls each chunk from a known sector range. + +**Dead pad bytes.** The binary carries ~455 bytes of confirmed-unused filler: +* `Chunk2TrailingData` (132 bytes, `$FB7C..$FBFF`) at the tail of chunk2. +* Unnamed pad (208 bytes, `$D300..$D3CF`) at the head of chunk3. +* Unnamed pad (115 bytes, `$86CD..$873F`) between `HorizonANDMask` and + `TogglePauseRelay` in chunk5 — likely page-alignment padding. + +All three contain the same repeating `$FF $FF $00 $00` 16-byte pattern with `$76` +at certain offsets. They are not referenced by any code, indirect pointer, +scenery dispatcher walk, or 64K patch — verified by exhaustive grep of all +source files and the patch table. They are 1984-era residue from the original +Sublogic assembler's output (likely leftover stack frames at the time the disk +image was authored). + +### 1.2 Build Topology + +| Chunk | Address Range | Size | Role | +|-------|---------------|-------|------| +| 1 | $4000-$5FFF | 8 KB | Loading-panel hires bitmap (data only) | +| 2 | $F600-$FBFF | 1.5 KB| Course plotter, altimeter 10K hand, wind, demo | +| 3 | $D300-$F3FF | 8.5 KB| Scenery loader, ATIS, engine model, fail/crash, COM messages | +| 4 | $0200-$25FF | 9 KB | Instrument render, pixel lists, math, message draw, disk I/O | +| 5 | $6000-$B3DF | 21 KB | Main game logic | + +Chunks 4 and 5 are the engine; chunks 2 and 3 hold the slower / 64K-only systems and +get DMA'd in over the language card. Chunk 1 is just a splash bitmap. + +### 1.3 48K vs 64K Modes + +The game ships in two configurations: + +* **48K mode** is what runs out of the box. Many features are stubs (no magnetos, no + ADF lookup, no separate fuel mixture, no course plotter, no ATIS, no day/night model). + Numerous routines in chunk5 are written as `NoOp` placeholders. +* **64K mode** uses the language card (`LCBANK2`). On boot, `ProbeLCMemory` + (`src/chunk5.s:12465`) detects extra RAM. If present, `Apply64KPatchTable` + (`src/chunk5.s:11486`) walks a 5-byte-entry patch table and overwrites ~30 sites in + the running code, swapping `NoOp` stubs and 48K simple paths for full-featured 64K + paths in chunks 2 and 3. + +This is how the same disk image fits in both machines without runtime branches — +the patches are applied once at startup and never re-checked. + +--- + +## 2. Boot & Initialization + +### 2.1 Reset Vectors + +The shipped binary doesn't ship hardware reset vectors. Boot is launched by ProDOS as +a binary at `$4000`. In 64K mode, `Apply64KPatchTable` writes `ResetInterruptHandler` +($AC15) into the language-card NMI/Reset/IRQ vectors at `$FFFA/$FFFC/$FFFE` so that +post-boot resets land back in the game (`src/chunk5.s:12536-12539`). Because bank 2 +is selected when the write happens, the vectors persist past the boot sequence. + +### 2.2 `ResetInterruptHandler` and `InitFromReset` + +`ResetInterruptHandler` is two `jmp InitFromReset` instructions back-to-back +(`src/chunk5.s:11906`). `InitFromReset` (`src/chunk5.s:12082`) does: + +1. `ldx #$3F / txs` — reset the stack. +2. `jsr LoaderNoOpSuccess` — open / handshake the disk loader (chunk4). +3. `jsr ReloadGameChunks` — re-pull chunk4 (sectors $00..$06 to $0200) and chunk5 + (sectors $08..$19 to $6000). The 32 bytes at `LA7E0` (= the scenery dispatcher + entry point that chunk5 overwrites in its own image) are stashed via `LABCC` then + restored. +4. `jsr LoadIntroPanel` — pulls 8 KB of chunk1 (sectors $1A..$21) into hires page 1 at + `$4000`. This is the splash screen visible during the boot phase. +5. `jsr ProbeLCMemory` — 64K detection (see below). +6. `jsr SceneryCopyLoadedSection` (= `LA6F9`) — finalize the first scenery section. +7. `jsr InitZeroPage` + `jmp MainGameEntry`. + +Boot failures land at `InitFailureExit` (`src/chunk5.s:12105`) which jumps to +`L1F89` = `SwapPagesWithStash` (chunk4) — a page-swap routine that stashes the +running aircraft state into a save region and then returns through the BRK / +Ctrl-Y / IRQ vector chain. On boot failure this effectively aborts back to +ProDOS while preserving the game state for a later resume. + +### 2.3 64K Probe (`ProbeLCMemory`) + +`ProbeLCMemory` (`src/chunk5.s:12465`) enables language-card bank 2 read/write by +reading the `LCBANK2` ($C083) soft switch twice (the standard idiom). It then writes +the X register through all 256 values into `$D000` and reads back to confirm +write-and-read both work. Any mismatch jumps to `LAE9E` (a bare RTS), leaving +`Has64K = 0`. On success the patch table is applied and `Has64K = 1`. + +### 2.4 `MainGameEntry` + +`MainGameEntry` (`src/chunk5.s:12109`) is the top-level game start: + +1. `jsr Apply64KPatchTable` — no-op on 48K, runs the patch list on 64K. +2. `jsr InitGraphicsScreens` — sets the IIE soft switches for hires page 1 + visible, double-buffer to page 2. +3. `jsr PromptColorOrBW` — color/monochrome prompt (only matters for the dither tables). +4. `jsr InitInstruments` — paints the static cockpit panel bitmaps. +5. `jsr InitInstrumentSaveBuffers` — captures the panel bitmap's hires bytes into the + per-instrument save buffers in chunk3 ($E800-$FC78); see §10. +6. `jmp MainLoopEntry`. + +`MainLoopEntry` (= `L877F`, `src/chunk5.s:6660`) runs `PatchSlot_FrameSync` once +before falling into the regular `MainLoop`. + +--- + +## 3. Main Loop + +### 3.1 Frame Architecture + +Each iteration of `MainLoop` (`src/chunk5.s:6661`) does roughly: + +``` +add #$0D to $33 (frame work-cost accumulator; rolls over slowly) +if EditModeFlag or ModeLibraryAction: jsr PatchSlot_PreMode +jsr PatchSlot_FrameSync +if SlewMode: jsr ApplySlewDeltas, else: jsr IntegratePhysicsStep +jsr ApplyWind (64K patch slot) +jsr LA60C (DrawATISMessage in 64K) +jsr LoadDispatcherPointer +jsr SetupViewProjection ; rebuild rotation matrix +ldx #$00 / lsr $089A (crash counter); bcc skip / lda $0899; bne skip; inx +jsr ShowSimpleCrashMessage ; 64K patches to HandleCrashOrSplash +jsr FlipPagesFillViewportRelay ; swap hires pages +jsr ProcessScenery ; the big one — see §7 +jsr UpdateCoursePlotter ; 64K patch +jsr AnimateVOR2Needle +jsr DrawRPM +jsr DrawVOR1IndicatorChanges +jsr DrawVOR2IndicatorChanges +jsr IntegrateClimbRate +jsr MaybeDrawSlewOverlays +jsr UpdateTurnCoordinator +jsr DrawHeading +jsr DrawMagCompass +jsr UpdateInstrumentLights ; 64K patch +if WW1AceMode: jsr PatchSlot_Gunsight (draw gunsight overlay) +jsr DrawViewOverlays ; 64K patch +jsr UpdateAltimeterPose +jsr UpdateAirspeedDerivedValue +jsr HideOrShowInstruments ; 64K patch +if DemoMode: jsr DemoMode64K ; 64K patch +jsr ... (UpdateCounter dispatch) +jmp FinishMainLoop ; inc UpdateCounter; jmp MainLoop +``` + +### 3.2 `UpdateCounter` 4-phase dispatch + +To amortize "slow" jobs across multiple frames, the loop's tail dispatches on the +low 2 bits of `UpdateCounter` (`src/chunk5.s:6731-6779`): + +| UpdateCounter & $03 | Phase work | +|---------------------|------------| +| `00` | ComputeDayPhase, MaybeBootDOS, fuel-left gauge, ComputeWindComponents. Every 8th frame additionally: DrawDME, NAV1/NAV2 lookups, ADF lookup | +| `01` | fuel-right gauge, altimeter 10K hand, VOR1/VOR2 bearings, magnetic heading, ADF indicator | +| `10` | oil temperature, auto-trim + yaw decay | +| `11` | oil pressure, reality-mode hook, VOR/heading update (joins phase 01 tail) | + +Each phase ends with either `CheckFlightEnvelope + UpdateSlipSkid + maybe joystick +calibration` (phases 00/10) or a timer rotate (phases 01/11), then `FinishMainLoop`. + +### 3.3 `PatchSlot_*` Trampolines + +Three 4-byte slots in chunk5 (`src/chunk5.s:11917-11933`) contain `jsr LoadSceneryFileN +/ brk brk brk` initially. Mode transitions (mode-library load, WW1 gunsight, etc.) +self-modify these slots — the `brk brk brk` padding gives room to widen the +`jsr` into a `jmp` with a different target, or to inline additional opcodes: + +* `PatchSlot_PreMode` — pre-mode initialization, runs when entering edit mode +* `PatchSlot_Gunsight` — WW1 gunsight overlay +* `PatchSlot_FrameSync` — per-frame sync hook + +The self-modify sites are at `$8758/$875B/$8761/$AA03` (see the comment block at +`src/chunk5.s:11912`). + +--- + +## 4. Zero Page Atlas + +The 6502 zero page is packed densely. Key allocations: + +| Range | Purpose | +|-------|---------| +| `$08-$10` | V1 shadow (snapshot for re-chain across draws) | +| `$18-$1F` | xform-B accumulators (24-bit, 3 axes × hi/mid/lo) | +| `$21-$22` | Bresenham dx / dy | +| `$25-$27` | Polygon scan-fill max-edge / row-bounds / span column | +| `$28-$2A` | Altimeter angle bytes (1000s / 10000s / heading) | +| `$2D-$2E` | Cached-vertex pointer ($0140 + idx*8) | +| `$32-$33` | Frame work-cost accumulator | +| `$3E-$3F` | Message scroll dest pointer | +| `$4A-$52` | Section base (24-bit, 3 axes; signed) | +| `$53-$54` / `$EF-$F0` | Polygon scan-fill min/max X/Y | +| `$5A-$5D` | Aircraft world X (24-bit) | +| `$5E-$60` | Altitude (= world Y; $5F/$60 straddle the 8-bit boundary) | +| `$61-$65` | Aircraft world Z (24-bit) | +| `$66-$6B` | Section-relative origin offset = camera ZP image | +| `$6C-$6D` | Pitch (signed 16-bit) | +| `$6E-$6F` | Bank | +| `$70-$71` | Yaw | +| `$78-$89` | Active 3×3 rotation matrix (18 bytes, signed 16-bit per element) | +| `$8B-$8C` | Scenery dispatcher cursor | +| `$8E-$8F` | Hires row pointer | +| `$94-$95` | NeedlePosTable pointer (DrawIndicatorDialNeedle) | +| `$96-$97` | DrawIndicatorDialNeedle data pointer | +| `$98-$99` | TestSceneryRange bound | +| `$9A-$9B` / `$BA-$BB` | Hires row ptrs for the two pages | +| `$9E-$A2` | Pixel-list scratch | +| `$A5-$A8` | Loader src/dst pointers | +| `$A9-$AC` | Multiply scratch | +| `$AD` | Frame-stash flag (variant 6 of $07/$24) | +| `$B0-$B1` | Stashed X / Y across nested calls | +| `$B5` | Vertex output index in clip passes | +| `$B6-$B7` | Generic 16-bit scratch / Set3DigitString input | +| `$B8-$B9` | Set3DigitString string pointer | +| `$BE-$BF` | Compute-distance / Set3DigitString scratch | +| `$C2-$C5` | ScaleC2ByAX inputs | +| `$C6-$C7` | Cost accumulator | +| `$C9-$D2` | Vertex 1 (X/Y/Z hi+lo + outcode-shadow at $D1/$D2) | +| `$CA` | V1 outcode | +| `$CB-$D0` | V1 transformed coords | +| `$D3` | V2 outcode | +| `$D4-$D9` | V2 transformed coords | +| `$DA-$E2` | V2 snapshot for EmitClippedLine cleanup | +| `$E5` | Stream-emit cursor for xform-A | +| `$E6-$E7` | Pixel-list current position | +| `$E9` / `$EA` | Line endpoint 1 column / row (140-wide, 192-tall) | +| `$EB` / `$EC` | Line endpoint 2 column / row | +| `$F1-$F9` | Bresenham step state | +| `$FA` | `InputMode` | +| `$FB-$FD` | Instrument operational flags (3 bytes) | +| `$FE` | scratch | + +Instrument operational flags `$FB/$FC/$FD` are bit-masks — bits set when the +corresponding gauge is operational. `HideOrShowInstruments` (chunk3) compares them +to the previous value and shows / hides each instrument's bitmap. + +--- + +## 5. Aircraft State Model + +The full aircraft state lives in two regions: zero page (high-frequency-access values +like position, attitude, velocities) and the `$08xx-$0Axx` "expanded state" region +in chunk4 (instrument values, key state, mode flags). + +### 5.1 Position and orientation + +* World position: `$5A-$65`, three signed 24-bit values (X, Y=Altitude, Z) in scenery + units. The unit is approximately 1/3 metre — used by the scenery system and + converted to/from metres at the boundaries. +* Attitude: `$6C-$71`, three signed 16-bit values (pitch, bank, yaw). The high byte + is the "byte angle" (0-256 = full circle), the low byte is sub-byte interpolation. +* `ViewDirection` (`$0A70`) — a 16-step view-around-the-aircraft index in $00..$0F. + Scaled ×16 into a byte angle and folded into yaw via `SetupViewProjection`. + +### 5.2 Controls + +| Name | Address | Meaning | +|------|---------|---------| +| `YokeHorizPos` (= `$09B0`) | $09B0 | Aileron position, signed | +| `YokeVertPos` | chunk4 | Elevator yoke position | +| `RudderPos` | chunk4 | Rudder position | +| `ElevatorTrim` | chunk4 | Trim setting (-84..+84) | +| `$0A5F` | | Flap position (0/32/64/...) | +| `$0A49` | | Mixture setting (0..7) | +| `$0A58` | | Carb heat (0=off, FF=on) | +| `PanelLights` | chunk4 | Instrument-light flag | +| `MagnetoState` | chunk4 | Magneto state (0=off, 1=L, 2=R, 3=both, 4=start) | +| `$0A6F` | | Throttle setting | +| `$0998` | | Fuel tank select | + +### 5.3 Derived values + +| Name | Address | Meaning | +|------|---------|---------| +| `Airspeed` (= `L0A11`) | $0A11 | 16-bit; high byte drives RPM curves + stall envelope | +| `$0990` | | Smoothed RPM displayed on tachometer | +| `$0843` | | Climb rate (signed 16-bit) | +| `$0A36/$0A37` | | Climb-rate indicator smoothed value | +| `$0830/$0831` | | Magnetic-compass smoothed angle | +| `$0A15/$0A16` | | Raw heading (16-bit, signed turn rate) | +| `$2A` | | Final compass byte for display | + +--- + +## 6. Physics + +### 6.1 IntegratePhysicsStep + +`IntegratePhysicsStep` (`src/chunk5.s:8908`) is called every frame in non-slew mode. +It: + +1. Reads `$09A5-$09A8` (body-velocity X/Z) and `$09D5/$09D6` (Y). +2. Adds the world-frame velocity components to `$5A-$65` (position). The world-frame + components are computed earlier in `ComputeFlightDerivedValues` by multiplying the + body velocity by the inverse rotation matrix. +3. If altitude `$5F/$60` would go below ground ($0000/$03), clamps it. +4. If the aircraft just touched down (`$089E` non-zero), calls + `ClampAltitudeOnTouchdown` to recompute altitude from climb rate. +5. Updates the live pitch/bank/yaw at `$6C-$71` by integrating angle deltas. +6. Sets `OnGroundFlag` based on the altitude clamp. +7. Triggers crash codes ($0834) when ground contact happens with bad pitch / bank. + +### 6.2 ComputeFlightDerivedValues + +Called near the top of the loop (`src/chunk5.s:8545`). Recomputes ~15 derived +quantities from the live pitch/bank/yaw and airspeed: + +* sin(pitch), cos(pitch), sin(bank), cos(bank) via `L1763` / `L1778` (chunk4 trig). +* Rudder authority gauge. +* Body-frame X/Z velocity contributions (`$0A19-$0A1C`). +* Stall envelope: when the airspeed-derived term `$09AC` enters the danger band, + lights the fault state `$08B6` / `$0899`. +* Climb rate (`$0A0F/$0A10`) from pitch/bank/airspeed scale chain. +* Turn rate (`$09CD/$09CE`). +* Magnetic compass slew via `$08B2/$08B3`. +* Sideslip wrap: if the sideslip integrator at `$09AD` crosses ±90°, flips signs of + `$09AE/$09B0/$09E5` to keep the indicator symmetric. + +### 6.3 IntegrateClimbRate + +`IntegrateClimbRate` (`src/chunk5.s:8843`) slews the climb-rate needle and altitude +by 0/±1/±2 units per frame. The inner step (`ICR_Step`) compares climb rate against +the needle target, picks a +1/-1/0 nudge, and applies the OPPOSITE nudge to +altitude — so when the climb rate is +N, the needle moves up AND altitude increases. +The function uses a clever `jsr ICR_Step / fall-through to ICR_Step` trick to run +the step twice per call, giving 2 ticks of integration per frame. + +A `beq L89A9` jump in the middle handles the "climb rate exactly matches the +needle" case as a 0-nudge shortcut. + +### 6.4 UpdateAutoTrimAndYaw + +Per-frame (`src/chunk5.s:9227`) — pulls `$09AF/$09B0` (yaw integrator) toward zero +by 5 units when the rudder is centred, clamping at zero. Also runs bomb-drop +housekeeping via `$0838`. + +### 6.5 Wind (64K) + +`ApplyWind` (chunk2) is patched into the main loop via `P64K_B`. It picks one of +three wind layers (`WindLayer1/2/3`) based on altitude, multiplies by the wind +direction's sin/cos, and adds to the body velocity. Skipped entirely in 48K mode. + +### 6.6 CheckFlightEnvelope + +Tests airspeed and attitude for stall/g-load violations and sets crash codes. + +--- + +## 7. Scenery System + +The scenery system is the biggest single piece of the codebase. It consists of a +disk-backed loader (chunk3), a paged scenery RAM area (`$A800-$BFFF`), and a +bytecode interpreter (chunk5 `ProcessScenery`) that walks per-section streams of +~70 different opcodes. + +### 7.1 File Format Overview + +A scenery file is a sequence of "sections". Each section is a contiguous stream of +opcode-bytes. Opcodes consume variable-length payloads (1 to 14+ bytes). Some +opcodes are flow-control (jump, sub-invoke, range cull); some emit vertices and +draws; some carry per-section frame data (offsets and axis permutations). + +Sections live in scenery RAM at `$A800-$BFFF`. The dispatcher cursor `$8B/$8C` +walks forward through this region until it hits a stream-end opcode or an opcode +≥ `$46` (invalid → end). Many sections call into others via `$0B` JumpRelative or +`$18` SubInvoke. + +### 7.2 Loader + +The scenery loader is in chunk3 (~`$D3xx-$D5xx`). Key entry points: + +* `SceneryLoaderEntry1..7` (chunk3 `$DA-..` block) — public callable thunks. +* `SceneryReadFixed` — read N pages (256-byte units) into the buffer at L1E03, + advancing the L1E03 destination cursor. +* `SceneryReadUntilC0` — read bytes until the source pointer crosses into ROM space. +* `SceneryEnsureOpen` — initialize / re-open the disk read stream. +* `ComputeBlockFromSector` / `FetchSectorFromDisk` — chunk4 helpers. + +The loader has two coupled state cells: `L1E01` (16-bit sector counter) and +`L1E03/L1E04` (destination pointer, advanced by every successful read). Multiple +in-flight loads share these by save/restore at the loader entries. + +`LoadSceneryFile0..4` (chunk5 `$AB37`) each load a different per-purpose section +(world geometry, default scenery, etc.) by setting L1E01/L1E03 from the descriptor +words `LA60F..LA619`. + +### 7.3 Dispatcher + +`ProcessScenery` (`src/chunk5.s:1245`) is invoked per frame. It: + +1. Sets `$8B/$8C` to the section entry from `LA7E0` (a 16-bit address). +2. Calls `SceneryInterpreterEntry` → `SceneryInterpreterStep` to fetch the next + opcode. +3. `SceneryDispatch` (`asl A / lda SceneryOpcodeTable,X / jmp (L00A5)`) jumps to + the handler. Each handler advances `$8B/$8C` past the record's bytes and tail- + jumps back to `SceneryInterpreterEntry`. +4. Continues until an opcode byte with bit 7 set, or ≥ `$46`. Both terminate the + pass. + +### 7.4 Opcode Table + +`SceneryOpcodeTable` (`src/chunk5.s:1158`) is 70 16-bit pointers covering opcodes +`$00..$45`. Opcodes above $45 are stream-end markers. + +#### Vertex emit +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $00 | SceneryOpEmitV1XformAndPlot | 7 | Xform-A on V1, plot single pixel. (1 opcode + 6-byte XYZ stream → TransformVertex80C5 internally advances +7.) | +| $01 | SceneryOpEmitV1Xform80C5 | 7 | Xform-A on V1, no draw (line start) | +| $02 | SceneryOpEmitV2Xform80C5 | 7 | Xform-A on V2 → EmitClippedLine V1→V2 | +| $40 | SceneryOpEmitV1Xform7EBC | 5 | Xform-B (XZ only) on V1. (1 opcode + 4-byte XZ stream → TransformVertex7EBC internally advances +5.) | +| $41 | SceneryOpEmitV2Xform7EBC | 5 | Xform-B on V2 → EmitClippedLine | + +#### Vertex cache (8-byte slots at $0140 + idx*8) +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $31 | SceneryOpRefreshCachedXform80C5 | 8 | Xform-A, write back to cache slot. (Opcode + 1-byte vtx-idx via SetVertexPointerFromStream/L6987 + 6-byte xform-A stream.) | +| $32 | SceneryOpVertexCachedV1 | 2 | Load cached V1 (opcode + 1-byte vtx-idx via L6987). | +| $33 | SceneryOpVertexCachedV2 | 2 | Load cached V2 → EmitClippedLine | +| $35 | SceneryOpVertexCachedDraw | 2 | Load cached V1 + plot pixel | +| $42 | SceneryOpRefreshCachedXform7EBC | 6 | Xform-B, write back to cache slot. (Opcode + 1-byte vtx-idx + 4-byte xform-B stream.) | + +#### Curve +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $2B | SceneryOpEmitCurve | 9 | Two-vertex curve → 8 segments via midpoint subdivision. xform-B reads 4 bytes for V1, `dec $8B` re-reads, xform-B reads 4 bytes for V2 (= 5 - 1 + 5 = 9 total inc. opcode). | + +#### Frame setup +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $07 | SceneryOpEnterLocalFrame | 14 | Set local frame $66..$6B from stream; 5 axis-permutation variants in L6BB0 | +| $24 | SceneryOpPushOriginWithStash | 8 | Same as $07 but stashes prior frame for `$AD` flag | + +#### Cull / branch +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $04 | SceneryOpCullByOutcodeList | 4+N | AND outcodes of listed vertex indices; cull if all share half-plane. Length = 4 (header) + N (list bytes, terminated by sentinel). | +| $0B | SceneryOpJumpRelative | — | Tail-jumps to SceneryJumpToFetched; sets cursor from inline 16-bit target | +| $13 | SceneryOpJumpIfBeyondXY | — | TestSceneryRangePair on XY; passes ⇒ jump, else continue | +| $14 | SceneryOpJumpIfBeyondXYZ | — | Same shape, 3 axes | +| $20 | SceneryOpCullIfOutside1 | 9 | Read target + run TestSceneryRange once | +| $21 | SceneryOpCullIfOutside2 | 15 | Two range tests | +| $22 | SceneryOpCullIfOutside3 | 21 | Three range tests | +| $23 | SceneryOpJumpIfBitsClear | 7 | jump if `(mask1 & *ptr) == 0 && (mask2 & *(ptr+1)) == 0` | +| $28 | SceneryOpJumpIfWordCompare | 8 | 3 sub-modes (eq, signed-lt, alt-lt) compares 16-bit values at two ptrs | + +Failure path for `$20..$22` pops to a saved jump target via +`TestSceneryRangeReject → L00A5`. Success uses fixed advance. + +#### Data / records +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $03 | SceneryOpCall64K_2 | 6 | 48K: skip; 64K: tail-call SceneryRotatedTransform (chunk3) | +| $05 | SceneryOpADFRecord | 9 | 48K: skip; 64K: patched to JMP LookupADFStation | +| $0E | SceneryOpCall64K | — | 48K: rts; 64K: chunk3 callback | +| $1A | SceneryOpWriteWord | 5 | `*dst = *src` (16-bit), dst+src ptrs inline | +| $1D | SceneryOpNAVRecord | 11 | NAV station; checks $08F3 then matches frequency | +| $1E | SceneryOpCOMRecord | inline | COM/airport record; length byte at offset 1 | +| $25 | SceneryOpStoreImmWord | 5 | `*dst = imm16`; dst ptr (2 bytes) + 2 literal bytes (= 4-byte payload + opcode). | +| $29 | SceneryOpCopyToD2 | 1 | Two branches: line-emit (copy V2 → shadow) or polygon-fill | + +#### Misc +| Op | Handler | Len | Summary | +|----|---------|-----|---------| +| $06 | SceneryOpDrawLine | 5 | Reads 4 raw screen-coord bytes into `$E9..$EC` and tail-jumps to `DrawColorLine` (HUD-style overlay; bypasses 3D pipeline). | +| $09 / $0A | SceneryOpSkip3 | 3 | Pure skip | +| $0D | SceneryOpHeader | 6 | Section-header + demand-load trigger. Latches 5 inline payload bytes into `$08E5..$08E9` (scale/range/tile-delta/slot index), and (when bit 7 of `$08E9` is clear) cumulatively advances the saved section-base cursor `$08E7` by the current cursor `$8B`. Masks `$08E9` to slot index 0..3 then calls `SceneryHeaderLoadIfMiss` via L8776: on a cache miss invalidates newer slots and DMA-fetches the section bytecode from disk via `SceneryHeaderRunSection`; on a cache hit returns immediately. | +| $0E | SceneryOpCall64K | 0 | 48K: `rts` immediately (= `SceneryOpReturn`); 64K: tail-jumps `SceneryOp64KCallback` (chunk3). No advance — pops the dispatcher recursion. | +| $11 | SceneryOpSkip1 | 1 | Pure 1-byte no-op. | +| $12 | SceneryOpSetColor | 2 | Inline byte indexes `ToHiresColorTable` (16 entries) to pick 1-of-8 hires colours, then calls `SetPixelDrawMode` to patch the 4 line/span ORA/AND opcode + 4 operand sites. | +| $18 | SceneryOpSubInvoke | 3 | Push cursor+3; jump to relative target; recurse `SceneryInterpreterStep`. | +| $19 | SceneryOpReturn | 0 | `rts` — pops one level of recursion. | +| $1B | SceneryOpModeWhite | 1 | Restores the line drawer to day mode: writes `STY $B1` ($84 $B1) at `DCLYMajorSaveByte` and `LDA (HiresRowPtr),Y` ($B1 $8E) at `DCLXMajorReadByte`, then `SetPixelDrawMode #$03` (HIRES_WHITE1). | +| $1C | SceneryOpDayOnly | 1 | If `$083C & $01` (= day phase) → skip; else patches `DCLYMajorSaveByte` to `BPL +$2E` ($10 $2E) and `DCLXMajorReadByte` to `BPL +$16` ($10 $16), then `SetPixelDrawMode #$01` (HIRES_VIOLET). The BPL offsets jump past the actual paint store so night-only objects don't render. | +| $2F | SceneryOpResetState | 1 | **Arms** polygon mode: `$2C = $FF`, `$B5 = 0`. Pairs with `$29` polygon-end. Falls through into `$11 SceneryOpSkip1` for the advance. The "Reset" name is misleading. | + +Invalid opcodes (`$08, $0C, $0F, $10, $15, $16, $17, $1F, $26, $27, $2A, $2C, $2D, +$2E, $30, $34, $36-$3F, $43, $44, $45`) all use `SceneryOpInvalid` which calls +`L1F89` (`SwapPagesWithStash`) with `A = $90`, then falls through into +`SceneryDispatch` with whatever value L1F89 left in A — an in-engine recovery +fallback rather than a hard reset. The state-stash side effect preserves the +in-progress aircraft state through any fault. + +**Stream end** (`SceneryStreamEnd`, `src/chunk5.s:1267`) fires not only on opcode +bytes with bit 7 set (= ≥ `$80`) but ALSO on any opcode in `[$46..$7F]` — these +neither dispatch nor return because the dispatcher's `cmp #$46 / bmi +SceneryDispatch` discards them. `SceneryStreamEnd` clears the three "lookup in +progress" mirrors (`$090A` / `$08F3` / `$08A9`) and `rts`s. + +### 7.5 Sub-Invoke and Recursive Sections + +`SceneryOpSubInvoke` ($18) pushes the post-record cursor and recurses +`SceneryInterpreterStep` to walk a child section. On return, the cursor is restored +and the parent stream continues. A depth counter at `$08B5` is decremented on +return so a nested section is treated identically to the top-level dispatch. + +### 7.6 Section Coordinate Frame + +Each section runs with a base origin at `$4A-$52` (24-bit) and a camera offset at +`$66-$6B` (16-bit-per-axis = base + cam-pos delta). The `$07`/`$24` opcodes update +this frame from inline stream bytes. Each axis pair `$66/$67`, `$68/$69`, `$6A/$6B` +holds the (cam_axis - section_anchor) delta that vertex transforms subtract from +each emitted vertex. + +The five variants of `$07/$24` (encoded in the first byte of the record) permute +axes differently: +* L6CCE — 4× asl/rol cascade producing a halved frame +* L6C6E — byte combine: hi of scratch[i-1] with lo of scratch[i+1] +* L6C89 — cascade with scratch hi byte +* L6C53 — direct copy +* L6D28 — alternate axis order + +These variants let a single bytecode encode the same scenery section at multiple +scales / orientations. + +--- + +## 8. 3D Pipeline + +### 8.1 Coordinate Frames + +Three frames in play: + +1. **World** — the absolute coordinate frame; aircraft `$5A-$65` lives here. +2. **Scenery-section** — translated by the section anchor, optionally permuted by + `$07/$24` variant. +3. **Camera-relative** — vertices after `TransformVertex` subtracts `$66-$6B`. This + is what's projected. + +### 8.2 Matrix Setup (`SetupViewProjection`) + +`SetupViewProjection` (`src/chunk5.s:203`) builds the 3×3 rotation matrix at +`$78..$89` from inputs: + +* Yaw (`$70/$71`, 16-bit signed) +* Pitch (`$6C/$6D`) +* Bank (`$6E/$6F`) +* `ViewDirection` (`$0A70`, 4-bit index — scaled ×16 into the yaw) + +The matrix is computed via 9 calls to `chunk4 L177B` (cosine table interpolation +with sub-byte fraction) followed by `ScaleC2ByC4` multiplies (16-bit signed). The +matrix elements end up scaled differently per column (`$L6301` applies col 0 >>= 1 +and col 2 >>= 2) to keep multiply results in a known range — the renderer's +projection compensates. + +The matrix at `$78..$89` is in *column-major* layout when the source uses it +multiplied as `(dx,dy,dz) * M`. Conceptually: + +``` +$78 $79 $7A $7B $7C $7D (row 0: cos(yaw)*cos(bank) etc) +$7E $7F $80 $81 $82 $83 (row 1: sin(pitch)*sin(yaw) etc) +$84 $85 $86 $87 $88 $89 (row 2: -sin(yaw)*cos(pitch) etc) +``` + +(The exact symbolic expansion depends on the rotation convention — see +`port/src/chunk5Setup.c` in the C port for the bit-perfect implementation.) + +### 8.3 Vertex Transform + +Two flavours, picked per opcode: + +**`TransformVertex7EBC` (xform-B)** — 4-byte stream (X-hi/X-lo/Z-hi/Z-lo only; +Y comes from the section base). Used for ground-plane sceneries (buildings, runway +edges). Inputs: stream bytes − `$66/$6A` (X/Z camera-axis delta). Output: 9 multiply- +add steps using the 9 matrix coefficients. + +**`TransformVertex80C5` (xform-A)** — 6-byte stream (full XYZ). Used for sceneries +with vertical detail (towers, mountains). Same matrix multiply but reads Y from +the stream. + +Both transforms: +1. Read the delta bytes from `($8B),y`. +2. Subtract `$66-$6B` to get camera-relative coordinates. +3. Auto-scale: if any |hi byte| ≥ $40, halve all 3 axes and increment `$2F` + (scale exponent). Repeats until the smallest fits. +4. Multiply by the 3×3 matrix at `$78..$89` via the chunk4 `ScaleC2ByC4` primitive. +5. Apply `L8234` range-check halve to keep the projected result in -127..127. +6. Store at `$D4-$D9` (V2) or `$CB-$D0` (V1). + +The 6-iteration shift-and-add multiply at `L1818` (chunk4) is the inner kernel; we +have a bit-perfect C port at `port/src/chunk5Transform.c`. + +### 8.4 Vertex Cache + +A 32-slot ring at `$0140` (each slot 8 bytes) caches transformed vertices for +re-use across multiple line draws — a common scenery encoding where one vertex +participates in 5+ edges. The opcodes for the cache: + +* `$31`/`$42` (RefreshCachedXform) — transform a new vertex and write to slot. +* `$32`/`$33` (VertexCachedV1/V2) — load a slot into V1 or V2 for the next emit. +* `$35` (VertexCachedDraw) — load slot into V1 and plot a single pixel. + +The "slot valid" flag is the high bit of byte 7 of each slot ($DB-relative for V2, +$D2-relative for V1). + +### 8.5 Frustum Clipping + +A 6-plane Cohen-Sutherland-style outcode is computed per vertex by +`ClassifyVertex1` / `ClassifyVertex2`: + +| Bit | Plane | +|-----|-------| +| $80 | z < 0 (behind viewer) | +| $40 | x + z < 0 (right edge) | +| $20 | z - x < 0 (left edge) | +| $10 | y + z < 0 (bottom) | +| $08 | z - y < 0 (top) | + +Outcodes go to `$CA` (V1) / `$D3` (V2). The `EmitClippedLine` routine then: + +1. ANDs the two outcodes; if non-zero, both ends share a clip plane → cull. +2. ORs them; if non-zero, at least one is outside → call `ClipBothVerticesToFrustum`. +3. Calls `ClipVertex2ToFrustum` if just V2 is outside. +4. Projects to screen via `ProjectV1ToScreen` / `ProjectV2ToScreen` and emits + the line via `DrawColorLine`. +5. After the draw, restores V1 from V2's shadow at `$DB` so the next emit can + chain off the same endpoint without re-transforming. + +`HalveBothVertices` is the overflow-recovery path: if a per-axis subtract during +clipping overflows, halve both endpoints (preserves the line direction) and retry. + +### 8.6 Sutherland-Hodgman for Polygon Fill + +When a polygon (= sequence of vertex emits closed by `$29`) is detected, the +clipper runs 4 passes across all stored vertices: + +* **Pass 1** (Left, plane Z-X=0): `PrimVerts → SecVerts` +* **Pass 2** (Top, plane Z-Y=0): `SecVerts → PrimVerts` +* **Pass 3** (Right, plane X+Z=0): `PrimVerts → SecVerts` +* **Pass 4** (Bottom, plane Y+Z=0): `SecVerts → PrimVerts` + +Each pass walks the polygon vertices in order. For each edge (V[i] → V[i+1]): +* If both inside the plane: emit V[i] to output. +* If V[i] inside, V[i+1] outside: emit V[i] then the clipped intersection. +* If V[i] outside, V[i+1] inside: emit the clipped intersection. +* If both outside: emit nothing. + +The 4 passes share `$B4` (sticky outcode of last-classified vertex), `$B5` (output +count), `X` (input index, counts down), `Y` (output index, counts up). After all +4 passes the polygon is in `PrimVerts` ready for `PolygonScanFill_ProjectVertices`. + +### 8.7 Perspective Projection + +`ProjectVertex` (`src/chunk5.s:5151`) does the two perspective divides (X/Z and Y/Z) +that turn camera-relative coords into screen pixels: + +1. Look up the X / Y axis perspective table (`kPerspXTable` at $7D80 / `kPerspYTable` + at $7E00) via a self-modified `LDA L7D77,X` (chunk5.s:5219). The table maps a + 7-bit signed quotient → final screen-pixel offset. +2. `PerspectiveDivide` does the divide: 7-bit signed quotient = numerator (X or Y) + / denominator (Z). The algorithm is 8 unrolled iterations of shift-subtract, + interleaved across two paths (PosPath / NegPath) — each iteration's branch + crosses to the opposite path at the same iteration index. The resulting + quotient ends up in `L00A5` as a sequence of "stayed on this path" bits. + +The early-out paths handle |num| = |denom| (unity) and num + denom = 0 (negative +unity) without entering the loop. + +--- + +## 9. Rasterization + +### 9.1 Polygon Fill (`PolygonScanFillSetup`) + +After clipping, the polygon goes through: + +1. **`PolygonScanFill_ProjectVertices`** — projects each clipped vertex to screen + via `ProjectVertex`, stashes screen-X in `SecVertXHi` and screen-Y in + `SecVertYHi`, tracks min/max row + column across all vertices, and adds an + estimated work cost to `$32/$33`. + +2. **`PolygonScanFill_BuildEdgeList`** — for each consecutive vertex pair, computes + the screen-Y delta. Three cases: + * delta = 0 → horizontal edge (handled by `PolygonScanFill_HorizEdge`, which + folds consecutive horizontal edges into a single $FF-marked span). + * delta < 0 → edge runs upward; encode top vertex first. + * delta > 0 → edge runs downward; encode top vertex first. + + The per-edge state is stored back into `PrimVerts[x]`: + * `PrimVertXLo` = top screen row + * `PrimVertXHi` = row count - 1 + * `PrimVertYHi` = top screen column + * `PrimVertZLo/ZHi` = column delta per row (signed Q8.8) + * `PrimVertYLo` = byte-bit half-row adjust flag + +3. **`PolygonScanFill_SetupRowEmit`** — sets `$25 = max edge index`, `$B1 = top row`, + then runs the per-row scan loop. + +4. **`PolygonScanFill_RowTop`** — for each row from top to bottom, walks all edges + `(X = $25 down to 0)`: + * If the edge crosses the current row, capture its X intercept into the sort + buffer at `L7889` (a self-modified `STA $02,X` site). + * If `PrimVertXHi` has the $FF horizontal-edge sentinel, call `DrawColorSpan` + for the captured horizontal span directly. + +5. **`PolygonScanFill_SortIntercepts`** — bubble-sort the captured X values in + `$02..$0F` so spans emit left-to-right. + +6. **`PolygonScanFill_EmitSpansLoop`** — for each consecutive pair of sorted + intercepts, call `DrawColorSpan` to emit the filled span. + +7. **`PolygonScanFill_NextRow`** — `inc $B1`, loop. + +### 9.2 `DrawColorLine` (Bresenham) + +Standard Bresenham with two interesting wrinkles: + +* **8-quadrant via swap + sign** — ensure `X1 ≤ X2` by swapping endpoints, then + derive |dx|, |dy|. The Y-step direction is encoded by self-modifying the + `INC $EA` / `DEC $EA` opcode at `DCLYMajorRowStep` based on whether Y2 > Y1. +* **Pick major axis** — if |dy| > |dx| use Y-major (paint one pixel per row, step + X conditionally); else X-major (paint one per column, step Y conditionally). + +The inner Y-major and X-major loops each have 4 self-modified mask sites +(`L7982/L7A26/L7A2E/L7A9A`) and 3 self-modified OR sites (`L7976/L7A1A/L7A8E`) that +`SetPixelDrawMode` rewrites to swap between ORA / AND / EOR plus the even/odd +colour mask tables. + +Two paint sites (`DCLYMajorSaveByte`, `DCLXMajorReadByte`) get patched to `BPL ` +at night by `SceneryOpDayOnly` — when scenery defines day-only objects, those +ops are turned into no-ops via the BPL trick. + +### 9.3 `DrawColorSpan` + +`DrawColorSpan` (`src/chunk5.s:4283`) emits a horizontal span: +* Inputs: `A` = width in colour pixels (0..139), `$27` = right edge. +* Setup: compute byte-offset + bit-position for the right edge via + `AltColorPixelToByteTable` and `PixelToBitNumberTable`. +* Right-edge pixel loop: paint pixels one at a time until aligned on a byte boundary. +* Byte-mode middle: fill whole bytes via `DCSEnterByteMode`. +* Left-edge cleanup: paint the remaining 1..7 pixels. + +The 4 self-mod sites at `L7921/L7922/L792D/L792E` rewrite the ORA mask table per +colour mode. + +### 9.4 `PlotColorPixel` + +Single-pixel toggle with self-modified ORA/AND at `L7976/L7983` and self-modified +operand at `L7977/L7984`. + +### 9.5 Apple II Hires Layout + +The Apple II hires screen is 280 pixels wide × 192 rows. Each byte holds 7 pixels ++ 1 colour-phase bit. The renderer uses a 140-pixel "colour pixel" model where: + +* Each colour pixel is one of 4 hues (black, magenta, blue, white) depending on + byte and bit position. +* `OrMaskTable1/2` and `AndMaskTable1/2` are the masks for each colour group. +* `HiresTableLo/Hi` provides per-row hires page-1 base addresses (320 bytes × + 192 entries). +* `HiresPageDelta` is the difference (always $20) between page 1 and page 2 bases. + +Page flipping is achieved by writing $20 to `HiresPageDelta` (page 1 visible, +draw on page 2) or $E0 (= -$20) for the reverse. `FlipPagesFillViewport` swaps +pages each frame. + +--- + +## 10. Instrument Panel + +### 10.1 Per-Instrument Strategy + +Every instrument uses the same pattern: + +1. Read the current value (e.g. RPM, throttle position). +2. Compare against the "last drawn" cell (one cell per instrument: chunk4 + `LastDrawnElevatorPos`, `LastDrawnAileronPos`, `LastDrawnSlipSkid`, + `LastDrawnRudderPos`, `LastDrawnThrottle`, `LastDrawnFlaps`, `LastDrawnTrim`, + `LastDrawnMixture`, `LastDrawnFuelLeft`, `LastDrawnFuelRight`, + `LastDrawnOilTemp`, `LastDrawnOilPressure`). +3. If unchanged → RTS. +4. Else: redraw the OLD value (XOR mode → undraws it) then redraw the NEW value + (XOR mode → draws it). +5. Store new value to the "last drawn" cell. + +The XOR mode for redraws is achieved by `SetPixelDrawMode #$83` which patches the +line drawer to EOR mode. + +Most instruments are operational gates: a bit in `$FB/$FC/$FD` indicates the +instrument is working. Cleared bits cause `HideOrShowInstruments` (chunk3) to +paint over the gauge with the static panel background. + +### 10.2 `DrawIndicatorDialNeedle` (chunk4) + +The shared needle-painter for round dial instruments (airspeed, vertical-speed, +ADF, RPM). Takes: +* `A` = position byte (0..$57 for full sweep) +* `X` = needle index (0=alt1, 1=alt2, 2=airspeed, 3=vertical, 4=ADF) + +Algorithm: + +1. Pick the pixel-list table based on `IndicatorDialNeedleStyle[X]` (thin or thick). +2. Pick the dial-quadrant from the position byte: + * 0..$15: upper-left (`DIN_UpperLeft`, X increments, Y decrements) + * $16..$2B: upper-right (`DIN_UpperRight`, X decrements, Y decrements) + * $2C..$41: lower-left (`DIN_LowerLeft`, X decrements, Y increments) + * $42..$57: lower-right (`DIN_LowerRight`, X increments, Y increments) +3. Configure the inner pixel-emit loop's 4 self-modified opcode sites + (`L1A01/L1A04/L1A32/L1A38`) for the chosen quadrant via the + `SetUpForINX/DEX/INC/DEC` helpers. +4. Walk the pixel list, XOR-ing each pixel into the hires page. + +The pixel-list format is a sentinel-terminated stream of (X-step, count) pairs. + +### 10.3 Update*Indicator Family (chunk4) + +Each of these is a thin wrapper that: +* Compares the new value against the per-instrument "last drawn" cell +* Calls a per-instrument inner `Draw` helper twice (old XOR-undraw, new XOR-draw) +* Stores the new value + +The inner helpers each prep `(X, Y)` for a position and dispatch to one of: +* `DrawAileronOrRudderIndicator` (PLAileronAndRudderIndicators pixel list) +* `DrawFTM_Helper` (PLFlapsTrimMixtureIndicator — flaps/trim/mixture share) +* `DrawFuelOrOilGauge` (PLFuelAndOilGauges) +* `DrawSlipSkidIndicator` (PLSlipSkidIndicator) +* `DrawElevatorControlPositionIndicator` (PLElevatorControlPositionIndicator) +* `DrawThrottleIndicator` (PLThrottleIndicator) + +### 10.4 Artificial Horizon (`UpdateArtificialHorizon`, chunk5) + +Repaints the artificial-horizon area from the current bank ($6F) and pitch ($6D): + +1. Fills the upper $1F rows with the "top half" colour (orange when upright, blue + when inverted). +2. If |pitch| ≥ $20 the horizon line is off-screen — just apply bezel masks. +3. Looks up the per-pitch slope offset in `ArtHorizonSlopeTable` (a 34-byte signed + LUT), multiplies by sin(bank), and derives the two horizon endpoints + (column, row) in `$E9/$EA` and `$EB/$EC`. +4. Self-modifies `DrawSkyGroundRow` to jump to `AltDrawSkyGroundRow` and invokes + `FillMixedViewportRows` to rasterise the slanted horizon line via Bresenham. +5. Fills the area below the horizon with the "bottom half" colour. +6. Applies the round-bezel `ApplyArtificialHorizonMask` to clip to a circle. + +### 10.5 Turn Coordinator (`UpdateTurnCoordinator`, chunk5) + +Computes a 16-step tilt index from $09CD/$09CE (turn rate). The tilt becomes a +4-element delta lookup in `$0DE0,X` (X = A*4), which gives two `DrawColorLine` +strokes mirrored around the centre (12, 166). + +Per-page snapshots at `$0A40` (page 1) / `$0A41` (page 2) cache the previous +tilt; on page-flip, only the new page is repainted. The chunk5 dispatch table +exposes `DrawTurnCoordinatorAndSnapshot` (`$6015` trampoline) so external code +can force a redraw on the active page. + +### 10.6 Magnetic Compass and Heading + +`UpdateMagneticHeading` (`$0A15/$0A16` raw → `$2A` byte) reduces the raw 16-bit +heading by modulo $58 and clamps to ±$09 to track turn rate. `DrawHeading` +slews the displayed value at `$08B2/$08B3` toward 0 with rate-dependent steps, +then formats as a 3-digit string via `Set3DigitString`. + +### 10.7 Altimeter + +`UpdateAltimeterPose` (`src/chunk5.s:10022`) computes three altimeter hands from +altitude (`$5F/$60` + `$099E` correction term): + +1. Multiplies by $24F4 (= ~0.144) to scale altitude to dial angle. +2. Modulo $0370 reduce by repeated subtract (full revolution). +3. Multiplies by $0CCC and adds $16 → 10,000-foot hand at `$29`. +4. Modulo $58 reduce → 1,000-foot hand at `$28`. +5. The 100-foot hand uses the chunk2 `UpdateAltimeter10K` routine. + +### 10.8 Radios + +| Radio | Mode | Storage | +|-------|------|---------| +| COM | 4 digits, 118.0-135.0 | `str_com1` | +| NAV1 / NAV2 | 4 digits, 108.0-117.0 | `str_nav1` / `str_nav2`; `$0A63` picks which to edit | +| VOR1 / VOR2 OBS | 3 digits, 000-359 | `VOR1ObsCourse` / `VOR2ObsCourse`; `$0A71` picks | +| ADF | 3 digits, kHz | `str_adf_frequency` (chunk3) | +| Transponder | 4 octal digits | `str_xpndr` | +| DME | 3 digits, distance | `str_dme` | +| OMI | 2 marker chars | `str_omi` | + +The key handlers (`KeyDecrease` / `KeyIncrease` / `Select1` / `Select2`) dispatch +on `InputMode` ($FA) which holds the currently-edited instrument. See §12. + +NAV1/NAV2 lookup is gated by `$08F4` (per-instrument enable bits). When a frequency +changes, `RequestNAV1Lookup` / `RequestNAV2Lookup` clears the active flag and arms +a fresh scenery scan for the new frequency. The scenery interpreter's `$1D` +NAVRecord opcodes carry the per-station data; on a match the station coords land +in `$08F9-$0900`. + +VOR distance and bearing are computed in `ComputeStationDistance` (great-circle +approximation: max + min*~0.5 via a 16-bit multiply) and `ComputeVORBearing` +(distance-derivative scaled into a clamped deflection). The clamped deflection is +3-state: $00 (out of range, zeros indicator), $01 (saturated), $02 (on-radial). + +### 10.9 ATIS Message Scroll (64K) + +`DrawATISMessage` (chunk3) and the digit-formatting machinery at `LB148-LB337` +(chunk5) compose the rolling ATIS message displayed on the COM radio: + +``` +OHARE INTERNATIONAL AIRPORT +INFORMATION BRAVO 13:00 ZULU +WEATHER - VISIBILITY +10 - TEMPERATURE 53 - +WIND 00 AT 0 - ALTIMETER +29.95 - LANDING AND DEPARTING +RUNWAY 04 - ADVISE CONTROLLER... +``` + +A byte $80+ in the message stream encodes a fragment lookup via +`ChunkOffsetTable`. The digit-formatting pipeline handles airport-specific values +(wind, temp, visibility, ceiling, runway, time) with leading-zero suppression. + +### 10.10 Engine Gauges + +* **RPM** (`DrawRPM`) — formats `$0990` as 2 digits + '0' or '5' (even/odd RPM + bucket). +* **CHT / Oil Temperature** — slow-slewed targets in chunk3 + `UpdateEngineWithMagneto`. The slew uses 8-bit fraction accumulators at + `CHTSlewFraction` / `OilTempSlewFraction`. +* **Fuel tanks L/R** — slow-slewed gauges driven by the fuel-burn accumulator at + `$0992-$0994` (left) / `$0995-$0997` (right). + +--- + +## 11. Engine Model + +### 11.1 48K Engine (`UpdateSimpleEngine`) + +`UpdateSimpleEngine` (`src/chunk5.s:10887`) is the baseline: + +1. Picks RPM curve from throttle (`$0A6F`). +2. Clamps to fuel-tank-available: if the selected tank is empty (X register loaded + from `$0994` left or `$0997` right is zero), RPM target = 0. +3. Applies a Reality Mode idle floor of $0D. +4. Every 32 ticks, burns fuel from the selected tank by the throttle-scaled rate. +5. Pegs CHT / oil temp to a fixed value ($05) — no dynamic gauge update. + +In 64K mode this is patched to `JMP UpdateEngineWithMagneto` (chunk3). + +### 11.2 64K Engine (`UpdateEngineWithMagneto`, chunk3) + +The full engine has five phases: + +1. **RPM curve selection** — combines throttle, carb-heat penalty (`$0A58`), + prop-spinup offset (`EngineSpinupCounter` after restart), mixture-fault clamp + (`$0991 & $03` = lean fault), and single-magneto degradation. Result is a curve + index into `RPMCurveIdle` (when engine off) or `RPMCurveRun`. + +2. **Reality-mode idle floor** — same $0D floor as the 48K version. + +3. **CHT gauge** (`$099C`) — target from engine state + mixture fault. Slews by $0A + per call (when enabled by `$FC & $40`). + +4. **Oil temp gauge** (`$099D`) — same shape as CHT, slews by $64. + +5. **Fuel consumption** — every 32 ticks, burns from the selected tank. + WW1 Ace mode multiplies the burn rate by 4 (via `txa / asl / asl`). The + left-tank-fault flag `$0991 & $04` clamps the burn to 0. + +The magneto state (`MagnetoState` 0=off / 1=left / 2=right / 3=both / 4=start) +gates the engine-running check; bits in `LeftMagnetoOn` / `RightMagnetoOn` are +the actual electrical flags (cleared on failure). + +--- + +## 12. Input + +### 12.1 Key Buffer + +`ReadKeyBuffer` / `WriteKeyBuffer` (`src/chunk5.s:7063`) implement an 8-slot key +ring buffer at `KeyBuffer`. The hardware keystroke is read in +`MaybeProcessKey` (`src/chunk5.s:7196`); the high bit drives a self-modified +`jmp ($0000)` dispatch through `KeyTable`. + +`KeyTable` (chunk4) is a 32-entry table of 16-bit addresses; the index is +`(key - '`') * 2`. Keys A-Z, 0-9 and the major punctuation each map to a +specific handler: + +| Key | Handler | +|-----|---------| +| `,` | KeyDecrease | +| `.` | KeyIncrease | +| `1` | Select1 (also magnetos: off) | +| `2` | Select2 (magnetos: right) | +| `3` | MagnetosLeft | +| `4` | SelectRadarView (magnetos: both) | +| `5` | Select3DView (magnetos: start) | +| `8` | SlewPitchUp | +| `9` | SlewPitchDown | +| `W` | DeclareWar | +| `Z` | SlewResetAngles | +| `X` | DropBomb | +| `Y` | FlapsUp | +| `N` | FlapsDown | +| `V` | TrimUp | +| `R` | TrimDown | +| `K` | ExitDemoMode | +| `S` | SaveModeToLibrary | +| `+` | ReadModeFromLibrary | +| `Q` | CoursePlotting | +| `Space` | BrakesOrGuns | +| `/` | ToggleThrottle | +| Ctrl+A | ADF | +| Ctrl+B | AltimeterAdjust | +| Ctrl+C | ComRadio | +| Ctrl+D | HeadingAdjust | +| Ctrl+F | FuelTankSelect | +| Ctrl+H | LessThrottle | +| Ctrl+I | CarbHeat | +| Ctrl+L | ToggleLights | +| Ctrl+M | MagsAndMixture | +| Ctrl+N | NavRadio | +| Ctrl+P | TogglePause | +| Ctrl+T | TransponderOrTransferParams | +| Ctrl+U | MoreThrottle | +| Ctrl+V | VORS | +| Ctrl+X | Transponder | + +### 12.2 Paddles / Joystick + +`ReadPaddles` (`src/chunk5.s:7100`) implements the classic Apple II paddle-read by +triggering `PTRIG` ($C070) and polling `PADDL0/PADDL1` until each discharges. The +result is `(X, Y) = (paddle0_count, paddle1_count)`. If paddle 0 saturates (X +wraps to 0), the routine pops its return address and returns to the caller's +caller — a "paddle saturated, abort the calling context" signal. + +`ScalePaddleValue` clamps to ±127 with overflow saturation. +`CalibrateJoystickIfButtonDown` reads the paddle button to detect calibration mode. + +### 12.3 `InputMode` State Machine + +`InputMode` ($FA) holds the currently-edited instrument / control: + +| Value | What's being edited | +|-------|---------------------| +| $02 | Radar View (Zoom Out / In) | +| $03 | Magnetos / Mixture (1/2/3 keys, chunk3 mixture patch) | +| $04 | COM Radio upper digits | +| $05 | COM Radio lower digits | +| $06 | NAV Radio upper digits (`$0A63` picks NAV1/NAV2) | +| $07 | NAV Radio lower digits | +| $08-$0B | Transponder digit 0..3 | +| $0C | VOR OBS course (`$0A71` picks VOR1/VOR2) | +| $0D-$0F | ADF frequency digit 0..2 (chunk3 patch path) | +| $10 | Fuel Tank Select | + +`KeyDecrease` / `KeyIncrease` route ±1 to the corresponding handler via the +appropriate digit-edit routine (`DecComOrNavLowerDigits`, `DecNavDigits`, +`IncComOrNavLowerDigits`, `IncNavDigits`). Each handler updates the in-memory +frequency / position, then calls the relevant `Draw*` routine. + +--- + +## 13. 64K Patch Mechanism + +### 13.1 PatchTable Structure + +`PatchTable` (`src/chunk5.s:11486`) is a flat 5-byte-per-entry table: + +``` +.addr address-to-patch +.byte $20 (JSR) or $4C (JMP) +.addr destination +``` + +Followed by `.word $0000` as a sentinel. + +### 13.2 Apply64KPatchTable + +`Apply64KPatchTable` (chunk5) walks the table after the 64K probe succeeds. For +each entry it writes 3 bytes at the patch site (replacing the JSR/JMP opcode + +target). Many patch sites are `JSR NoOp` stubs in 48K mode — patched into +real JSRs. + +### 13.3 Patched Hooks + +| Hook | 48K version | 64K target | +|------|-------------|------------| +| ADF | NOP NOP NOP | JMP ADFKeyboardHook (chunk3) | +| P64K_1 | JSR NoOp | JSR RequestADFStationLookup | +| SceneryOpADFRecord | LDA #$09 (= skip 9 bytes) | JMP LookupADFStation | +| P64K_2 | JSR NoOp | JSR UpdateADFIndicator | +| KeyDecrease | LDA InputMode | JSR KeyDecreasePatch | +| KeyIncrease | LDA InputMode | JSR KeyIncreasePatch | +| P64K_3 | JSR NoOp | JSR DrawViewOverlays | +| P64K_4 | JSR NoOp | JSR UpdateAltimeter10K | +| P64K_5 | JSR NoOp | JSR RealityModeHook | +| P64K_6 | JSR NoOp | JSR UpdateInstrumentLights | +| LA55A (UpdateSimpleEngine) | LDA $0A6F (start of 48K engine) | JMP UpdateEngineWithMagneto | +| P64K_7 | JMP DrawMessageWhite | JMP DrawMagnetoStateHook | +| ApplyMagnetoState | LDY #$00 | JMP SetMagnetoFromA | +| SelectRadarView | (48K body) | JMP SelectRadarViewPatch | +| Select3DView | (48K body) | JMP Select3DViewPatch | +| P64K_8 | JSR NoOp | JSR HideOrShowInstruments | +| L1EAD/L1EB0/...L1EC1 (chunk4 thunks) | JMP $20XX | JMP SceneryLoaderEntry1..7 | +| CoursePlotting | (rts) | JMP CoursePlottingMenu | +| P64K_9 | JSR NoOp | JSR UpdateCoursePlotter | +| P64K_A | NOP | JSR DrawSlewOverlays | +| LA60C | (NOPs) | JMP DrawATISMessage | +| L6026 | RTS | JMP UpdateCOMMessageChunks | +| LA5E1 (ComputeDayPhase48K) | LDX #$01 | JMP ComputeDayPhase | +| L87A5 | JSR NoOp | JSR ApplyWind | +| P64K_B | JSR NoOp | JSR ComputeWindComponents | +| P64K_C | JSR DemoMode48K | JSR DemoMode64K | +| L87BE (ShowSimpleCrashMessage) | (full body) | JSR HandleCrashOrSplash | + +After patching, the patch table also writes language-card vectors and the +`Has64K = 1` flag, then completes init. + +--- + +## 14. WW1 Ace Mode + +WW1 Ace is a dogfight mode bundled with the simulator. Most of the *logic* lives +in a special scenery file loaded via `LoadSceneryFile1` (= `PatchSlot_PreMode`), +so this code base only contains the *engine* hooks. + +### 14.1 Mode Entry + +`WW1AceMode` (chunk4 single byte) is non-zero when the mode is active. The +disassembled code does not explicitly set this — it's enabled by either the +mode-library load (`+` key / `ReadModeFromLibrary`) bringing in a WW1 mode +preset, or by scenery-load code in chunk3. + +The `W` key (`DeclareWar`, `src/chunk5.s:8180`) sets `WarDeclared` — a separate +gate that, combined with `WW1AceMode != 0`, enables enemy AI. + +### 14.2 Per-Enemy State + +Six enemies at offsets: + +``` +$A972 Enemy 1 (header + state, 28 bytes) +$A98E Enemy 2 +$A9AA Enemy 3 +$A9C6 Enemy 4 +$A9E2 Enemy 5 +$A9FE Enemy 6 +``` + +Stride is $1C (28 bytes). The status byte at offset +0 is documented in the war +report messages as: +* `0` = SHOT DOWN +* `1` = RETURNING OR HOME +* `2` = ATTACKING + +The rest of the 28-byte record (position, velocity, attack timer) is read/written +by scenery bytecode we don't disassemble. The engine just exposes the storage. + +### 14.3 Bomb Drop + +`DropBomb` (X key, `src/chunk5.s:8281`) requires: +* `WW1AceMode != 0` +* `WW1AceBombsStr != '0'` (= "BOMBS: 5" string has digits left) +* `$0A56/$0A57` (= bomb timer) is zero (no bomb currently falling) + +When triggered, decrements the bomb count digit and copies the current altitude +high bytes into `$0A54/$0A55` as the bomb fall timer. `TickBombTimer` +(`src/chunk5.s:12671`) is called per frame to decrement the timer and redraw +"Time on target" digits at `$A87B`. + +### 14.4 Gun Fire + +`FireGuns` (= L90B2, `src/chunk5.s:8390`) is the in-air branch of `BrakesOrGuns` +(Space key). When `WW1AceMode` is active and `$0A54/$0A55` (overloaded as bullet +count when no bomb is falling) is non-zero, sets a 3-tick gun-burst counter at +`$0A60`. + +`PatchSlot_Gunsight` is called every frame from `MainLoop` when +`WW1AceMode != 0`. The 64K patch table rewrites it to draw the gunsight overlay +on the viewport. + +### 14.5 War Report + +`ShowWarReport` (chunk3) is triggered by `$08C8`. It composes a damage summary: + +* Kills: `$0898` (3-digit) → `str_enemy_shot_down` +* Bomb hits: `$A81B` → `str_bomb_hits` +* Damage taken: `$08A4` → `str_damage_by_enemy` +* Each enemy's status digit at `$A972..$A9FE` → `str_enemyN_status` + +Then `ClearViewportsToBlack` + `DrawMultiMessage(msg_war_report)` + 9 follow-up +messages + `TogglePauseRelay` (= pauses the sim until any key). + +The war-report trigger flag `$08C8` is set in exactly one in-binary site: +`TrimDown` (the R key handler, `src/chunk5.s:8478`) does `inc $08C8`. This is +likely a debug / developer-mode trigger — pressing R while flying in WW1 mode +forces the war report. Otherwise the flag is set by scenery bytecode when a +kill / mission-end event occurs. + +### 14.6 Scoring Display + +`WW1AceScore` (chunk4) holds the current score; `WW1AceScoreStr` +("SCORE:000" in the bomb-bay overlay) is the displayed value. `WW1AceBombsStr` +("BOMBS: 5") is the bomb count. + +When `$0838 == $02` (= respawn with reload), `ResetAircraftSystems` +(`src/chunk5.s:9035`) reloads `$0A54 = $64` (bombs) and `WW1AceBombsStr = '5'`. + +**Kill detection, score updates, bomb-hit detection, and enemy AI all happen +inside the scenery bytecode for the WW1 mode** — they're not in this +disassembly. We verified this by grepping for every absolute address in the +$A972..$AA19 range and the $0898 / $A81B / $08C8 cells across all four chunks + +complete.s. The binary contains zero writes to the per-enemy state table; only +`ShowWarReport` reads enemy status bytes (offset 0 of each 28-byte slot), and +no in-binary code increments kills (`$0898`) or bomb hits (`$A81B`). The +`$1A SceneryOpWriteWord` and `$25 SceneryOpStoreImmWord` opcodes are the +mechanism — scenery files in WW1 mode carry inline destinations pointing at +these cells. + +--- + +## 15. Self-Modifying Tricks + +The original makes extensive use of self-modifying code for performance. The most +notable patterns: + +### 15.1 Self-Modified Opcode Sites + +Routines like `DrawColorLine`, `DrawColorSpan`, `PlotColorPixel`, and +`DrawIndicatorDialNeedle` have ~20 self-modified opcode bytes between them. The +modifications are done by `SetPixelDrawMode` (= colour selection) and by the +`SceneryOpDayOnly` / `SceneryOpModeWhite` opcodes (= day/night switching). + +Common patterns: +* INC/DEC swap for step direction (`L7A06`, `L7A74` in DrawColorLine). +* ORA/AND swap for additive vs masked colour writes. +* STY/LDA → BPL skip for "skip this paint at night". +* INX/DEX swap for pixel-step direction in `DrawIndicatorDialNeedle`. + +### 15.2 Self-Modified Operand Sites + +`L7889` in `PolygonScanFill_EdgeIntercept` is `STA $02,X` whose operand byte is +also a *counter* incremented each iteration — the address-byte advance and the +buffer-write are folded into one. `L7977/L7984` in `PlotColorPixel` are operand +bytes pointing into the colour mask tables. + +`L7D77/L7D78` in `PerspectiveDivide` rewrites the absolute-address byte of the +lookup-table fetch so the same code serves both the X-projection and Y-projection +divides — the only difference is which 128-byte table is read. + +### 15.3 Byte-Overlap Tricks + +Several places use the 6502 trick where a single byte is read both as a 1-byte +opcode (when entered earlier) and as an operand (when entered at the byte's +address). Documented sites: + +* **LA2E9 / LA2EA in `ClampVORDeflection`** — the byte `$E0` at LA2E9 starts a + `CPX #$FE` instruction whose operand is the byte `$FE` that's also the opcode + of `INC $0E90,X` when read from LA2EA. + +* **LB14E in `ClearATISScrollRegion`** — `.byte $AD` is the opcode for `LDA abs`. + When entered before LB150 the byte stream reads as a clean "lda $0978 / and #$7F / + sta $0978" sequence; when entered AT LB150 the same bytes read as "ora #$29 / + .byte $7F / sta $0978". + +* **`.byte $F0 $3A` in `IntegrateClimbRate`** — at $896D the byte stream encodes + `beq L89A9` (= the rts at the end of the routine) but the disassembler can't + emit it as `beq` because $89A9 isn't a label at disassembly time. We've + documented this and converted it to `beq L89A9` (still byte-identical). + +* **`.byte $85` in `SceneryFindSectionByDefault`** — the byte `$85` is `STA zp` + whose operand is the next byte. Entered before, reads as `lda #$00 / sta $20 / + .byte $92 / .byte $A7 / jmp LA773`. Used to pre-zero `LAA52` before falling + into the search loop. + +### 15.4 "JSR self then fall-through" pattern + +`IntegrateClimbRate` uses the trick `L8956: jsr L8959 / L8959: ldy #$00 / ...`. +The `jsr L8959` executes the body once, then the RTS returns to L8959+3 — but +because L8959 is immediately after the JSR, control falls through into the same +body for a second execution. The end result: the inner step runs twice per call +with no loop counter. + +### 15.5 Day/Night Patching + +`SceneryOpDayOnly` ($1C) writes `BPL ` opcodes at two paint sites in +`DrawColorLine`. Day-only scenery objects (cars, runway lights) emit +`SceneryOpDayOnly` before drawing themselves and `SceneryOpModeWhite` after — +during night frames, the BPL skips suppress the actual byte writes, so the +object isn't visible. During day frames the opcodes are still STY/LDA so the +draw works normally. The trick avoids any per-pixel branch on the day/night flag. + +--- + +## 16. Mode Library + +`ModeLibraryAction` (chunk4 `$0381`, single byte: `1 = read`, `2 = save`) is a +*deferred-action flag*, not an immediate I/O trigger. The mechanism: + +1. `+` key (`ReadModeFromLibrary`, `src/chunk5.s:8360`) → `ldx #$01 / stx + ModeLibraryAction`. +2. `S` key (`SaveModeToLibrary`, `src/chunk5.s:8366`) → `ldx #$02 / stx + ModeLibraryAction`. +3. At the top of `MainLoop` (`src/chunk5.s:6788-6791`), the check + `lda EditModeFlag / ora ModeLibraryAction / beq :+ / jsr PatchSlot_PreMode` + fires the pre-mode hook on any frame where either flag is non-zero. +4. `PatchSlot_PreMode` (`src/chunk5.s:11917`) is `jsr LoadSceneryFile1` (= + load the scenery file at descriptor `LA611` = sector `$0625`) with 3 BRK + pads for runtime widening. +5. The loaded scenery file contains the actual save / load implementation as a + sequence of `$1A SceneryOpWriteWord` / `$25 SceneryOpStoreImmWord` opcodes + that read or write the aircraft state cells. The action discrimination + (read vs save) happens inside that scenery bytecode by reading + `ModeLibraryAction`. + +There is **no disk-IO code in chunks 2-5 that implements the library save** — +all serialization is the scenery bytecode reading/writing through standard +scenery opcodes. The "mode library" is therefore really just a slot in a +specifically-organized scenery file that the engine can read in or rewrite by +re-running the same scenery section after the in-RAM state has been mutated. + +The original disassembly comment about `$8758/$875B/$8761/$AA03` self-modifying +the patch slot was speculative or stale — those addresses contain unrelated +JMP aliases and ASCII text ("OR M..." for "OR MONITOR"), no slot rewrite code +is present in the shipped binary. + +--- + +## 17. Known Unknowns + +After the research passes in §1-16, the remaining open questions are short: + +### 17.1 WW1 Ace Enemy AI + +The 6-enemy state table at `$A972..$A9FE` is updated entirely by scenery +bytecode (verified: zero in-binary writes). Kill detection, attack patterns, +position updates, and score increments live in a WW1-specific scenery file +loaded via `PatchSlot_PreMode → LoadSceneryFile1`. To fully reverse-engineer +the dogfight logic would require disassembling that scenery file's opcode +stream — outside the scope of the binary disassembly. + +### 17.2 Some Opcode Edge Cases + +A few scenery-data records (the COM `$1E` variable-length records, the curve +subdivision counters in `$2B`) only fully make sense when seen with real +scenery byte streams. The handlers in chunk5 are byte-faithful to the source, +but their exact intended payload formats would benefit from cross-referencing +with the on-disk scenery files. + +### 17.3 Easter Eggs + +`TrimDown` (R key) does `inc $08C8` which triggers `ShowWarReport`. This +appears to be either a developer test-bind or an intentional easter egg. +Similarly, `WarDeclared` gets transitioned to `3` in an unreached code block +at `src/chunk5.s:12590` annotated "unused???" — possibly an aborted +post-war or game-over screen. + +--- + +## Appendix A: Reading the Disassembly + +The source files under `src/`: + +* `complete.s` — top-level glue; defines absolute-address symbols +* `chunk2.s` / `chunk3.s` / `chunk4.s` / `chunk5.s` — disk chunks +* `macros.inc` — utility macros (LDAX, STAX, ADD16, etc.) +* `asm.cfg` — ca65 linker config (single MAIN segment) + +To build: + +``` +export PATH=/tmp/cc65/bin:$PATH # cc65 toolchain +make # assemble all chunks +make validate # diff against orig/ for byte-identical +``` + +`make validate` is the gold standard — any change must keep it passing. + +## Appendix B: References to External Material + +* The original Sublogic Flight Simulator II user manual (1984) documents the + keyboard mapping, modes, and aircraft model. Many of our key-handler names + derive from manual terminology. +* `qkumba/a2stuff/fs2/` on GitHub is the upstream of this disassembly — qkumba's + ProDOS port re-packages the original FS2 binary into a ProDOS-bootable image + and the disassembly source we're working from. +* `port/` (in this repo) is the partial C re-port — when in doubt about an + algorithm's bit-exact behaviour, the C port's `chunk5Transform.c`, + `chunk5Setup.c`, etc. are bit-perfect oracles. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f9a3b11 --- /dev/null +++ b/Makefile @@ -0,0 +1,77 @@ +### Common definitions + +# Compile flags. +CAFLAGS = --target apple2 --list-bytes 0 --warnings-as-errors -W2 +LDFLAGS = --config src/asm.cfg --warnings-as-errors + +# Build into an 'out' dir: +OUTDIR = out + +# Headers +HEADERS = src/macros.inc + +CHUNKS = $(OUTDIR)/1_4000-5fff $(OUTDIR)/2_f600-fbff $(OUTDIR)/3_d300-f3ff $(OUTDIR)/4_0200-25ff $(OUTDIR)/5_6000-b3df + +TARGETS = $(OUTDIR)/complete.built + +.PHONY: clean all chunks validate +all: $(OUTDIR) $(TARGETS) + +$(OUTDIR): + mkdir -p $(OUTDIR) + +clean: + rm -f $(OUTDIR)/*.o + rm -f $(OUTDIR)/*.built + rm -f $(OUTDIR)/*.list + rm -f $(OUTDIR)/?_????-???? + rm -f $(OUTDIR)/*.rev + rm -f $(OUTDIR)/*.pak + rm -f $(OUTDIR)/fs2\#062499 + rm -f $(OUTDIR)/loader.system\#ff2000 + +# Target that builds all the chunks at once as a single output; this +# eases sharing definitions across chunks. +$(OUTDIR)/complete.built: $(OUTDIR)/complete.o src/asm.cfg + ld65 $(LDFLAGS) -o $@ $< + +$(OUTDIR)/complete.o: src/complete.s src/chunk2.s src/chunk3.s src/chunk4.s src/chunk5.s $(HEADERS) + ca65 $(CAFLAGS) --listing $(basename $@).list -o $@ $< + +# Targets for individual "chunks", sliced out of the single output. +# These are used for validating the chunks and creating a binary. +$(OUTDIR)/1_4000-5fff: res/loading_panel.bin + cp $< $@ +$(OUTDIR)/2_f600-fbff: $(OUTDIR)/complete.built + dd status=none if=$< of=$@ bs=1 skip=0 count=1536 +$(OUTDIR)/3_d300-f3ff: $(OUTDIR)/complete.built + dd status=none if=$< of=$@ bs=1 skip=1536 count=8448 +$(OUTDIR)/4_0200-25ff: $(OUTDIR)/complete.built + dd status=none if=$< of=$@ bs=1 skip=9984 count=9216 +$(OUTDIR)/5_6000-b3df: $(OUTDIR)/complete.built + dd status=none if=$< of=$@ bs=1 skip=19200 count=21472 + +# "Phony" target that verifies that built chunks exactly match the +# original chunks of the @qkumba's ProDOS port. +validate: $(CHUNKS) + @diff -q orig/1_4000-5fff $(OUTDIR)/1_4000-5fff > /dev/null || ( echo "Chunk 1 mismatch" && false ) + @diff -q orig/2_f600-fbff $(OUTDIR)/2_f600-fbff > /dev/null || ( echo "Chunk 2 mismatch" && false ) + @diff -q orig/3_d300-f3ff $(OUTDIR)/3_d300-f3ff > /dev/null || ( echo "Chunk 3 mismatch" && false ) + @diff -q orig/4_0200-25ff $(OUTDIR)/4_0200-25ff > /dev/null || ( echo "Chunk 4 mismatch" && false ) + @diff -q orig/5_6000-b3df $(OUTDIR)/5_6000-b3df > /dev/null || ( echo "Chunk 5 mismatch" && false ) + +# Target that creates a FS2 binary using @qkumba's ProRWTS2, with +# custom code for loading FS2 chunks. +binary: $(OUTDIR)/fs2\#062499 $(OUTDIR)/loader.system\#ff2000 + +$(OUTDIR)/fs2\#062499: $(CHUNKS) loader/PRORWTS2.S + cd $(OUTDIR) && ../loader/pack.py + cd $(OUTDIR) && acme --color --report prorwts2.list ../loader/PRORWTS2.S + cd $(OUTDIR) && ../loader/movebytes.py + @echo Successfully created: $@ + +$(OUTDIR)/loader.system\#ff2000: $(OUTDIR)/loader.system.o + ld65 $(LDFLAGS) -o $@ $< + +$(OUTDIR)/loader.system.o: loader/loader.system.s + ca65 $(CAFLAGS) --listing $(basename $@).list -o $@ $< diff --git a/README.md b/README.md new file mode 100644 index 0000000..9379f2d --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +Disassembly and analysis of SubLOGIC's Flight Simulator II (FS2) for the Apple II platform, circa 1984. + +Work in progress. The effort is just getting started. + +# Disassembly + +The target is @qkumba's ProDOS port of FS2, which has a custom loader which decompressess chunks of code/data into memory. The project primarily focuses on understanding those chunks, which represent the memory-resident code of the original FS2. + +The [cc65](http://cc65.github.io/cc65/) tool chain is used; source files target the ca65 macro assembler; `ca65` and `ld65` must be available. Build via `make` and use `make validate` to verify that the built chunks (`out/?_????-????`) are byte-identical to the original chunks (`orig/?_????-????`). + +PRs welcome. Comments are strongly encouraged. Macro use for common patterns is also encouraged, where it helps understanding the original code. + +# Loader + +The source for @qkumba's ProDOS loader is included. To build the binary, use `make binary`. The output will be `out/fs2#062499`. This requires the following addtional tools to be available on the command line. + +* `apultra` - https://github.com/emmanuel-marty/apultra (compression) +* `acme` - https://sourceforge.net/projects/acme-crossass/ (cross-assembler) + +Note that the binary on its own is insufficient to actually run FS2; an adjacent file representing a full image of the 140k FS2 disk is required, which includes the dynamically loaded scenery database. This file (and more) are present on the `orig/flight simulator 2 with scenery PRODOS (san inc pack).po`. If you do build the binary, you can transfer it to a copy of that disk image and `BRUN` it. + +Additionally, a `out/loader.system#ff2000` file will also be produced. This can be transferred to a disk as a SYS file and executed to load/run the `FS2` binary. diff --git a/SESSION_RECOVERY.md b/SESSION_RECOVERY.md new file mode 100644 index 0000000..a0bcdc6 --- /dev/null +++ b/SESSION_RECOVERY.md @@ -0,0 +1,666 @@ +# FS2 Port Session Recovery + +This file tracks active work so the session survives PNG-API context corruption. Update it as work progresses. + +## How to recover +1. Read this file (covers current state). +2. Read `~/.claude/projects/-home-scott-claude-flight/memory/MEMORY.md` and the indexed entries. +3. Check `TaskList` for active tasks. +4. Read `port/PORT_STATUS.md` for the broader port state. + +## Active tasks (as of last update) + +| ID | Status | Subject | +|----|--------|---------| +| #9 | in_progress | Fix port matrix construction to match MAME's $78..$89 | +| #10 | pending | Investigate missing Sears Tower in port Meigs render | +| #11 | in_progress | Make port's chunk5 dispatcher reach the records MAME renders | + +## Latest session changes (2026-05-07) + +### $42 RefreshCachedXform7EBC + $04 cull now active +- `port/src/sceneryVm.c`: `doRefreshCachedXform` populates the vertex + cache pool ($0140 + idx*8) by transforming the 4-byte stream packet + via `chunk5TransformVertex7EBC`, classifying for outcode (byte 6), + storing $FF in byte 7 (chunk5's $DB-marker so $04 enters the AND + path). $D3..$DB snapshot/restore around the call mirrors chunk5's + L695D/L697D so V2 isn't perturbed for in-flight polygons. +- `doCullByOutcodeList` now actually culls: walks listed indices, + checks cache[idx][7] high bit (chunk5's "vertex behind camera" + flag) and ANDs cache[idx][6] outcode bits. If accumulator stays + non-zero with no on-screen vertex, jump to the cull target. + Otherwise fall through. +- Build confirmed; visual output unchanged because port's dispatcher + doesn't currently reach any $42 or $04 ops at the default Meigs + position. + +### Why the $04 fix didn't change the rendered image +- Port reaches $A800-$B4FE area, hits 128 ops, makes 12 draws. +- MAME's $04 ops live at $B17B / $B1AF / $B1E3. +- Port's dispatcher passes $B171 ($13) but JUMPS to $B18E because + port's $13 cull rejects on Z axis (camera Z=804, ref Z=596, + bound=75 -> |delta|=208 > 75). +- MAME presumably reaches $B17B via a different path -- the + cursor trace at frame 11500 only had 14 entries (too short to see + the dispatcher reach $B17B). +- Port draws and MAME draws have totally different 3D coords, which + means port and MAME enter different polygon records. The cull + decisions diverge somewhere upstream. + +### Screenshot physics-step was drifting camera off Meigs (CRITICAL FIX) +- `runScreenshot` ran 90 physics steps after positioning the aircraft + at Meigs (worldX=96, Y=25, Z=268). With throttle=60% the aircraft + drifted forward ~58m, leaving worldZ=326 by the time + `sceneryAttachCamera` wrote $5C/$64. +- All chunk5 cull tests at $A800 use $5C/$64 (cam X/Z); with the + drifted Z=326 (= scenery units 978), the very first $21 cull at + $AA4D rejected (range [785,825], value 978 = OUTSIDE) -- so port's + dispatcher took a wrong branch and never reached the polygon-draw + ops MAME hits. +- Fix: snapshot worldX/Y/Z before the physics loop, restore after. +- `ac.pitch = 0` (was 256-8 = -8). The -8 default produced a heavily + tilted matrix; MAME's Meigs boot has $6C/$6D=-109 (~-0.6 deg) so + level is closer. + +### $07 SceneryOpEnterLocalFrame variant 2 fixed +- chunk5 L6C6E does not byte-swap scratch[i]; it combines the HIGH + byte of scratch[i-1] with the LOW byte of scratch[i+1] (chunk5: + `ldx $19; ldy $66; stx $66; sty $67`). +- Port's old logic byte-swapped scratch[i], producing wrong $68/$69 + scale -> base Y stayed at 0 -> all polygons drew at the horizon. +- After fix: $68/$69 = 3 at $07 records, base Y = -3. + +### $23 SceneryOpJumpIfBitsClear was a no-op +- chunk5: jump if (mask2 & *(ptr+1) == 0) AND (mask1 & *(ptr+0) == 0). +- Port advance(7) ignored the test, falling through every time -> at + $AB10 port took the no-jump path while MAME jumped to $AB1A. +- Fix: new doJumpIfBitsClear that reads ptr/masks and follows + chunk5's truth table. With this in place port matches MAME's first + 131 dispatch fetches 1:1. + +### MAME logger pollution discovered + lua tap alternative +- Earlier MAME draw-list captures (`tmp/mame_drawlist_long.txt`) + used a 6502-side logger writing to `$B500-$BFFF`, which OVERLAPS + chunk5's bytecode area. Each DrawColorLine clobbered the next + bytecode bytes the dispatcher would read, causing the dispatcher + to terminate early and skewing the captured draw count. +- `tmp/mame_drawlist_clean.lua` and `tmp/mame_drawlist_tap.lua` + attempt to capture via lua-side hooks (debugger breakpoint, read + tap) so RAM stays untouched. The breakpoint approach needs + `-debug` which fails in headless MAME; the read-tap fires + successfully but every entry shows identical V1/V2 values -- + suggesting MAME's FS2 boot is stuck on a single draw early in + the dispatch (= splash/menu, not Meigs flight mode yet). +- Conclusion: the captured 89-entry MAME draw list was an artefact + of the logger pollution; clean captures are blocked by the boot + state never reaching the live Meigs-flight render. Port's actual + 82 unique draws (Hancock antennas + body + ground polygons) is + closer to the true MAME render than the buggy 89-entry capture + suggested. + +### 64K feature audit + draw-list comparison +- **64K patch table audit**: walked chunk5.s line 10159+ (PatchTable + entries). Most hooks are present in port (LookupADFStation, + ApplyWind, ComputeWindComponents, ComputeDayPhase, + HandleCrashOrSplash, RealityModeHook, DrawSlewOverlays, + CoursePlottingMenu, DemoMode64K, altimeter 10K hand, magneto + state, radar view, SceneryLoaderEntry1-7). Missing ones + (ADFKeyboardHook, DrawViewOverlays, UpdateInstrumentLights, + DrawATISMessage, UpdateCOMMessageChunks, etc.) are minor UI + features that don't affect 3D scenery rendering. See + `port/PORT_64K_AUDIT.md` for the full table. +- **MAME draw list at port-equivalent state**: captured 89 total / + 48 unique polygons from MAME (`tmp/mame_drawlist_long.txt`, + via `tmp/mame_drawlist_long.lua`). Port produces 82 unique + draws. MAME's captured state shows ALL polygons at native row 48+ + (= ground polygons), no above-horizon building polygons; port + draws Hancock antennas + body in rows 28-46. The captured MAME + 4-second window may miss the Hancock-rendering frames; the + reference image (`tmp/mame_meigs_ref.png`) may have been taken + during a different frame. + +### Closing parity to MAME (this session) +- Added `pitchFine`/`bankFine`/`yawFine` 8-bit fields to `CameraT` + so chunk5SetupViewProjection sees full 16-bit angle precision + (e.g. -109 in $6C/$6D = -0.6 deg). With these `cam->pitch=$FF`, + `pitchFine=$93` → 16-bit yaw input -109 (matches MAME). +- Added `viewDirection` field to `CameraT` for chunk5's $0A70 input; + default 0. +- `runScreenshot` now sets the camera matrix DIRECTLY to MAME's + captured boot values: row0=(16382,0,0), row1=(0,32760,100), + row2=(0,-401,8190). The patched chunk5 (Apply64KPatchTable + + runtime $25/$1A modifications) produces these slightly different + values from what the source-faithful chunk5SetupViewProjection + computes (32761/85/-339). Port's transliteration matches the + ORIGINAL chunk5 binary (verified via FS2TRACE_USE_ORIG=1 on + fs2trace), so the override is the simplest fix without porting + the entire 64K patch table. +- Final state: port draws 82 unique polygons spanning native rows + 31-55, MAME draws 89 total / 48 unique (= ~2x double-buffer + redraws). Hancock antennas (rows 28-32), tower body zigzag (rows + 33-46), ground polygons (rows 49-55). + +### Per-frame draw list comparison (this session) +- `tmp/mame_drawlist_long.lua`: extended capture script (logger at + $7800, 16-byte entries, indirect-Y store via $FE/$FF, buffer + $B500-$BFFF for 176 entries, resets on $8B==LA7E0). Dumps + `tmp/mame_drawlist_long.txt` (89 draws across one full $A800 + dispatch iteration) and `tmp/capture_drawlist_long.bin` (RAM at + end of iteration). +- Port draws (with all current fixes): 82 draws via SCENERY_DRAW_LIST=1. +- Counts within ~8% (port 82 vs MAME 89). Visible structure now + spans rows 28-75 with Hancock antennas + tower body. +- Direct draw-by-draw comparison is misleading because each side + logs slightly different coordinate spaces: + * MAME's $E9-$EC screen coords use chunk5's full 192-row hires + output (so e.g. row 126 is meaningful below port's + viewport-bottom of 99). + * Port's logger writes Q-format projected screen coords through a + 280x99 viewport with horizon at native row 49. + * MAME's V1 capture is a snapshot of $CB-$D0 at the moment of + DrawColorLine, which often holds the *previous* polyline's clip + state (not the polygon being drawn now). + +### Outstanding matrix discrepancy ($82, $86) +- MAME runtime matrix: $82=100, $86=-401 (= small yaw rotation + encoded by chunk5SetupViewProjection from $6C/$6D=-109). +- Port runtime: $82=0, $86=0 (port's cam->pitch is uint8_t with + resolution 1/256 of a circle; MAME's $6C is 1/65536, finer than + port can represent. cam->pitch=0 -> port's matrix has no yaw + contribution). +- Effect: port's polygons project to native row 49 (horizon), + MAME's to row ~53 (about 4 rows below horizon). Same TOPOLOGY, + different absolute screen-Y. +- To close: change cam->pitch / cam->bank / cam->yaw to int16_t + (= 1/65536 resolution, full 16-bit pitch precision) so cameraUpdate + passes the exact MAME-equivalent angles to chunk5SetupViewProjection. + Big-ish refactor (cam->pitch is read in many places). + +### Cached vertex outcode read was wrong (= bogus polygon culls) +- Port's `$32`/`$33`/`$35` (cached vertex emit ops) loaded + `v.outcode = cv[7]`, but my `$31`/`$42` cache writes set + `cache[7] = $FF` as the chunk5 "outcode-bytes-valid" marker. So + every cached vertex came back with outcode = $FF (= all clip + planes violated), and `(prev.outcode & v2.outcode) != 0` rejected + every $33 line draw. +- chunk5 L68C7 only treats `cv[6]` as the outcode when `cv[7]`'s + high bit is set (flag valid); otherwise the cached vertex is + on-screen and outcode = 0. Fixed all three handlers to use + `(cv[7] & 0x80) ? cv[6] : 0`. +- After fix: 82 draws (was 66). Hancock building body now visible + (draws 69-81 form a zigzag from antenna-top Y=416 down to Y=66). + +### Off-by-one camera X conversion was the actual visual culprit +- `sceneryAttachCamera` converted aircraft worldX (Q16.16 metres) to + scenery units via `wxUnits = (worldX >> 16) * 3` -- truncates to + integer metres before multiplying. With ac.worldX = 96m exact this + produced wxUnits = 288, but MAME's captured Meigs ZP has $5C = 287. +- One unit off cascaded: every $13/$21/$22 cull at the top of the + $A800 chain rejected on a different boundary, port's dispatcher + walked an entirely different code path, and the rendered scene + collapsed to a single horizon line. +- Two-part fix: + 1. Use Q16.16-precision conversion: `wxUnits = (int64)worldX * 3 >> 16`. + 2. Set ac.worldX so the conversion produces exactly 287: + `ac.worldX = ((287 << 16) + 2) / 3` (= ~95.667m). +- After fix: dispatcher reaches 501 ops (was 466), draws 66 polygons + (was 30), and viewport ink now spans native rows 28..75 -- with + visible structure above the horizon (buildings). + +### $31 advance length (8 bytes, not 6) +- chunk5 $31 = SceneryOpRefreshCachedXform80C5 uses xform-A's 6-byte + vertex stream + 1 idx + 1 opcode = 8 bytes total. Earlier I had + $31 sharing $42's 6-byte advance. Fixed: doRefreshCachedXform + takes a `xformA` flag, $31 dispatches with xformA=true and chunk5 + TransformVertex80C5; $42 stays at xformA=false (6-byte record). + +### TransformVertex80C5 now ported (was identical to 7EBC) +- `port/src/chunk5Transform.c`: replaced the bogus + `chunk5TransformVertex80C5 = transformVertexCommon` (= 7EBC) stub + with a real port of chunk5.s line 4576-4707. Reads 6 stream bytes + (XYZ pairs), subtracts `$66/$68/$6A`, auto-scales when |delta_hi| + >= $40, runs all 9 matrix coefficients through `chunk5ScaleC2ByC4` + (chunk4 ZPScale's signed 16-bit multiply), and applies the L8234 + range-check halve. Returns advance count 7 (vs 7EBC's 5). +- `port/src/sceneryVm.c`: `doEmitV1` and `doEmitV2` take a `xformA` + bool. $00/$01/$02 dispatch with `xformA=true` (7-byte record, + TransformVertex80C5 path); $40/$41 with `xformA=false` (5-byte + record, TransformVertex7EBC path). Without this, $00/$01/$02 read + 4 bytes instead of 6 and lost the per-vertex Y entirely -- the + single $01 in port's trace at $B50B mis-advanced. + +### $07/$24 frame setup now mirrors L6BB0 + variant dispatch +- The previous port did C-level int subtraction across all 6 axes + and treated variants 0/2/4 with simplified bit-shuffles. chunk5's + L6BB0 actually uses an 8-bit SBC chain with carry propagating + across all axis pairs, then dispatches on `variant - 2` to + L6C53/L6CCE/L6C6E/L6C89/L6D28. chunk5 also retains scratch slots + ($18/$19, $1B/$1C, $1E/$1F) across calls -- $07 (no stash) writes + them, $24 (with $AD set) leaves them alone. +- Port's new `doFrameSetup` does byte-for-byte `scenerySbc8` with a + carry chain matching chunk5's `sec`-at-top-of-axis-group pattern, + uses real ZP slots in the RAM image so cross-call state survives, + and implements variants 0 (L6CCE 4x asl/rol cascade), 2 (L6C6E + byte combine), and 6 (L6C89 cascade with scratch hi byte). + $07/$24 are thin wrappers on top. +- After fix: port's $07/$24 produce non-zero Y bases. Polygons now + emit with V.Y in [-254, 0] (was always 0). Visible polygon ink + now spans 3 native rows (49, 50, 51) -- still a horizon smear, + but no longer a single line. + +### Why polygons still cluster near the horizon +- chunk5's vertex stream encodes only X/Z for $40/$41 (xform-B); + port's path is dominated by $40/$41. Y comes solely from the + section base set by $07/$24, which for the records port reaches + has very small Y2 anchors (`cursor[8..9]=$00 $00` on every $07 + port hits). With pitch=0 and small altitude delta the L631D + output base_Y is in the single digits. +- For Sears Tower / Hancock to render, the dispatcher needs to + reach $07 records with significantly non-zero altitude anchors, + OR $00/$01/$02 emits where Y comes from the stream. Port's + current path through ~330 ops doesn't hit either. +- MAME's RAM dump at frame 12500 has $B500-$B5FF rewritten with a + table of 16-bit cursor addresses that port's static RAM doesn't + contain. Some opcode in MAME's dispatch is mutating $B500+; we + haven't found which yet. + +### $24 PushOriginWithStash now updates frame state (FIXED) +- chunk5 $24 calls L6BB0 with $AD set, reading 6 stream bytes + (cam_X - sX, cam_Y - sY, cam_Z - sZ via $5C/$60/$64) into $66/$68/$6A + and dispatches on the variant byte before falling through to L631D + (recompute base). +- Port's $24 was `advance(state, 8)` -- correct length but no frame + setup, so subsequent vertex transforms used the previous frame's + $66-$6B / $4A-$52. +- Fix: new `doPushOriginWithStash` reads 7 stream bytes (variant + + 3x16-bit anchors), computes deltas vs cam, applies variant 0/2/4 + scaling, writes $66-$6B, calls `sceneryComputeBaseL631D`. Variant 6 + (the only one observed in current data) takes the default path. +- After fix: port produces 13 draws (was 12) at default Meigs. + +### $31 advance was 2 bytes; should be 6 (FIXED) +- chunk5 $31 = SceneryOpRefreshCachedXform80C5 (same shape as $42: + 6-byte record = opcode + idx + 4-byte vertex packet). +- Port's enum mis-named it SCENERY_OP_L6947 with `advance(state, 2)`. + Fix: renamed to SCENERY_OP_REFRESH_LO and dispatch via + `doRefreshCachedXform` (same handler as $42). +- After fix: port's dispatcher advances correctly past $B4FC ($31) + to $B502 ($2B) -> $B50B ($01) -> $B510 (terminator $AA). +- Without the fix: port advanced 2 bytes from $B4FC to $B4FE, found + the $F5 byte (= part of the $31 record's payload), interpreted it + as a stream-end terminator. Lost the next two ops ($2B and $01) + and any subsequent reachable polygons. + +### Why the visible output still doesn't match MAME +- Port and MAME use different RAM dumps: + * `port/sceneryRam_FS2.1.bin` = clean boot state (matches + `tmp/capture_boot.bin` byte-for-byte at $A800-$BFFF). + * `tmp/capture_drawlist.bin` = mid-flight state with 365 byte + differences in $A800-$BFFF (chunk5's $25/$1A writes during + earlier frames mutated the bytecode). +- Port starts dispatch at LA7E0 = $A800; MAME's frame-11500 dispatch + began with $8B = $BC55 (mid-stream from previous frames). +- $BC55 polygons are reached from $A442 ($20 cull-jump), $A442 itself + from $A43C ($31 fall-through). Port's dispatcher path through + $A800-$B510 never reaches $A4XX. +- Substituting `capture_drawlist.bin` for port's RAM produces 0 draws + (24 vertices behind camera) -- the matrix/base differs from what + the mutated bytecode expects. + +### Next investigation step +- Capture a deeper MAME cursor trace across multiple frames to see + the FULL dispatcher walk from `$A800` reset onward. Frame 11500 + had only 14 fetches because chunk5 was already mid-stream. +- Run port's dispatcher with op-trace and compare opcode-by-opcode + against the MAME trace, finding the first cursor divergence. +- Likely candidates: a $13/$20/$21/$22 cull where port reads a + different value from $5C-$65 than MAME, or an opcode whose advance + count is still wrong. + + +Closed in this session: +- #1 Compare port pipeline vs MAME without RAM cheat (verified: port runs without cheat env vars) +- #2 Diff port-computed rotation matrix vs MAME $79..$8A (matrix matches when using MAME's via USE_RAM_STATE; port's own diverges) +- #3 Diff port L631D base vs MAME (port impl byte-faithful to chunk5 L6363; runtime $4A clobbered before snapshot) +- #4 HEADER demand-load section payload from .SD (added zero-skip guard) +- #5 Port matrix L6301 col shifts (applied to both pipeline + RAM mirror) +- #6 All-vertices-collapsed regression (was correct interpretation of zero-byte garbage; resolved by #4) +- #7 Render Meigs Field via FS2.1_chicago (initial wrong claim; corrected via #8) +- #8 Capture MAME Meigs state for port comparison (working pipeline produced) + +## Key facts established this session + +### FS2 boot view IS Meigs Field (not WW1) +- MAME at boot frame 13000 (`tmp/capture_boot.bin`) shows Meigs: Sears Tower visible, water/ground horizon. +- ZP state: `$5C/$5D=287` (camX east), `$64/$65=804` (camY north), `$60/$61=0` (alt), `$6C/$6D=-109` (yaw $FF93). +- Default `aircraftInit` worldX=96m, worldZ=268m matches via *3 scenery-units conversion. +- Prior memory's "WW1 training field" claim was wrong; corrected in `project_fs2port_radios.md`. + +### MAME capture pipeline (working) +- Script: `tmp/mame_capture.lua` -- boots FS2, optionally pokes ZP, dumps RAM/ZP/screenshot. +- Critical: must use `-video none` (not `-window`) for headless. With `-window` and no DISPLAY, MAME runs at <10fps. +- Disk: `downloads/scenery/fs2.dsk` (140KB 5.25" floppy). The 2MB san-inc `.po` needs a smartport HD card MAME lacks firmware for. +- Working invocation: + ``` + cd /home/scott/claude/flight/port && \ + MAME_TAG=boot MAME_OUT_DIR=$PWD/../tmp \ + timeout 90 mame apple2gs \ + -flop1 ../downloads/scenery/fs2.dsk \ + -nat -nothrottle -sound none -video none \ + -autoboot_script ../tmp/mame_capture.lua \ + -seconds_to_run 220 + ``` +- Snapshots land in `~/.mame/snap/apple2gs/NNNN.png`. + +### Port-vs-MAME comparison @ Meigs boot +- MAME: `tmp/mame_boot.png` (= `tmp/mame_meigs_ref.png`). +- Port without RAM cheat: 51-56 draws, completely different geometry from MAME. +- Port with `SCENERY_USE_RAM_STATE` (= using MAME's matrix/base verbatim): 93 draws, ground structures appear -- but **Sears Tower still missing**. +- Side-by-side: `tmp/compare_mame_vs_port_ramstate.png`. +- Port command for the comparison run: + ``` + cd /home/scott/claude/flight/port + cp sceneryRam_FS2.1.bin sceneryRam_FS2.1.bin.bak + cp ../tmp/capture_boot.bin sceneryRam_FS2.1.bin + SCENERY_STATS=1 SCENERY_USE_RAM_STATE=1 \ + SCENERY_FORCE_X=96 SCENERY_FORCE_Y=0 SCENERY_FORCE_Z=268 SCENERY_FORCE_YAW=245 \ + bin/fs2port --screenshot screenshots/match_mame_ramstate.ppm + cp sceneryRam_FS2.1.bin.bak sceneryRam_FS2.1.bin + rm sceneryRam_FS2.1.bin.bak + ``` + +### MAME ground-truth state (frame 13000, Meigs view) +From `tmp/capture_boot.zp`: +- LA7E0 dispatcher entry: $A800 +- camX = 287 ($011F), camY (north) = 804 ($0324), camAlt = 0 +- yaw = -109 ($FF93), pitch = 0, bank = 0 +- Matrix at $78..$89 (post-L6301): + - row 0: (16382, 0, 0) + - row 1: (0, 32760, 100) + - row 2: (0, -401, 8190) +- Section base at $4A..$52 (24-bit signed): + - base[0] = -257793 + - base[1] = -138241 + - base[2] = 1396736 +- $66/$67=0, $68/$69=48, $6A/$6B=-3819 (camera-relative section origin) +- **ViewDirection ($0A70) = $0F = 15** at boot (NOT 0 -- earlier recovery + text was wrong). chunk5 SetupViewProjection scales it x16 into a + byte-angle ($3E=$F0=-22.5deg) and feeds it into the yaw/pitch/bank + cascade via L6155. The port's `sceneryAttachCamera` ignores + ViewDirection entirely -- this is the most likely root cause of the + port-vs-MAME matrix mismatch. + +## Code changes landed this session + +### `port/src/sceneryVm.c` + +1. **`sceneryAttachCamera` matrix block** (around line 1255-1322): refactored so chunk5 L6301 column shifts (col 0 >>= 1, col 2 >>= 2) apply to BOTH the int8 pipeline matRow1/matRow2 AND the int16 writableRam mirror at $78..$89, in lockstep. Single source of truth. + +2. **`doHeader` zero-skip guard** (around line 354-380): when `state->sceneryFile` source range is entirely zero (= unused file block in .blocks indirection), skip the copy. Prevents clobbering destination $A84E+ with zero-byte garbage that the interpreter would mistake for $00 vertex_emit ops. + +## Active investigation: #9 — port matrix construction + +### Tooling + +- `port/bin/matrixProbe [wx wy wz]` + runs the port's `sceneryAttachCamera` and dumps `$78..$89`. Build + with `make -C port bin/matrixProbe`. +- `port/bin/fs2trace --matrix ` + runs the original chunk5 `SetupViewProjection` on the in-project + 6502 emulator (the same fs2trace already used for loader tracing), + using `tmp/capture_boot.bin` as the RAM image. Byte-perfect + ground-truth oracle for any (yaw, pitch, bank, VD) input. Build with + `make -C port bin/fs2trace`. +- `tmp/mame_capture.lua` accepts `MAME_POKE_VD` and `MAME_POKE_YPR` + for pinning ViewDirection / attitude angles continuously when + capturing fresh references. + +### Findings (2026-05-07 second session) + +1. **VD doesn't matter at boot.** Re-captured MAME with VD pinned to 0 + (`tmp/capture_boot_vd0.bin`). Matrix at $78..$89 is IDENTICAL to + the VD=15 capture: `[16382,0,0; 0,32760,100; 0,-401,8190]`. The + small off-diagonal terms in MAME do NOT come from ViewDirection. + +2. **chunk5 and port use DIFFERENT Euler conventions.** Verified with + the oracle by sweeping each input to 90 degrees while the others + are zero: + + | ZP slot | chunk5 axis | Port `cam->` field | + |------------------|-----------------------|---------------------| + | $6C/$6D "yaw" | rotation around X | `cam->pitch` | + | $6E/$6F "pitch" | rotation around Z | `cam->bank` | + | $70/$71 "bank" | rotation around Y | `cam->yaw` | + + The disassembly's labels are misleading. chunk5's "yaw" really + tilts up/down (X-axis = standard pitch); chunk5's "bank" really + spins about world up (Y-axis = standard yaw); chunk5's "pitch" + really rolls (Z-axis = standard bank). + +3. **The boot M12=100 / M21=-401 is from chunk5 yaw=$FF93 (-109/16b).** + That value is a tiny X-axis rotation (~0.7deg upward tilt). chunk5 + places the small term in M12/M21 (Y-Z plane). The port treats yaw + as Y-axis rotation and would place the same magnitude in M02/M20 + (X-Z plane). Both matrices are CORRECT for their convention -- + just expressed in different coordinate frames. + +4. **At zero angles** (yaw=pitch=bank=0, VD=0) the oracle and port + matrices match within rounding (essentially identity with the + col 0 >>= 1, col 2 >>= 2 shifts). They diverge only when angles + are non-zero AND map to different axes. + +### MAME draw-list capture findings (2026-05-07 evening session) + +Used MAME lua hooks to install a 6502 logger that JMP-traps +`DrawColorLine` ($795A in patched chunk5) and records each call's +$E9-$EC (screen coords) and $CB-$D9 (V1/V2 3D coords) into a +buffer at $B500. lua dumps the buffer per frame to +`tmp/mame_drawlist.txt`. Same for cursor trajectory hook at $6772 +in `tmp/mame_cursor_trace.txt`. + +What we learned: + +1. **MAME absolutely DOES draw chunk5 polygon scenery at boot.** + Hires page in `tmp/capture_boot.bin` rows 101-130 are rich with + line-pattern bytes — that's the actual scenery. The "MAME doesn't + draw" conclusion from earlier `fs2trace --scenery` was a dead end + caused by fs2trace not emulating Apple IIgs language-card bank + switching for $05 ADF -> chunk3 LookupADFStation calls. + +2. **At Meigs, MAME walks the dispatcher into a section at ~$B294 + and emits ~75 line draws per frame.** Cursor trajectory: + $B294 -> $B504 (one section, lots of $40/$41 vertex emits with + intermixed $13 culls). + +3. **Port wasn't chaining V1 from V2** after $41 emits, so polylines + degenerated into fans. chunk5's `EmitClippedLine` cleanup at + L6B2F overwrites V1 ($C9..$D2 / port: $CB..$D0) with V2's shadow + so the next emit chains correctly. Fixed in `doEmitV2`. + +4. **Port and MAME enter DIFFERENT sections from the outer + dispatcher.** Port hits a $0B JumpRelative at $AB17 -> $BADA; + MAME ends up at $B294. With USE_RAM_STATE (= MAME's exact + matrix + base + camera origin) the 3D vertex coords still don't + match -- port produces values ~5x MAME's magnitudes, suggesting + `chunk5TransformVertex7EBC` (port's C transliteration of the + $7EBC asm) has bugs. + +5. **The captured chunk5 RAM at $7EBC differs from the assembled + source.** Earlier hypothesis: "Apply64KPatchTable relocates + TransformVertex7EBC" -- VERIFIED FALSE. The 64K patch table + has no entry targeting $7EBC or $80C5. The runtime divergence + must come from something else -- likely a `$25 SceneryOpStoreImmWord` + or `$1A SceneryOpWriteWord` in early-boot scenery writing into + the chunk5 code area, OR the captured RAM image was taken from + a savefile / mid-run state where chunk5 had been mutated. + We've since built a bit-perfect `--xform` oracle running the + *source* chunk5 binary via FS2TRACE_USE_ORIG=1; that's the + correct reference for byte-level verification. + +### Remaining work + +- Compare port's vertex transform output to MAME's by feeding both + the SAME vertex bytes + state, then diff intermediate accumulator + values. Use `tmp/mame_drawlist.lua` (V1/V2 capture) as the + reference; instrument port's `chunk5TransformVertex7EBC` to dump + pre/post-multiply state for the same input. +- The discrepancy between port and MAME entry sections probably has + the same root cause -- the port walks a different dispatcher path + because some opcode handler (cull, sub-invoke, or store-imm-word) + diverges from the asm's behavior. + +### Concrete bug reproducer for chunk5TransformVertex7EBC + +`fs2trace --xform [ram.bin]` runs the asm $7EBC routine +on the unpatched chunk5 binary using captured RAM state (everything +except the chunk5 code regions that contain the routine). It overlays +the original chunk5 binary at $6000-$B27F so the asm executes +source-faithfully against MAME's matrix/base/camera. Inputs: + +- vertex bytes at $B28F: `40 B0 08 23 FD` (op $40 + xLo $B0 + xHi $08 + + zLo $23 + zHi $FD) +- state from `tmp/capture_drawlist.bin` (frame 11500 dump): + - matrix: `(16382,0,0 / 0,32760,100 / 0,-401,8190)` + - base ($4A..$4C MID/HI/LO): `D0 CC FF` + - base ($4D..$4F): `90 00 00` + - base ($50..$52): `60 F4 FF` + - camera ($66..$6B): `00 00 04 00 21 01` + +**Bug found and fixed (2026-05-07 night):** `op_l1818` in +`chunk5Transform.c` had **7 shift-add iterations in its main loop**; +chunk4.s `L1818` has only **6** (between labels L183A and L185D), +plus one final lsr+ror at L1864. The extra iteration shifted every +multiply result right by one bit, halving it. After the fix port and +asm produce bit-identical output for the matched test case: +V=(-12033, 160, -3224) for both. Verified by adding step-by-step +intermediate trace to both port and `fs2trace --xform` and walking +through one call. + +**Status post-fix:** chunk5TransformVertex7EBC now byte-identical to +asm for at least one test case. 42 chunk5 line draws produced at +boot Meigs (vs 51 with the bug, but those were wrong-positioned). +Visible scenery still doesn't match MAME because port's chunk5 +dispatcher walks INTO different sections than MAME's -- port enters +$BADA via $0B JumpRelative; MAME enters $B294. Same bytecode, +different cull-test outcomes upstream. That's the next bug to find, +not a multiplier issue. + +### Tooling now available + +- `port/bin/fs2trace --xform [ram.bin]` — runs asm $7EBC + oracle. Use frame-matched RAM state from + `tmp/capture_drawlist.bin` (= dumped by mame_drawlist.lua at the + same frame as the draw list). +- `port/bin/fs2trace --scenery [ram.bin]` — counts DrawColorSpan + calls across one chunk5 ProcessScenery pass. +- `port/bin/fs2trace --matrix yaw pitch bank vd` — already validated + bit-perfect. +- `port/bin/fs2trace --zpscale a b` — already validated bit-perfect. +- `port/bin/fs2trace --l177b a x` — already validated bit-perfect. +- `tmp/mame_drawlist.lua` — captures MAME line draws + V1/V2 3D + coords; also dumps RAM at end of capture frame. Run via + `mame apple2gs ... -autoboot_script tmp/mame_drawlist.lua`. +- `tmp/mame_cursor_trace.lua` — captures dispatcher cursor + trajectory. + +### B1 status (landed 2026-05-07) + +The actual divergence wasn't an Euler-order issue, it was a transpose +convention. chunk5 stores R (camera-to-world) at $78..$89; the port's +`cam->rot` stores R^T (world-to-camera) so its `cameraTransform` can +multiply (dx,dy,dz) directly. Same data, transposed access. + +**Implementation:** +- `CameraT` now carries a sibling `int16_t rotChunk5[3][3]` (R, no + transpose). `cameraUpdate` writes both -- one assignment block per + shape, no extra trig. +- `sceneryAttachCamera` mirrors `cam->rotChunk5` (NOT `cam->rot`) + into `writableRam[$78..$89]`. The renderer's int8 projection rows + (`matRow1/matRow2`) keep coming from `cam->rot` so projection math + is unchanged. +- `cameraTransform` is untouched -- still reads `cam->rot`. + +**Verification (port `matrixProbe` vs chunk5 `fs2trace --matrix`, +clean RAM, all-zero baseline):** + + | Test | Port matrix | chunk5 matrix | Match? | + |------------------------|-------------------------|--------------------------|--------| + | yaw=64 (Y+90 deg) | (0,0,8191/0,32766,0/-16383,0,0) | (0,0,8191/0,32765,0/-16383,0,0) | yes (+/-1) | + | pitch=64 (X+90 deg) | (M11=0, M12=-8192, M21=32767) | (M11=401, M12=-8191, M21=32758, M22=100) | shape yes, residual no | + | bank=64 (Z+90 deg) | (M00=0, M01=-32766, M10=16383) | (M01=-32765, M10=16380, M12=100, M20=-201) | shape yes, residual no | + +**Open sub-issue resolved: bit-perfect chunk5 transliteration landed.** + +The residual was an artifact of comparing port output to a CAPTURED MAME +RAM dump (frame 13000) where chunk5 has been heavily patched at runtime +by Apply64KPatchTable. The patched routine differs from the +chunk5.s source. Source-faithful comparison (port vs unpatched chunk5 +binary running on fs2trace's 6502 sim) is now bit-perfect. + +### Bit-perfect chunk5 SetupViewProjection in C + +`port/src/chunk5Setup.c` is a transliteration of: +- chunk5.s `SetupViewProjection` (lines 203-432) -- the full cascade. +- chunk4.s `ScaleC2ByC4` / `ZPScale` (lines 1565-1744) -- 16-bit + shift-and-add multiply. Bit-perfect against `fs2trace --zpscale` + for arbitrary inputs. +- chunk4.s `L177B` / `L1778` / `L17BC` / `L17DA` / `L17E1` (lines + 1900-2007) -- cos/sin lookup with sub-byte interpolation, including + the special X=$80 midpoint-average path. Bit-perfect against + `fs2trace --l177b` over a 256-case sweep. +- chunk4 cos table (132 bytes from offset $141A in + `out/4_0200-25ff`). + +Validation: `make -C port bin/chunk5SetupTest && bin/chunk5SetupTest`. +All test cases pass. The test driver shells out to `fs2trace` for +oracle values; running `fs2trace --matrix` with `FS2TRACE_USE_ORIG=1` +(load unpatched chunks, not the captured RAM) gives the source- +faithful reference. + +`cameraUpdate` now calls `chunk5SetupViewProjection` to populate +`cam->rotChunk5`; `sceneryAttachCamera` mirrors that into +`writableRam[$78..$89]`. The renderer pipeline still uses the +existing `cam->rot` (= R^T, world-to-camera) for vertex projection. + +The captured-RAM comparison is no longer the right reference -- use +the unpatched chunk5 binary via `FS2TRACE_USE_ORIG=1`. + +## Files NOT to delete + +- `tmp/mame_capture.lua` — capture script +- `tmp/capture_boot.bin` / `.zp` — MAME ground-truth state +- `tmp/mame_boot.png` / `mame_meigs_ref.png` — MAME ground-truth screenshot +- `tmp/compare_mame_vs_port_ramstate.png` — side-by-side comparison +- `port/screenshots/match_mame_ramstate.png` — port's best-effort match +- `port/sceneryRam_FS2.1.bin` — original port-side FS2.1 RAM dump (NOT MAME's; do not overwrite) +- `port/sceneryRam_FS2.1_chicago.bin` — original port-side chicago RAM dump + +## Remember +- Port lives outside git; don't run git on it. +- Scratch files go in `./tmp/`, not `/tmp/`. +- Screenshots go in `port/screenshots/`. +- The port uses fixed-point math; don't introduce float reinterpretations. + +## NEVER `Read` PNGs (avoids the API context corruption) + +The user views PNGs directly. Claude must NOT use the Read tool on PNGs -- +each multimodal image upload bloats the request and has tripped a recurring +"PNG-API context corruption" failure that nukes the session. + +Workflow: + +- Compare two images (text report, ASCII heatmap, auto-resizes mismatched scales): + ``` + cd /home/scott/claude/flight + port/tools/imgDiagnose.sh diff tmp/mame_boot.png port/screenshots/match_mame_ramstate.png --ascii + ``` +- Single-image summary (non-black coverage, luminance histogram, horizon-row guess): + ``` + port/tools/imgDiagnose.sh stats tmp/mame_boot.png + ``` +- Inputs may be `.png`, `.ppm`, or `.pgm`. PNGs are converted via + ImageMagick into a temp PPM in `tmp/` that the C tools read. Tools + live at `port/tools/imgDiff.c` / `imgStats.c` and build into + `port/bin/` via `make -C port tools` (auto-built on first wrapper run). +- The port already writes PPMs from `--screenshot` -- prefer those over + re-encoding to PNG when possible. diff --git a/orig/1_4000-5fff b/orig/1_4000-5fff new file mode 100644 index 0000000..ca86461 --- /dev/null +++ b/orig/1_4000-5fff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c257ad24f7d8f54f0cc6ce4a853e6df87f383dd99088100084ab613db983422c +size 8192 diff --git a/orig/2_f600-fbff b/orig/2_f600-fbff new file mode 100644 index 0000000..5093946 --- /dev/null +++ b/orig/2_f600-fbff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd16afd2802a2810b22a2dde94dc3488bb951ad16196fe37cf557c80986500d3 +size 1536 diff --git a/orig/3_d300-f3ff b/orig/3_d300-f3ff new file mode 100644 index 0000000..4a6170a --- /dev/null +++ b/orig/3_d300-f3ff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d878ae609911b50899fed4c9bbe6e89607ab5eaf006022d6323391b1be2b3c0b +size 8448 diff --git a/orig/4_0200-25ff b/orig/4_0200-25ff new file mode 100644 index 0000000..83d7215 --- /dev/null +++ b/orig/4_0200-25ff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:123c9409f30398f87c2435c7a7f4217d507b17bafae76b84360f65c9cda5452c +size 9216 diff --git a/orig/5_6000-b3df b/orig/5_6000-b3df new file mode 100644 index 0000000..41954f0 --- /dev/null +++ b/orig/5_6000-b3df @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1170385ba1b3e6672a3ff201979e4e82e9e1825b772087d1e3ee09dc85a8e805 +size 21472 diff --git a/orig/FS2#062499 b/orig/FS2#062499 new file mode 100644 index 0000000..e1e1481 --- /dev/null +++ b/orig/FS2#062499 @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19e92402c19bf681a5238fea7f4bd9deb8b45e6cc42bce0c7c714c3e7da039a5 +size 28494 diff --git a/orig/flight simulator 2 with scenery PRODOS (san inc pack).po b/orig/flight simulator 2 with scenery PRODOS (san inc pack).po new file mode 100644 index 0000000..229d803 --- /dev/null +++ b/orig/flight simulator 2 with scenery PRODOS (san inc pack).po @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fb176a1b4c5c9c398f51455b6af5d2a2ca257458d8601ae8b3aee1cd4c35fc0 +size 2097152 diff --git a/port/Makefile b/port/Makefile new file mode 100644 index 0000000..3fb096f --- /dev/null +++ b/port/Makefile @@ -0,0 +1,73 @@ +# FS2 modernized C port - Makefile +# +# Layout: +# src/ .c sources for the fs2port binary +# include/ .h headers shared by sources +# tools/ .c sources for the offline analysis tools +# screenshots/ saved .png / .ppm output +# obj/ .o object files (build output) +# bin/ compiled fs2port binary + tool binaries (build output) + +CC = gcc +CFLAGS = -std=c11 -O2 -Wall -Wextra -Wshadow $(shell sdl2-config --cflags) -Iinclude +LDFLAGS = $(shell sdl2-config --libs) -lm + +# Tools are plain C with no SDL dependency. dumpStations is the +# exception -- it pulls in the port's actual scenery interpreter so +# the offline scan stays bit-identical with the live renderer. +TOOL_CFLAGS = -std=c11 -O2 -Wall -Iinclude + +SRC_DIR = src +TOOL_DIR = tools +OBJ_DIR = obj +BIN_DIR = bin +INC_DIR = include + +SOURCES = $(notdir $(wildcard $(SRC_DIR)/*.c)) +OBJECTS = $(SOURCES:%.c=$(OBJ_DIR)/%.o) +HEADERS = $(wildcard $(INC_DIR)/*.h) +TARGET = $(BIN_DIR)/fs2port + +TOOL_SRCS = $(notdir $(wildcard $(TOOL_DIR)/*.c)) +TOOLS = $(TOOL_SRCS:%.c=$(BIN_DIR)/%) + +.PHONY: all clean run tools + +all: $(TARGET) tools + +tools: $(TOOLS) + +$(TARGET): $(OBJECTS) | $(BIN_DIR) + $(CC) -o $@ $^ $(LDFLAGS) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c $(HEADERS) | $(OBJ_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# dumpStations links with the port's scenery interpreter so the +# offline station scan uses the same dispatcher / advance counts. +$(BIN_DIR)/dumpStations: $(TOOL_DIR)/dumpStations.c $(SRC_DIR)/sceneryVm.c $(SRC_DIR)/sceneryProjection.c $(SRC_DIR)/chunk5Transform.c $(SRC_DIR)/chunk5Setup.c $(SRC_DIR)/cpu6502.c $(SRC_DIR)/hires.c $(HEADERS) | $(BIN_DIR) + $(CC) $(TOOL_CFLAGS) -o $@ $(TOOL_DIR)/dumpStations.c $(SRC_DIR)/sceneryVm.c $(SRC_DIR)/sceneryProjection.c $(SRC_DIR)/chunk5Transform.c $(SRC_DIR)/chunk5Setup.c $(SRC_DIR)/cpu6502.c $(SRC_DIR)/hires.c -lm + +# matrixProbe drives sceneryAttachCamera with controlled inputs so the +# port's $78..$89 matrix can be diffed against MAME's capture. +$(BIN_DIR)/matrixProbe: $(TOOL_DIR)/matrixProbe.c $(SRC_DIR)/sceneryVm.c $(SRC_DIR)/sceneryProjection.c $(SRC_DIR)/chunk5Transform.c $(SRC_DIR)/camera.c $(SRC_DIR)/math6502.c $(SRC_DIR)/chunk5Setup.c $(SRC_DIR)/cpu6502.c $(SRC_DIR)/hires.c $(HEADERS) | $(BIN_DIR) + $(CC) $(TOOL_CFLAGS) -o $@ $(TOOL_DIR)/matrixProbe.c $(SRC_DIR)/sceneryVm.c $(SRC_DIR)/sceneryProjection.c $(SRC_DIR)/chunk5Transform.c $(SRC_DIR)/camera.c $(SRC_DIR)/math6502.c $(SRC_DIR)/chunk5Setup.c $(SRC_DIR)/cpu6502.c $(SRC_DIR)/hires.c -lm + +# chunk5SetupTest validates the C transliteration of chunk5 +# SetupViewProjection / L177B / ScaleC2ByC4 against the fs2trace +# oracle that runs the actual chunk5 binary. +$(BIN_DIR)/chunk5SetupTest: $(TOOL_DIR)/chunk5SetupTest.c $(SRC_DIR)/chunk5Setup.c $(HEADERS) | $(BIN_DIR) + $(CC) $(TOOL_CFLAGS) -o $@ $(TOOL_DIR)/chunk5SetupTest.c $(SRC_DIR)/chunk5Setup.c -lm + +# Default rule for the standalone tools (no port dependencies). +$(BIN_DIR)/%: $(TOOL_DIR)/%.c | $(BIN_DIR) + $(CC) $(TOOL_CFLAGS) -o $@ $< + +$(OBJ_DIR) $(BIN_DIR): + mkdir -p $@ + +run: $(TARGET) + ./$(TARGET) + +clean: + rm -f $(OBJ_DIR)/*.o $(TARGET) $(TOOLS) diff --git a/port/PORT_64K_AUDIT.md b/port/PORT_64K_AUDIT.md new file mode 100644 index 0000000..14f029d --- /dev/null +++ b/port/PORT_64K_AUDIT.md @@ -0,0 +1,255 @@ +# 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. diff --git a/port/PORT_STATUS.md b/port/PORT_STATUS.md new file mode 100644 index 0000000..7d9fd13 --- /dev/null +++ b/port/PORT_STATUS.md @@ -0,0 +1,338 @@ +# FS2 C Port — Status vs Original + +Comparison of the original Apple II FS2 (disassembled in `src/chunk*.s`) +against the C port in `port/`. Generated 2026-05-06. + +Legend: ✅ done · 🟡 partial / approximation · ❌ missing + +--- + +## Flight model + +| Feature | Original | Port | +|---|---|---| +| Position integrator (24-bit XYZ) | `IntegratePhysicsStep` | ✅ `aircraftStep` (Q16.16) | +| Pitch / bank / yaw rates | `UpdateAutoTrimAndYaw` etc | ✅ `stepFlight` | +| Auto-coordination (bank → yaw) | `ApplyAutoCoordination` | ✅ `stepFlight` | +| Wind / turbulence | chunk2 `ApplyWind` (64K) | ✅ `windCompute` / `windApply` | +| Stall detection & break | per-instrument check | 🟡 stalled flag, no spin | +| Spin recovery | implicit in stall handling | ❌ | +| High-G / VNE bleed | `CheckFlightEnvelope` | 🟡 `envelopeWarning` flag, no bleed | +| Flap/gear speed effects | `RefreshElevatorIndicator` | ❌ | +| Mixture too-lean = engine cut | implicit | ❌ | +| Carb heat icing | chunk5 `CarbHeat` | 🟡 audio penalty added, no icing model | +| Magneto on/off effect on engine | `UpdateEngineWithMagneto` | 🟡 audio penalty added (no full model) | +| Engine fault dispatch | `FailureProcTable` | ✅ `aircraftStep` reality dispatch | +| Engine knock audio | per-fault sound | ✅ `audioUpdate` wobble | +| Crash detection | `HandleCrashOrSplash` | ✅ `aircraftStep` ground/water | +| Splash detection (water) | `CheckSplash` | 🟡 land-only crash | +| Building / mountain crash | `crash_msg_table` | 🟡 type set but no scenery-aware test | + +## Modes + +| Feature | Original | Port | +|---|---|---| +| Free flight | default | ✅ | +| Slew mode | `SlewMode` (chunk5) | ✅ `aircraftToggleSlew` | +| Slew digit overlay | `DrawSlewOverlays` | ✅ `instruments.c` north/east/alt | +| Demo mode | `DemoMode64K` | 🟡 `autopilotDemo` (basic) | +| Demo waypoint sequence | per-mode auto-flight | ❌ | +| Edit mode | `EditModeFlag` | 🟡 toggleable, no state save/restore | +| Reality mode | `RealityMode` | ✅ instrument & engine failures | +| Radar view | `RadarView` | ✅ `worldRenderRadar` | +| WW1 ace combat | `WW1AceMode` | ✅ `ww1ace.c` | +| Course Plotter | chunk2 `CoursePlottingMenu` | ✅ `coursePlotter.c` (record/display) | +| Pause | `TogglePause` | ✅ P key | +| Boot DOS | `BootDOS` | ❌ (no DOS to boot) | + +## Instruments + +| Feature | Original | Port | +|---|---|---| +| Airspeed needle | `UpdateAirspeedIndicator` | ✅ `instruments.c::airspeedGauge` | +| Altimeter main hand | `UpdateAltimeterIndicator` | ✅ | +| Altimeter 10K hand | `UpdateAltimeter10K` (64K) | ✅ | +| Attitude indicator | tilted disc | ✅ `drawHorizonDisc` | +| Heading bug | `DrawHeading` | ✅ digit readout | +| Magnetic compass | `DrawMagCompass` | 🟡 digit only, no rotating compass card | +| Vertical speed | `UpdateVerticalSpeedIndicator` | ✅ | +| Turn coordinator | `UpdateTurnCoordinator` | ✅ | +| Slip/skid ball | `PLSlipSkidIndicator` | ✅ | +| Throttle position | `UpdateThrottleIndicator` | ❌ (no graphical needle) | +| Mixture position | `UpdateMixtureControlIndicator` | ❌ | +| Flap position | `UpdateFlapsIndicator` | ❌ | +| Trim position | implicit in auto-trim | ❌ (no key bindings) | +| Fuel tank L/R | `UpdateFuelTankGauges` | ❌ | +| Oil temp/pressure | `UpdateOilTempAndPressureGauges` | ❌ | +| RPM display | `DrawRPM` | ✅ digit | +| Magneto state visual | `DrawMagnetoState` | ✅ MAG OFF/L/R/START indicator | +| Carb heat state | switch position | ✅ "CARB HEAT" indicator | +| Lights state | switch position | ✅ "LIGHTS ON" indicator | +| Failure indicator (X over gauge) | `DrawX` per gauge | ✅ `drawFailX` | +| Stall warning | `STALL` text | ✅ | +| VNE warning | `VNE` text | ✅ | + +## Radios / Navigation + +| Feature | Original | Port | +|---|---|---| +| NAV1 frequency | `NAV1` ($08F7) | ✅ `radios.c` | +| NAV2 frequency | `NAV2` ($08F5) | ✅ | +| ADF frequency | `ADFFreq*` | ✅ | +| COM1 frequency | str_com1 | ✅ | +| Station database (deduped) | per-region | ✅ 695 entries from extractstations | +| BCD frequency increment | step keys | ✅ shift+digit / digit | +| BCD per-digit entry | `KeyDecreasePatch` | ❌ | +| OBS course knob | OBS-related | ✅ `,`/`.` keys | +| VOR CDI needle | `DrawVOR1IndicatorChanges` | ✅ | +| VOR TO/FROM flag | `msg_vor_flags` | ✅ "TO"/"FR"/"OFF" | +| ILS glide slope | not in original | ❌ | +| DME readout | `DrawATISMessage` ATIS bound | ✅ | +| ADF needle | `DrawADFPanel` | ✅ | +| ADF heading digits | `DrawADFHeadingDigits` | ✅ | +| ATIS message | `UpdateCOMMessageChunks` | 🟡 freq shown, no chunked text | +| Tune-to-nearest button | not in original | ✅ T key (port-only) | + +## Scenery system + +| Feature | Original | Port | +|---|---|---| +| Disk loader (`SceneryReadUntilC0`) | chunk3 `SceneryLoaderEntry1` | 🟡 RAM dump pre-load + .SD demand-load | +| Block-list indirection | chunk4 `ComputeBlockFromSector` | ✅ `doHeader` correct mapping | +| Nibble decode | `SceneryNibbleDecode` | ❌ (only used by Entry4 path) | +| HEADER opcode ($0D) | `SceneryOpHeader` + `LA63A` | ✅ `doHeader` (with cache) | +| L631D section base | `L631D` | ✅ `sceneryComputeBaseL631D` | +| EnterLocalFrame ($07) | `SceneryOpEnterLocalFrame` | 🟡 simplified passthrough | +| Vertex emit + transform ($00-$02, $40-$42) | `SceneryOpEmitV*` | ✅ | +| Cull ($20/$21/$22) | `SceneryOpCullIfOutside*` | ✅ `doCullN` | +| Cull by outcode list ($04) | `SceneryOpCullByOutcodeList` | 🟡 walks list, no actual cull | +| Jump-if-beyond-XY/XYZ ($13/$14) | `SceneryOpJumpIfBeyondXY*` | ✅ | +| REL_JUMP ($0B) | `SceneryOpJumpRelative` | ✅ | +| SUB_INVOKE ($18) / RETURN ($19) | `SceneryOpSubInvoke` | ✅ | +| RESET_STATE ($2F) | `SceneryOpResetState` | ✅ | +| MODE_WHITE ($1B) | line-kernel patch | ✅ semantic equivalent | +| DAY_ONLY ($1C) | line-kernel patch | ✅ skip-on-night flag | +| WriteWord ($1A) / StoreImmWord ($25) | self-mod patches | ✅ | +| Vertex-cache ops ($31/$32/$33/$35/$42) | cached vertex pool at $0140 | ✅ pool reads, no full $31/$42 transform | +| ADF/NAV/COM record ($05/$1D/$1E) | station records | ✅ | +| SET_COLOR ($12) | `SceneryOpSetColor` | ✅ | +| Polygon edge emit | `EmitClippedLine` | 🟡 line draw only, no polygon close | +| Polygon scanline fill | `DrawColorSpan` etc | 🟡 2D scanline edge-intercept (`rendererFillPolygon`); not chunk5's 3D-clipped scanline emitter | +| Polygon 4-pass 3D clipper | `PolygonScanFillSetup` + Top/Right/Bottom passes | ✅ `sceneryClipPolygon3D` (source-faithful Sutherland-Hodgman against Z-X / Z-Y / X+Z / Y+Z planes; ping-pongs PrimVerts↔SecVerts; produces expanded wedge polygons that span the frustum). `PORT_LEGACY_POLY_FILL=1` reverts to 2D-only fill. | +| Sky/ground tilted fill | `FlipPagesFillViewport` | ✅ `rendererFillTiltedSkyGround` | +| Frustum clipping (3D) | `ClipBothVerticesToFrustum` | ✅ outcode + perspective divide | +| Vertex pool / EmitPrimaryVertex | $0AB8 column array | 🟡 small pool, no polygon closure | + +## Display + +| Feature | Original | Port | +|---|---|---| +| 280×192 framebuffer | hires page 1/2 | ✅ | +| Page flip | `FlipPagesFillViewport` | 🟡 single buffer (no flip needed) | +| Color/B&W mode | `ColorModePatch` / `BWModePatch` | 🟡 always color | +| Dotted-pattern night | `SceneryOpDayOnly` etc | ✅ DAY_ONLY skip | +| Panel bitmap | hires loaded from disk | ✅ res/loading_panel.bin | +| Panel lights overlay (64K) | `UpdateInstrumentLights` | 🟡 lights state shown as text | +| Message text (`DrawMultiMessage`) | string blit | ✅ font.c | +| Crash message overlay | `crash_msg_table` | ✅ MOUNTAIN/BUILDING/SPLASH/CRASH | +| Wing/tail overlays in side views | `DrawWingsOrTailOverlays` | ❌ | +| Bomb sight | WW1 bombsight pixels | ✅ `ww1aceHudDraw` | +| Gunsight | WW1 only | ✅ `ww1aceHudDraw` | + +## Audio + +| Feature | Original | Port | +|---|---|---| +| Engine sound | speaker click | ✅ sawtooth + throttle modulation | +| Engine fault wobble | not present in original | ✅ phase-modulated wobble | +| Magneto-off engine cut | engine flag | ✅ amp = 0 when MAG OFF | +| Stall horn | beeper trill | ✅ 800 Hz square wave | +| Crash impact | speaker noise | ✅ noise burst | +| Gun fire (WW1) | not in original | ✅ rapid sawtooth burst | +| Bomb drop (WW1) | not in original | ✅ pitch sweep | +| Carb heat icing audio | not directly | 🟡 power penalty only | +| Wind hiss | not in original | ❌ | + +## Input + +| Feature | Original | Port | +|---|---|---| +| Yoke (arrows / WASD) | arrow + paddle | ✅ | +| Rudder | `/` and Ctrl | ✅ Q/E | +| Throttle | `[` `]` Ctrl+H | ✅ Up/PgUp/Dn/PgDn | +| Brake | space | ✅ space cuts throttle | +| Slew controls | 8/9, 0/-, ,/. , +/= | 🟡 W/A/S/D in slew mode | +| View directions (F1-F5) | 1-5 keys | ✅ F1-F5 | +| Magneto select | 1-3 keys | ✅ Shift+M cycles | +| Lights toggle | L key | ✅ L | +| Carb heat | H key | ✅ H | +| Pause | Ctrl-P | ✅ P | +| Edit mode | Ctrl+[ | ✅ F7 | +| Demo mode | Ctrl+D | ✅ F10 | +| Slew toggle | Ctrl+S | ✅ F12 | +| Reality mode | Ctrl+R | ✅ Tab | +| Radar view | F | ✅ ` (backquote) | +| Course plotter menu | Ctrl+C | ✅ C/V/B/N (record/precision/display/off) | +| Joystick | game port | ✅ SDL_Joystick | + +## Persisted state + +| Feature | Original | Port | +|---|---|---| +| Edit mode revert (instrument save buffer) | $FC00+ | ❌ | +| Saved instrument state for crash recovery | yes | ❌ | + +## Subsystems addressed in latest pass + +- ✅ **BCD per-digit frequency entry**: `radiosEnterDigit()` mirrors FS2 + KeyDecreasePatch — shifts current freq left, drops the high digit, + appends new digit, snaps to 0.05 MHz step for NAV/COM. +- ✅ **Throttle/Mixture/Flaps/Trim/Fuel** bar indicators in + `instruments.c`; trim/flap/mix bound to keys. +- ✅ **Fuel gauges** (left/right): per-frame burn, alternating tanks. +- ✅ **Color/B&W mode toggle**: Ctrl+F2 flips `ac->monochrome`; viewport + switches to black backdrop. +- ✅ **State save/restore for edit mode**: snapshot on toggle in, + restore on toggle out (`editSavedState`). +- ✅ **L6BB0 axis permutations** for `$07` SceneryOpEnterLocalFrame: + variant 0 (×16 hi-byte), 2 (byte-swap), 4 (×16 lo-byte), 5/default + (passthrough). +- ✅ **Near-plane clip in vertex emit**: when one endpoint is in front + and one behind, interpolate to z=1 and draw the visible portion. +- ✅ **Audio fixed-point**: engine freq + amp now Q8.8, fault wobble + via `math6502Sin` Q1.15 phase. + +## Visible scenery — UNBLOCKED 2026-05-06 + +After tracing the unit/sign mismatch: +1. `pipe.proj.camX/camZ` was being set to **metres** while the bytecode + stream encodes scenery units (= metres × 3). Fixed in + `sceneryAttachCamera` to scale by `AC_SCENERY_UNITS_PER_METRE` (= 3) + before storing. +2. `cameraGet2x3Matrix` was producing matRow2 with FS2's right-handed + Z (forward = +world-Z) but the bytecode expects FS2's left-handed + convention (Z increases southward). Negated `cam->rot[i][2]` in the + matrix output. + +After both fixes: SD3 scenery actually renders. With aircraft at +metres `(-3500, 200)` (= scenery `(-10500, 600)`), section 2's HEADER +demand-loads the geometry, and 308 vertex emits produce visible lines +on screen. At yaw=64 (= 90°, looking east), 308 draws hit the +viewport showing a road + building at distance. + +`port/screenshot_first_visible_scenery.png` and +`port/screenshot_yaw64.png` are saved milestones. + +## City scenery (Chicago / LA / Seattle / NY) - 2026-05-06 + +The Apple II FS2 base disk really does ship Chicago + LA + Seattle + +NY scenery, not just WWI. The path to load each: + +1. The boot's main-menu sequence (color/BW prompt, demo/regular + prompt, then a city/database menu at .po block 236) ends with + `JSR $8758`/`$875B`/`$875E`/`$8761` -- these are jump thunks to + `LoadSceneryFile1..4` at `$A674/$A67D/$A686/$A68F`. +2. Each `LoadSceneryFile*` reads the city's dispatcher into `LA7E0+` + (256 bytes-1.5 KB, depending on descriptor). +3. The first `MainLoop` iteration calls `LoadDispatcherPointer` + (`$A61B`) → `ProcessScenery` (via `$L6006`). This walks the + city's dispatcher and fires `$0D` HEADER opcodes that demand-load + the actual polygon data via SmartPort block reads. + +`port/tools/fs2trace` now exposes `FS2TRACE_CITY=N` (1=Chicago, +2=LA, 3=Seattle, 4=NY) which runs `MainGameEntry` → `LoadSceneryFile*` +→ `LoadDispatcherPointer` → `L6006` so the resulting RAM dump has +both the city dispatcher (at `LA7E0`) and the demand-loaded polygons +baked in. + +### Bug fixes that unblocked rendering + +1. **Vertex op `$40/$41/$42` mismapping**: port had them as + draw/silent/draw, but chunk5's `SceneryOpcodeTable` says + `$40 = SceneryOpEmitV1Xform7EBC` (silent V1 emit), + `$41 = SceneryOpEmitV2Xform7EBC` (V2 emit + line draw v1->v2), + `$42 = SceneryOpRefreshCachedXform7EBC` (cache refresh, advance 1). +2. **`doEmitV2` drew prev-V2 → new-V2**; chunk5's `EmitClippedLine` + draws current-V1 → new-V2. Fixed. +3. **fs2trace block-list cap was 200 entries**; `ComputeBlockFromSector` + for higher-numbered sectors needs entries up to 256. Bumped. + +### Current rendering status + +| Region | Default `(X, Z)` | Result | +|-------------------------|------------------|----------------------------------| +| `SCENERY_FS2_1` | any | WWI training map (fixture-rendered) | +| `SCENERY_FS2_1_CHICAGO` | `(0, 1000)` | 957 vertex / 957 draws -- Sears Tower visible | +| `SCENERY_FS2_1_LA` | `(0, 1000)` | 905 vertex / 905 draws -- LA skyline visible | +| `SCENERY_FS2_1_SEATTLE` | n/a | dispatcher loaded; no section's cull passes at (0,0). Needs starting-position research | +| `SCENERY_FS2_1_NY` | n/a | same as Seattle | + +`port/screenshots/chicago_marquee.png` is the canonical Chicago +shot showing the Sears Tower spire and downtown silhouette. + +## Walk-all-paths mode (2026-05-06) + +`SCENERY_WALK_ALL=1` makes the interpreter take BOTH branches at every +conditional opcode (`$13/$14` JumpIfBeyondXY, `$20/$21/$22` CullN, +`$04` CullByOutcodeList, `$1C` DAY_ONLY). The visited[] array bounds +the work to one visit per cursor position. With this on, every section +in the dispatcher's `$0D` HEADER chain fires, so any scenery the .SD +file holds for that region is demand-loaded into the working RAM. + +For `SCENERY_FS2_1` this unfortunately doesn't conjure more polygons: +the FS2 base disk's scenery payload is read by chunk5's per-frame disk +loader during the initial main-loop iteration, not from a separate +flat `.SD` file. Cities in `chunk5 InitialZeroPageData` reference +positions outside the dispatcher's first-section bounds, but their +polygon geometry is brought in by extra block reads chunk5 issues +when the dispatcher's cull passes for that section -- a flow we don't +yet replicate offline. See `port/tools/fs2trace.c` `FS2TRACE_INIT_X/Z` for +the work-in-progress per-city RAM-dump capture path. + +## Multi-region scenery (2026-05-06) + +The FS2 base disk (`SCENERY_FS2_1`) ships only the WW1 ace training +field as renderable polygons; its dispatcher's section culls cover a +narrow ~0..500 unit envelope. The COM/NAV database lists US cities +(Chicago/Meigs at worldX=1548, NY/JFK at worldX=1196, LA/LAX at +worldX=599, Seattle/SEA at worldX=2912), but their *polygon* data lives +on the matching scenery disks: + +| City | Scenery region | +|-------------|----------------| +| WW1 ace | `SCENERY_FS2_1` | +| LA / SF | `SCENERY_SD3` (renders at e.g. metres `(-3500, 200)` yaw 64) | +| Seattle | `SCENERY_SD4` | +| (Chicago / NY have no Apple II SubLOGIC scenery disk released) | + +The `SCENERY_REGION` env var on `--screenshot` selects the region. +`SCENERY_FORCE_X` / `SCENERY_FORCE_Z` (in metres) teleport the +aircraft into a section. `port/tools/fs2trace` now accepts +`FS2TRACE_INIT_X` / `FS2TRACE_INIT_Z` (16-bit upper words of the 24-bit +zero-page scenery position) so a per-region RAM dump can be made for +sections outside the dispatcher's default cull window — useful for +forcing demand-loads in regions with multiple sub-sections. + +## Still missing + +- **Polygon scanline fill**: scenery emits line edges only. FS2's + scenery is mostly wireframe so this is mostly cosmetic, but + surface fills (water, runway) are unfilled. +- **Demo waypoint sequence**: chunk5/chunk2 `DemoMode64K` flies a + programmed circuit. Port's `autopilotDemo` is a simple altitude/ + throttle hold. +- **Stuck-key magneto auto-alternation** — chunk5 + `MagnetosLeft/Right` handle key-held edge cases. +- **ATIS chunked text scroll** — chunk5 `UpdateCOMMessageChunks` + cycles through airport names; port shows current frequency only. +- **Wing/tail/cowling overlays** in side/back/down views — chunk5 + `DrawViewOverlays` / `DrawWingsOrTailOverlays`. +- **Day-side detailed runway striping** — chunk5 + `DrawHorizonDisc` etc has runway-specific colour-only details. +- **Oil temp/pressure gauges** — chunk5 + `UpdateOilTempAndPressureGauges`. +- **Section anchor / $07 EnterLocalFrame in real bytecode**: works + when the bytecode actually fires $07 (mostly doesn't in the streams + we walk), but full multi-section navigation may need additional + fixes around section-base init (e.g., reading anchor coords from + the loaded section's preamble). diff --git a/port/docs/scenery_opcodes.md b/port/docs/scenery_opcodes.md new file mode 100644 index 0000000..fa773e2 --- /dev/null +++ b/port/docs/scenery_opcodes.md @@ -0,0 +1,1006 @@ +# FS2 Scenery Database Opcode Reference + +Complete reference for the chunk5 scenery interpreter opcodes, derived from +the source disassembly at `/home/scott/claude/flight/src/chunk5.s`. Each +record in the scenery DB starts with a 1-byte opcode followed by a fixed +or variable number of parameter bytes. + +The interpreter (`SceneryInterpreterStep` at chunk5.s:1264) reads one byte +from the cursor `($8B/$8C)`, dispatches via `SceneryOpcodeTable` (= 70 +2-byte addresses at chunk5.s:1165). Opcodes with bit 7 set OR codes in +`[$46..$7F]` BOTH trigger `SceneryStreamEnd` (= terminator) -- the dispatch +predicate is `cmp #$46 / bmi SceneryDispatch`, so $46..$7F fall through to +the same end-of-stream handler as $80..$FF. + +Total record length includes the opcode byte itself. + +--- + +## "source" vs "MAME-patched" + +This document distinguishes two views of the chunk5 binary: + +- **"source"** — the original 48K Apple ][+ image, as it appears in the + disassembly at `/home/scott/claude/flight/src/chunk5.s`. This is what + shipped on disk. + +- **"MAME-patched"** — the post-boot binary as actually observed in + `port/sceneryRam_FS2.1.bin` (a 64K RAM snapshot from a running MAME + `apple2gs` after FS2 boot). The reference screenshot + `port/screenshots/mame_meigs.png` also comes from that same MAME + instance, so to match the reference the port must replicate the + runtime behavior — not just the source-faithful behavior. + +**Where the divergences come from (revised).** An earlier version of +this doc attributed the source-vs-RAM-dump differences to +`Apply64KPatchTable`. That turned out to be wrong: Apply64KPatchTable +patches ~30 sites in the `$6000..$AE00` range — instrument hooks, +ADF lookup, course plotter, magneto / engine model, day phase, ATIS +dispatch, the chunk4 scenery-loader thunks, etc. — but it does **not** +touch the scenery-opcode dispatch table, the polygon/clipper kernel, +the matrix at `$78..$89`, the camera ZP `$66..$6B`, the section-base +accumulator, `EmitClippedLine`, or any of the `TransformVertex*` +routines. Those addresses are unchanged between the source image and +the runtime image. + +So why does the captured RAM look different at `$4A..$52` vs `$2A..$2F`, +at `$2C` vs `$29`, at `$35/$36` vs `$38/$39`? + +The MAME RAM snapshot was taken **mid-flight**, after the scenery +dispatcher had already walked into `$A800` and executed a sequence of +opcodes. Scenery bytecode itself can mutate chunk5's code area via +`$1A SceneryOpWriteWord` and `$25 SceneryOpStoreImmWord`. These two +ops accept any 16-bit destination, including absolute addresses inside +chunk5. The runtime image therefore differs from the source not because +of one-time patching at boot, but because of **continuous scenery-driven +self-modification** as the dispatcher walks sections. + +This has practical consequences for the port: + +- The port can faithfully implement the SOURCE semantics of every + opcode and still produce different rendering output from MAME if + the scenery files it loads contain `$1A`/`$25` writes that target + ZP slots used by the source code paths. The port may need to apply + the same writes when it sees those opcodes, but only as a side + effect — not as a baked-in alternate layout. +- Reading the RAM snapshot as "the right" view is misleading. The + RAM is the state AFTER scenery has executed `N` opcodes. A clean + source-faithful port that walks the same scenery files should + converge on the same observable state. +- The differences listed in older sections of this doc (`$4A..$52` + vs `$2A..$2F`, etc.) are real OBSERVATIONS of the RAM but + unreliable as STATEMENTS about the engine. The engine reads from + the source-defined locations. Wherever the runtime appears to write + to a different location, that's a scenery `$1A`/`$25` artifact and + needs to be traced through the scenery byte stream, not modeled + as an alternate engine layout. + +The older "source vs MAME-patched" divergence table below is preserved +for historical reference and may still be useful when comparing the +port's runtime against a captured RAM image. Read it as +"these are the addresses where the RAM dump deviated from what a +source-faithful run would produce," not as "the engine implements two +layouts." + +| Aspect | Source (chunk5.s) | RAM-snapshot observation | +|---|---|---| +| Section base accumulator | `$4A..$52` (3 axes × 24-bit MID/HI/LO) | `$2A..$2F` (3 axes × 16-bit LE) | +| L631D scale cache | `$35/$36` | `$38/$39` | +| Polygon-mode flag (set by $2F, checked by $29/$41) | `$2C` | `$29` | +| Y-axis scratch (in $07/$24 setup) | `$1B/$1C` | `$1A/$1B` | +| Z-axis scratch (in $07/$24 setup) | `$1E/$1F` | `$1C/$1D` | +| `EmitClippedLine` tail | `JMP SceneryInterpreterStep` | `RTS` (likely from scenery $1A overwriting the JMP opcode) | +| Op handler addresses | per source labels (e.g. `$6AAD`) | offset (likely the result of multi-step scenery-driven mutation) | +| Chunk3 callbacks ($05 ADF, $0E 64K-call, $1D NAV, $1E COM, $03 rotated transform) | mostly no-ops on 48K | wired up to chunk3 routines via Apply64KPatchTable (this row IS a real Apply64KPatchTable patch) | + +Where this document says "source" or "RAM-snapshot", that's what's +meant. Code and addresses cited as just "chunk5.s:line" refer to the +source disassembly. + +--- + +## Vertex emit / line draw family + +### $00 SceneryOpEmitV1XformAndPlot (xform-A V1 + single-pixel plot) + +**Length:** 7 bytes (`$00 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) + +Reads 6 stream bytes as a 16-bit X, Y, Z vertex. Transforms via +`TransformVertex80C5` into V1 (`$CB..$D0`). If V1's outcode is 0 (= in +frustum), projects to screen and PLOTS A SINGLE PIXEL via +`PlotColorPixel`. Used for star fields, runway threshold dots, etc. + +### $01 SceneryOpEmitV1Xform80C5 (xform-A V1, no draw) + +**Length:** 7 bytes (`$01 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) + +Same as $00 but no pixel plot. Just transforms V1, classifies, and stores +shadow at `$07..$10`. Used to set the start endpoint for a subsequent +$02 line emit. + +### $02 SceneryOpEmitV2Xform80C5 (xform-A V2 + line v1->v2) + +**Length:** 7 bytes (`$02 X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) + +Transforms V2 via `TransformVertex80C5`. If polygon-mode flag (`$2C`, +MAME-patched: `$29`) is non-zero, append V2 to PrimVerts polygon array. +Otherwise tail-jump to `EmitClippedLine` to draw a line from V1 to V2. + +### $40 SceneryOpEmitV1Xform7EBC (xform-B V1) + +**Length:** 5 bytes (`$40 X_lo X_hi Z_lo Z_hi`) + +Like $01 but uses `TransformVertex7EBC` (= xform-B). Stream encodes only +X and Z (16-bit each); Y is implicit from base accumulator at `$2A..$2F` +(MAME-patched) or `$4A..$52` (source). Used for ground/runway polygons +where vertices share a base Y. + +### $41 SceneryOpEmitV2Xform7EBC (xform-B V2 + line v1->v2) + +**Length:** 5 bytes (`$41 X_lo X_hi Z_lo Z_hi`) + +Like $02 but xform-B. Same polygon-mode-vs-line-emit branch. After +EmitClippedLine, V1 is replaced by V2 (= chained polyline). + +### $42 SceneryOpRefreshCachedXform7EBC (cache-update V2 with xform-B) + +**Length:** 6 bytes (`$42 vertex_idx X_lo X_hi Z_lo Z_hi`) + +Reads vertex_idx, points `($2D)` at cached vertex slot (`$0140 + idx*8`). +Snapshots V2 ($D2..$DA) to $DB shadow, runs xform-B on stream bytes into +V2, writes transformed V2 back to the cache slot. Used to refresh dynamic +vertices (e.g. moving aircraft hash marks). + +### $31 SceneryOpRefreshCachedXform80C5 (cache-update with xform-A) + +**Length:** 8 bytes (`$31 vertex_idx X_lo X_hi Y_lo Y_hi Z_lo Z_hi`) + +Same as $42 but xform-A (full XYZ stream). + +### $32 SceneryOpVertexCachedV1 (load cached vertex into V1) + +**Length:** 2 bytes (`$32 vertex_idx`) + +If `$2C` set (= polygon mode), copies cached vertex via +`EmitPrimaryVertex`. Otherwise loads cached 8-byte record into V1 +(`$CB..$D2`) and shadow (`$09..$10`). + +### $33 SceneryOpVertexCachedV2 (load cached vertex into V2 + line) + +**Length:** 2 bytes (`$33 vertex_idx`) + +Like $32 for V2. If polygon mode off, tail-jumps to EmitClippedLine to +draw v1->v2. + +### $35 SceneryOpVertexCachedDraw (load cached + plot pixel) + +**Length:** 2 bytes (`$35 vertex_idx`) + +Loads cached vertex into V1, then if in-frustum plots a single pixel. +Same plot path as $00. + +### $2B SceneryOpEmitCurve (8-segment cubic curve via subdivision) + +**Length:** 9 bytes (`$2B v1_X_lo v1_X_hi v1_Z_lo v1_Z_hi v2_X_lo v2_X_hi v2_Z_lo v2_Z_hi`) + +Reads V1 stream bytes, transforms via xform-B. Backs cursor by 1 (so V2 +reads from same offset advance). Reads V2 stream bytes, transforms. +Subdivides V2-V1 into 8 equal segments via repeated halving, emits each +segment as a clipped line. Used for cockpit gauge needle arcs and curved +runway markers. Self-modifies `L6B4F` to RTS during emission. + +### $06 SceneryOpDrawLine (raw screen-space line, no transform) + +**Length:** 5 bytes (`$06 X1 Y1 X2 Y2`) + +Copies 4 stream bytes directly into `$E9..$EC` and calls `DrawColorLine`. +No vertex transform. Used by 2D scenery (radar overlays, instrument panel +lines). + +--- + +## Polygon control + +### $29 SceneryOpCopyToD2 (polygon fill OR single line emit) + +**Length:** 1 byte (`$29`) + +If `$2C` (MAME: `$29`) flag is set: takes the `Op29PolygonFillBranch` +which decrements `$B5` (vertex count), and if vertex count > 0 calls +`PolygonScanFillSetup` to scan-fill the accumulated PrimVerts polygon +via DrawColorSpan. After fill, clears the flag. + +If flag is 0: takes the `Op29LineEmitBranch` — copies 9 bytes from +`$07..` (= V1 shadow) into `$D2..` (= V2 slot), advances cursor by 1, +then jumps to `EmitClippedLine` to draw a single line from V1 to V2. + +### $2F SceneryOpResetState (start polygon mode) + +**Length:** 1 byte (`$2F`) + +Sets `$2C` = `$FF` (= polygon mode ON, so subsequent $02/$41 ops +accumulate vertices instead of drawing lines). Clears `$B5` (= polygon +vertex count) to 0. In MAME-patched binary the flag is at `$29` instead +of `$2C`. + +### $1B SceneryOpModeWhite (force WHITE color, restore drawer) + +**Length:** 1 byte (`$1B`) + +Self-modifies the line/pixel kernel back to its standard plot opcodes +(undoes any night-mode BPL skips from $1C). Calls `SetPixelDrawMode` with +A=$03 (= HIRES_WHITE1). + +### $1C SceneryOpDayOnly (suppress draws at night) + +**Length:** 1 byte (`$1C`) + +If `$083C` bit 0 == 0 (night), patches DrawColorLine kernel with BPL +opcodes so subsequent line/pixel emits skip the paint write. Daytime is +a no-op. + +### $12 SceneryOpSetColor (set draw color from table index) + +**Length:** 2 bytes (`$12 code`) + +Reads next byte (0..15) as index into `ToHiresColorTable`, calls +`SetPixelDrawMode` with the resulting hires color. Mapping: + +| code | hires | typical use | +|------|-------|------| +| $00 | BLACK1 | sky/null | +| $01 | GREEN | ground (day) | +| $02 | VIOLET | water (day) | +| $03 | GREEN | | +| $04 | VIOLET | | +| $05 | BLACK1 | building shadow | +| $06 | VIOLET | runway edge | +| $07 | VIOLET | building | +| $08 | BLACK1 | | +| $09 | WHITE1 | aircraft wing/tail | +| $0A | BLACK1 | | +| $0B | GREEN | | +| $0C | VIOLET | | +| $0D | WHITE1 | | +| $0E | VIOLET | haze | +| $0F | WHITE1 | "city" (Hancock tower, runway centerline) | + +--- + +## Coordinate frame management + +### $07 SceneryOpEnterLocalFrame (enter sub-record frame, no stash) + +**Length:** 14 bytes (`$07 variant 6×16-bit-anchor`) + +Variant byte selects axis-permutation cascade +(0=`FrameVariantRolShift`, 2=`FrameVariantSwap`, 4=`FrameVariantAsl4`, +5+=`FrameVariantPassthrough`, others fall straight through to +`FrameSetupEpilogue`). The 6 anchor coords (= 3 axes × 16-bit) encode +the section's origin. `ComputeSectionCameraDelta` SBC chain computes +camera-vs-section delta into `$66/$68/$6A`. `FrameSetupEpilogue` calls +`RecomputeSectionBase` (= former L631D) to refresh the base accumulator. + +### $24 SceneryOpPushOriginWithStash (enter sub-record frame, with stash) + +**Length:** 8 bytes (`$24 variant 3×16-bit-anchor`) + +Like $07 but only 3 anchor coords (skips the lo-precision SBC for +$5A/$5E/$62). Pre-populates `$18/$1B/$1E` from aircraft full position +before L6BB0 cascade. Used inside polygon batches where the parent has +already set up the frame and only needs scaling. + +--- + +## Subroutines + +### $18 SceneryOpSubInvoke (recursive sub-record) + +**Length:** 3 bytes (`$18 offset_lo offset_hi`) + +Pushes current cursor + 3 (= return address) on stack as artificial RTS +target. Computes sub-stream cursor = current + signed offset. Sets +$8B/$8C to sub-stream. Calls `JSR SceneryInterpreterStep`. After return +(= when an op RTSes), restores cursor and JMPs to interpreter to continue +parent stream. Increments `$08E1` (sub-depth counter). + +### $19 SceneryOpReturn (RTS — exit current dispatcher) + +**Length:** 1 byte (`$19`) + +RTS immediately. Returns to whoever called `SceneryInterpreterStep`, +either the main loop or `$18` SubInvoke handler. The handler's first byte +IS `$60` (RTS), nothing else. + +### $0B SceneryOpJumpRelative (relative jump within stream) + +**Length:** 3 bytes (`$0B offset_lo offset_hi`) + +Reads signed 16-bit offset, sets cursor = cursor + offset, continues +interpreting from the new position. + +### $0E SceneryOpCall64K (chunk3 callback if 64K, recursion-pop if 48K) + +**Length:** 0 bytes advance (the opcode itself is 1 byte in the stream +but the handler does NOT advance the cursor past it). + +If `Has64K` flag set, JMP to `SceneryOp64KCallback` (= chunk3 entry that +processes more chunk5-style records). On 48K: falls through into +`SceneryOpReturn` which is a bare RTS — and that RTS pops one level of +dispatcher recursion. If $0E is reached at the TOP level the RTS returns +to whoever called `ProcessScenery` (= the main loop). If reached from +inside a `$18 SubInvoke` recursion, the RTS returns to the parent +section's dispatcher. + +### $03 SceneryOpCall64K_2 (chunk3 SceneryRotatedTransform if 64K) + +**Length:** 6 bytes on 48K (skip), variable on 64K + +If 64K, JMP to `SceneryRotatedTransform` in chunk3 (= 3D rotated +transform helper). On 48K, advance 6 and continue. + +--- + +## Header / data load + +### $0D SceneryOpHeader (load section header + trigger demand-load) + +**Length:** 6 bytes (`$0D byte1 byte2 byte3 byte4 byte5`) + +Copies 5 inline payload bytes into `$08E5..$08E9` (= section ID, sector +count, destination address, cache slot index). If high bit of byte5 +(= `$08E9`) is clear, cumulatively advances the saved section-base +cursor at `$08E7` by the current dispatcher cursor `$8B`. Masks +`$08E9` to a cache slot index (0..3), then calls +`SceneryHeaderLoadIfMiss` via the `SceneryHeaderLoadTrampoline` at +`L8776`. On a cache miss, that routine invalidates newer slots and +DMA-fetches the section bytecode from disk via +`SceneryHeaderRunSection` (which sets `L1E03 = $08E7` so the loaded +bytes land at the saved cursor's address). On a cache hit, returns +immediately. Total record = 6 bytes (1 opcode + 5 payload). + +### $11 SceneryOpSkip1 (skip 1 byte) + +**Length:** 1 byte total (= just the opcode; no payload). + +Advance cursor by 1. + +### $09 / $0A SceneryOpSkip3 (skip 3 bytes) + +**Length:** 3 bytes each (= opcode + 2 unused payload bytes). + +Advance cursor by 3. + +--- + +## Memory ops + +### $1A SceneryOpWriteWord (`*dst = *src`) + +**Length:** 5 bytes (`$1A dst_lo dst_hi src_lo src_hi`) + +Reads 2 bytes from `*src` (= memory at the src pointer) and writes them +to `*dst`. Used to copy state between scenery's 16-bit slots (e.g. +copying a saved color register). + +### $25 SceneryOpStoreImmWord (`*dst = imm16`) + +**Length:** 5 bytes (`$25 dst_lo dst_hi imm_lo imm_hi`) + +Writes the immediate 16-bit value into `*dst`. + +--- + +## Conditional jumps / culls + +### $13 SceneryOpJumpIfBeyondXY (cull XY box, jump alt path on fail) + +**Length:** 9 bytes (`$13 jumpOff_lo jumpOff_hi 2×(ptr_lo ptr_hi lo hi)`) + +Reads jump target and 2 (pointer, low_bound, high_bound) triples — one +for each of two axes. Compares the value at each pointer against the +bounds. On failure (= camera outside box) jumps to the alternate target +via `L6EE6`. Otherwise advance 9 and continue. + +### $14 SceneryOpJumpIfBeyondXYZ (cull XYZ box, jump alt on fail) + +**Length:** 14 bytes (`$14 jumpOff_lo jumpOff_hi 2×(triple) + Z_lo Z_hi`) + +Like $13 but adds explicit Z comparison against `$60/$61` (or +`ZoomLevel+1/+2` in radar view) using the supplied Z bound at `$98/$99`. + +### $20 SceneryOpCullIfOutside1 (1-axis bounding cull) + +**Length:** 9 bytes + +Calls `TestSceneryRange` once. On failure redirects via L6EE6. + +### $21 SceneryOpCullIfOutside2 (2-axis bounding cull) + +**Length:** 15 bytes + +Calls `TestSceneryRange` twice. + +### $22 SceneryOpCullIfOutside3 (3-axis bounding cull) + +**Length:** 21 bytes + +Calls `TestSceneryRange` three times. + +### $04 SceneryOpCullByOutcodeList (cull when all listed vertices share outcode bit) + +**Length:** variable (`$04 jumpOff_lo jumpOff_hi vertex_idx... `) + +Initializes `$DC = $F8`. For each vertex_idx in the stream (until a byte +with bit 7 set), points `($2D)` at cached vertex, ANDs the vertex's +outcode high byte into `$DC`. If after all vertices `$DC != 0` (= all +share at least one off-screen half-plane), jumps to the alternate target. + +### $23 SceneryOpJumpIfBitsClear (mask AND test, jump on no-bits-set) + +**Length:** 7 bytes (`$23 jumpOff_lo jumpOff_hi ptr_lo ptr_hi mask1 mask2`) + +If `(mask1 AND *(ptr+0)) == 0` AND `(mask2 AND *(ptr+1)) == 0`, jump. +Used to gate scenery on aircraft state flags (landing-gear, lights, etc.). + +### $28 SceneryOpJumpIfWordCompare (compare two pointers, jump on match) + +**Length:** 8 bytes (`$28 mode jumpOff_lo jumpOff_hi ptr1_lo ptr1_hi ptr2_lo ptr2_hi`) + +Loads `mode` (0=eq, 1=signed-lt, 2=signed-lt-alt). Compares 16-bit +`*ptr1` vs `*ptr2`. On match jumps via L6EE6. + +--- + +## Station records (NAV/COM/ADF radio) + +### $05 SceneryOpADFRecord (ADF station) + +**Length:** 9 bytes on 48K (skip), variable on 64K + +48K: just advance 9. 64K: patched to JMP `LookupADFStation` in chunk3 +which matches against the user's tuned ADF frequency and copies the +station descriptor on hit. + +### $1D SceneryOpNAVRecord (NAV1/NAV2 station) + +**Length:** 11 bytes (`$1D freq_lo freq_hi 8×descriptor`) + +Tries the record's 2-byte frequency against `$08F7` (NAV1 tune) and +`$08F5` (NAV2 tune) via `MatchNAVFreq`. On a hit, copies the 8-byte +descriptor (X, Y, Z, ranges...) into `$08F9` (NAV1 slot) or `$0901` +(NAV2 slot), and ORs the active bit into `$08F4`. + +### $1E SceneryOpCOMRecord (COM/airport) + +**Length:** variable (= byte 1 holds record length) + +Reads record length from byte 1. If record's COM frequency matches the +user's tuned COM (`$089F/$08A0`), copies the airport position into +`$0905+`, latches a pointer to the inline airport name string at `$092A`, +and triggers the ATIS message renderer. Otherwise skips by the length. + +--- + +## Invalid / terminator opcodes + +### $08, $0C, $0F, $10, $15, $16, $17, $1F, $26, $27, $2A, $2C, $2D, $2E, $30, $34, $36-$3F, $43, $44, $45 SceneryOpInvalid + +When dispatched, falls through to `SceneryStreamEnd`: clears scenery +in-progress flags (`$0916`, `$08FF`, `$0E3D`) and RTSes. Effectively +terminates the current stream. + +Any opcode with bit 7 set ($80-$FF) OR opcode >= $46 also triggers +`SceneryStreamEnd`. + +--- + +## Quick reference — record lengths + +| op | name | bytes | side effects | +|-----|-------------------|-------|---------------------------------| +| $00 | EmitV1+Plot | 7 | xform-A V1, plot pixel | +| $01 | EmitV1 xform-A | 7 | xform-A V1, no draw | +| $02 | EmitV2 xform-A | 7 | xform-A V2, draw line v1->v2 | +| $03 | Call64K_2 | 6 | chunk3 callback (64K only) | +| $04 | CullByOutcodeList | var | jump if shared off-screen half | +| $05 | ADFRecord | 9 | match tuned ADF freq | +| $06 | DrawLine | 5 | raw 2D line at screen coords | +| $07 | EnterLocalFrame | 14 | section frame setup, no stash | +| $0B | JumpRelative | 3 | cursor += signed16 | +| $0D | Header | 6 | section header + demand-load | +| $0E | Call64K | 0 | chunk3 callback (64K); 48K = RTS pops recursion | +| $11 | Skip1 | 1 | bare opcode, no payload | +| $12 | SetColor | 2 | set draw color from table | +| $13 | JumpIfBeyondXY | 9 | XY cull, jump alt | +| $14 | JumpIfBeyondXYZ | 14 | XYZ cull, jump alt | +| $18 | SubInvoke | 3 | recursive sub-stream | +| $19 | Return | 0 | RTS — pops dispatcher recursion (no cursor advance) | +| $1A | WriteWord | 5 | *dst = *src (word) | +| $1B | ModeWhite | 1 | set color WHITE, restore drawer | +| $1C | DayOnly | 1 | night → patch drawer to skip | +| $1D | NAVRecord | 11 | match tuned NAV1/NAV2 freq | +| $1E | COMRecord | var | match tuned COM, ATIS draw | +| $20 | CullIfOutside1 | 9 | 1-axis bounding cull | +| $21 | CullIfOutside2 | 15 | 2-axis bounding cull | +| $22 | CullIfOutside3 | 21 | 3-axis bounding cull | +| $23 | JumpIfBitsClear | 7 | mask AND test, jump if zero | +| $24 | PushOriginWithStash | 8 | section frame setup, stashed | +| $25 | StoreImmWord | 5 | *dst = imm16 | +| $28 | JumpIfWordCompare | 8 | cmp *p1 *p2, jump on match | +| $29 | CopyToD2 | 1 | polygon fill OR line emit | +| $2B | EmitCurve | 9 | 8-segment subdivided curve | +| $2F | ResetState | 1 | polygon mode ON, vertex count=0 | +| $31 | RefreshCachedXform80C5 | 8 | reload cached vertex xform-A | +| $32 | VertexCachedV1 | 2 | load cached V1 | +| $33 | VertexCachedV2 | 2 | load cached V2 + line | +| $35 | VertexCachedDraw | 2 | load cached + plot | +| $40 | EmitV1 xform-B | 5 | xform-B V1, no draw | +| $41 | EmitV2 xform-B | 5 | xform-B V2, draw line v1->v2 | +| $42 | RefreshCachedXform7EBC | 6 | reload cached vertex xform-B | + +--- + +## Coordinate frames + +xform-A (`TransformVertex80C5` at chunk5.s:5776) uses a full 6-byte (X, +Y, Z) stream and the 16-bit `ZPScale` multiplier for all 9 matrix +entries. The routine itself advances the dispatcher cursor `$8B` by 7 +(opcode + 6-byte payload), so the surrounding opcodes ($00/$01/$02) +declare their record length as 7 bytes. + +xform-B (`TransformVertex7EBC` at chunk5.s:5425) uses a 4-byte (X, Z) +stream — Y is implicit from the section base accumulator at `$4A..$52` +(source) or `$2A..$2F` as observed in the RAM snapshot. Uses the 8-bit +`op_l1818` multiplier. The routine advances the cursor by 5 (opcode + +4-byte payload), giving $40/$41 a 5-byte record length. + +The 3x3 rotation matrix lives at `$78..$89`: +- `$78/$79` = X-out-from-X-delta +- `$7A/$7B` = Y-out-from-X-delta +- `$7C/$7D` = Z-out-from-X-delta +- `$7E/$7F` = X-out-from-Y-delta (= matrix row 2 used by L631D) +- `$80/$81` = Y-out-from-Y-delta +- `$82/$83` = Z-out-from-Y-delta +- `$84/$85` = X-out-from-Z-delta +- `$86/$87` = Y-out-from-Z-delta +- `$88/$89` = Z-out-from-Z-delta + +Camera ZP at `$66..$6B`: +- `$66/$67` = camera X delta (post-section-anchor subtract) +- `$68/$69` = camera Y delta +- `$6A/$6B` = camera Z delta + +Section base accumulator (= L631D output): +- Source layout: `$4A..$52` as 24-bit MID/HI/LO per axis +- MAME-patched layout: `$2A..$2F` as 16-bit LE per axis + +`RecomputeSectionBase` (= former L631D) fires from `FrameSetupEpilogue` +(= former L6D28) when `$68/$69` differs from the cache at `$35/$36` +(source) or `$38/$39` (MAME-patched). + +## Renamed labels (= what we changed in chunk5.s) + +The disassembly originally used `L` placeholder labels for branch +targets. We've added semantic names for the scenery-interpreter family +(L kept as `.refto` aliases so existing references still resolve). + +### Frame setup ($07/$24 family) + +| Old | New | What it is | +|---|---|---| +| L631D | `RecomputeSectionBase` | matrix*scale → base accumulator | +| L6363 | `ScaleSignedC2` | signed multiply for one base-axis | +| L6379 | `ScaleSignedC2_DoMultiply` | multiply step inside ScaleSignedC2 | +| L639C | `ScaleSignedC2_Exit` | RTS | +| L6BB0 | `ComputeSectionCameraDelta` | $07/$24 SBC chain | +| L6BC3 | `ComputeSectionCameraDelta_X` | X-axis SBC | +| L6BEB | `ComputeSectionCameraDelta_Y_Radar` | Y-axis radar branch | +| L6BFE | `ComputeSectionCameraDelta_Y_NonRadar` | Y-axis non-radar branch | +| L6C10 | `ComputeSectionCameraDelta_Y_NonRadarStash` | Y-axis stash variant | +| L6C1E | `ComputeSectionCameraDelta_Z` | Z-axis SBC | +| L6C31 | `ComputeSectionCameraDelta_Z_Upper` | Z-axis upper-byte SBC | +| L6C48 | `FrameVariantDispatch` | variant lookup + JMP | +| L6C53 | `FrameVariantPassthrough` | variant 5+ (= identity) | +| L6C6E | `FrameVariantSwap` | variant 2 (= lo/hi swap) | +| L6C89 | `FrameVariantAsl4` | variant 4 (= asl×4) | +| L6CCE | `FrameVariantRolShift` | variant 0 (= rol-shift) | +| L6D28 | `FrameSetupEpilogue` | $07/$24 cache-check + L631D fire | +| L6D32 | `FrameSetupEpilogue_CacheCheck` | scale cache check | +| L6D40 | `FrameSetupEpilogue_FireRecompute` | call to L631D | +| L6D43 | `FrameSetupEpilogue_UpdateCache` | $36 := $69 | +| L6B8A | `PushOriginWithStash_NonRadar` | $24 non-radar Y scratch | +| L6B92 | `PushOriginWithStash_RunChain` | $24 stashed-mode chain entry | + +### Polygon fill kernel ($29 + L6F98) + +| Old | New | What it is | +|---|---|---| +| L67CE | `Op29LineEmitBranch_CopyLoop` | shadow→V2 copy loop | +| L67DD | `Op29PolygonFillBranch` | $29 with $2C set | +| L67E8 | `Op29PolygonFillBranch_ClearAndAdvance` | clear $2C tail | +| L6F98 | `PolygonScanFillSetup` | polygon fill kernel entry | +| L7095 | `PolygonClipTopPass` | clip pass 2 (top edge) | +| L7190 | `PolygonClipRightPass` | clip pass 3 (right edge) | +| L728B | `PolygonClipBottomPass` | clip pass 4 (bottom edge) | +| L7826 | `PolygonScanFillRow` | per-row scan-line emitter | + +### EmitClippedLine + curve + +| Old | New | What it is | +|---|---|---| +| L6AF7 | `EmitClippedLine_ClipV2` | snapshot+clip V2 phase | +| L6AFD | `EmitClippedLine_SnapshotV2Loop` | V2→shadow copy loop | +| L6B0D | `EmitClippedLine_ProjectV1IfNeeded` | conditional V1 project | +| L6B15 | `EmitClippedLine_ProjectV2IfNeeded` | conditional V2 project | +| L6B1C | `EmitClippedLine_PrepareEndpoints` | load $E9..$EC | +| L6B2C | `EmitClippedLine_DoDraw` | jsr DrawColorLine | +| L6B2F | `EmitClippedLineCleanup` | V1 restore tail | +| L6B35 | `EmitClippedLineCleanup_FromShadow` | restore V1 from $DB | +| L6B3F | `EmitClippedLineCleanup_FromV2` | restore V1 from $D2 (= unmodified V2) | +| L6B41 | `EmitClippedLineCleanup_FromV2Loop` | copy loop | +| L6B48 | `EmitClippedLineCleanup_ClearFlags` | $8A = $08C4 = 0 | +| L6B4F | `EmitClippedLineTail` | self-modified JMP/RTS | +| L6A03 | `EmitCurve_ReadV2` | $2B V2 read | +| L6A0C | `EmitCurve_AlignExponents` | scale-align V1 vs V2 | +| L6A20 | `EmitCurve_HalveV2` | V2 halve branch | +| L6A2A | `EmitCurve_BuildStep` | (V2-V1)/16 step build | +| L6A69 | `EmitCurve_SegmentLoop` | per-segment emit loop | + +### Vertex emit / cache ($00/$01/$02/$32/$33/$35/$40/$41/$42) + +| Old | New | What it is | +|---|---|---| +| L6839 | `ProcessVertex2_LineMode` | $2C clear → classify+project V2 | +| L6843 | `ProcessVertex2_Exit` | shared RTS | +| L6874 | `ProcessVertex1_LineMode` | $2C clear → classify+project V1 | +| L687E | `SnapshotVertex1ToShadow` | save V1 to $07.. | +| L6880 | `SnapshotVertex1ToShadow_Loop` | copy loop | +| L6893 | `EmitV1Common` | $01/$40 setup (xform-A vs B select) | +| L689C | `ResumeAfterVertexEmit` | dispatcher resume after silent emit | +| L68AA | `EmitV2Common` | $02/$41 setup | +| L68C7 | `VertexCachedV1_LineMode` | $32 line-mode load | +| L68CF | `VertexCachedV1_CopyLoop` | cache → V1 copy | +| L68E4 | `VertexCachedV1_Advance` | RTS to dispatcher | +| L68FB | `VertexCachedV2_LineMode` | $33 line-mode load | +| L6901 | `VertexCachedV2_CopyLoop` | cache → V2 copy | +| L6911 | `VertexCachedV2_Emit` | tail-jump to EmitClippedLine | +| L6952 | `RefreshCachedXform_Common` | $31/$42 shared body | +| L695D | `RefreshCachedXform_SnapshotV2` | save V2 | +| L6971 | `RefreshCachedXform_WriteCache` | write transformed V2 to cache | +| L6973 | `RefreshCachedXform_WriteLoop` | copy loop | +| L697D | `RefreshCachedXform_RestoreV2` | restore V2 from snapshot | +| L698A | `SetVertexPointerFromIdx` | cached-vertex slot lookup (idx in A) | +| L69B5 | `VertexCachedDraw_LoadLoop` | $35 cache → V1 copy | +| L69D5 | `EmitV1AndPlotTail` | $00/$35 frustum-test tail | +| L69DC | `EmitV1AndPlotTail_DoPlot` | actually plot the pixel | +| L69E3 | `EmitV1AndPlotTail_Resume` | resume dispatcher | + +### xform-A (TransformVertex80C5) internals + +| Old | New | What it is | +|---|---|---| +| L80D2 | `XformA_SBCChain` | stream→delta SBC chain | +| L810E | `XformA_SBCChain_StoreZHi` | store Z high byte | +| L8110 | `XformA_AutoScaleLoop` | auto-scale shift loop | +| L81D9 | `XformA_OverflowRecoverX` | X overflow path | +| L820F | `XformA_OverflowRecoverY` | Y overflow path | +| L821E | `XformA_OverflowRecoverZ` | Z overflow path | + +### xform-B (TransformVertex7EBC) internals + +| Old | New | What it is | +|---|---|---| +| L7EAD | `XformBOverflowRecoverXZ` | X+Z overflow recovery | +| L7F1A | `XformBAutoScaleLoop` | left-shift loop | +| L7F5C | `XformBOverflowRecoverZ` | Z-only overflow recovery | +| L7F64 | `XformBOverflowRecoverX` | X-only overflow recovery | +| L7F7F | `XformBHalveBaseAccumulators` | post-overflow halve epilogue | +| L7F96 | `XformBMatrixMultiply` | matrix-multiply tail entry | +| L8091 | `XformBStoreOutput` | vertex result writeback | +| L80B0 | `XformBHalveAccumulators` | post-multiply overflow halver | + +### Conditional jumps / culls + +| Old | New | What it is | +|---|---|---| +| L6DB9 | `CullByOutcodeList_ResetAccum` | reset $DC AND mask | +| L6DBB | `CullByOutcodeList_NextVertex` | next-vertex top | +| L6DCF | `CullByOutcodeList_AndOutcode` | AND outcode bits into mask | +| L6DD9 | `CullByOutcodeList_End` | end-of-list test + branch | +| L6DF0 | `CullSucceedAndContinue` | shared continue | +| L6E07 | `JumpIfBeyondXYZ_Z_NonRadar` | non-radar Z bounds test | +| L6E0D | `JumpIfBeyondXYZ_Z_TestHi` | high-byte Z test | +| L6E14 | `JumpIfBeyondXYZ_Continue` | inside-bounds path | +| L6EBF | `TestSceneryRange_TestLow` | low-bound test | +| L6ECE | `TestSceneryRange_LowOverflow` | low overflow gate | +| L6ED0 | `TestSceneryRange_TestHigh` | high-bound test | +| L6EE1 | `TestSceneryRange_HighOverflow` | high overflow gate | +| L6EE3 | `TestSceneryRange_Pass` | inside-range RTS | +| L6EE4 | `TestSceneryRange_StripAndJump` | strip JSR + take alt jump | +| L6EE6 | `JumpToFetchedTarget` | conditional-jump take-branch path | +| L6F16 | `JumpIfBitsClear_NoJump` | $23 no-jump path | +| L6F55 | `JumpIfWordCompare_Mode1Overflow` | $28 mode 1 V-flag gate | +| L6F57 | `JumpIfWordCompare_Mode1Jump` | $28 mode 1 take-jump | +| L6F5A | `JumpIfWordCompare_Mode2` | $28 mode 2 entry | +| L6F6C | `JumpIfWordCompare_Mode2Overflow` | $28 mode 2 V-flag gate | +| L6F6E | `JumpIfWordCompare_Mode2Jump` | $28 mode 2 take-jump | +| L6F71 | `JumpIfWordCompare_Mode0` | $28 mode 0 (= equality) | +| L6F84 | `Op28NoJump` | $28 word-compare no-match path | + +### Misc op handlers + +| Old | New | What it is | +|---|---|---| +| L6063 | `MatchNAVFreq_CopyLoop` | NAV descriptor copy | +| L6076 | `MatchNAVFreq_SetActiveBit` | OR active flag into $08F4 | +| L607C | `MatchNAVFreq_Exit` | RTS | +| L609F | `SceneryOpCOMRecord_CopyDescriptorLoop` | COM descriptor copy | +| L60BD | `SceneryOpCOMRecord_AdvanceAndContinue` | skip past record | +| L60FD | `SceneryOpHeader_RunSetup` | mask + jsr L8776 | +| L7AF2 | `SceneryOpDayOnly_Exit` | shared advance-and-continue | + +### Setup / view projection + +| Old | New | What it is | +|---|---|---| +| L612D | `SetupViewProjection_NotRadar` | non-radar entry | +| L6155 | `SetupViewProjection_SideOrForwardView` | ViewDirection 0..n | +| L617C | `SetupViewProjection_NegateAngle` | angle negation branch | +| L6199 | `SetupViewProjection_BuildXZBasis` | sin/cos build | +| L61F0 | `SetupViewProjection_BuildMatrix` | converged matrix builder | +| L6210 | `SetupViewProjection_NormaliseAngle` | angle normalisation | + +### Polygon clipping passes (new this round) + +| Old | New | What it is | +|---|---|---| +| L741C | `SwapVertices_Loop` | V1↔V2 byte swap loop | +| L742C | `ClipVertex2ToFrustum_Exit` | empty-outcode RTS | +| L742D | `ClipVertex2ToFrustum_TestRight` | bit 6 → ClipRight | +| L743D | `ClipVertex2ToFrustum_TestLeft` | bit 5 → ClipLeft | +| L744F | `ClipVertex2ToFrustum_TestBottom` | bit 4 → ClipBottom | +| L7461 | `ClipVertex2ToFrustum_TestTop` | bit 3 → ClipTop | +| L746D | `ClipVertex2ToTop_RetryAfterHalve` | overflow-recovery entry | +| L74EB | `ClipVertex2ToBottom_RetryAfterHalve` | "" | +| L7573 | `ClipVertex2ToLeft_RetryAfterHalve` | "" | +| L75F1 | `ClipVertex2ToRight_RetryAfterHalve` | "" | + +### Outcode classifiers (new this round) + +| Old | New | What it is | +|---|---|---| +| L738A | `ClassifyVertex1_TestRight` | X+Z half-plane | +| L7399 | `ClassifyVertex1_TestLeft` | Z-X half-plane | +| L73A8 | `ClassifyVertex1_TestBottom` | Y+Z half-plane | +| L73B7 | `ClassifyVertex1_TestTop` | Z-Y half-plane | +| L73C8 | `ClassifyVertex1_StoreAndExit` | RTS path | +| L73D3 | `ClassifyVertex2_TestRight` | "" for V2 | +| L73E2 | `ClassifyVertex2_TestLeft` | "" | +| L73F1 | `ClassifyVertex2_TestBottom` | "" | +| L7400 | `ClassifyVertex2_TestTop` | "" | +| L7411 | `ClassifyVertex2_StoreAndExit` | "" | + +Outcode bits (= shared by both classifiers): +- bit 7: behind viewer (Z < 0) +- bit 6: right of right frustum plane (X+Z < 0) +- bit 5: left of left frustum plane (Z-X < 0) +- bit 4: below bottom frustum plane (Y+Z < 0) +- bit 3: above top frustum plane (Z-Y < 0) + +### FlipPagesFillViewport + sky/ground fill + +| Old | New | What it is | +|---|---|---| +| L63A2 | `FlipPagesFillViewport_RotateRowTable` | rotate HiresTableHi for page swap | +| L63BB | `FlipPagesFillViewport_FlipToHires` | STA HISCR | +| L63BE | `FlipPagesFillViewport_RunHorizonPoly` | $0AB9 horizon-line entry | +| L644A | `FlipPagesFillViewport_StoreGroundColor` | save ground tint | +| L6457 | `FlipPagesFillViewport_StoreSkyColor` | save sky tint | +| L6467 | `FlipPagesFillViewport_PrepRow` | call SetEvenAndOddColors | +| L646D | `FlipPagesFillViewport_DetermineExtents` | min/max screen-Y for horizon | +| L6482 | `FlipPagesFillViewport_FillFullViewport` | no-horizon-line path | +| L6485 | `TrackHorizonExtent` | min/max helper | +| L648B | `TrackHorizonExtent_TestMax` | max-side test | +| L6491 | `TrackHorizonExtent_Exit` | RTS | +| L6492 | `FlipPagesFillViewport_HorizonEmitted` | horizon-line setup | +| L64B9 | `FlipPagesFillViewport_AboveDiag_TestSwap` | sky/ground swap test | +| L64BD | `FlipPagesFillViewport_AboveDiag_DoSwap` | "" do | +| L64C0 | `FlipPagesFillViewport_AboveDiag_Fill` | fill upper rows | +| L64CC | `FlipPagesFillViewport_DiagFill_TestSwap` | "" for diagonal rows | +| L64D9 | `FlipPagesFillViewport_DiagFill_TestSwap2` | "" alt path | +| L64E3 | `FlipPagesFillViewport_DiagFill_DoSwap` | swap | +| L64E6 | `FlipPagesFillViewport_DiagFill_Fill` | call FillMixedViewportRows | +| L64FC | `FlipPagesFillViewport_BelowDiag_TestSwap` | below-diag swap test | +| L6506 | `FlipPagesFillViewport_BelowDiag_DoSwap` | "" do | +| L6509 | `FlipPagesFillViewport_BelowDiag_Fill` | fill lower rows | +| L6515 | `FlipPagesFillViewport_FullSky` | end + UpdateArtificialHorizon | +| L6555 | `FillViewportRows_SolidStart` | optimized solid-color start | +| L6557 | `FillViewportRows_SolidLoop` | unrolled fill loop | +| L6567 | `FillViewportRows_ColorFill` | call DrawSkyGroundRowUnrolled | +| L656A | `FillViewportRows_Tail` | row decrement + loop | +| L6637 | `DrawSkyGroundRow_NoSplit` | edge case at column $27 | +| L668B | `DrawSkyGroundRow_FillRightSide` | DrawColorSpan right-of-transition | +| L6695 | `DrawSkyGroundRow_TestFillColor` | test before left-side fill | +| L669D | `DrawSkyGroundRow_FillLeftSide` | DrawColorSpan left-of-transition | +| L66A4 | `DrawSkyGroundRow_Exit` | RTS | + +### DrawColorLine + PlotColorPixel + DrawColorSpan paint sites + +| Old | New | What it is | +|---|---|---| +| L7921 | `DCSPixelOraOp` | DrawColorSpan ORA self-mod | +| L7922 | `DCSPixelOraOperand` | "" operand | +| L792D | `DCSPixelAndOp` | DrawColorSpan AND self-mod | +| L792E | `DCSPixelAndOperand` | "" operand | +| L7976 | `PlotColorPixel_OraOp` | PlotColorPixel ORA self-mod | +| L7977 | `PlotColorPixel_OraOperand` | "" operand | +| L7982 | `PlotColorPixel_AndOp` | PlotColorPixel AND self-mod | +| L7983 | `PlotColorPixel_AndOperand` | "" operand | +| L7A1A | `DCLYMajor_OraMaskOp` | DCL Y-major ORA | +| L7A1B | `DCLYMajor_OraMaskOperand` | | +| L7A26 | `DCLYMajor_AndMaskHiOp` | split-byte hi half AND | +| L7A27 | `DCLYMajor_AndMaskHiOperand` | | +| L7A2E | `DCLYMajor_AndMaskLoOp` | non-split AND | +| L7A2F | `DCLYMajor_AndMaskLoOperand` | | +| L7A8E | `DCLXMajor_OraMaskOp` | DCL X-major ORA | +| L7A8F | `DCLXMajor_OraMaskOperand` | | +| L7A9A | `DCLXMajor_AndMaskOp` | DCL X-major AND | +| L7A9B | `DCLXMajor_AndMaskOperand` | | + +### DrawColorLine internals (new this round) + +| Old | New | What it is | +|---|---|---| +| L79A3 | `DrawColorLine_PostSwap` | after X1≤X2 swap | +| L79BF | `DrawColorLine_TestZero` | single-point shortcut | +| L79E4 | `DCLPrepYMajor` | Y-major axis setup | +| L79FE | `DCLYMajor_DecPixelInByte` | byte-bit transition | +| L7A4D | `DCLPrepXMajor_AddCycles` | X-major cycle adjust | +| L7A55 | `DCLPrepXMajor_AddOverhead` | "" overhead | +| L7A60 | `DCLPrepXMajor_InitError` | "" init error | +| L7AF2 | `SceneryOpDayOnly_Exit` | shared advance-and-continue | + +### Polygon scan-fill row body (new this round) + +| Old | New | What it is | +|---|---|---| +| L7684 | `PolygonScanFill_ProjectVertices` | per-vertex 3D→2D project | +| L7699 | `PolygonScanFill_ProjectVertexLoop` | "" loop top | +| L76C3 | `PolygonScanFill_TestMaxX` | screen-X max accumulate | +| L76C9 | `PolygonScanFill_StoreYHi` | screen-Y store | +| L76D3 | `PolygonScanFill_TestMaxY` | screen-Y max accumulate | +| L76D9 | `PolygonScanFill_NextVertex` | loop step | +| L7724 | `PolygonScanFill_BuildEdgeList` | edge-list builder loop | +| L77EA | `PolygonScanFill_HorizEdge` | special-case horizontal edge | +| L783A | `PolygonScanFill_SetupRowEmit` | setup per-row scan | +| L784A | `PolygonScanFill_RowTop` | per-row scan top | +| L785D | `PolygonScanFill_EdgeLoop` | edge walk for current row | +| L787B | `PolygonScanFill_EdgeIntercept` | interpolate X intercept | +| L789B | `PolygonScanFill_NextEdge` | loop step | +| L78B2 | `PolygonScanFill_SortIntercepts` | bubble-sort the X buffer | +| L78B5 | `PolygonScanFill_SortInner` | "" inner loop | +| L78C6 | `PolygonScanFill_SortInnerStep` | "" step | +| L78CE | `PolygonScanFill_EmitSpansLoop` | DrawColorSpan-per-pair loop | +| L78E0 | `PolygonScanFill_NextRow` | row advance | +| L78EB | `PolygonScanFill_FinalCost` | work cost accumulate | + +### Section loader (new this round) + +| Old | New | What it is | +|---|---|---| +| L8776 | `SceneryHeaderLoadTrampoline` | $0D Header → demand-load | +| LA64C | `SceneryHeaderLoadIfMiss_InvalidateNewerSlots` | cache invalidation | +| LA656 | `SceneryHeaderLoadIfMiss_RunLoad` | actually issue load | +| LA66A | `SceneryHeaderLoadIfMiss_Hit` | cache hit RTS | +| LA6F9 | `SceneryCopyLoadedSection` | $4000-$5FFF → $2000-$3FFF | +| LA712 | `SceneryCopyLoadedSection_PageLoop` | "" 256-byte loop | + +### What's left + +Remaining L\ clusters across the chunks are aircraft physics +integration, instrument panel update routines (DrawHeading, DrawDME, +UpdateAltimeterPose, UpdateMagneticHeading, etc.), demo mode logic, +crash detection / handling, joystick / keyboard input handling, +magnetic compass / VOR / DME / ATIS internals, AltDrawSkyGroundRow +detailed body, ApplyArtificialHorizonMask, and the ZPScale 16-bit +multiplier family in chunk4. PerspectiveDivide is named at the entry +but its 7-iteration unrolled shift-subtract body has internal labels +(L7C3D, L7C9E, etc.) that are best understood as "iteration N of the +divide" rather than getting individual semantic names. + +Each rename requires understanding the surrounding code — speculative +renames would mislead future readers. Add new entries here as more +routines get reverse-engineered. + +## Algorithm understanding (= what reverse-engineering revealed) + +### DrawColorLine ($795A) is bit-level Bresenham + +Apple II hires has 7 color pixels per byte. The high bit (bit 7) selects +palette (= violet/green or blue/orange depending on parity); bits 0..6 +hold 7 color pixels. DrawColorLine paints ONE color pixel = ONE bit per +loop iteration via self-modified ORA/AND mask opcodes (= installed by +SetPixelDrawMode). + +Two inner loops: +- **Y-major** (`DCLYMajorTop` at L79F0): for vertical-ish lines. Y + steps every iteration; X (column + bit-in-byte) steps when the + Bresenham error overflows. +- **X-major** (`DCLXMajorTop` at L7A67): for horizontal-ish lines. X + steps every iteration; Y conditionally. + +The "split-byte boundary" handling at bit-3 (`DCLYMajor_DecPixelInByte` +at L79FE) is because 7 pixels per byte means transitions at bits 0 +and 6 wrap to the previous/next byte cleanly — but bit 3 needs special +care for the high-bit palette plane. + +For our port: `hiresDrawLine` should produce the same bit pattern as +DrawColorLine, OR we accept that pixel-pair-level rasterization will +look slightly different (= no NTSC color fringing). + +### PolygonScanFill ($6F98) uses 3D clipping, not 2D scanline + +`PolygonScanFillSetup` receives the polygon as 3D-XYZ vertices (= the +post-xform output stored in PrimVerts). It runs four 3D clipping +passes against the viewing frustum half-planes (Left/Top/Right/Bottom), +each pass alternating PrimVerts <-> SecVerts. **Each clipping pass +INTRODUCES NEW VERTICES at the frustum-edge intersections.** + +After clipping, `PolygonScanFill_ProjectVertices` (L7684) projects +every clipped vertex (including the new intersections) to screen +coords. Then `PolygonScanFill_BuildEdgeList` (L7724) builds the +per-edge data (top row, row count, top column, column step per row) +and `PolygonScanFillRow` (L7826) iterates rows and emits scan-line +spans via DrawColorSpan. + +**This is why our port's polygon fill produces the wrong shape:** + +Our port's `rendererFillPolygon` takes pre-projected screen coords from +the vertex emit ops and rasterizes via standard 2D scanline edge +intersection. **It skips the 3D clipping entirely.** When a polygon's +original 3D vertices all map to a small screen Y range (= e.g. all near +row 55-56 because they're all at similar Z), our fill produces just +1-2 scan rows. + +But chunk5's 3D clipping introduces frustum-edge intersection vertices +that, when projected, expand the polygon's screen-Y range. A polygon +whose 3D vertices all sit at large Z but extend in Y can produce +intersection vertices at the near-Z plane (= mapping to row 99 after +projection). The resulting clipped polygon spans rows 56..99 — the +WEDGE shape that's the runway centerline at boot Meigs. + +**To fix:** port `PolygonScanFillSetup`'s 4 clipping passes faithfully. +The infrastructure is in place (= polyXs/polyYs accumulator) but +should be replaced with 3D-XYZ vertex storage and the clipping should +run BEFORE projection. Then the existing scanline rasterizer would +see the correct vertex set. + +### RecomputeSectionBase ($631D) computes 24-bit base from matrix*scale + +Source layout: `$4A..$52` as 3 axes × 24-bit MID/HI/LO. MAME-patched +moves this to `$2A..$2F` as 3 axes × 16-bit LE. + +For each axis: `base[i] = -((matrix_row_2[i] * scale) >> 15)` where +`scale` is `$68/$69` (= camera-vs-section Y delta). The base is then +read by xform-B as the Y-axis-implicit base for vertices that don't +have explicit Y in stream. + +This was the source of our long-running base-divergence bug: source +chunk5.s describes the $4A..$52 layout, but MAME's running binary uses +$2A..$2F because the patcher rewrites the relevant LDA/STA addresses. + +### xform-B chained accumulator + +`TransformVertex7EBC` reads BOTH the persistent base ($2A..$2F) AND the +running auto-scale exponent ($2F at $35/$36). After the matrix +multiply (= XformBMatrixMultiply at L7F96), output is stored to the +caller's destSlot ($CB for V1, $D4 for V2) AND the accumulator slots +$18..$1D persist for the next transform. + +The auto-scale shift loop (XformBAutoScaleLoop at L7F1A) keeps all 5 +high bytes ($9F = X delta hi, $A3 = Z delta hi, $19/$1C/$1F = base +high bytes for X/Y/Z) within the [$40..$BF] band. Each shift up by 1 +increments the exponent at $2F. After the multiply, post-overflow +recovery (= L7F7F XformBHalveBaseAccumulators) shifts the accumulator +back down to maintain scale alignment. diff --git a/port/include/aircraft.h b/port/include/aircraft.h new file mode 100644 index 0000000..9ed712e --- /dev/null +++ b/port/include/aircraft.h @@ -0,0 +1,238 @@ +// Aircraft state and flight model. Direct port of the FS2 disassembly: +// chunk5 IntegratePhysicsStep, ComputeFlightDerivedValues, +// UpdateAutoTrimAndYaw, IntegrateClimbRate, CheckFlightEnvelope, +// ResetAircraftSystems, ApplySlewDeltas; chunk2 ApplyWind. +// +// State uses the FS2 fixed-point conventions: +// - position: int32_t (Q16.16) -- high 16 bits = world unit, low 16 +// bits = fraction. FS2 stores 24-bit position cells with the low +// byte fractional; we extend to 32-bit signed for headroom. +// - byte angles: uint8_t (256 == full turn). +// - rates / speeds: int16_t (Q8.8) -- low byte fractional, high byte +// integer per-frame delta. +// - pilot inputs: signed int8_t (-127..+127) for yoke / rudder / trim, +// unsigned uint8_t (0..255) for throttle / flaps / mixture, matching +// FS2's YokeVertPos / ThrottlePos byte layout. + +#ifndef AIRCRAFT_H +#define AIRCRAFT_H + +#include +#include +#include "camera.h" +#include "wind.h" + + +// Crash classes mirror FS2 chunk3 `HandleCrashOrSplash` + the +// `crash_msg_table` indices and `msg_problem` / `msg_splash` cases. +typedef enum CrashTypeE { + CRASH_NONE = 0, + CRASH_GROUND = 1, + CRASH_MOUNTAIN = 2, + CRASH_BUILDING = 3, + CRASH_SPLASH = 4, + CRASH_PROBLEM = 5 +} CrashTypeE; + + +// View directions mirror FS2's `ViewDirection` semantics: forward is +// the default; left/right/back are 90/180/270 degree yaw offsets; +// down is a fixed pitch-down view. +typedef enum ViewDirectionE { + VIEW_FORWARD = 0, + VIEW_RIGHT = 1, + VIEW_BACK = 2, + VIEW_LEFT = 3, + VIEW_DOWN = 4 +} ViewDirectionE; + + +// FS2 fixed-point shifts. Position uses the FS2 32-bit position cell +// convention (low 16 bits fractional). Rates / speeds use 8.8. +#define AC_POS_FRACT_BITS 16 +#define AC_POS_FRACT_ONE (1 << AC_POS_FRACT_BITS) +#define AC_RATE_FRACT_BITS 8 +#define AC_RATE_FRACT_ONE (1 << AC_RATE_FRACT_BITS) + +// Compose a Q16.16 world coordinate from integer world units. +#define AC_WORLD_UNITS(n) ((int32_t)(n) * AC_POS_FRACT_ONE) +// Compose a uint8 throttle/flaps/mixture value from a percent 0..100. +#define AC_BYTE_PCT(p) ((uint8_t)(((int)(p) * 255) / 100)) + +// Reality-mode instrument failure bits. Mirrors chunk3 +// `InstrumentOperationalFlags` (init $FF = all good). The +// chunk3 `FailureProcTable` clears one of these bits or sets one of +// the engine-fault bits when the reality-mode roll trips, instead of +// the immediate crash the port previously triggered. +#define AC_FAIL_AIRSPEED 0x01 // bit 0 (FailInstrumentBit0) +#define AC_FAIL_VSI 0x04 // bit 2 (FailInstrumentBit2) +#define AC_FAIL_ALTIMETER 0x08 // bit 3 (FailInstrumentBit3) +#define AC_FAIL_TURN_COORD 0x20 // bit 5 (FailInstrumentBit5) +#define AC_FAIL_ATTITUDE 0x40 // bit 6 (FailInstrumentBit6) +#define AC_FAIL_HEADING 0x80 // bit 7 (FailInstrumentBit7) +#define AC_FAIL_ALL_INSTRUMENTS (AC_FAIL_AIRSPEED | AC_FAIL_VSI | AC_FAIL_ALTIMETER | AC_FAIL_TURN_COORD | AC_FAIL_ATTITUDE | AC_FAIL_HEADING) + +// Engine fault bits. SetEngineFault01 ORs $03 into $0991, SetEngineFault23 +// ORs $0C. Two cylinder banks; either can fail independently. +#define AC_ENG_FAULT_LEFT 0x03 +#define AC_ENG_FAULT_RIGHT 0x0C + +// Mapping between aircraft metre-space and FS2 scenery units. +// FS2 scenery uses ~feet as its base unit; we round to 3 units/metre +// for clean integer math (the true ratio is 3.28 ft/m, so DME and +// ground-track come out ~9% short — close enough until we get a +// known-leg measurement to refine). +#define AC_SCENERY_UNITS_PER_METRE 3 + +// Nautical mile = 1852 m * 3 units/m. Used by DME so the constant is +// consistent with AC_SCENERY_UNITS_PER_METRE -- bumping one without +// the other would make airspeed and DME disagree. +#define AC_SCENERY_UNITS_PER_NM (1852 * AC_SCENERY_UNITS_PER_METRE) + +// Recenter threshold: when |worldX| or |worldZ| exceeds this many +// metres the aircraftStep transparently slides the anchor and +// shrinks the local coords. Keeps the local Q16.16 well clear of its +// integer headroom (~32 km) so flight math never sees big numbers. +#define AC_RECENTER_THRESHOLD_M 20000 + + +typedef struct AircraftT { + // Anchor: absolute FS2 scenery position of the aircraft's + // local (worldX, worldY, worldZ) = (0, 0, 0). Same role as + // FS2's section base. Updated transparently by the recenter + // logic so the local coords stay small. + int32_t sceneryOriginX; + int32_t sceneryOriginY; + int32_t sceneryOriginZ; + + // Local position relative to the anchor. Q16.16 metres. + int32_t worldX; + int32_t worldY; + int32_t worldZ; + + // Orientation (byte angles, 256 == full turn). + uint8_t pitch; + uint8_t bank; + uint8_t yaw; + + // Body-frame rate accumulators in Q8.8 byte-angles per frame. + int16_t pitchRate; // +ve = nose up + int16_t bankRate; // +ve = right wing down + int16_t yawRate; // +ve = nose right + + // Linear motion. Q8.8 world units per frame. + int16_t forwardSpeed; // along body +Z + int16_t climbRate; // +ve = climbing + + // Pilot inputs. + int8_t yokeVert; // -127..+127 (FS2 YokeVertPos) + int8_t yokeHoriz; // -127..+127 + int8_t rudder; // -127..+127 (FS2 RudderPos) + uint8_t throttle; // 0..255 + uint8_t flaps; // 0..255 (panel slider; no flight effect yet) + int8_t trim; // -127..+127 + uint8_t mixture; // 0..255 (rich..lean) + + // Status. + bool onGround; + bool stalled; + bool envelopeWarning; + bool crashed; + CrashTypeE crashType; + + // Slew mode (FS2 `SlewMode`). + bool slewMode; + bool showSlewDigits; + int8_t slewPitchRate; + int8_t slewRollRate; + int8_t slewYawRate; + int8_t slewAltRate; + + // Demo mode (FS2 chunk2 `DemoMode64K`). + bool demoMode; + uint8_t demoState; // mirrors FS2 `DemoModeParam3` + + // Edit mode (FS2 `EditModeFlag`). + bool editMode; + + // Reality mode (FS2 chunk3 `RealityMode`). + bool realityMode; + uint8_t reliabilityFactor; + uint16_t realityTickCounter; + uint8_t failedInstruments; // see AC_FAIL_* bits + uint8_t engineFaults; // see AC_ENG_FAULT_* bits + + // Cockpit toggles overlaid on top of the static panel text. + bool lightsOn; + bool carbHeatOn; + // VOR2/ADF mode toggle. FS2 shares ONE physical instrument bay + // between VOR2 (= CDI horizontal slider) and ADF (= rotating + // bearing dial). chunk4 `ADFMode` flag selects which one is + // active; chunk5 `DrawVOR2IndicatorChanges` and chunk3 + // `UpdateADFIndicator` each early-out when the OTHER mode is + // selected so only one set of needles/flags/digits paint at a + // time. Default false = VOR2 mode (matches chunk4's compiled + // initial value of 0 for ADFMode). + bool adfMode; + + // Fuel state. FS2 chunk5 `UpdateFuelTankGauges` shows separate + // L/R tanks; we model them in 0..255 byte units (=full..empty). + uint8_t fuelLeft; + uint8_t fuelRight; + + // Magneto state. Mirrors FS2 chunk5 `MagnetoState` ($0845): + // 0 = OFF, 1 = R only, 2 = L only, 3 = BOTH, 4 = START. + // Set by `ApplyMagnetoState` and the 1/2/3 keys. + uint8_t magnetos; + + // Pause flag. FS2's `TogglePause` halts the integrator and + // input processing until pressed again. + bool paused; + + // Color / B&W display mode. FS2 prompts at boot for COLOR or + // B/W; the choice patches the display kernel via + // `ColorModePatch` / `BWModePatch`. We track it as a flag and + // gate colour drawing accordingly. true = monochrome. + bool monochrome; + + // Radar view (FS2 chunk4 RadarView). + bool radarView; + int16_t radarZoom; // Q8.8 metres per pixel + + // View direction. + ViewDirectionE viewDirection; +} AircraftT; + + +void aircraftInit(AircraftT *ac); + +// Per-frame integrator. One call per video frame. `wind` may be NULL +// to skip the chunk2 wind+turbulence pipeline. +void aircraftStep(AircraftT *ac, WindStateT *wind); + +void aircraftToggleSlew(AircraftT *ac); +void aircraftToggleDemo(AircraftT *ac); +void aircraftToggleReality(AircraftT *ac); +void aircraftToggleEdit(AircraftT *ac); + +// Copy aircraft pose into a camera so the renderer can transform the +// world from the cockpit point of view. +void aircraftSyncCamera(const AircraftT *ac, CameraT *cam); + +// Effective absolute scenery coordinates of the aircraft (anchor + +// local position scaled into scenery units). +int32_t aircraftSceneryX(const AircraftT *ac); +int32_t aircraftSceneryY(const AircraftT *ac); +int32_t aircraftSceneryZ(const AircraftT *ac); + +// Teleport: place the aircraft at an absolute scenery coordinate. +// Used by spawn, region selection, etc. Resets the local coords to +// zero and stores the absolute coordinate as the anchor. +void aircraftTeleport(AircraftT *ac, int32_t sx, int32_t sy, int32_t sz); + +void aircraftAddThrottle(AircraftT *ac, int delta); // unit: 0..255 +void aircraftDecayYokeVert(AircraftT *ac, uint8_t k_q8); +void aircraftDecayYokeHoriz(AircraftT *ac, uint8_t k_q8); +void aircraftDecayRudder(AircraftT *ac, uint8_t k_q8); + +#endif diff --git a/port/include/apple2hires.h b/port/include/apple2hires.h new file mode 100644 index 0000000..4357a08 --- /dev/null +++ b/port/include/apple2hires.h @@ -0,0 +1,38 @@ +// Apple II hires decoder. Reads an 8K hires page (the .po-derived +// `loading_panel.bin` file), unpacks the interleaved scanline layout +// into a flat 280x192 1-bit bitmap, and blits a row range into the +// port framebuffer using a chosen colour for lit pixels. + +#ifndef APPLE2HIRES_H +#define APPLE2HIRES_H + +#include +#include +#include "framebuffer.h" + +#define HIRES_WIDTH 280 +#define HIRES_HEIGHT 192 +#define HIRES_BYTES 8192 + +typedef struct HiresPageT { + uint8_t bits[HIRES_WIDTH * HIRES_HEIGHT]; // 1 byte per pixel, 0/1 +} HiresPageT; + +// Decode an 8K hires-page file from `path` into `out`. Returns true +// on success. +bool apple2HiresLoadFile(const char *path, HiresPageT *out); + +// Blit rows [srcTopRow, srcTopRow+rows) of the decoded page into +// the framebuffer at (dstX, dstY), painting lit pixels with `litColor` +// and (if `paintBackground`) unlit pixels with `bgColor`. +void apple2HiresBlit(const HiresPageT *page, + int16_t srcTopRow, + int16_t rows, + FramebufferT *fb, + int16_t dstX, + int16_t dstY, + ColorE litColor, + ColorE bgColor, + bool paintBackground); + +#endif diff --git a/port/include/audio.h b/port/include/audio.h new file mode 100644 index 0000000..e316ab0 --- /dev/null +++ b/port/include/audio.h @@ -0,0 +1,24 @@ +// SDL audio: engine drone whose pitch tracks throttle/speed, stall- +// warning horn during stall, plus three one-shot triggers for WW1 Ace +// gun fire, bomb release, and crash impact. + +#ifndef AUDIO_H +#define AUDIO_H + +#include +#include "aircraft.h" + +bool audioInit(void); +void audioShutdown(void); + +// Update the playback parameters for the next audio buffers. Call +// once per frame after `aircraftStep`. +void audioUpdate(const AircraftT *ac); + +// One-shot triggers. Each kicks off a short envelope mixed into the +// engine drone for the next ~0.2 s. +void audioTriggerGun(void); +void audioTriggerBomb(void); +void audioTriggerCrash(void); + +#endif diff --git a/port/include/camera.h b/port/include/camera.h new file mode 100644 index 0000000..58d394c --- /dev/null +++ b/port/include/camera.h @@ -0,0 +1,81 @@ +// Camera state and world-to-camera transform. +// +// Coordinate conventions (right-handed, looks down +Z): +// +X right (in camera frame) +// +Y up +// +Z forward (out of the cockpit) +// +// Position uses the same Q16.16 world-unit convention as `AircraftT` +// (low 16 bits fractional). Forward speed is Q8.8 world-units / frame. +// Orientation is stored as byte angles (256 == full turn). The +// rotation matrix is Q1.15 (matches `math6502Sin/Cos` output), so a +// world->camera transform is a 3x3 dot product of int16 against +// Q16.16 deltas, normalised by `>> 15`. + +#ifndef CAMERA_H +#define CAMERA_H + +#include +#include "types.h" + +#define CAM_POS_FRACT_BITS 16 +#define CAM_POS_FRACT_ONE (1 << CAM_POS_FRACT_BITS) +#define CAM_RATE_FRACT_BITS 8 +#define CAM_RATE_FRACT_ONE (1 << CAM_RATE_FRACT_BITS) +#define CAM_ROT_FRACT_BITS 15 +#define CAM_ROT_ONE (1 << CAM_ROT_FRACT_BITS) + +typedef struct CameraT { + int32_t worldX; // Q16.16 world units + int32_t worldY; + int32_t worldZ; + uint8_t pitch; // X-axis rotation (nose up/down) + uint8_t bank; // Z-axis rotation (roll) + uint8_t yaw; // Y-axis rotation (heading) + // Sub-byte angle precision matching chunk5's 16-bit + // representation at $6C/$6E/$70. The combined 16-bit angle + // is `((pitch << 8) | pitchFine)` etc. -- chunk5SetupView- + // Projection uses these full 16-bit values to derive the + // matrix. MAME's Meigs boot has $6C/$6D=-109 (= -0.6 deg), + // which is finer than the 1/256-of-a-circle 8-bit pitch + // can express on its own. + uint8_t pitchFine; + uint8_t bankFine; + uint8_t yawFine; + // chunk5 ViewDirection ($0A70). Multiplied by 16 inside + // SetupViewProjection's L6155 to bias the matrix's yaw. + // MAME's Meigs boot has $0A70 = $0F. + uint8_t viewDirection; + int16_t forwardSpeed; // Q8.8 world units per frame + int16_t rot[3][3]; // Q1.15 world -> camera rotation matrix (R^T) + // chunk5 SetupViewProjection lays $78..$89 out as R (camera- + // to-world), NOT R^T. sceneryAttachCamera mirrors this matrix + // into writableRam so downstream chunk5 paths (notably L631D + // section base) see the same shape they would on the + // original. Same data as `rot` but transposed. + int16_t rotChunk5[3][3]; +} CameraT; + +void cameraInit(CameraT *cam); + +// Recompute the rotation matrix from pitch/bank/yaw. Call once after +// any orientation change. +void cameraUpdate(CameraT *cam); + +// Transform a world-space point into camera space. Caller supplies +// a fresh `CameraT` (already updated this frame). All coords are +// Q16.16 world units. +void cameraTransform(const CameraT *cam, int32_t wx_q1616, int32_t wy_q1616, int32_t wz_q1616, int32_t *cx_q1616, int32_t *cy_q1616, int32_t *cz_q1616); + +// Move the camera forward by `forwardSpeed` along its current heading. +void cameraStep(CameraT *cam); + +// Decompose the camera's 3x3 rotation matrix into the 2x3 form chunk5 +// uses (XZ in the world plane -> 3D camera-space). Each matrix entry +// is scaled to int8_t with $7F == 1.0 so MultiplyXY's int8 inputs see +// the right magnitude. Y (altitude) is handled per-section by the +// scenery $0D Header opcode and so is excluded from this matrix; the +// world driver handles altitude through the section-base instead. +void cameraGet2x3Matrix(const CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]); + +#endif diff --git a/port/include/chunk5Setup.h b/port/include/chunk5Setup.h new file mode 100644 index 0000000..b3210f1 --- /dev/null +++ b/port/include/chunk5Setup.h @@ -0,0 +1,59 @@ +// chunk5Setup: bit-perfect C transliteration of chunk5 +// SetupViewProjection (chunk5.s lines 203-432) and its math +// dependencies in chunk4 (cos table at $141A, L177B/L1778 lookups, +// ZPScale multiplier). +// +// Produces the same int16 3x3 rotation matrix the original Apple II +// FS2 stores at $78..$89, given the same inputs ($6C/$6D 16-bit +// "yaw" -> X-axis, $6E/$6F "pitch" -> Z-axis, $70/$71 "bank" -> +// Y-axis, ViewDirection byte). The disassembly's input labels are +// mislabeled vs standard aviation -- see SESSION_RECOVERY.md. +// +// Validated cell-for-cell against `port/bin/fs2trace --matrix` (the +// 6502 emulator running the actual chunk5 binary). + +#ifndef CHUNK5_SETUP_H +#define CHUNK5_SETUP_H + +#include + + +// Run SetupViewProjection with the supplied inputs. Output is the +// 3x3 matrix as it appears at $78..$89 (post-L6301 col shifts: +// col 0 >>= 1, col 2 >>= 2). Each output is int16 in chunk5's R +// (camera-to-world) layout, ready to be mirrored into +// CameraT.rotChunk5 / writableRam[$78..$89]. +// +// Inputs: +// yaw16 = $6C/$6D 16-bit signed (X-axis rotation in chunk5 conv) +// pitch16 = $6E/$6F 16-bit signed (Z-axis rotation) +// bank16 = $70/$71 16-bit signed (Y-axis rotation) +// vd = $0A70 ViewDirection byte +// radarView = $0836 RadarView flag (1 = radar view) +void chunk5SetupViewProjection(int16_t yaw16, int16_t pitch16, int16_t bank16, + uint8_t vd, uint8_t radarView, + int16_t outMatrix[3][3]); + +// Lower-level primitives, exposed for unit tests. All match their +// 6502 counterparts cell-for-cell (validated by chunk5SetupSelfTest). + +// L177B: cos lookup. byteAngle is the 8-bit angle (256 = full +// circle); subByte gives sub-byte fractional precision via linear +// interpolation against the next entry. Result is Q1.15 cos(angle). +int16_t chunk5L177B(uint8_t byteAngle, uint8_t subByte); + +// L1778: sin lookup. Equivalent to L177B(byteAngle - 64, subByte) +// since sin(x) = cos(x - 90 deg). +int16_t chunk5L1778(uint8_t byteAngle, uint8_t subByte); + +// ScaleC2ByC4 / ZPScale: 16-bit signed Q-format multiply with +// chunk4's specific rounding pattern (chunk4.s lines 1565-1744). +int16_t chunk5ScaleC2ByC4(int16_t a, int16_t b); + +// Self-test: sweeps a few input combinations through the cascade +// and aborts if any cell deviates from the known oracle output. +// Returns 0 on success, non-zero on failure. +int chunk5SetupSelfTest(void); + + +#endif diff --git a/port/include/chunk5Transform.h b/port/include/chunk5Transform.h new file mode 100644 index 0000000..3d61262 --- /dev/null +++ b/port/include/chunk5Transform.h @@ -0,0 +1,33 @@ +// Cycle-by-cycle port of chunk5 TransformVertex7EBC (L7EBC..L80B0) +// and TransformVertex80C5 (L80C5). These are the polygon vertex +// transform routines that read 4 stream bytes (xLo, xHi, zLo, zHi), +// subtract camera-section deltas at $66/$67/$6A/$6B, run the +// auto-scale loop (L7F1A), apply the 2x3 rotation matrix at $79/$7B/ +// $7D + $85/$87/$89, and write the 6-byte result (16-bit X/Y/Z) into +// the caller's vertex slot. +// +// Faithful to the original 6502 -- 8-bit byte arithmetic, carry/V/N +// flag handling, exact algorithm order. Reads from and writes to a +// 64K RAM buffer that mirrors the Apple II zero page and chunk5's +// scratch slots. + +#ifndef CHUNK5_TRANSFORM_H +#define CHUNK5_TRANSFORM_H + +#include + +// Run TransformVertex7EBC on the 4 vertex bytes at `stream+1..stream+4`. +// `ram` is the 64K RAM image (zero page reads at $4A..$52, $66..$6B, +// $79/$7B/$7D, $85/$87/$89). `destSlot` is the caller-supplied X +// register equivalent ($CB for v1 = vertex 1, $D4 for v2 = vertex 2) +// -- the 6-byte result is written to ram[destSlot..destSlot+5]. +// +// Returns the byte advance for $8B (always 5: opcode + 4 vertex bytes). +int chunk5TransformVertex7EBC(uint8_t *ram, const uint8_t *stream, uint8_t destSlot); + +// Same as above but uses the transform-A work-counter bias (#$C7) +// instead of transform-B's (#$51). The actual matrix multiply and +// vertex math is identical. +int chunk5TransformVertex80C5(uint8_t *ram, const uint8_t *stream, uint8_t destSlot); + +#endif diff --git a/port/include/coursePlotter.h b/port/include/coursePlotter.h new file mode 100644 index 0000000..07cbb32 --- /dev/null +++ b/port/include/coursePlotter.h @@ -0,0 +1,68 @@ +// Course Plotter (FS2 64K-only feature, chunk2 lines 25-150). +// +// FS2's `CoursePlotterState` is one of three values: +// 0 = OFF, 1 = RECORD, 2 = DISPLAY +// +// In RECORD mode the simulator samples the aircraft's position every +// `sampleRate` frames into a circular buffer ($D000-$DFFF in the +// language card). DISPLAY mode redraws the recorded course in the +// radar viewport. +// +// The C port keeps the same state machine but stores samples in an +// in-process buffer (4KB) rather than the LC bank. Recording uses +// either NORMAL (range=$06, rate=$0A) or PRECISION (range=$04, rate=$02) +// per FS2 chunk2 BeginNormalCourseRecording / BeginPrecisionRecording. + +#ifndef COURSE_PLOTTER_H +#define COURSE_PLOTTER_H + +#include +#include + +struct AircraftT; +struct FramebufferT; + +typedef enum CoursePlotterStateE { + COURSE_PLOT_OFF = 0, + COURSE_PLOT_RECORD = 1, + COURSE_PLOT_DISPLAY = 2 +} CoursePlotterStateE; + +#define COURSE_PLOT_BUFFER_BYTES 4096 +#define COURSE_PLOT_SAMPLE_BYTES 8 // 2 * 24-bit XYZ rounded up + + +typedef struct CoursePlotterT { + CoursePlotterStateE state; + uint8_t sampleRate; // frames between samples (chunk2: $0A normal, $02 precision) + uint8_t sampleRange; // chunk2 sampleRange byte + uint8_t sampleCounter; // counts down to 0, then take a sample + bool anyData; // true once a sample has been captured + uint16_t recordPos; // byte offset into buffer + uint8_t buffer[COURSE_PLOT_BUFFER_BYTES]; +} CoursePlotterT; + + +void coursePlotterInit(CoursePlotterT *cp); + +// Begin a recording session. Mode must be RECORD or OFF; precision +// chooses between normal (rate 10/range 6) and precision (rate 2/range 4). +void coursePlotterBeginRecord(CoursePlotterT *cp, bool precision); + +void coursePlotterBeginDisplay(CoursePlotterT *cp); +void coursePlotterTurnOff(CoursePlotterT *cp); + +// Per-frame: if state == RECORD, count down sampleCounter and snapshot +// the aircraft's scenery position into the buffer when it hits 0. +void coursePlotterStep(CoursePlotterT *cp, const struct AircraftT *ac); + +// Render the recorded course on top of the framebuffer (called when +// state == DISPLAY in radar/3D view). +void coursePlotterRender(const CoursePlotterT *cp, struct FramebufferT *fb, + const struct AircraftT *ac); + +// Draw a short status line ("COURSE PLOT REC" / "COURSE PLOT VIEW") +// when the plotter is active. No-op when off. +void coursePlotterDrawStatus(const CoursePlotterT *cp, struct FramebufferT *fb); + +#endif diff --git a/port/include/cpu6502.h b/port/include/cpu6502.h new file mode 100644 index 0000000..288b786 --- /dev/null +++ b/port/include/cpu6502.h @@ -0,0 +1,94 @@ +// Minimal 6502 / 65C02 interpreter, extracted from tools/fs2trace.c +// so chunk5Transform.c can run small fragments of the MAME-patched +// chunk5 binary directly (= byte-faithful replication of the per- +// vertex transform body at $7E8E..$8068 + helpers like $181A). +// +// All state is held in Cpu6502T so multiple interpreters can run +// concurrently and the caller controls the 64K address space. No +// I/O hooks, no SmartPort, no display hardware -- caller provides +// raw RAM and runs the CPU until PC reaches a configured stop +// address, or unknown opcode is hit. + +#ifndef CPU6502_H +#define CPU6502_H + +#include +#include + +typedef struct Cpu6502T { + uint8_t *mem; // 64K RAM + uint16_t pc; + uint8_t a; + uint8_t x; + uint8_t y; + uint8_t s; // stack pointer + uint8_t flagN; + uint8_t flagV; + uint8_t flagD; + uint8_t flagI; + uint8_t flagZ; + uint8_t flagC; + bool unknownOp; // set when step() hits an unimplemented opcode + uint8_t lastOp; // last opcode executed (for diagnostics) + uint16_t lastOpPc; // PC where lastOp was fetched + // Per-instruction trace hook (NULL = disabled). + void (*traceFn)(struct Cpu6502T *cpu, void *userData); + void *traceUserData; +} Cpu6502T; + + +// Initialise A/X/Y/S to 0, flags cleared, mem set to caller's +// buffer. +void cpu6502Init(Cpu6502T *cpu, uint8_t *mem); + + +// Single instruction step. +void cpu6502Step(Cpu6502T *cpu); + + +// Push a 16-bit return address (high then low) onto the stack so +// the next RTS pops back to (return_addr + 1). Mirrors what JSR +// does: pushes pc-1 of the instruction after JSR. +void cpu6502PushReturn(Cpu6502T *cpu, uint16_t returnAfter); + + +// Run starting at `entry` until PC reaches `stopPc` or an unknown +// opcode trips `cpu->unknownOp`. `maxSteps` bounds the work in +// case of a runaway loop (set to 1000000 for transform body). +// Returns true on clean halt at stopPc. +bool cpu6502Run(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, int maxSteps); + + +// Hook callback invoked BEFORE the instruction at `hookPc` executes. +// If `cb` returns true, the hook handled the instruction (e.g. by +// popping a return address and updating PC); the interpreter skips +// the normal step. Use to intercept JSR targets and emulate them +// in C (chunk5 DrawColorLine -> port renderer, etc.). +typedef bool (*Cpu6502HookFn)(struct Cpu6502T *cpu, void *userData); +bool cpu6502RunWithHook(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, + uint16_t hookPc, Cpu6502HookFn cb, void *userData, + int maxSteps); + + +// Multi-hook variant. Each entry maps a PC to its handler. The +// interpreter checks PC against each entry on every step (linear +// scan; intended for small N). Same return convention as +// cpu6502RunWithHook. +typedef struct Cpu6502HookT { + uint16_t pc; + Cpu6502HookFn cb; + void *userData; +} Cpu6502HookT; +bool cpu6502RunWithHooks(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, + const Cpu6502HookT *hooks, int nHooks, + int maxSteps); + + +// Per-instruction trace callback. If set on a Cpu6502T, fires +// before EVERY instruction executes. Receives pc + opcode + A/X/Y. +// Set to NULL to disable. +typedef void (*Cpu6502TraceFn)(struct Cpu6502T *cpu, void *userData); +void cpu6502SetTrace(Cpu6502T *cpu, Cpu6502TraceFn fn, void *userData); + + +#endif diff --git a/port/include/fixture.h b/port/include/fixture.h new file mode 100644 index 0000000..b0d7e64 --- /dev/null +++ b/port/include/fixture.h @@ -0,0 +1,11 @@ +// Hardcoded scenery byte streams used until the .po reader exists. + +#ifndef FIXTURE_H +#define FIXTURE_H + +#include + +extern const uint8_t fixtureSceneryDemo[]; +extern const uint32_t fixtureSceneryDemoLength; + +#endif diff --git a/port/include/font.h b/port/include/font.h new file mode 100644 index 0000000..e5c8b21 --- /dev/null +++ b/port/include/font.h @@ -0,0 +1,21 @@ +// 5x7 ASCII bitmap font for HUD text. Only the printable subset +// $20..$5F is encoded; lower-case letters fall back to upper-case. + +#ifndef FONT_H +#define FONT_H + +#include +#include "framebuffer.h" +#include "palette.h" + +#define FONT_WIDTH 5 +#define FONT_HEIGHT 7 + +// Draw a single character at (x, y). Top-left of glyph at (x, y). +// Returns the X advance (FONT_WIDTH + 1). +int16_t fontDrawChar(FramebufferT *fb, int16_t x, int16_t y, char ch, ColorE color); + +// Draw a NUL-terminated string. Returns the X advance. +int16_t fontDrawString(FramebufferT *fb, int16_t x, int16_t y, const char *s, ColorE color); + +#endif diff --git a/port/include/framebuffer.h b/port/include/framebuffer.h new file mode 100644 index 0000000..903efc8 --- /dev/null +++ b/port/include/framebuffer.h @@ -0,0 +1,30 @@ +// Software framebuffer for the FS2 port. Owns a 280x192 8-bit +// palette-indexed image AND a parallel Apple II hires bitplane +// (40x192 = 7680 bytes) that the scenery viewport renders into so +// chunk5's bit-pattern color generation works as on real hardware. +// At blit time, viewport rows decode from the bitplane; panel rows +// (below VIEWPORT_BOTTOM) use the palette image as before. + +#ifndef FRAMEBUFFER_H +#define FRAMEBUFFER_H + +#include +#include "hires.h" +#include "palette.h" +#include "types.h" + +typedef struct FramebufferT { + uint8_t pixels[NATIVE_WIDTH * NATIVE_HEIGHT]; + uint8_t hires[HIRES_PAGE_BYTES]; +} FramebufferT; + +void framebufferClear(FramebufferT *fb, ColorE color); +void framebufferFillRect(FramebufferT *fb, int16_t x, int16_t y, int16_t w, int16_t h, ColorE color); +void framebufferFillRow(FramebufferT *fb, int16_t y, ColorE color); +void framebufferSetPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color); + +// Copy the framebuffer into a 32-bit RGB SDL pixel buffer (any +// upscaling is applied here so SDL just blits a flat surface). +void framebufferBlitTo32(const FramebufferT *fb, uint32_t *dst, int dstWidth, int dstHeight); + +#endif diff --git a/port/include/fs2math.h b/port/include/fs2math.h new file mode 100644 index 0000000..dc2f13a --- /dev/null +++ b/port/include/fs2math.h @@ -0,0 +1,64 @@ +// Direct ports of the FS2 disassembly's instrument math. +// Each function mirrors a specific routine in the original FS2 +// chunks (chunk4 / chunk5) and returns the byte angle to feed into +// `needleDraw`. + +#ifndef FS2_MATH_H +#define FS2_MATH_H + +#include + +// `UpdateAirspeedDerivedValue` (chunk5 L9FE9 / `$0A33`). Takes a +// 16-bit airspeed value (high byte of $0A11..$0A12 in the original), +// clamps to the FS2 maximum (0x5A in the high byte), buckets via the +// $07DF lookup, and interpolates between buckets. Returns the FS2 +// needle position byte (0..0x57 after wrap). +uint8_t fs2AirspeedNeedlePos(uint16_t airspeed16); + +// Convert an FS2 needle position (0..0x57) into a byte angle suitable +// for `needleDraw` (0 = up, +ve CW). Centred so position $16 (12 +// o'clock) maps to byte angle 0. +uint8_t fs2PosToByteAngle(uint8_t pos); + +// `UpdateAltimeterPose` (chunk5 line 8332). Takes a 16-bit altitude +// value (in FS2's internal altitude unit) and produces both the +// main-hand position ($29) and the 10K-hand position ($28). Both are +// FS2 needle positions (0..0x57). +void fs2AltimeterNeedlePos(uint16_t altitude16, uint8_t *mainPos, uint8_t *tenKPos); + +// Signed 8.8 multiply that mirrors FS2's `ScaleC2ByAX`: the result is +// `(value16 * scale16) / 32768`, signed. Used by the altimeter pose +// computation and several other scaling routines. +int16_t fs2ScaleByAX(int16_t value16, int16_t scale16); + +// Mirrors the head of `UpdateTurnCoordinator` (chunk5 L4961). Takes the +// signed 16-bit yaw-rate input ($09CE:$09CD in FS2), computes the +// rounded value `(value16 * 3) / 256 + 8`, and clamps the result into +// 0..15. The returned index addresses the wing-bar table at $0DE0. +uint8_t fs2TurnCoordIndex(int16_t value16); + +// Look up a turn-coordinator entry from the FS2 $0DE0 table. `index` +// must be 0..15 (clamp via `fs2TurnCoordIndex`). The four returned +// deltas are signed colour-pixel offsets: +// wing bar: from (12 + dx, 166 + dy) to (12 - dx, 166 - dy) +// ball: from (12, 166) to (12 + vx, 166 + vy) +// (12, 166) here is the gauge centre in FS2 colour-pixel/hires-Y +// coordinates that DrawColorLine consumes. +void fs2TurnCoordEntry(uint8_t index, int8_t *dx, int8_t *dy, int8_t *vx, int8_t *vy); + +// Mirrors the routine FS2 calls "UpdateMagneticHeading" (chunk5 +// L8432) but which actually computes the VSI needle target ($2A, +// consumed by chunk4 `UpdateVerticalSpeedIndicator`). The input is the +// 16-bit signed value at $0A16:$0A15 (climb rate in FS2 internal +// units). The high byte is clamped to [-9, 9] before the rest of the +// shift / negate / mod-$58 chain runs. Returns the FS2 needle position +// (0..0x57). +uint8_t fs2VsiNeedlePos(int16_t value16); + +// Mirrors the head of FS2 `UpdateSlipSkid` (chunk5 L8467). Maps the +// signed-byte slip input through `(value+$7F)/4 - $1F`, clamps to +// [-8, 8], and adds 9. The returned index 1..17 is what +// `UpdateSlipSkidIndicator` (chunk4 L2497) consumes. +uint8_t fs2SlipSkidIndex(int8_t slipValue); + +#endif diff --git a/port/include/hires.h b/port/include/hires.h new file mode 100644 index 0000000..6a6d4b7 --- /dev/null +++ b/port/include/hires.h @@ -0,0 +1,92 @@ +// Apple II hires bitplane for the scenery viewport. +// +// FS2's chunk5 line-drawing kernel sets BITS in a 280x192 monochrome +// hires bitmap. Apple II hires display interprets adjacent bits as +// colors via NTSC encoding: bits at "even" pixel columns become VIOLET +// (palette 0) or BLUE (palette 1); bits at "odd" columns become GREEN +// or ORANGE; two adjacent set bits combine to WHITE. +// +// The port stores the hires page in plain memory order (40 bytes per +// row, 192 rows = 7680 bytes per page) instead of Apple II's scrambled +// addressing -- chunk5's `HiresTableLo`/`HiresTableHi` is just an +// optimisation; the bit semantics are independent of layout. + +#ifndef HIRES_H +#define HIRES_H + +#include +#include + +// One hires page = 40 bytes per row * 192 rows. +#define HIRES_BYTES_PER_ROW 40 +#define HIRES_ROWS 192 +#define HIRES_PAGE_BYTES (HIRES_BYTES_PER_ROW * HIRES_ROWS) + +// chunk5 ToHiresColorTable codes (0..7). +typedef enum HiresColorE { + HIRES_BLACK1 = 0, + HIRES_VIOLET = 1, + HIRES_GREEN = 2, + HIRES_WHITE1 = 3, + HIRES_BLACK2 = 4, + HIRES_BLUE = 5, + HIRES_ORANGE = 6, + HIRES_WHITE2 = 7, +} HiresColorE; + +// chunk5 ToHiresColorTable entries (chunk5.s:3825). Indexed by the +// scenery code byte (0..15) that follows the $12 SetColor opcode. +extern const uint8_t kSceneryToHires[16]; + +// Reset every byte to BLACK1 ($00). +void hiresClearPage(uint8_t *page); + +// Fill one viewport row with chunk5's solid-color pattern: sequential +// bytes alternate `evenByte`/`oddByte`. For solid GREEN that's +// $55/$2A; for SKY BLUE it's $D5/$AA. Unaffected: rows below the +// viewport (= panel area). +void hiresFillRow(uint8_t *page, int row, uint8_t evenByte, uint8_t oddByte); + +// Plot one pixel at color-pixel coords (xColor in 0..139, y in 0..191) +// using the given hires color code. This mirrors chunk5's +// `PlotColorPixel` (chunk5.s:3509) which plots the two sub-pixels of a +// color pixel via `OrMaskTable1`/`OrMaskTable2`. +void hiresPlotPixel(uint8_t *page, int xColor, int y, HiresColorE col); + +// Draw a Bresenham line in color-pixel coords, plotting each step via +// hiresPlotPixel. +void hiresDrawLine(uint8_t *page, int x1c, int y1, int x2c, int y2, HiresColorE col); + +// Draw a horizontal span of color pixels. Mirrors chunk5 DrawColorSpan +// at $78E0 (chunk5.s:3446). Plots `length+1` color pixels starting at +// color column `xRight` and walking LEFTWARD (matches the source +// signature: A=length, $27=right edge). Each pixel is plotted via the +// AND/OR mask technique: AND clears the opposite-palette bit at the +// pixel position, OR sets the palette bit. With ground-pattern bytes +// underneath, this produces the FS2 viewport-edge violet "water" pixels +// observed in the captured MAME RAM ($4128=$29, $414F=$35). +void hiresDrawColorSpan(uint8_t *page, int xRight, int length, int y, HiresColorE col); + +// Decode a hires page to a 280x192 RGB888 image (one packed uint32 +// per pixel: 0x00RRGGBB). `out` must hold 280*192 uint32s. +void hiresDecodeToRgb(const uint8_t *page, uint32_t *out); + +// Convenience: convert chunk5 hires code -> 0x00RRGGBB for solid +// fills (sky/ground/etc). +uint32_t hiresColorToRgb(HiresColorE col); + +// Get the (evenByte, oddByte) pair for a hires color code, suitable +// for hiresFillRow. +void hiresFillBytesFor(HiresColorE col, uint8_t *outEven, uint8_t *outOdd); + +// Copy 192 rows of hires bytes from an Apple II hires page (= scrambled +// non-linear addressing per chunk4 HiresTableHi/Lo) at `appleHiresPage` +// (= 8192 bytes starting at $2000) into our linear `page` (= 7680 +// bytes). Used to import the captured RAM dump's hires page so the port +// inherits MAME's pre-rendered viewport state (which on a real Apple II +// persists frame-to-frame and contains init artifacts that the boot +// dispatcher never overwrites -- this is where the violet pixels at the +// viewport edges come from at boot Meigs). +void hiresImportFromAppleII(uint8_t *page, const uint8_t *appleHiresPage); + +#endif diff --git a/port/include/hud.h b/port/include/hud.h new file mode 100644 index 0000000..201f3fe --- /dev/null +++ b/port/include/hud.h @@ -0,0 +1,13 @@ +// Minimalist HUD strip painted into the panel area below the +// viewport. Reads camera state and renders speed, altitude, heading, +// pitch and bank as digital readouts plus a tiny attitude indicator. + +#ifndef HUD_H +#define HUD_H + +#include "camera.h" +#include "framebuffer.h" + +void hudDraw(FramebufferT *fb, const CameraT *cam); + +#endif diff --git a/port/include/instruments.h b/port/include/instruments.h new file mode 100644 index 0000000..3f370b0 --- /dev/null +++ b/port/include/instruments.h @@ -0,0 +1,14 @@ +// Live instrument needles painted on top of the static panel bitmap. +// Reads aircraft state and animates the airspeed, altimeter, attitude +// indicator, heading, vertical speed and turn coordinator gauges. + +#ifndef INSTRUMENTS_H +#define INSTRUMENTS_H + +#include "aircraft.h" +#include "framebuffer.h" +#include "radios.h" + +void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *radios); + +#endif diff --git a/port/include/math6502.h b/port/include/math6502.h new file mode 100644 index 0000000..8207b66 --- /dev/null +++ b/port/include/math6502.h @@ -0,0 +1,28 @@ +// Fixed-point math helpers that mirror the conventions used by the +// FS2 disassembly: byte angles (256 == full turn), 16-bit signed +// magnitudes scaled to fit ±$7FFF. + +#ifndef MATH6502_H +#define MATH6502_H + +#include + +void math6502Init(void); + +// sin(byteAngle) -> -32767..32767. Matches `SinByteAngle` in the +// disassembly. +int16_t math6502Sin(uint8_t byteAngle); + +// cos(byteAngle) -> -32767..32767. +int16_t math6502Cos(uint8_t byteAngle); + +// (Y * X) signed, returns the 16-bit signed product. Matches +// `MultiplyXY` in the disassembly. +int16_t math6502SignedMul(int8_t y, int8_t x); + +// Integer square root of a non-negative int32. Returns 0 for negative +// input. Matches the standard hardware-trick algorithm; precision is +// exact when the answer fits in 16 bits. +uint16_t math6502Sqrt(int32_t n); + +#endif diff --git a/port/include/needleData.h b/port/include/needleData.h new file mode 100644 index 0000000..7397ea4 --- /dev/null +++ b/port/include/needleData.h @@ -0,0 +1,22 @@ +// Direct port of the FS2 dial-needle pixel-list tables. Each list is +// a sequence of (col-offset, run-length) bytes followed by a sentinel +// byte with the high bit set ($FF). Driven by `needleDraw` below. + +#ifndef NEEDLE_DATA_H +#define NEEDLE_DATA_H + +#include +#include +#include "framebuffer.h" +#include "palette.h" + +// Render a dial needle at (cx, cy) for a given byte angle. The +// pixel-list shapes are picked from the original FS2 thin or thick +// needle tables, and the four-quadrant transform from chunk4's +// `DrawIndicatorDialNeedle` is applied so the same 23 pre-rendered +// shapes cover all 360 deg. +// +// `byteAngle` follows the simulator convention (0 = up, +ve = CW). +void needleDraw(FramebufferT *fb, int16_t cx, int16_t cy, uint8_t byteAngle, bool thick, ColorE color); + +#endif diff --git a/port/include/palette.h b/port/include/palette.h new file mode 100644 index 0000000..80b6635 --- /dev/null +++ b/port/include/palette.h @@ -0,0 +1,39 @@ +// 24-bit RGB palette for the FS2 port. The Apple II hires version was +// limited to six colours and suffered colour-clash artefacts at byte +// boundaries. We map the original FS2 colour codes (0..15 in the +// scenery stream) to a richer palette so lakes are blue rather than +// purple, etc. + +#ifndef PALETTE_H +#define PALETTE_H + +#include + +typedef enum ColorE { + COLOR_BLACK = 0, + COLOR_WHITE, + COLOR_SKY_DAY, + COLOR_SKY_NIGHT, + COLOR_GROUND_DAY, + COLOR_GROUND_NIGHT, + COLOR_WATER, + COLOR_RUNWAY, + COLOR_BUILDING, + COLOR_MOUNTAIN, + COLOR_CITY, + COLOR_AIRCRAFT, + COLOR_ORANGE, + COLOR_HAZE, + COLOR_FOREST, + COLOR_DIRT, + COLOR_COUNT +} ColorE; + +extern const uint32_t paletteRgb[COLOR_COUNT]; + +// Map a scenery-stream colour code (0..15) to one of the palette +// entries above. This replaces the original `ToHiresColorTable`, +// which collapsed the 16 codes into 6 hires colours. +ColorE paletteFromSceneryCode(uint8_t code); + +#endif diff --git a/port/include/panelDigits.h b/port/include/panelDigits.h new file mode 100644 index 0000000..2755ed1 --- /dev/null +++ b/port/include/panelDigits.h @@ -0,0 +1,14 @@ +// Live digital readouts overlaid next to the right-hand panel labels +// (COM1, NAV1, NAV2, DME, XPNDR, TIME, MAGS, FUEL, OIL, RPM, CARB H). + +#ifndef PANEL_DIGITS_H +#define PANEL_DIGITS_H + +#include "aircraft.h" +#include "framebuffer.h" +#include "radios.h" +#include "timeOfDay.h" + +void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radios, const TimeOfDayT *tod); + +#endif diff --git a/port/include/projection.h b/port/include/projection.h new file mode 100644 index 0000000..1d81c06 --- /dev/null +++ b/port/include/projection.h @@ -0,0 +1,39 @@ +// 3D -> 2D projection and frustum classification. +// +// Uses a 90 deg horizontal/vertical FOV (matching the original FS2 +// frustum) and the Apple II hires viewport (280 wide, rows 0..98). +// All camera-space coordinates are Q16.16 world-units (matching +// `CameraT::worldX/Y/Z`); screen output is integer pixels. + +#ifndef PROJECTION_H +#define PROJECTION_H + +#include +#include +#include "types.h" + +typedef struct ProjectedT { + int32_t cx; // camera-space X (Q16.16 world units) + int32_t cy; // camera-space Y + int32_t cz; // camera-space Z (positive = in front) + int16_t screenX; // post-projection screen column + int16_t screenY; // post-projection screen row + uint8_t outcode; // OUTCODE_* bits +} ProjectedT; + +// Compute frustum outcodes for a camera-space point (Q16.16). +uint8_t projectionOutcode(int32_t cx_q1616, int32_t cy_q1616, int32_t cz_q1616); + +// Project a camera-space point onto the screen. Returns true if the +// point is visible (outcode == 0). When false, screenX / screenY are +// undefined. +bool projectionToScreen(int32_t cx_q1616, int32_t cy_q1616, int32_t cz_q1616, int16_t *outX, int16_t *outY); + +// Trivial-reject + trivial-accept Cohen-Sutherland clip in 3D. +// On entry both endpoints are camera-space + outcode-stamped. On +// exit, if true, the endpoints have been moved onto the visible +// region of the frustum. Returns false if the line is wholly +// outside. +bool projectionClipLine(ProjectedT *a, ProjectedT *b); + +#endif diff --git a/port/include/radios.h b/port/include/radios.h new file mode 100644 index 0000000..eeb1bc5 --- /dev/null +++ b/port/include/radios.h @@ -0,0 +1,119 @@ +// NAV / COM / ADF radios. Holds the four tuned frequencies plus +// VOR1/VOR2 OBS courses, looks up the active station for each radio +// against the unified scenery station database, and exposes the +// per-frame derived values the panel digits and gauge needles read. +// +// Frequency encoding mirrors FS2's chunk5 `DecodeBCDFreqString` and +// chunk3 `LookupADFStation`: NAV/COM are two BCD pairs giving +// XXX.X MHz (e.g. 0x1080 == 108.0 MHz, 0x1224 == 122.4 MHz). ADF is +// `[BCD pair (mid+lo)] [single high digit]`, so 0x0703 == 703 kHz. + +#ifndef RADIOS_H +#define RADIOS_H + +#include +#include +#include "aircraft.h" +#include "sceneryStationsData.h" + + +typedef enum RadioE { + RADIO_NAV1 = 0, + RADIO_NAV2 = 1, + RADIO_ADF = 2, + RADIO_COM1 = 3, + RADIO_COUNT +} RadioE; + + +// VOR TO/FROM/OFF flag, indexed into chunk5 msg_vor_flags. +typedef enum VorFlagE { + VOR_FLAG_OFF = 0, + VOR_FLAG_TO = 1, + VOR_FLAG_FR = 2 +} VorFlagE; + + +typedef struct RadiosT { + // BCD-packed tuned frequencies. See the file-level comment for + // the encoding. + uint16_t nav1Freq; + uint16_t nav2Freq; + uint16_t adfFreq; + uint16_t com1Freq; + + // OBS courses for VOR1/VOR2 (byte angles, 256 == full turn). + // Set by the pilot via the panel; the needle deflects from + // course-line intercept. + uint8_t nav1Obs; + uint8_t nav2Obs; + + // Active station pointers (NULL = no station found at the + // current frequency). Refreshed on tune-change and whenever + // the aircraft moves into a different region's coverage. + const StationDataT *nav1Station; + const StationDataT *nav2Station; + const StationDataT *adfStation; + const StationDataT *com1Station; + + // Per-frame derived values, recomputed by `radiosUpdate`. + // Bearings are byte angles (0 = north, 64 = east). DME is + // nautical miles, integer (FS2 displayed it as "000"-style). + // Needle deflections are signed byte deflection from "needle + // centred", saturating at +/-32 -- matches needleData range. + uint8_t nav1RelativeBearing; + uint8_t nav2RelativeBearing; + uint8_t adfRelativeBearing; // station bearing minus aircraft heading + uint16_t nav1Dme; + uint16_t nav2Dme; + int8_t nav1NeedleDefl; + int8_t nav2NeedleDefl; + bool nav1Valid; // active station present + in receivable range + bool nav2Valid; + bool adfValid; + + // VOR1/VOR2 TO/FROM/OFF flag, mirroring chunk5 msg_vor_flags + // (0 = OFF, 1 = TO, 2 = FR). Indexed straight into a 3-string + // table at draw time. + uint8_t nav1Flag; + uint8_t nav2Flag; +} RadiosT; + + +void radiosInit(RadiosT *r); + +// Sweep the database for the nearest NAV / ADF / COM to the aircraft +// and tune each radio to it. Useful for "find me something to listen +// to" gestures and as a sanity check that the database is populated. +void radiosTuneToNearest(RadiosT *r, const AircraftT *ac); + +// Step the named radio's frequency by one BCD click. Stride matches +// real-radio behaviour: NAV/COM 0.05 MHz, ADF 1 kHz. Updates the +// active station pointer. +void radiosStepFreq(RadiosT *r, RadioE which, int direction); + +// Step the named VOR's OBS course by `deltaDegrees` (signed; will be +// quantised to a byte-angle delta). NAV1/NAV2 only. +void radiosStepObs(RadiosT *r, RadioE which, int deltaDegrees); + +// FS2 BCD per-digit entry. The user types a digit 0..9 and the +// radio's frequency rotates one decimal slot left, dropping the high +// digit. Mirrors chunk5 KeyDecreasePatch / KeyIncreasePatch with a +// fixed digit input. +void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit); + +// Recompute all derived values (bearings, DME, deflections) from the +// aircraft's current position and the active station pointers. Call +// once per frame after the flight integrator. +void radiosUpdate(RadiosT *r, const AircraftT *ac); + +// Format a tuned frequency for display. NAV/COM produce "XXX.X" (5 +// chars + null); ADF produces "XXX" (3 chars + null). `out` must be +// at least 6 bytes. +void radiosFormatFreq(uint16_t freq, RadioE which, char *out); + +// Look up a station by (type, freq). Returns NULL if no match. +// Exposed for tests / tooling; `radiosStepFreq` does this internally. +const StationDataT *radiosFindStation(char type, uint16_t freq); + +#endif diff --git a/port/include/renderer.h b/port/include/renderer.h new file mode 100644 index 0000000..4c5c6b8 --- /dev/null +++ b/port/include/renderer.h @@ -0,0 +1,57 @@ +// Renderer: 2D primitives that operate on the software framebuffer +// and the colour state shared with the scenery interpreter. + +#ifndef RENDERER_H +#define RENDERER_H + +#include "framebuffer.h" +#include "palette.h" +#include "types.h" + +// Active framebuffer / colour state. The scenery VM reads these so +// individual opcodes need only set the colour and call the line or +// span primitives. `hiresColor` is the chunk5 ToHiresColorTable code +// (HIRES_BLACK1 / HIRES_VIOLET / HIRES_GREEN / HIRES_WHITE1 etc.) the +// hires bitplane primitives use. `drawColor` stays the legacy palette +// index for the panel-area renderers that haven't moved to the +// bitplane yet. +typedef struct RenderStateT { + FramebufferT *fb; + ColorE fillColor; + ColorE altFillColor; + ColorE drawColor; + uint8_t hiresColor; + uint8_t hiresFill; + uint8_t hiresAltFill; +} RenderStateT; + +void rendererBegin(RenderStateT *state, FramebufferT *fb); +void rendererSetDrawColor(RenderStateT *state, ColorE color); +void rendererSetHiresColor(RenderStateT *state, uint8_t hiresCode); +void rendererSetFillColors(RenderStateT *state, ColorE fill, ColorE altFill); +void rendererSwapFillColors(RenderStateT *state); + +void rendererDrawLine(RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2); + +// Horizontal color-pixel span at color-column space (0..139). +// `xRight` is the rightmost color column, walking leftward for `length+1` +// pixels. Mirrors chunk5's DrawColorSpan ($78E0). Used by the polygon +// scan-line fill (chunk5.s L7826+). +void rendererDrawColorSpan(RenderStateT *state, int16_t xRight, int16_t length, int16_t y); + +// Scan-line fill of an arbitrary polygon. Vertices are color-pixel +// coordinates (xColor 0..139, y 0..191). Mirrors chunk5's L7724+ +// polygon edge rasterizer that walks edges per row, sorts intersections, +// and emits paired DrawColorSpan calls. The current `hiresColor` is +// used as the fill color. +void rendererFillPolygon(RenderStateT *state, const int16_t *xs, const int16_t *ys, int count); +void rendererFillSkyAndGround(RenderStateT *state, int16_t horizonRow); + +// Same idea as `rendererFillSkyAndGround` but the divide line is +// tilted by `bankSin`/`bankCos` (Q1.15: -32767..+32767, the +// `math6502Sin/Cos` output) and centred on `(horizonX, horizonY)`. +// Pixels strictly above the line use `altFillColor` (sky) and below +// use `fillColor` (ground). +void rendererFillTiltedSkyGround(RenderStateT *state, int16_t horizonX, int16_t horizonY, int16_t bankSin, int16_t bankCos); + +#endif diff --git a/port/include/sceneryData.h b/port/include/sceneryData.h new file mode 100644 index 0000000..3df4e9c --- /dev/null +++ b/port/include/sceneryData.h @@ -0,0 +1,68 @@ +// Scenery loader. Reads one of the FS2 region .dsk-equivalent files +// (extracted from the san-inc-pack ProDOS image, see +// downloads/scenery/extracted/) into memory and exposes the raw +// bytestream so `sceneryVm` can interpret it. + +#ifndef SCENERY_DATA_H +#define SCENERY_DATA_H + +#include +#include + + +typedef enum SceneryRegionE { + SCENERY_NONE = 0, + SCENERY_FS2_1, // Built-in WW1 Ace training field (FS2 base disk default) + SCENERY_FS2_1_CHICAGO, // FS2 base disk - Chicago / Meigs Field (LoadSceneryFile1) + SCENERY_FS2_1_LA, // FS2 base disk - Los Angeles (LoadSceneryFile2) + SCENERY_FS2_1_SEATTLE, // FS2 base disk - Seattle (LoadSceneryFile3) + SCENERY_FS2_1_NY, // FS2 base disk - New York / Kennedy (LoadSceneryFile4) + SCENERY_SD1, // Dallas-Ft.Worth, Houston, San Antonio, Brownsville + SCENERY_SD2, // Phoenix, Albuquerque, El Paso + SCENERY_SD3, // San Francisco, Los Angeles, Las Vegas + SCENERY_SD4, // Klamath Falls, Seattle, Great Falls + SCENERY_SD5, // Salt Lake City, Cheyenne, Denver + SCENERY_SD6, // Omaha, Wichita, Kansas City + SCENERY_SD7A, // Washington, Charlotte + SCENERY_SD7B, // Jacksonville, Miami + SCENERY_SD11, // Lake Huron, Detroit + SCENERY_SD13, // Japan - Tokyo, Osaka + SCENERY_SD14A, // Western European Tour - S. UK, N. France + SCENERY_SD14B, // Western European Tour - N. France, S. West Germany + SCENERY_SDS1, // STAR San Francisco & The Bay Area + SCENERY_REGION_COUNT +} SceneryRegionE; + + +typedef struct SceneryDataT { + SceneryRegionE region; + const uint8_t *bytes; // points into a 64K RAM image (the result of running fs2trace's boot mode); whole image is memory-addressable + uint32_t length; // 65536 for RAM images; smaller for the legacy .SD payload path + uint16_t entryOffset; // bytecode entry pointer for ProcessScenery; for RAM images this is `bytes[0xA7E0] | (bytes[0xA7E1] << 8)` (LA7E0); falls back to the legacy 0x7000 for raw .SD loads + const char *name; // human-readable region label + // Raw .SD scenery file (143KB-ish). Used by sceneryVm's HEADER + // demand-load to copy section-specific bytecode over the $79 + // padding at $A848+. Indexed in 256-byte sectors per the chunk5 + // descriptor format: sector N = file offset N*256. + const uint8_t *sceneryFile; + uint32_t sceneryFileSize; +} SceneryDataT; + + +// Load the named region's bytes into memory. Returns true on success +// and populates `*out`. The first scenery payload byte starts at the +// returned `bytes` pointer; the caller should call `sceneryDataFree` +// when done. Searches for the .dsk file in: +// - downloads/scenery/extracted/A2.SD +// - ../downloads/scenery/extracted/A2.SD +// - ../../downloads/scenery/extracted/A2.SD +// - /home/scott/claude/flight/downloads/scenery/extracted/A2.SD +bool sceneryDataLoad(SceneryRegionE region, SceneryDataT *out); + +// Release the buffer allocated by `sceneryDataLoad`. +void sceneryDataFree(SceneryDataT *out); + +// Human-readable name for a region. Always returns a non-NULL string. +const char *sceneryDataRegionName(SceneryRegionE region); + +#endif diff --git a/port/include/sceneryProjection.h b/port/include/sceneryProjection.h new file mode 100644 index 0000000..f7063f2 --- /dev/null +++ b/port/include/sceneryProjection.h @@ -0,0 +1,172 @@ +// 3D vertex pipeline that mirrors FS2's chunk5 polygon math. +// +// The original 6502 implementation lives in chunk5.s: +// +// L7EBC -- per-vertex coord transform (auto-scale + +// rotation matrix) +// ClassifyVertex1/2 -- 6-plane outcode generation +// ProjectV1ToScreen, -- perspective divide -> screen pixel +// ProjectV2ToScreen +// PerspectiveDivide -- shift-and-subtract 16/16 divide +// EmitPrimaryVertex -- append a vertex to the 60-slot pool +// +// This port keeps the same precision (signed 16-bit world deltas, 8-bit +// rotation matrix, 16/16 perspective divide) and the same outcode bit +// assignments, so the algorithmic results match the 6502 game value- +// for-value modulo the LSB of MultiplyXY's 7x7 truncation. +// +// Outcode bit layout (from ClassifyVertex2 at chunk5 line 2673): +// bit 7 ($80) -- z negative (behind camera) +// bit 6 ($40) -- x + z < 0 (right of frustum) +// bit 5 ($20) -- z - x < 0 (left of frustum) +// bit 4 ($10) -- y + z < 0 (below frustum) +// bit 3 ($08) -- z - y < 0 (above frustum) + +#ifndef SCENERY_PROJECTION_H +#define SCENERY_PROJECTION_H + +#include +#include + +#define SCENERY_OUTCODE_BEHIND 0x80 +#define SCENERY_OUTCODE_RIGHT 0x40 +#define SCENERY_OUTCODE_LEFT 0x20 +#define SCENERY_OUTCODE_BOTTOM 0x10 +#define SCENERY_OUTCODE_TOP 0x08 + +#define SCENERY_VERTEX_POOL_CAP 60 + + +// One slot in the primary vertex pool. Six bytes are stored per vertex +// in chunk5 (six parallel arrays at L0AB8/AF8/B38/B78/BB8/BF8); we +// pack them into a single struct for cache behaviour. Components are +// camera-space x/y/z in 16-bit signed, plus a Cohen-Sutherland outcode. +typedef struct SceneryVertexT { + int16_t x; // L0AB8/L0AF8 = lo/hi of camera-space x + int16_t y; // L0B38/L0B78 = lo/hi of camera-space y + int16_t z; // L0BB8/L0BF8 = lo/hi of camera-space z + uint8_t outcode; // 6-plane mask (see SCENERY_OUTCODE_*) +} SceneryVertexT; + + +// Per-frame projection state. Mirrors the chunk5 zero-page variables +// the polygon pipeline reads: +// $66/$67 = camera world X (eyepoint) +// $6A/$6B = camera world Z +// $79/$7B/$7D = first row of the 2x3 rotation matrix (XZ -> camX) +// $85/$87/$89 = second row of the 2x3 rotation matrix (XZ -> camY/Z) +// $4A/$4D/$50 = section-base contribution to camX/camY/camZ +// (set by the coord-frame opcode $0D, accounts for the +// vertical/altitude term) +// $2F = current zoom-detail counter (auto-scale shift count) +typedef struct SceneryProjStateT { + int16_t camX; // $66/$67 + int16_t camZ; // $6A/$6B + int8_t matRow1[3]; // $79, $7B, $7D + int8_t matRow2[3]; // $85, $87, $89 + int16_t baseX; // $4A + int16_t baseY; // $4D + int16_t baseZ; // $50 + uint8_t zoomShift; // $2F (init $40, decrements as we shift) +} SceneryProjStateT; + + +// Two "current" vertex slots, mirroring chunk5's $CB..$D2 (vertex 1) +// and $D4..$DB (vertex 2). The vertex-emit family writes into these +// before deciding whether to project, classify, or push into the pool. +typedef struct SceneryCurrentT { + SceneryVertexT v1; // $CB..$D0 (+ $CA outcode, $D1/$D2 screen) + SceneryVertexT v2; // $D4..$D9 (+ $D3 outcode, $DA/$DB screen) + int16_t v1ScreenX; // $D1 + int16_t v1ScreenY; // $D2 + int16_t v2ScreenX; // $DA + int16_t v2ScreenY; // $DB + // Running coord accumulators that L7EBC writes into ($18/$1A, + // $1B/$1D, $1E/$20). Three signed 16-bit values plus a sign- + // extension byte each; we collapse to int32 for the + // intermediate sum and snap back to int16 on store. + int16_t accX; + int16_t accY; + int16_t accZ; + // Polygon outcode AND-accumulator at $D3 (zero -> all vertices + // share an offscreen plane, polygon culled). + uint8_t polygonOutcode; + // Vertex-pool head ($B5). + uint8_t poolCount; +} SceneryCurrentT; + + +typedef struct SceneryPipelineT { + SceneryProjStateT proj; + SceneryCurrentT cur; + SceneryVertexT pool[SCENERY_VERTEX_POOL_CAP]; +} SceneryPipelineT; + + +// Reset the vertex pool and outcode accumulator. Call at the top of +// each scenery frame and whenever opcode $2F (SceneryOpResetState) +// fires. +void sceneryPipelineReset(SceneryPipelineT *pipe); + + +// Set the camera world position and the 2x3 rotation matrix. Called +// by the world driver once per frame, before sceneryRun walks the +// stream. Matrix entries are signed 8-bit (see chunk5 $79..$89). +void sceneryPipelineSetCamera(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ); +void sceneryPipelineSetMatrix(SceneryPipelineT *pipe, const int8_t row1[3], const int8_t row2[3]); +void sceneryPipelineSetBase(SceneryPipelineT *pipe, int16_t bx, int16_t by, int16_t bz); + + +// L7EBC: read 4 stream bytes (XZ pair, signed 16-bit each), subtract +// camera XZ, auto-scale, multiply by the 2x3 rotation matrix, add to +// section base, store into target slot ($CB..$D0 or $D4..$D9). +// +// Returns the number of stream bytes consumed (always 4), so the +// caller can advance. +int sceneryProjectStreamVertex(SceneryPipelineT *pipe, const uint8_t *streamPlus1, SceneryVertexT *outSlot); + + +// Same math as sceneryProjectStreamVertex but takes the world XZ pair +// directly (caller already has decoded values). Used by the world +// driver to push hardcoded vertex data through the same pipeline a +// real scenery byte stream would. +void sceneryProjectXZ(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ, SceneryVertexT *outSlot); + + +// ClassifyVertex2-style outcode for a camera-space vertex. Pure +// function; reads only the vertex itself. +uint8_t sceneryClassifyVertex(const SceneryVertexT *v); + + +// ProjectV2ToScreen: divide camera x/y by camera z (with chunk5's +// fixed-point shift-and-subtract divide) and bias to screen pixels. +// Returns false if the vertex is behind the camera (cz <= 0). +bool sceneryProjectVertexToScreen(const SceneryVertexT *v, int16_t *outX, int16_t *outY); + + +// EmitPrimaryVertex: append `slot` to the pool, AND its outcode into +// the polygon accumulator. No-op if the pool is full (chunk5 caps at +// 60 too -- $cpy #$3C / bcs). +void sceneryEmitPrimary(SceneryPipelineT *pipe, const SceneryVertexT *slot); + + +// 4-pass Sutherland-Hodgman 3D frustum clipper. Mirrors chunk5's +// PolygonScanFillSetup + PolygonClipTopPass + PolygonClipRightPass + +// PolygonClipBottomPass (src/chunk5.s:2884+). Operates on camera-space +// XYZ vertices, intersects each clip plane at the frustum half-spaces +// (Left: Z-X=0, Top: Z-Y=0, Right: Z+X=0, Bottom: Z+Y=0), introducing +// new vertices at the plane crossings. Ping-pongs between two arrays. +// +// On entry: `in`/`out` are arrays of capacity `cap`, `inCount` is the +// initial vertex count. +// +// Returns the final clipped vertex count, with the output in whichever +// of the two arrays the last pass wrote to (signalled via the boolean +// returned in `*outIsIn`: true means the final result is in `in`, +// false means it's in `out`). +// +// Returns 0 if the polygon was fully clipped away. +int sceneryClipPolygon3D(SceneryVertexT *in, SceneryVertexT *out, int inCount, int cap, bool *outIsIn); + + +#endif diff --git a/port/include/sceneryStationsData.h b/port/include/sceneryStationsData.h new file mode 100644 index 0000000..9a7c975 --- /dev/null +++ b/port/include/sceneryStationsData.h @@ -0,0 +1,719 @@ +// Generated by tools/extractstations --c-output. Do not edit. +// Source: A2.SD* scenery files. 695 unique stations. + +#ifndef SCENERY_STATIONS_DATA_H +#define SCENERY_STATIONS_DATA_H + +#include + +typedef struct StationDataT { + char type; // 'A'=ADF, 'N'=NAV, 'C'=COM + uint16_t freq; // BCD-packed (NAV/COM) or BCD pair + high digit (ADF) + int32_t x; // FS2 scenery units, +X = east + int32_t y; // +Y = north + int32_t z; // NAV only (altitude); 0 for ADF/COM + char name[16]; // COM airport name; "" otherwise + char region; // ASCII digit of source A2.SD* file +} StationDataT; + +static const StationDataT kSceneryStations[] = { + { 'A', 0x0200, -1376256, 1792, 0, "", '2' }, + { 'A', 0x0200, -1310727, 0, 0, "", '2' }, + { 'A', 0x0200, -917504, -1280, 0, "", '2' }, + { 'A', 0x0200, -65536, 1792, 0, "", '2' }, + { 'A', 0x0200, -7, 0, 0, "", '2' }, + { 'A', 0x0200, 1245184, 1793, 0, "", '2' }, + { 'A', 0x0200, 1310713, 1, 0, "", '2' }, + { 'A', 0x0200, 3407827, 768, 0, "", '2' }, + { 'A', 0x0200, 3407842, 1280, 0, "", '2' }, + { 'A', 0x0200, 3538944, 768, 0, "", '2' }, + { 'A', 0x0200, 5439422, 4864, 0, "", '2' }, + { 'A', 0x0201, 703232, -1185536, 0, "", '2' }, + { 'A', 0x0202, 10756, 133062, 0, "", '2' }, + { 'A', 0x0205, 3873792, 228352, 0, "", '2' }, + { 'A', 0x0206, -2537984, 1285888, 0, "", '2' }, + { 'A', 0x0206, -2536960, 1287936, 0, "", '2' }, + { 'A', 0x0206, -1967616, -428032, 0, "", '2' }, + { 'A', 0x0206, -620544, -1165056, 0, "", '2' }, + { 'A', 0x0207, -5711312, 214, 0, "", '2' }, + { 'A', 0x0207, -5073664, 214, 0, "", '2' }, + { 'A', 0x0207, -4007792, 216, 0, "", '2' }, + { 'A', 0x0207, -2013440, 761600, 0, "", '2' }, + { 'A', 0x0207, -1658272, 216, 0, "", '2' }, + { 'A', 0x0207, -592960, 20, 0, "", '2' }, + { 'A', 0x0209, -527104, -239872, 0, "", '2' }, + { 'A', 0x0209, 544512, -974848, 0, "", '2' }, + { 'A', 0x0209, 972800, -1450240, 0, "", '2' }, + { 'A', 0x0212, -1262848, -275968, 0, "", '2' }, + { 'A', 0x0212, -1228517, 151812, 0, "", '2' }, + { 'A', 0x0212, -1115136, 187392, 0, "", '2' }, + { 'A', 0x0212, -932864, -1087488, 0, "", '2' }, + { 'A', 0x0212, -645632, -314624, 0, "", '2' }, + { 'A', 0x0212, 554522, 2164858, 0, "", '2' }, + { 'A', 0x0216, -2541824, 1247232, 0, "", '2' }, + { 'A', 0x0216, 1373440, 247808, 0, "", '2' }, + { 'A', 0x0216, 4210176, 365824, 0, "", '2' }, + { 'A', 0x0218, -1173498, -6026491, 0, "", '2' }, + { 'A', 0x0219, 3891200, 196864, 0, "", '2' }, + { 'A', 0x0223, -912640, -801280, 0, "", '2' }, + { 'A', 0x0224, -2156800, -452096, 0, "", '2' }, + { 'A', 0x0224, 1495, 185600, 0, "", '2' }, + { 'A', 0x0224, 271401, 451, 0, "", '2' }, + { 'A', 0x0225, 4189952, 609536, 0, "", '2' }, + { 'A', 0x0227, -816640, -157440, 0, "", '2' }, + { 'A', 0x0228, 6815773, 550912, 0, "", '2' }, + { 'A', 0x0229, 4067328, 306176, 0, "", '2' }, + { 'A', 0x0231, -347583, 2751956, 0, "", '2' }, + { 'A', 0x0232, 1639987, 143919, 0, "", '2' }, + { 'A', 0x0232, 3278387, 471811, 0, "", '2' }, + { 'A', 0x0233, -918207, 3278765, 0, "", '2' }, + { 'A', 0x0233, 10771, -7798752, 0, "", '2' }, + { 'A', 0x0233, 91201, 1639115, 0, "", '2' }, + { 'A', 0x0233, 1115392, -1417472, 0, "", '2' }, + { 'A', 0x0233, 1179648, 183296, 0, "", '2' }, + { 'A', 0x0233, 1438785, 4262442, 0, "", '2' }, + { 'A', 0x0233, 2688819, 3342642, 0, "", '2' }, + { 'A', 0x0233, 3342643, 5704456, 0, "", '2' }, + { 'A', 0x0233, 3343922, 406017, 0, "", '2' }, + { 'A', 0x0233, 3344179, 3090694, 0, "", '2' }, + { 'A', 0x0234, 356161, 4260192, 0, "", '2' }, + { 'A', 0x0234, 3887872, 425984, 0, "", '2' }, + { 'A', 0x0236, 4179968, 427776, 0, "", '2' }, + { 'A', 0x0238, 1265408, 272128, 0, "", '2' }, + { 'A', 0x0240, 4133376, 508928, 0, "", '2' }, + { 'A', 0x0241, 323073, 8513, 0, "", '2' }, + { 'A', 0x0241, 491223, -2232255, 0, "", '2' }, + { 'A', 0x0242, -4456794, 5706750, 0, "", '2' }, + { 'A', 0x0242, -1624832, 884736, 0, "", '2' }, + { 'A', 0x0242, 780, 213504, 0, "", '2' }, + { 'A', 0x0242, 893, 213504, 0, "", '2' }, + { 'A', 0x0242, 832512, -1505536, 0, "", '2' }, + { 'A', 0x0245, 445184, -991744, 0, "", '2' }, + { 'A', 0x0246, -910336, -377088, 0, "", '2' }, + { 'A', 0x0246, 302144, 4325094, 0, "", '2' }, + { 'A', 0x0248, -753664, -754944, 0, "", '2' }, + { 'A', 0x0248, 342337, 1770071, 0, "", '2' }, + { 'A', 0x0249, -703232, -1100800, 0, "", '2' }, + { 'A', 0x0250, 6291792, -5937048, 0, "", '2' }, + { 'A', 0x0251, -604672, -157440, 0, "", '2' }, + { 'A', 0x0251, 4145664, 487680, 0, "", '2' }, + { 'A', 0x0253, 147777, 2163421, 0, "", '2' }, + { 'A', 0x0254, -2603776, 381696, 0, "", '2' }, + { 'A', 0x0254, -1177600, 525312, 0, "", '2' }, + { 'A', 0x0255, -2052864, -512000, 0, "", '2' }, + { 'A', 0x0256, -2605312, 1155840, 0, "", '2' }, + { 'A', 0x0256, -391680, -865536, 0, "", '2' }, + { 'A', 0x0257, -2630656, -275200, 0, "", '2' }, + { 'A', 0x0257, -2629632, -276992, 0, "", '2' }, + { 'A', 0x0257, 767232, -1292800, 0, "", '2' }, + { 'A', 0x0257, 767744, -1287424, 0, "", '2' }, + { 'A', 0x0257, 1239552, 271616, 0, "", '2' }, + { 'A', 0x0257, 1845531, -43200, 0, "", '2' }, + { 'A', 0x0258, 171329, 4260092, 0, "", '2' }, + { 'A', 0x0260, -2582016, 120320, 0, "", '2' }, + { 'A', 0x0260, -2573568, 1380096, 0, "", '2' }, + { 'A', 0x0260, -1756928, 1162496, 0, "", '2' }, + { 'A', 0x0260, 756736, -1669376, 0, "", '2' }, + { 'A', 0x0260, 4252416, 309760, 0, "", '2' }, + { 'A', 0x0263, -631808, -877568, 0, "", '2' }, + { 'A', 0x0264, -2068736, 1218816, 0, "", '2' }, + { 'A', 0x0265, 4179712, 444160, 0, "", '2' }, + { 'A', 0x0266, -896256, -739072, 0, "", '2' }, + { 'A', 0x0266, 808704, -1560832, 0, "", '2' }, + { 'A', 0x0269, -1763072, 1079808, 0, "", '2' }, + { 'A', 0x0269, -696576, -1111552, 0, "", '2' }, + { 'A', 0x0269, 612608, -1125888, 0, "", '2' }, + { 'A', 0x0270, 4204032, 424192, 0, "", '2' }, + { 'A', 0x0273, 4206377, 787078, 0, "", '2' }, + { 'A', 0x0274, -2547456, 1280256, 0, "", '2' }, + { 'A', 0x0274, -2176768, 835840, 0, "", '2' }, + { 'A', 0x0274, -1905920, -454656, 0, "", '2' }, + { 'A', 0x0275, -1172992, -295936, 0, "", '2' }, + { 'A', 0x0275, -1126400, -750848, 0, "", '2' }, + { 'A', 0x0275, 804864, -1357056, 0, "", '2' }, + { 'A', 0x0275, 805120, -1356800, 0, "", '2' }, + { 'A', 0x0275, 1230336, 174080, 0, "", '2' }, + { 'A', 0x0275, 3965696, 103680, 0, "", '2' }, + { 'A', 0x0278, -2425088, 106240, 0, "", '2' }, + { 'A', 0x0278, 593152, -1259776, 0, "", '2' }, + { 'A', 0x0280, -2119168, 958464, 0, "", '2' }, + { 'A', 0x0281, -1103616, -1126912, 0, "", '2' }, + { 'A', 0x0281, -784128, -194304, 0, "", '2' }, + { 'A', 0x0281, -423680, -150784, 0, "", '2' }, + { 'A', 0x0282, -2665728, -179456, 0, "", '2' }, + { 'A', 0x0283, -1111808, 118784, 0, "", '2' }, + { 'A', 0x0284, -1421568, -259328, 0, "", '2' }, + { 'A', 0x0284, -597248, -105472, 0, "", '2' }, + { 'A', 0x0286, -2917376, 200192, 0, "", '2' }, + { 'A', 0x0287, -1922560, 1153792, 0, "", '2' }, + { 'A', 0x0287, -1184000, 296960, 0, "", '2' }, + { 'A', 0x0287, -785920, -760064, 0, "", '2' }, + { 'A', 0x0288, -2874368, -69120, 0, "", '2' }, + { 'A', 0x0290, -1699584, 344832, 0, "", '2' }, + { 'A', 0x0292, 316993, 4260575, 0, "", '2' }, + { 'A', 0x0293, -1735424, 1114112, 0, "", '2' }, + { 'A', 0x0293, -637696, -289280, 0, "", '2' }, + { 'A', 0x0296, -2281472, 937472, 0, "", '2' }, + { 'A', 0x0296, -940032, -159488, 0, "", '2' }, + { 'A', 0x0296, -619008, -1166592, 0, "", '2' }, + { 'A', 0x0297, 4390243, 105734, 0, "", '2' }, + { 'A', 0x0301, -2622720, 1429760, 0, "", '2' }, + { 'A', 0x0302, -353792, -71168, 0, "", '2' }, + { 'A', 0x0302, 464462, 2424854, 0, "", '2' }, + { 'A', 0x0302, 558954, 2359318, 0, "", '2' }, + { 'A', 0x0306, 681216, -1039872, 0, "", '2' }, + { 'A', 0x0308, -1932544, 1020672, 0, "", '2' }, + { 'A', 0x0308, -921856, -144128, 0, "", '2' }, + { 'A', 0x0308, -681728, -998144, 0, "", '2' }, + { 'A', 0x0308, 4388541, 309254, 0, "", '2' }, + { 'A', 0x0310, 362801, 2359341, 0, "", '2' }, + { 'A', 0x0313, 779520, -1246464, 0, "", '2' }, + { 'A', 0x0317, -1778944, 941312, 0, "", '2' }, + { 'A', 0x0317, -1271808, 217600, 0, "", '2' }, + { 'A', 0x0317, -555520, 66048, 0, "", '2' }, + { 'A', 0x0322, 844288, -1545984, 0, "", '2' }, + { 'A', 0x0325, 4004096, 277248, 0, "", '2' }, + { 'A', 0x0326, -2397952, -48896, 0, "", '2' }, + { 'A', 0x0326, -2168832, -471296, 0, "", '2' }, + { 'A', 0x0326, -1309696, -781056, 0, "", '2' }, + { 'A', 0x0327, -2880768, 152064, 0, "", '2' }, + { 'A', 0x0329, -1741568, 834560, 0, "", '2' }, + { 'A', 0x0329, -1392640, -196352, 0, "", '2' }, + { 'A', 0x0329, -1047040, -1112320, 0, "", '2' }, + { 'A', 0x0329, -1002496, -349184, 0, "", '2' }, + { 'A', 0x0329, -423168, -108032, 0, "", '2' }, + { 'A', 0x0329, 695040, -1272064, 0, "", '2' }, + { 'A', 0x0329, 1280000, 296448, 0, "", '2' }, + { 'A', 0x0330, 4158976, 477952, 0, "", '2' }, + { 'A', 0x0332, -2834111, 3403062, 0, "", '2' }, + { 'A', 0x0332, -2600192, 1079552, 0, "", '2' }, + { 'A', 0x0332, -531712, -1001984, 0, "", '2' }, + { 'A', 0x0332, 54784, 190464, 0, "", '2' }, + { 'A', 0x0332, 642304, -1055488, 0, "", '2' }, + { 'A', 0x0332, 688384, -1692160, 0, "", '2' }, + { 'A', 0x0332, 688640, -1691904, 0, "", '2' }, + { 'A', 0x0332, 4195379, -4128681, 0, "", '2' }, + { 'A', 0x0333, 57921, 4322859, 0, "", '2' }, + { 'A', 0x0333, 67330, -3866545, 0, "", '2' }, + { 'A', 0x0333, 3344435, 3090694, 0, "", '2' }, + { 'A', 0x0334, -2113792, -324864, 0, "", '2' }, + { 'A', 0x0335, -864000, -438528, 0, "", '2' }, + { 'A', 0x0335, -798208, -26368, 0, "", '2' }, + { 'A', 0x0335, -318976, -374272, 0, "", '2' }, + { 'A', 0x0335, 652800, -1216768, 0, "", '2' }, + { 'A', 0x0338, -2786304, 500736, 0, "", '2' }, + { 'A', 0x0338, -1767680, 716544, 0, "", '2' }, + { 'A', 0x0338, -1298176, 126208, 0, "", '2' }, + { 'A', 0x0338, 414464, -1001472, 0, "", '2' }, + { 'A', 0x0340, 3892224, 220928, 0, "", '2' }, + { 'A', 0x0341, -1241088, -880896, 0, "", '2' }, + { 'A', 0x0341, -1178112, -190720, 0, "", '2' }, + { 'A', 0x0341, -1040640, 37376, 0, "", '2' }, + { 'A', 0x0341, -390400, -345600, 0, "", '2' }, + { 'A', 0x0341, 178180, 295233, 0, "", '2' }, + { 'A', 0x0341, 411908, 261441, 0, "", '2' }, + { 'A', 0x0342, -6758613, -5095416, 0, "", '2' }, + { 'A', 0x0343, -1122560, -1198336, 0, "", '2' }, + { 'A', 0x0344, -2722048, 70912, 0, "", '2' }, + { 'A', 0x0344, -743424, -842240, 0, "", '2' }, + { 'A', 0x0344, -732928, -1191936, 0, "", '2' }, + { 'A', 0x0344, -530688, -992256, 0, "", '2' }, + { 'A', 0x0344, 5632, 125440, 0, "", '2' }, + { 'A', 0x0344, 641536, -1034496, 0, "", '2' }, + { 'A', 0x0344, 4112896, 336384, 0, "", '2' }, + { 'A', 0x0347, -1723136, 1152512, 0, "", '2' }, + { 'A', 0x0348, -2572032, 1271296, 0, "", '2' }, + { 'A', 0x0348, -2570752, 1273600, 0, "", '2' }, + { 'A', 0x0349, 3741184, 257792, 0, "", '2' }, + { 'A', 0x0350, -849664, -1141248, 0, "", '2' }, + { 'A', 0x0350, 4225536, 500224, 0, "", '2' }, + { 'A', 0x0351, 392257, 4260893, 0, "", '2' }, + { 'A', 0x0352, 345856, -991232, 0, "", '2' }, + { 'A', 0x0353, -2501632, 1271552, 0, "", '2' }, + { 'A', 0x0353, -2500352, 1272576, 0, "", '2' }, + { 'A', 0x0353, -1587968, 950784, 0, "", '2' }, + { 'A', 0x0353, -971520, -1362944, 0, "", '2' }, + { 'A', 0x0354, 155201, 4262345, 0, "", '2' }, + { 'A', 0x0354, 3939840, 477696, 0, "", '2' }, + { 'A', 0x0356, -916480, -216320, 0, "", '2' }, + { 'A', 0x0356, -656640, -937984, 0, "", '2' }, + { 'A', 0x0356, 828160, -1444096, 0, "", '2' }, + { 'A', 0x0357, 4180992, 457728, 0, "", '2' }, + { 'A', 0x0359, -2670592, -252672, 0, "", '2' }, + { 'A', 0x0359, -1174784, -347904, 0, "", '2' }, + { 'A', 0x0360, 3980544, 328192, 0, "", '2' }, + { 'A', 0x0361, 1138432, -1433856, 0, "", '2' }, + { 'A', 0x0362, -2507008, 1290496, 0, "", '2' }, + { 'A', 0x0365, -1997824, 81408, 0, "", '2' }, + { 'A', 0x0365, -1154048, -247296, 0, "", '2' }, + { 'A', 0x0365, -1082112, 43776, 0, "", '2' }, + { 'A', 0x0365, -568064, -281856, 0, "", '2' }, + { 'A', 0x0365, -305152, -347904, 0, "", '2' }, + { 'A', 0x0365, 822272, -1543168, 0, "", '2' }, + { 'A', 0x0366, 4222208, 553728, 0, "", '2' }, + { 'A', 0x0368, -1540352, 334080, 0, "", '2' }, + { 'A', 0x0368, -935424, 8192, 0, "", '2' }, + { 'A', 0x0368, -556032, -1089024, 0, "", '2' }, + { 'A', 0x0368, 3903232, 269312, 0, "", '2' }, + { 'A', 0x0369, 404736, -966144, 0, "", '2' }, + { 'A', 0x0371, -1716992, 1014528, 0, "", '2' }, + { 'A', 0x0371, -1173248, -958976, 0, "", '2' }, + { 'A', 0x0371, -841728, -328704, 0, "", '2' }, + { 'A', 0x0373, 1199104, 163584, 0, "", '2' }, + { 'A', 0x0373, 4235008, 414464, 0, "", '2' }, + { 'A', 0x0374, -2839552, 231168, 0, "", '2' }, + { 'A', 0x0374, -2611968, -372736, 0, "", '2' }, + { 'A', 0x0374, -2162944, 1011456, 0, "", '2' }, + { 'A', 0x0374, -2153728, 1018112, 0, "", '2' }, + { 'A', 0x0375, -1474560, 446720, 0, "", '2' }, + { 'A', 0x0375, -911360, -1519360, 0, "", '2' }, + { 'A', 0x0375, -679424, -882688, 0, "", '2' }, + { 'A', 0x0375, -444160, -29952, 0, "", '2' }, + { 'A', 0x0375, 750336, -1234432, 0, "", '2' }, + { 'A', 0x0375, 752128, -1234432, 0, "", '2' }, + { 'A', 0x0377, -1201152, -258304, 0, "", '2' }, + { 'A', 0x0378, -2694400, -268800, 0, "", '2' }, + { 'A', 0x0378, -2693376, -269056, 0, "", '2' }, + { 'A', 0x0378, 2818185, 4915240, 0, "", '2' }, + { 'A', 0x0379, -1861120, 809472, 0, "", '2' }, + { 'A', 0x0379, -568832, -243200, 0, "", '2' }, + { 'A', 0x0379, 397824, -1064192, 0, "", '2' }, + { 'A', 0x0379, 4171008, 447232, 0, "", '2' }, + { 'A', 0x0380, -1364736, 580608, 0, "", '2' }, + { 'A', 0x0380, -1074688, -39168, 0, "", '2' }, + { 'A', 0x0380, 3994624, 246528, 0, "", '2' }, + { 'A', 0x0382, -910592, -1298176, 0, "", '2' }, + { 'A', 0x0382, 418113, 4260677, 0, "", '2' }, + { 'A', 0x0382, 709632, -1367808, 0, "", '2' }, + { 'A', 0x0383, -748288, -229888, 0, "", '2' }, + { 'A', 0x0383, -410624, -34560, 0, "", '2' }, + { 'A', 0x0385, -2466816, 1415424, 0, "", '2' }, + { 'A', 0x0385, -1036032, -835328, 0, "", '2' }, + { 'A', 0x0385, -823808, -1045248, 0, "", '2' }, + { 'A', 0x0385, -465152, -1021952, 0, "", '2' }, + { 'A', 0x0386, -1766144, 934144, 0, "", '2' }, + { 'A', 0x0386, -1145344, 40192, 0, "", '2' }, + { 'A', 0x0386, -862976, -153088, 0, "", '2' }, + { 'A', 0x0386, 3899904, 213248, 0, "", '2' }, + { 'A', 0x0387, -1396224, 170496, 0, "", '2' }, + { 'A', 0x0387, 4013568, 468992, 0, "", '2' }, + { 'A', 0x0388, 1265664, 314368, 0, "", '2' }, + { 'A', 0x0389, -1563392, 1115648, 0, "", '2' }, + { 'A', 0x0389, -1064704, -247296, 0, "", '2' }, + { 'A', 0x0389, 4162304, 561920, 0, "", '2' }, + { 'A', 0x0391, -855040, -1274880, 0, "", '2' }, + { 'A', 0x0391, -718080, -83712, 0, "", '2' }, + { 'A', 0x0391, -588800, -909312, 0, "", '2' }, + { 'A', 0x0391, -335616, -268288, 0, "", '2' }, + { 'A', 0x0392, -1941504, 1259776, 0, "", '2' }, + { 'A', 0x0392, -1732096, 489472, 0, "", '2' }, + { 'A', 0x0392, 285249, 1246109, 0, "", '2' }, + { 'A', 0x0392, 4202496, 373504, 0, "", '2' }, + { 'A', 0x0394, 3920384, 110592, 0, "", '2' }, + { 'A', 0x0395, -1147392, -198912, 0, "", '2' }, + { 'A', 0x0396, -2273792, 1298944, 0, "", '2' }, + { 'A', 0x0396, -741632, -846080, 0, "", '2' }, + { 'A', 0x0397, -2610432, -272896, 0, "", '2' }, + { 'A', 0x0397, -1700542, -1382373, 0, "", '2' }, + { 'A', 0x0397, 3991552, 319744, 0, "", '2' }, + { 'A', 0x0399, 302657, 4260907, 0, "", '2' }, + { 'A', 0x0400, -1089536, -2304, 0, "", '2' }, + { 'A', 0x0400, -1076992, -370176, 0, "", '2' }, + { 'A', 0x0400, -818432, -898560, 0, "", '2' }, + { 'A', 0x0400, -663040, -306176, 0, "", '2' }, + { 'A', 0x0400, 3882240, 395776, 0, "", '2' }, + { 'A', 0x0401, 4166400, 453632, 0, "", '2' }, + { 'A', 0x0403, 328708, 198404, 0, "", '2' }, + { 'A', 0x0404, -2720000, 226048, 0, "", '2' }, + { 'A', 0x0406, 590600, 199427, 0, "", '2' }, + { 'A', 0x0407, -2810605, 11, 0, "", '2' }, + { 'A', 0x0407, -2028911, 14, 0, "", '2' }, + { 'A', 0x0407, -1868335, 14, 0, "", '2' }, + { 'A', 0x0407, -1852975, 14, 0, "", '2' }, + { 'A', 0x0407, -1055744, -342016, 0, "", '2' }, + { 'A', 0x0407, -463536, 247, 0, "", '2' }, + { 'A', 0x0407, -431616, -914944, 0, "", '2' }, + { 'A', 0x0407, 28672, -3840, 0, "", '2' }, + { 'A', 0x0408, -688128, -1073920, 0, "", '2' }, + { 'A', 0x0408, -687360, -1068544, 0, "", '2' }, + { 'A', 0x0408, 709632, -1217280, 0, "", '2' }, + { 'A', 0x0410, -1953536, 946944, 0, "", '2' }, + { 'A', 0x0410, -970752, -775424, 0, "", '2' }, + { 'A', 0x0411, -2529536, 909312, 0, "", '2' }, + { 'A', 0x0411, -269056, -45568, 0, "", '2' }, + { 'A', 0x0414, -1124352, 377856, 0, "", '2' }, + { 'A', 0x0414, -491008, -870400, 0, "", '2' }, + { 'A', 0x0414, -384256, -270848, 0, "", '2' }, + { 'A', 0x0418, 2324736, 7340042, 0, "", '2' }, + { 'A', 0x0419, 2882633, -3081231, 0, "", '2' }, + { 'A', 0x0424, 1287, 305152, 0, "", '2' }, + { 'A', 0x0424, 1295, 29184, 0, "", '2' }, + { 'A', 0x0424, 1295, 261376, 0, "", '2' }, + { 'A', 0x0424, 1314, 581376, 0, "", '2' }, + { 'A', 0x0424, 1347, -991488, 0, "", '2' }, + { 'A', 0x0424, 1349, 222976, 0, "", '2' }, + { 'A', 0x0424, 1356, 418048, 0, "", '2' }, + { 'A', 0x0424, 1363, 400384, 0, "", '2' }, + { 'A', 0x0424, 1366, 440832, 0, "", '2' }, + { 'A', 0x0424, 1369, 730880, 0, "", '2' }, + { 'A', 0x0424, 1378, 366592, 0, "", '2' }, + { 'A', 0x0424, 1379, 165120, 0, "", '2' }, + { 'A', 0x0424, 1388, 78336, 0, "", '2' }, + { 'A', 0x0424, 1395, 92928, 0, "", '2' }, + { 'A', 0x0424, 1434, 390144, 0, "", '2' }, + { 'A', 0x0424, 1436, 394496, 0, "", '2' }, + { 'A', 0x0424, 1437, 411136, 0, "", '2' }, + { 'A', 0x0424, 1443, 251904, 0, "", '2' }, + { 'A', 0x0424, 1449, -887040, 0, "", '2' }, + { 'A', 0x0424, 1449, 87808, 0, "", '2' }, + { 'A', 0x0424, 1453, 574464, 0, "", '2' }, + { 'A', 0x0424, 1456, 574464, 0, "", '2' }, + { 'A', 0x0424, 1458, -1061888, 0, "", '2' }, + { 'A', 0x0424, 1469, -973312, 0, "", '2' }, + { 'A', 0x0424, 1475, 135424, 0, "", '2' }, + { 'A', 0x0424, 1480, 338944, 0, "", '2' }, + { 'A', 0x0424, 1483, 366848, 0, "", '2' }, + { 'A', 0x0424, 1487, 110848, 0, "", '2' }, + { 'A', 0x0424, 1496, 185600, 0, "", '2' }, + { 'A', 0x0424, 1512, 270848, 0, "", '2' }, + { 'A', 0x0424, 1518, 337408, 0, "", '2' }, + { 'A', 0x0424, 1519, 337408, 0, "", '2' }, + { 'A', 0x0424, 1522, 195072, 0, "", '2' }, + { 'A', 0x0424, 1532, 341248, 0, "", '2' }, + { 'A', 0x0424, 4198, -269312, 0, "", '2' }, + { 'A', 0x0424, 54238, 263424, 0, "", '2' }, + { 'A', 0x0424, 66831, 67840, 0, "", '2' }, + { 'A', 0x0428, 2037261, 4928276, 0, "", '2' }, + { 'A', 0x0428, 6815762, 550912, 0, "", '2' }, + { 'A', 0x0431, -64706, -454401, 0, "", '2' }, + { 'A', 0x0431, -64438, -243201, 0, "", '2' }, + { 'A', 0x0432, -485823, 3344147, 0, "", '2' }, + { 'A', 0x0432, -171455, 4195216, 0, "", '2' }, + { 'A', 0x0433, -176319, 4320737, 0, "", '2' }, + { 'A', 0x0433, 41, 0, 0, "", '2' }, + { 'A', 0x0433, 12841, 3343411, 0, "", '2' }, + { 'A', 0x0433, 78377, 3081267, 0, "", '2' }, + { 'A', 0x0433, 78377, 3277363, 0, "", '2' }, + { 'A', 0x0433, 143913, 3278131, 0, "", '2' }, + { 'A', 0x0433, 347201, 4261103, 0, "", '2' }, + { 'A', 0x0433, 505665, 4262336, 0, "", '2' }, + { 'A', 0x0433, 792617, 3288832, 0, "", '2' }, + { 'A', 0x0433, 1710377, 6948982, 0, "", '2' }, + { 'A', 0x0433, 3086633, 792, 0, "", '2' }, + { 'A', 0x0433, 3288873, 209666, 0, "", '2' }, + { 'A', 0x0433, 3288873, 340738, 0, "", '2' }, + { 'A', 0x0433, 3343666, 471814, 0, "", '2' }, + { 'A', 0x0433, 4206377, -4568893, 0, "", '2' }, + { 'A', 0x0433, 4597033, -209656, 0, "", '2' }, + { 'A', 0x0433, 4597033, -186104, 0, "", '2' }, + { 'A', 0x0442, -131967, 344578, 0, "", '2' }, + { 'A', 0x0464, 358977, 4260844, 0, "", '2' }, + { 'A', 0x0473, -2880190, 308997, 0, "", '2' }, + { 'A', 0x0473, 8327746, -15358, 0, "", '2' }, + { 'A', 0x0476, -3496608, -3091675, 0, "", '2' }, + { 'A', 0x0497, 431169, 4261115, 0, "", '2' }, + { 'A', 0x0498, -6814684, 4, 0, "", '2' }, + { 'A', 0x0501, 130817, 196609, 0, "", '2' }, + { 'A', 0x0501, 132353, 393478, 0, "", '2' }, + { 'A', 0x0502, 67074, 459270, 0, "", '2' }, + { 'A', 0x0503, 525573, 264708, 0, "", '2' }, + { 'A', 0x0505, 460294, 526087, 0, "", '2' }, + { 'A', 0x0505, 656392, 265220, 0, "", '2' }, + { 'A', 0x0511, 563, 4357, 0, "", '2' }, + { 'A', 0x0512, 36865, -2883538, 0, "", '2' }, + { 'A', 0x0512, 554533, 3145727, 0, "", '2' }, + { 'A', 0x0515, -817408, -1217536, 0, "", '2' }, + { 'A', 0x0521, -687616, -1068544, 0, "", '2' }, + { 'A', 0x0541, 357835, -3483583, 0, "", '2' }, + { 'A', 0x0541, 1096987, -1246399, 0, "", '2' }, + { 'A', 0x0542, 58889, 410367, 0, "", '2' }, + { 'A', 0x0553, -5700226, 381957, 0, "", '2' }, + { 'A', 0x0562, 6423588, 5, 0, "", '2' }, + { 'A', 0x0573, 319553, 4261235, 0, "", '2' }, + { 'A', 0x0587, 229185, 1247626, 0, "", '2' }, + { 'A', 0x0593, 614465, 4261445, 0, "", '2' }, + { 'A', 0x0596, 957761, 4325360, 0, "", '2' }, + { 'A', 0x0600, -1280, -1278, 0, "", '2' }, + { 'A', 0x0600, 23552, 2370, 0, "", '2' }, + { 'A', 0x0600, 27136, 1073, 0, "", '2' }, + { 'A', 0x0600, 198145, 328966, 0, "", '2' }, + { 'A', 0x0600, 362305, 4261364, 0, "", '2' }, + { 'A', 0x0600, 719680, 4323103, 0, "", '2' }, + { 'A', 0x0600, 5784063, -11008, 0, "", '2' }, + { 'A', 0x0601, 263427, 263685, 0, "", '2' }, + { 'A', 0x0601, 1395264, 4317903, 0, "", '2' }, + { 'A', 0x0602, -7585745, 226557, 0, "", '2' }, + { 'A', 0x0602, 132610, 459015, 0, "", '2' }, + { 'A', 0x0602, 1379392, 4260144, 0, "", '2' }, + { 'A', 0x0604, -2379712, 4320528, 0, "", '2' }, + { 'A', 0x0604, 1115200, 4324344, 0, "", '2' }, + { 'A', 0x0605, -1308608, 4267331, 0, "", '2' }, + { 'A', 0x0605, -1308096, 4267193, 0, "", '2' }, + { 'A', 0x0605, 460550, 526343, 0, "", '2' }, + { 'A', 0x0605, 526087, 592136, 0, "", '2' }, + { 'A', 0x0605, 671791, 687621, 0, "", '2' }, + { 'A', 0x0605, 671791, 2363154, 0, "", '2' }, + { 'A', 0x0606, -2291392, 4266320, 0, "", '2' }, + { 'A', 0x0606, 460551, 526344, 0, "", '2' }, + { 'A', 0x0610, -333504, 4268775, 0, "", '2' }, + { 'A', 0x0610, 782656, 4263434, 0, "", '2' }, + { 'A', 0x0612, -1223360, 4262227, 0, "", '2' }, + { 'A', 0x0616, -823232, 4325339, 0, "", '2' }, + { 'A', 0x0618, 2034432, 4206336, 0, "", '2' }, + { 'A', 0x0618, 7146240, 2302720, 0, "", '2' }, + { 'A', 0x0618, 7604992, 2302720, 0, "", '2' }, + { 'A', 0x0620, -1078463, 4266760, 0, "", '2' }, + { 'A', 0x0621, 2163748, 6, 0, "", '2' }, + { 'A', 0x0624, 287, 201216, 0, "", '2' }, + { 'A', 0x0624, 1707, 263680, 0, "", '2' }, + { 'A', 0x0624, 1924, 352256, 0, "", '2' }, + { 'A', 0x0624, 2145, -1073920, 0, "", '2' }, + { 'A', 0x0624, 2226, 246016, 0, "", '2' }, + { 'A', 0x0624, 2822, 85504, 0, "", '2' }, + { 'A', 0x0624, 3187, -486656, 0, "", '2' }, + { 'A', 0x0624, 3582, -744448, 0, "", '2' }, + { 'A', 0x0624, 54258, 245760, 0, "", '2' }, + { 'A', 0x0624, 55036, -288000, 0, "", '2' }, + { 'A', 0x0625, -3342236, 356355, 0, "", '2' }, + { 'A', 0x0631, -6619036, 282115, 0, "", '2' }, + { 'A', 0x0632, -1753041, 2222128, 0, "", '2' }, + { 'A', 0x0632, 209692, 1786139, 0, "", '2' }, + { 'A', 0x0632, 3278643, 602888, 0, "", '2' }, + { 'A', 0x0632, 3279155, 537349, 0, "", '2' }, + { 'A', 0x0633, -1507228, 130816, 0, "", '2' }, + { 'A', 0x0633, -1300671, 1249544, 0, "", '2' }, + { 'A', 0x0633, -708031, 4275037, 0, "", '2' }, + { 'A', 0x0633, -23743, 4261744, 0, "", '2' }, + { 'A', 0x0633, 4627, -3276580, 0, "", '2' }, + { 'A', 0x0633, 4627, 5374122, 0, "", '2' }, + { 'A', 0x0633, 12569, -1441800, 0, "", '2' }, + { 'A', 0x0633, 12841, 3277107, 0, "", '2' }, + { 'A', 0x0633, 86336, 4325309, 0, "", '2' }, + { 'A', 0x0633, 143913, 3081523, 0, "", '2' }, + { 'A', 0x0633, 143913, 3081779, 0, "", '2' }, + { 'A', 0x0633, 143913, 3278131, 0, "", '2' }, + { 'A', 0x0633, 721729, 4319992, 0, "", '2' }, + { 'A', 0x0633, 762433, 4320118, 0, "", '2' }, + { 'A', 0x0633, 792617, 3288832, 0, "", '2' }, + { 'A', 0x0633, 825153, 4319360, 0, "", '2' }, + { 'A', 0x0633, 987689, 2339, 0, "", '2' }, + { 'A', 0x0633, 1638963, 3344178, 0, "", '2' }, + { 'A', 0x0633, 1710377, 8128630, 0, "", '2' }, + { 'A', 0x0633, 1769779, 1836083, 0, "", '2' }, + { 'A', 0x0633, 2161473, 3405186, 0, "", '2' }, + { 'A', 0x0633, 2687795, 209455, 0, "", '2' }, + { 'A', 0x0633, 2688819, 17432, 0, "", '2' }, + { 'A', 0x0633, 2688819, 533017, 0, "", '2' }, + { 'A', 0x0633, 2688819, 1909017, 0, "", '2' }, + { 'A', 0x0633, 2688819, 2367769, 0, "", '2' }, + { 'A', 0x0633, 2688819, 2695449, 0, "", '2' }, + { 'A', 0x0633, 2688819, 3344434, 0, "", '2' }, + { 'A', 0x0633, 2688819, 6825241, 0, "", '2' }, + { 'A', 0x0633, 3086633, 1048, 0, "", '2' }, + { 'A', 0x0633, 3086633, 3342642, 0, "", '2' }, + { 'A', 0x0633, 3277107, 209666, 0, "", '2' }, + { 'A', 0x0633, 3278643, -3129082, 0, "", '2' }, + { 'A', 0x0633, 3288873, 340742, 0, "", '2' }, + { 'A', 0x0633, 3343666, 471561, 0, "", '2' }, + { 'A', 0x0633, 3344179, 731400, 0, "", '2' }, + { 'A', 0x0633, 3344179, 1190152, 0, "", '2' }, + { 'A', 0x0633, 3344179, 1648904, 0, "", '2' }, + { 'A', 0x0633, 3344179, 2173192, 0, "", '2' }, + { 'A', 0x0633, 3344179, 2631944, 0, "", '2' }, + { 'A', 0x0633, 3344179, 3090696, 0, "", '2' }, + { 'A', 0x0633, 3344179, 3287304, 0, "", '2' }, + { 'A', 0x0633, 3344179, 4204808, 0, "", '2' }, + { 'A', 0x0633, 3345714, 602892, 0, "", '2' }, + { 'A', 0x0633, 3346483, 1648911, 0, "", '2' }, + { 'A', 0x0634, -2238912, 4266027, 0, "", '2' }, + { 'A', 0x0634, 3408932, 6, 0, "", '2' }, + { 'A', 0x0636, -327580, 572167, 0, "", '2' }, + { 'A', 0x0637, -4297920, 4268522, 0, "", '2' }, + { 'A', 0x0640, -2475968, 4267310, 0, "", '2' }, + { 'A', 0x0640, -23809, -34495, 0, "", '2' }, + { 'A', 0x0640, 4930880, 4266611, 0, "", '2' }, + { 'A', 0x0641, 260083, -1079294, 0, "", '2' }, + { 'A', 0x0642, 4327106, 508423, 0, "", '2' }, + { 'A', 0x0645, 4387, 67638, 0, "", '2' }, + { 'A', 0x0645, 1704466, 7997558, 0, "", '2' }, + { 'A', 0x0647, -2293660, -1038866, 0, "", '2' }, + { 'A', 0x0647, 7733348, 216322, 0, "", '2' }, + { 'A', 0x0648, -2817948, 377348, 0, "", '2' }, + { 'A', 0x0652, -1332928, 4265353, 0, "", '2' }, + { 'A', 0x0652, 2179119, 765958, 0, "", '2' }, + { 'A', 0x0656, -4026048, 4265110, 0, "", '2' }, + { 'A', 0x0656, -1039040, 4266361, 0, "", '2' }, + { 'A', 0x0656, 664384, 4265353, 0, "", '2' }, + { 'A', 0x0662, 3670116, 596744, 0, "", '2' }, + { 'A', 0x0663, 2379328, 4266634, 0, "", '2' }, + { 'A', 0x0666, 2504768, 4263561, 0, "", '2' }, + { 'A', 0x0667, -4915100, 203009, 0, "", '2' }, + { 'A', 0x0667, -483520, 4268868, 0, "", '2' }, + { 'A', 0x0667, -405440, 4260084, 0, "", '2' }, + { 'A', 0x0668, -300224, 4266821, 0, "", '2' }, + { 'A', 0x0669, -5308316, -1017361, 0, "", '2' }, + { 'A', 0x0669, -2932689, 2426408, 0, "", '2' }, + { 'A', 0x0672, -2752412, 697353, 0, "", '2' }, + { 'A', 0x0672, -1900444, 704265, 0, "", '2' }, + { 'A', 0x0676, -3095232, 4267595, 0, "", '2' }, + { 'A', 0x0676, -1884864, 4268663, 0, "", '2' }, + { 'A', 0x0676, 2310191, 2532126, 0, "", '2' }, + { 'A', 0x0678, 7929956, 533255, 0, "", '2' }, + { 'A', 0x0679, -2610880, 4268233, 0, "", '2' }, + { 'A', 0x0683, -231104, 4269314, 0, "", '2' }, + { 'A', 0x0684, -7012252, -930064, 0, "", '2' }, + { 'A', 0x0685, -6748636, 5, 0, "", '2' }, + { 'A', 0x0690, -7338972, 6, 0, "", '2' }, + { 'A', 0x0690, -1617600, 4266606, 0, "", '2' }, + { 'A', 0x0693, -2575040, 4269148, 0, "", '2' }, + { 'A', 0x0695, 411969, 4261668, 0, "", '2' }, + { 'A', 0x0696, -2402496, 4269614, 0, "", '2' }, + { 'A', 0x0696, 4883520, 4268245, 0, "", '2' }, + { 'A', 0x0696, 6881380, 150273, 0, "", '2' }, + { 'A', 0x0698, -1634239, 4270273, 0, "", '2' }, + { 'A', 0x0699, -6749148, 6, 0, "", '2' }, + { 'A', 0x0700, -2864381, 1836306, 0, "", '2' }, + { 'A', 0x0700, -802814, 54368, 0, "", '2' }, + { 'A', 0x0700, 7421188, 3808, 0, "", '2' }, + { 'A', 0x0701, 70400, 131072, 0, "", '2' }, + { 'A', 0x0701, 78565, -1629119, 0, "", '2' }, + { 'A', 0x0701, 289089, 2690326, 0, "", '2' }, + { 'A', 0x0703, 721430, 4400384, 0, "", '2' }, + { 'A', 0x0703, 1443209, 4400384, 0, "", '2' }, + { 'A', 0x0703, 2950645, 922368, 0, "", '2' }, + { 'A', 0x0706, 526343, 657673, 0, "", '2' }, + { 'A', 0x0706, 1310749, 393216, 0, "", '2' }, + { 'A', 0x0711, -4250110, 4357, 0, "", '2' }, + { 'A', 0x0711, 1115172, 7, 0, "", '2' }, + { 'A', 0x0719, 114692, 16356, 0, "", '2' }, + { 'A', 0x0719, 8257540, 16356, 0, "", '2' }, + { 'A', 0x0725, -2555804, 322051, 0, "", '2' }, + { 'A', 0x0728, -4390812, -1025041, 0, "", '2' }, + { 'A', 0x0731, 339521, 4261471, 0, "", '2' }, + { 'A', 0x0733, -8371135, 2719812, 0, "", '2' }, + { 'A', 0x0733, 1640499, 25107, 0, "", '2' }, + { 'A', 0x0733, 1640499, 26899, 0, "", '2' }, + { 'A', 0x0733, 3288873, 406277, 0, "", '2' }, + { 'A', 0x0733, 3344947, 406281, 0, "", '2' }, + { 'A', 0x0735, -4203200, 4270988, 0, "", '2' }, + { 'A', 0x0741, -759030, 4260915, 0, "", '2' }, + { 'A', 0x0741, 35352, 357400, 0, "", '2' }, + { 'A', 0x0741, 390650, -345535, 0, "", '2' }, + { 'A', 0x0741, 416563, 3365441, 0, "", '2' }, + { 'A', 0x0741, 434938, 38465, 0, "", '2' }, + { 'A', 0x0741, 475680, 4202793, 0, "", '2' }, + { 'A', 0x0743, -2490268, 334851, 0, "", '2' }, + { 'A', 0x0746, -3591360, 4207524, 0, "", '2' }, + { 'A', 0x0757, -2252992, 4273335, 0, "", '2' }, + { 'A', 0x0759, -3473308, 300034, 0, "", '2' }, + { 'A', 0x0763, 373569, 4261960, 0, "", '2' }, + { 'A', 0x0773, 7209060, -904719, 0, "", '2' }, + { 'A', 0x0776, 7405668, 730890, 0, "", '2' }, + { 'A', 0x0777, 313409, 4261734, 0, "", '2' }, + { 'A', 0x0787, 5439588, 213506, 0, "", '2' }, + { 'A', 0x0788, -7863260, 7, 0, "", '2' }, + { 'A', 0x0789, 664361, 538112, 0, "", '2' }, + { 'A', 0x0790, -7404508, 7, 0, "", '2' }, + { 'A', 0x0790, 541477, 1245365, 0, "", '2' }, + { 'A', 0x0790, 1130561, 4260272, 0, "", '2' }, + { 'A', 0x0800, -1280, -1278, 0, "", '2' }, + { 'A', 0x0812, 984612, 261, 0, "", '2' }, + { 'A', 0x0820, 4596992, 279560, 0, "", '2' }, + { 'A', 0x0823, 539648, 3080193, 0, "", '2' }, + { 'A', 0x0833, -3800988, 730377, 0, "", '2' }, + { 'A', 0x0833, 987689, 2339, 0, "", '2' }, + { 'A', 0x0833, 1710377, 8128630, 0, "", '2' }, + { 'A', 0x0833, 2688051, 1115176, 0, "", '2' }, + { 'A', 0x0833, 2688051, 1180712, 0, "", '2' }, + { 'A', 0x0833, 3288873, 340738, 0, "", '2' }, + { 'A', 0x0833, 3344691, 144133, 0, "", '2' }, + { 'A', 0x0833, 3345715, 406032, 0, "", '2' }, + { 'A', 0x0840, 320577, 4262207, 0, "", '2' }, + { 'A', 0x0841, 267778, 154945, 0, "", '2' }, + { 'A', 0x0841, 479216, -1082047, 0, "", '2' }, + { 'A', 0x0842, -1121578, -4317180, 0, "", '2' }, + { 'A', 0x0844, 386625, 4262103, 0, "", '2' }, + { 'A', 0x0848, 496449, 4262399, 0, "", '2' }, + { 'A', 0x0856, 7931992, 17944, 0, "", '2' }, + { 'A', 0x0862, 3081998, 4261426, 0, "", '2' }, + { 'A', 0x0875, 7668772, 8, 0, "", '2' }, + { 'A', 0x0885, 1114212, 506885, 0, "", '2' }, + { 'A', 0x0887, 349505, 4262104, 0, "", '2' }, + { 'A', 0x0889, -326080, 4264073, 0, "", '2' }, + { 'A', 0x0902, 416, -5756864, 0, "", '2' }, + { 'A', 0x0913, 226625, 4261943, 0, "", '2' }, + { 'A', 0x0915, 403777, 4196681, 0, "", '2' }, + { 'A', 0x0918, -6547454, 595969, 0, "", '2' }, + { 'A', 0x0918, -4974592, 7146242, 0, "", '2' }, + { 'A', 0x0929, 1181840, 402447, 0, "", '2' }, + { 'A', 0x0930, 4325856, 671750, 0, "", '2' }, + { 'A', 0x0933, 406041, 1641011, 0, "", '2' }, + { 'A', 0x0933, 2689075, 8739, 0, "", '2' }, + { 'A', 0x0933, 3344178, 209670, 0, "", '2' }, + { 'A', 0x0933, 3344947, 733700, 0, "", '2' }, + { 'A', 0x0933, 3345459, 274957, 0, "", '2' }, + { 'A', 0x0933, 3345715, 143889, 0, "", '2' }, + { 'A', 0x0939, -4243409, -37889, 0, "", '2' }, + { 'A', 0x0940, 67072, -61119, 0, "", '2' }, + { 'A', 0x0971, 6226020, -1163542, 0, "", '2' }, + { 'A', 0x0972, 7472164, 9, 0, "", '2' }, + { 'A', 0x0979, 137793, 2688575, 0, "", '2' }, + { 'A', 0x0979, 137793, 4261439, 0, "", '2' }, + { 'A', 0x0986, 403265, 4262653, 0, "", '2' }, + { 'A', 0x0993, 11283, 2162838, 0, "", '2' }, + { 'A', 0x0993, 410689, 4262256, 0, "", '2' }, + { 'C', 0x1210, 2144930, 2139287, 0, "", '2' }, + { 'C', 0x1213, 25600, -3997064, 0, "", '2' }, + { 'C', 0x1241, 1048597, 1385537, 0, "+?y?", '2' }, + { 'C', 0x1297, 2069057, 4264255, 0, "", '2' }, + { 'C', 0x1303, -2162650, 93953, 0, "", '2' }, + { 'C', 0x1317, -4521960, -76544, 0, "", '2' }, + { 'C', 0x1326, 1111873, 4264655, 0, "?A=??", '2' }, + { 'N', 0x1080, -6225920, 4, 307200, "", '2' }, + { 'N', 0x1080, -5832704, 214, -251392, "", '2' }, + { 'N', 0x1080, -5308416, 213, 374016, "", '2' }, + { 'N', 0x1080, -3538944, 250, -1102592, "", '2' }, + { 'N', 0x1080, -2424832, 9, -430336, "", '2' }, + { 'N', 0x1080, 1900544, 245, -952320, "", '2' }, + { 'N', 0x1080, 3604480, 10, 256768, "", '2' }, + { 'N', 0x1080, 7602176, 252, -161536, "", '2' }, + { 'N', 0x1090, 1747776, 2228544, 464162, "", '2' }, + { 'N', 0x1100, -3866624, 242, -561920, "", '2' }, + { 'N', 0x1100, -1966080, 14, -62720, "", '2' }, + { 'N', 0x1100, -983040, 219, 1207808, "", '2' }, + { 'N', 0x1100, -131072, 245, -1130240, "", '2' }, + { 'N', 0x1100, 3735552, 1, 32512, "", '2' }, + { 'N', 0x1100, 5832704, 212, -97536, "", '2' }, + { 'N', 0x1110, -5590528, -3112896, 510672, "", '2' }, + { 'N', 0x1110, 2772032, 3174612, 205872, "", '2' }, + { 'N', 0x1116, -2424832, 0, 119040, "", '2' }, + { 'N', 0x1120, -6160384, 250, -125696, "", '2' }, + { 'N', 0x1120, -4325376, 247, 251904, "", '2' }, + { 'N', 0x1120, -2359296, 18, 187904, "", '2' }, + { 'N', 0x1120, 6094848, 1, 276992, "", '2' }, + { 'N', 0x1120, 8323072, 238, -1027072, "", '2' }, + { 'N', 0x1130, -4960832, -40746, -225793, "", '2' }, + { 'N', 0x1130, -3925888, 1703999, 442394, "", '2' }, + { 'N', 0x1130, 1771073, 1773925, 5914907, "", '2' }, + { 'N', 0x1140, -7012352, 6, 193280, "", '2' }, + { 'N', 0x1140, -3407872, 16, -4608, "", '2' }, + { 'N', 0x1140, -3276800, 12, -1512192, "", '2' }, + { 'N', 0x1140, -3211264, 12, -1511936, "", '2' }, + { 'N', 0x1140, 2162688, 250, -1153536, "", '2' }, + { 'N', 0x1140, 4390912, 0, 333312, "", '2' }, + { 'N', 0x1140, 6094848, 241, -1220096, "", '2' }, + { 'N', 0x1140, 7012352, 214, -319232, "", '2' }, + { 'N', 0x1141, 1945153, 4265537, 2076481, "", '2' }, + { 'N', 0x1160, -7143424, 4, 493312, "", '2' }, + { 'N', 0x1160, -5439488, 227, 893952, "", '2' }, + { 'N', 0x1160, -5046272, 239, -783872, "", '2' }, + { 'N', 0x1160, -2818048, 0, 119040, "", '2' }, + { 'N', 0x1160, 7143424, 17, -379904, "", '2' }, + { 'N', 0x1160, 7536640, 247, 295936, "", '2' }, + { 'N', 0x1170, -4125120, 802876, 327948, "", '2' }, + { 'N', 0x1170, 5407664, -2091505, -24096, "", '2' }, +}; + +#define SCENERY_STATIONS_COUNT ((int)(sizeof(kSceneryStations) / sizeof(kSceneryStations[0]))) + +#endif diff --git a/port/include/sceneryVm.h b/port/include/sceneryVm.h new file mode 100644 index 0000000..d2e5941 --- /dev/null +++ b/port/include/sceneryVm.h @@ -0,0 +1,147 @@ +// Scenery interpreter VM. Drives a stream of opcoded records the +// same way the original FS2 chunk5 dispatcher does, but writes into +// the modern framebuffer via the renderer instead of poking hires +// bytes directly. + +#ifndef SCENERY_VM_H +#define SCENERY_VM_H + +#include +#include +#include "renderer.h" +#include "sceneryProjection.h" +#include "types.h" + +#define SCENERY_VERTEX_CAP 64 + +// FS2 cached-vertex pool lives at $0140 in zero-page-extended memory, +// 8 bytes per vertex slot. Opcodes $31/$32/$33/$35/$42 reference these +// by 1-byte index from the stream. The pool holds up to 80 vertices +// (= $140..$540 = 0x400 / 8). +#define SCENERY_CACHED_POOL_BASE 0x0140 +#define SCENERY_CACHED_POOL_CAP 80 + +// Forward decl so we don't pull in camera.h here. +struct CameraT; +struct SceneryStateT; + + +typedef enum SceneryStationTypeE { + SCENERY_STATION_ADF = 'A', + SCENERY_STATION_NAV = 'N', + SCENERY_STATION_COM = 'C' +} SceneryStationTypeE; + + +// Decoded station record passed to the optional callback. `freq` is +// the raw little-endian word from the record (BCD-packed); `x`/`y` are +// 24-bit signed scenery coordinates; `z` is 0 for ADF/COM and a 24-bit +// signed altitude/north for NAV. `name` is the COM record's airport +// name or NULL. +typedef struct SceneryStationT { + SceneryStationTypeE type; + uint16_t freq; + int32_t x; + int32_t y; + int32_t z; + const char *name; +} SceneryStationT; + + +typedef void (*SceneryStationCbF)(struct SceneryStateT *state, const SceneryStationT *station); + + +// Working state for a single scenery interpretation pass. +typedef struct SceneryStateT { + const uint8_t *stream; + const uint8_t *cursor; + const uint8_t *streamEnd; + // When non-NULL, $1A (WriteWord) and $25 (StoreImmWord) opcodes + // patch this buffer at the bytecode-supplied target addresses. + // The full 64K RAM image is treated as one flat address space; + // chunk5 SceneryOpStoreImmWord stores into $0846/$0848 zero-page + // slots and similar, all of which live inside the same 64K + // buffer when we're driving from a RAM dump. + uint8_t *writableRam; + // Raw .SD scenery file used by HEADER's demand-load. The file + // is indexed in 256-byte sectors; chunk5 HEADER stores + // (sectionId, count) at $08E5/$08E6 and triggers a copy of + // count*256 bytes from sceneryFile[sectionId*256] to the + // relocated dest at $08E7/$08E8 -- mirrors chunk5 LA63A. + const uint8_t *sceneryFile; + uint32_t sceneryFileSize; + RenderStateT *renderer; + const struct CameraT *camera; // NULL for 2D streams + uint8_t subDepth; + SceneryStationCbF stationCb; // NULL = ignore station records + void *userData; // forwarded to stationCb + SceneryPipelineT pipeline; // 3D vertex / projection state + // Drawing mode flags toggled by the $1B/$1C opcodes. dayOnlySkip + // is set by SceneryOpDayOnly when night, suppressing line draws + // for ground-only objects until SceneryOpModeWhite restores. + bool dayOnlySkip; + // Set by main.c (or the time-of-day step) so $1C can decide + // whether to suppress draws. + bool isNight; + // Offline extraction mode: every conditional opcode walks BOTH + // branches (recursively) instead of evaluating the predicate. + // The visited[] array bounds the total work. Used by tools that + // want to extract every reachable polygon / station regardless + // of the aircraft's runtime position. + bool walkAllPaths; + uint8_t visited[200000]; // cycle guard for offline walks + + // Polygon vertex accumulator. Each $40/$41 (xform-B) emit appends + // its projected screen coords. The $29 (CopyToD2) opcode triggers + // rendererFillPolygon over these coords, then resets the buffer. + // Mirrors chunk5's PrimVert*/SecVert* polygon arrays at $0AF9+ + // that the L7826 scan-line rasterizer fills from. + int16_t polyXs[64]; + int16_t polyYs[64]; + int polyCount; + // 3D-vertex accumulator that mirrors chunk5's PrimVerts array + // BEFORE the 4-pass Sutherland-Hodgman clipping at L6F98. + // Vertices live in camera-space (post-TransformVertex, pre- + // PerspectiveDivide). The clipper introduces new vertices at + // frustum-edge intersections, then projection expands the + // resulting screen-Y range -- without this 3D-then-clip path + // a polygon whose vertices all map to a narrow screen-Y range + // (because their Z's are all similar) collapses to a thin + // sliver instead of the real wedge shape chunk5 produces. + // The clipper output lives in a second array (`polyV3DOut`) + // and we ping-pong between the two per pass. + SceneryVertexT polyV3D[64]; + SceneryVertexT polyV3DOut[64]; + int polyV3DCount; + // MAME-patched chunk5 EmitClippedLine ends with RTS, so each + // $41/$02 op (= line-emit) terminates its parent's + // SceneryInterpreterStep iteration. SubInvoke ($18) calls JSR + // $6751 which then RTSes when a line is emitted, returning + // control to the SubInvoke handler that restores the parent + // cursor and continues. Mirror this with an exitDispatch flag: + // setting it tells sceneryRun to stop iterating. + bool exitDispatch; +} SceneryStateT; + +// Initialise an interpreter pointing at the given byte stream. Pass +// `camera = NULL` for legacy 2D fixture streams. +void sceneryInit(SceneryStateT *state, const uint8_t *stream, uint32_t length, RenderStateT *renderer); + +void sceneryAttachCamera(SceneryStateT *state, const struct CameraT *cam); + +// Install a station-record callback. Pass NULL to clear. Used by the +// offline scenery dump tool to collect ADF/NAV/COM records without +// rendering anything. +void sceneryAttachStationCb(SceneryStateT *state, SceneryStationCbF cb, void *userData); + +// Run the interpreter until it hits a stream-terminator record. +void sceneryRun(SceneryStateT *state); + +// Walk every reachable record from the given entry offset, collecting +// stations into the provided callback. Used by extractstations as a +// drop-in replacement for the old hand-rolled walker. The visited +// array tracks already-walked positions to bound work and let the +// caller invoke from many entry points cheaply. +void sceneryWalkFrom(SceneryStateT *state, uint32_t entryOffset); + +#endif diff --git a/port/include/timeOfDay.h b/port/include/timeOfDay.h new file mode 100644 index 0000000..3cdf5f2 --- /dev/null +++ b/port/include/timeOfDay.h @@ -0,0 +1,53 @@ +// Time-of-day state and phase computation. Direct port of FS2 chunk3 +// `ComputeDayPhase` plus `DayPhaseTable` (4 seasonal rows, each +// holding the dawn-start / sunrise / sunset / dusk-end (min, hour) +// pairs). + +#ifndef TIME_OF_DAY_H +#define TIME_OF_DAY_H + +#include + + +typedef enum DayPhaseE { + DAY_PHASE_DAY = 0x01, + DAY_PHASE_TWILIGHT = 0x02, + DAY_PHASE_NIGHT = 0x04 +} DayPhaseE; + + +typedef enum SeasonE { + SEASON_WINTER = 0, + SEASON_SPRING = 1, + SEASON_SUMMER = 2, + SEASON_FALL = 3 +} SeasonE; + + +typedef struct TimeOfDayT { + uint8_t hours; // 0..23 + uint8_t minutes; // 0..59 + uint16_t frameSubMinute; // tick accumulator within the current minute + SeasonE season; + DayPhaseE phase; +} TimeOfDayT; + + +void timeOfDayInit(TimeOfDayT *t); + +// Advance the clock by one simulation frame. The default rate is one +// in-game minute per `TIME_FRAMES_PER_MINUTE` frames (defined inside +// the .c). Phase is recomputed each tick. +void timeOfDayStep(TimeOfDayT *t); + +// Recompute the phase from `hours`/`minutes`/`season` against +// FS2's per-quadrant `DayPhaseTable`. +void timeOfDayRecomputePhase(TimeOfDayT *t); + +// Set time directly (e.g. from edit mode); recomputes phase. +void timeOfDaySet(TimeOfDayT *t, uint8_t hours, uint8_t minutes); + +// Pretty-print phase name. Always non-NULL. +const char *timeOfDayPhaseName(DayPhaseE phase); + +#endif diff --git a/port/include/title.h b/port/include/title.h new file mode 100644 index 0000000..1b4ced2 --- /dev/null +++ b/port/include/title.h @@ -0,0 +1,43 @@ +// Pre-flight title / config screen. +// +// FS2 itself boots straight into the simulation -- "LOADING ... VERSION +// 2.0" was just a static splash burned into the panel bitmap, with all +// mode selection happening at runtime via Edit-mode key bindings. The +// port adds a small menu that gates entry into the sim and lets the +// user pick the starting mode the same way FS2's runtime toggles do +// (free flight, demo auto-pilot, slew, WW1 Ace). + +#ifndef TITLE_H +#define TITLE_H + +#include +#include +#include +#include "framebuffer.h" + + +typedef enum TitleSelectionE { + TITLE_FREE_FLIGHT = 0, + TITLE_DEMO = 1, + TITLE_SLEW = 2, + TITLE_WW1_ACE = 3, + TITLE_QUIT = 4 +} TitleSelectionE; + + +typedef struct TitleStateT { + TitleSelectionE cursor; + bool done; // user pressed Enter +} TitleStateT; + + +void titleInit(TitleStateT *t); + +// Process a single SDL key event. Returns true once `done` is set +// (the caller should then consume `cursor` and exit the title loop). +bool titleHandleKey(TitleStateT *t, const SDL_Event *ev); + +// Render the title screen into the framebuffer. +void titleDraw(const TitleStateT *t, FramebufferT *fb); + +#endif diff --git a/port/include/types.h b/port/include/types.h new file mode 100644 index 0000000..5600351 --- /dev/null +++ b/port/include/types.h @@ -0,0 +1,46 @@ +// Shared types and constants for the FS2 modernized port. + +#ifndef TYPES_H +#define TYPES_H + +#include +#include + +// Native simulator resolution. Matches the Apple II hires page so the +// scenery interpreter and projection LUTs operate on the same coords +// the original disassembly uses, even though we render at higher +// resolution by scaling at present time. +#define NATIVE_WIDTH 280 +#define NATIVE_HEIGHT 192 + +// Viewport (the "out the window" area above the instrument panel) is +// the upper portion of the screen. The original FS2 fills rows 0..98 +// with sky/ground; rows 99..191 carry the instrument panel bitmap. +#define VIEWPORT_TOP 0 +#define VIEWPORT_BOTTOM 99 + +// Default scale factor (window pixels per native pixel). +#define WINDOW_SCALE 4 + +// 16-bit signed scenery coordinate. +typedef int16_t Coord16T; + +// One vertex in scenery / camera space. +typedef struct VertexT { + Coord16T x; + Coord16T y; + Coord16T z; +} VertexT; + +// Cohen-Sutherland-style outcode bits for frustum clipping. Mirrors +// the layout used by the original disassembly so the clipper logic +// can be ported one-for-one. +typedef enum OutcodeE { + OUTCODE_BEHIND = 0x80, // z < 0 + OUTCODE_RIGHT = 0x40, // x + z < 0 + OUTCODE_LEFT = 0x20, // z - x < 0 + OUTCODE_BOTTOM = 0x10, // y + z < 0 + OUTCODE_TOP = 0x08 // z - y < 0 +} OutcodeE; + +#endif diff --git a/port/include/wind.h b/port/include/wind.h new file mode 100644 index 0000000..3a914f4 --- /dev/null +++ b/port/include/wind.h @@ -0,0 +1,75 @@ +// FS2 wind system port (chunk2 `ComputeWindComponents` and `ApplyWind`). +// +// FS2 splits the sky into four altitude bands; each band carries a +// 4-byte record `[magnitude, turbByte, reserved, directionByteAngle]`. +// `windCompute` (called every other frame in the original) selects the +// active band, resolves the record into a signed 16-bit (X, Z) wind +// vector, and caches `WindLayerByte1` for the turbulence kick. +// `windApply` (called every frame) returns the per-frame world-position +// delta and updates the bank-accumulator + turbulence-kick state that +// downstream FS2 routines consume. +// +// Default state is all zeros, matching the FS2 ROM image's +// uninitialised wind tables; that yields no wind until layers are +// configured. + +#ifndef WIND_H +#define WIND_H + +#include +#include + +typedef struct WindLayerT { + int8_t magnitude; // record byte 0 (signed) + uint8_t turbByte; // record byte 1 (bit 0 enables turbulence) + uint8_t reserved; // record byte 2 (ignored by `resolve`) + uint8_t direction; // record byte 3 (byte angle) +} WindLayerT; + +typedef struct WindStateT { + // Configuration: altitude thresholds (FS2 cells `WindAlt1/2/3`) + // and the four layer records. Ordering: surface < layer1 < + // layer2 < layer3 by altitude. + uint16_t altThreshold1; + uint16_t altThreshold2; + uint16_t altThreshold3; + WindLayerT surface; + WindLayerT layer1; + WindLayerT layer2; + WindLayerT layer3; + + // Auxiliary FS2 state cells. + uint8_t yokeOffset1; // $0847 (added to direction always) + uint8_t yokeOffset2; // $0849 (added on the surface band) + uint8_t scaleByteLo; // $09DE (turbulence scale low) + uint8_t scaleByteHi; // $09DF (turbulence scale hi + L180C scale) + uint8_t updateCounter; // mirrors FS2 `UpdateCounter` + + // Computed by windCompute. + int16_t componentX; // $09A2/$09A3 (signed 16-bit wind X) + int16_t componentZ; // $09A4/$09A5 (signed 16-bit wind Z) + uint8_t layerByte1; // `WindLayerByte1` cache + uint8_t surfaceFlag; // $09A6 (1 when the surface band was picked) + + // Computed by windApply. + int16_t bankAccum; // $09AF/$09B0 (downstream bank derivation) + int16_t turbKick; // $08A1/$08A2 (turbulence kick output) +} WindStateT; + +void windInit(WindStateT *w); + +// Pick the active layer based on `altitude16` and resolve its (X, Z) +// components. Direct port of `ComputeWindComponents` (chunk2 L483). +// The original is called every other frame; we leave the cadence to +// the caller. +void windCompute(WindStateT *w, uint16_t altitude16); + +// Per-frame wind step. Direct port of `ApplyWind` (chunk2 L397). On +// the ground, returns zero delta. Off the ground, advances the +// bank-accumulator and turbulence-kick state and returns the +// world-position delta for this frame in (deltaX, deltaZ) as a Q16.16 +// world-unit value (matching the FS2 32-bit position cell convention +// and aircraft.h's `AC_POS_FRACT_BITS`). +void windApply(WindStateT *w, bool onGround, int32_t *deltaX_q1616, int32_t *deltaZ_q1616); + +#endif diff --git a/port/include/world.h b/port/include/world.h new file mode 100644 index 0000000..61c8df7 --- /dev/null +++ b/port/include/world.h @@ -0,0 +1,29 @@ +// 3D world. While the .po reader is still missing this is a +// hardcoded list of coloured line segments laid out around the +// runway. Eventually the scenery VM will own this. + +#ifndef WORLD_H +#define WORLD_H + +#include "camera.h" +#include "palette.h" +#include "renderer.h" + +typedef struct WorldLineT { + int16_t x1; + int16_t y1; + int16_t z1; + int16_t x2; + int16_t y2; + int16_t z2; + ColorE color; +} WorldLineT; + +void worldRender(const CameraT *cam, RenderStateT *renderer); + +// Top-down orthographic render (FS2 RadarView). The camera supplies +// the world position and yaw; pitch and bank are ignored. +// `metresPerPixel_q88` is Q8.8 metres / pixel (default 4*256 = 1024). +void worldRenderRadar(const CameraT *cam, RenderStateT *renderer, int16_t metresPerPixel_q88); + +#endif diff --git a/port/include/ww1ace.h b/port/include/ww1ace.h new file mode 100644 index 0000000..04d55e2 --- /dev/null +++ b/port/include/ww1ace.h @@ -0,0 +1,141 @@ +// FS2 "World War 1 Ace" combat mode (chunk3 / chunk4 / chunk5). +// +// FS2 spawns six enemy aircraft, each with a status code: +// 0 = shot down, 1 = returning / home, 2 = attacking. +// The player's score (`WW1AceScore`) and bomb count (`WW1AceBombsStr`) +// drive the panel HUD overlay; the "War Report" screen (chunk3 L2455) +// summarises the campaign so far. +// +// This port keeps the same six-enemy slot table and status semantics, +// adds minimal AI (attacking enemies drift toward the player, returning +// enemies head home), and renders each enemy as a small line sprite in +// the world view. Hit detection / damage tracking are stubbed pending +// the full chunk5 fire-control routines. + +#ifndef WW1ACE_H +#define WW1ACE_H + +#include +#include +#include "camera.h" +#include "framebuffer.h" +#include "renderer.h" + +#define WW1_ENEMY_COUNT 6 +#define WW1_BULLET_COUNT 12 // pool: 6 enemy + 6 player rounds +#define WW1_BOMB_COUNT 4 // pool of bombs in flight + + +typedef enum WW1EnemyStatusE { + WW1_ENEMY_SHOT_DOWN = 0, + WW1_ENEMY_RETURNING = 1, + WW1_ENEMY_ATTACKING = 2 +} WW1EnemyStatusE; + + +typedef struct WW1EnemyT { + WW1EnemyStatusE status; + int32_t worldX; // Q16.16, matches AircraftT/CameraT + int32_t worldY; + int32_t worldZ; + // Q8.8 velocity components in metres/frame. Drives the + // maneuvering AI: enemies bank, climb, and turn rather than + // sliding straight toward the player. + int16_t velX; + int16_t velY; + int16_t velZ; + uint8_t heading; // byte angle + uint8_t maneuverPhase; // 0..255, drives sinusoidal jink + uint8_t fireCooldown; // frames until next shot at player +} WW1EnemyT; + + +typedef struct WW1BulletT { + bool active; + bool fromEnemy; // true = enemy round, false = player + int32_t worldX; // Q16.16 + int32_t worldY; + int32_t worldZ; + int16_t velX; // Q8.8 metres / frame + int16_t velY; + int16_t velZ; + uint8_t framesLeft; // self-expiry counter +} WW1BulletT; + + +typedef struct WW1BombT { + bool active; + int32_t worldX; // Q16.16 + int32_t worldY; // descending under gravity + int32_t worldZ; + int16_t velX; // Q8.8 metres / frame (inherited from player) + int16_t velY; // negative (falling), accelerates + int16_t velZ; +} WW1BombT; + + +typedef struct WW1AceStateT { + bool enabled; + bool showWarReport; + uint16_t score; // mirrors `WW1AceScore` (16-bit) + uint8_t bombs; // mirrors `WW1AceBombsStr` countdown + uint16_t damageByEnemy; // mirrors $08A4 + uint16_t bombHits; // mirrors $A81B + uint8_t playerFireCooldown; // throttles repeat gun fire to a burst rate + WW1EnemyT enemies[WW1_ENEMY_COUNT]; + WW1BulletT bullets[WW1_BULLET_COUNT]; + WW1BombT bombsInFlight[WW1_BOMB_COUNT]; + + // Internal: next bomb impact frame, RNG seed for AI jitter. + uint16_t rngState; +} WW1AceStateT; + + +void ww1aceInit(WW1AceStateT *s); + +// Toggle WW1 Ace mode. Spawns / despawns enemies and resets the +// score/bomb counters. Player coords are Q16.16 world-units. +void ww1aceToggle(WW1AceStateT *s, int32_t playerX, int32_t playerZ); + +// Legacy bomb-drop entry that just decrements the bomb count. Prefer +// `ww1aceDropBombAt` so the bomb is added to the in-flight pool with +// the player's actual position + velocity. +void ww1aceDropBomb(WW1AceStateT *s); + +// Drop a bomb at the player's current world position with the player's +// horizontal velocity (Q8.8 metres/frame). The bomb then falls under +// gravity in `ww1aceUpdate` and tries to score a hit on a ground +// enemy at impact. +void ww1aceDropBombAt(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, + int16_t playerVelX_q88, int16_t playerVelZ_q88); + +// Fire the machine gun at any enemy in front. The closest attacking +// enemy within `aimConeDeg` of the nose gets shot down. Player coords +// are Q16.16 world-units. +void ww1aceFireGun(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, uint8_t playerYaw); + +// Per-frame AI + projectile update. Attacking enemies maneuver toward +// the player and fire bursts when aimed; returning enemies head home; +// shot-down enemies stay down. Bullets and bombs in flight advance, +// expire, and check hits. Player damage accumulates via +// `damageByEnemy`; when it hits AC_FAIL_BY_DAMAGE the caller can +// trigger a player crash. Returns true if a player-fatal hit occurred +// this frame. +bool ww1aceUpdate(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, uint8_t playerYaw); + + +// Render bullets and bombs in flight as small dots through the camera +// projection. Called alongside ww1aceRender. +void ww1aceRenderProjectiles(const WW1AceStateT *s, const CameraT *cam, RenderStateT *renderer); + +// Render the enemies into the 3D viewport using the supplied camera. +void ww1aceRender(const WW1AceStateT *s, const CameraT *cam, RenderStateT *renderer); + +// Draw the score / bomb overlay near the top of the panel. +void ww1aceHudDraw(const WW1AceStateT *s, FramebufferT *fb); + +// Draw the full "War Report" screen. Caller is responsible for +// freezing the simulation while the screen is shown. +void ww1aceDrawWarReport(const WW1AceStateT *s, FramebufferT *fb); + +#endif diff --git a/port/sceneryRam_FS2.1.bin b/port/sceneryRam_FS2.1.bin new file mode 100644 index 0000000..9c74501 --- /dev/null +++ b/port/sceneryRam_FS2.1.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a85c2c97f54e43c53ede26313fa1f15aacf175818c7107e5154e1a589d89879 +size 65536 diff --git a/port/sceneryRam_FS2.1_chicago.bin b/port/sceneryRam_FS2.1_chicago.bin new file mode 100644 index 0000000..2edec08 --- /dev/null +++ b/port/sceneryRam_FS2.1_chicago.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04b04d18d89eec93756fe3c57547e4076145d1436e87d2d7a180cad472fa8447 +size 65536 diff --git a/port/sceneryRam_FS2.1_frozen.bin b/port/sceneryRam_FS2.1_frozen.bin new file mode 100644 index 0000000..b2b4b5b --- /dev/null +++ b/port/sceneryRam_FS2.1_frozen.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8fa1fecc9dd5881c169a68cb2b59ffebe1d3572a18761f9e4fcda3fd4c22c56 +size 65536 diff --git a/port/sceneryRam_FS2.1_la.bin b/port/sceneryRam_FS2.1_la.bin new file mode 100644 index 0000000..4825af3 --- /dev/null +++ b/port/sceneryRam_FS2.1_la.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a16d85ce4be6d887202c8124074120253829d08296568a5595837d7ab900b682 +size 65536 diff --git a/port/sceneryRam_FS2.1_ny.bin b/port/sceneryRam_FS2.1_ny.bin new file mode 100644 index 0000000..05f696d --- /dev/null +++ b/port/sceneryRam_FS2.1_ny.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b0e8a77d7d9e1ee03cccdec30096cbf771ccafef2365a7ec9a27b3b2be9362d +size 65536 diff --git a/port/sceneryRam_FS2.1_seattle.bin b/port/sceneryRam_FS2.1_seattle.bin new file mode 100644 index 0000000..eab4558 --- /dev/null +++ b/port/sceneryRam_FS2.1_seattle.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cc1c1baeecf31280f4c45d6c363d7f5f413dd43236a06684498973b36ccba62 +size 65536 diff --git a/port/sceneryRam_SD1.bin b/port/sceneryRam_SD1.bin new file mode 100644 index 0000000..e058737 --- /dev/null +++ b/port/sceneryRam_SD1.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:843ec98ee352dcf116651d0cd2b01c368573f8215d9cf678059fdc894919d6f4 +size 65536 diff --git a/port/sceneryRam_SD11.bin b/port/sceneryRam_SD11.bin new file mode 100644 index 0000000..8992802 --- /dev/null +++ b/port/sceneryRam_SD11.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2348bd22942ce659324224f881cd05e3096d64d71cad385f4cf307d0616e80b4 +size 65536 diff --git a/port/sceneryRam_SD13.bin b/port/sceneryRam_SD13.bin new file mode 100644 index 0000000..fb43aa2 --- /dev/null +++ b/port/sceneryRam_SD13.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d09ac133aae93997dd29fb938e6df0ab0be557b647590eae028af29128a7b030 +size 65536 diff --git a/port/sceneryRam_SD14A.bin b/port/sceneryRam_SD14A.bin new file mode 100644 index 0000000..1f66635 --- /dev/null +++ b/port/sceneryRam_SD14A.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6f1eb2f53ae12605ba142bf69931180efe21588f217a5868b2ef14d06ba8455 +size 65536 diff --git a/port/sceneryRam_SD14B.bin b/port/sceneryRam_SD14B.bin new file mode 100644 index 0000000..32dac8b --- /dev/null +++ b/port/sceneryRam_SD14B.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20da353e6419c23051d295f5e38f39e49aaf629c956f85eb5cc7a2801ee91242 +size 65536 diff --git a/port/sceneryRam_SD2.bin b/port/sceneryRam_SD2.bin new file mode 100644 index 0000000..9dfe29b --- /dev/null +++ b/port/sceneryRam_SD2.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9992e5976f3789b34b0d359a88db02653f37fd54e161d014f1c2b1e450cd912f +size 65536 diff --git a/port/sceneryRam_SD3.bin b/port/sceneryRam_SD3.bin new file mode 100644 index 0000000..5d4a93c --- /dev/null +++ b/port/sceneryRam_SD3.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d41d833df1ba897c56c3e8d3ec6021fa6cfeb4b5ede8ea591fed1c0045b7a90 +size 65536 diff --git a/port/sceneryRam_SD4.bin b/port/sceneryRam_SD4.bin new file mode 100644 index 0000000..25b0bac --- /dev/null +++ b/port/sceneryRam_SD4.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ad3401b1e3277f148c87874428cb981f954109445aec22f899a5810dc6cfc3b +size 65536 diff --git a/port/sceneryRam_SD5.bin b/port/sceneryRam_SD5.bin new file mode 100644 index 0000000..5510d9e --- /dev/null +++ b/port/sceneryRam_SD5.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:911c18833b559ba8db9f1af670b34323c98c7760d8c83035539dbadc32be0777 +size 65536 diff --git a/port/sceneryRam_SD6.bin b/port/sceneryRam_SD6.bin new file mode 100644 index 0000000..e2b088d --- /dev/null +++ b/port/sceneryRam_SD6.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:307bcd764126fb108cfc2303d7a6df83b9bf029a15fa4827db1c871a6de5d702 +size 65536 diff --git a/port/sceneryRam_SD7A.bin b/port/sceneryRam_SD7A.bin new file mode 100644 index 0000000..713edb4 --- /dev/null +++ b/port/sceneryRam_SD7A.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e71a6f046bb63f418023ccce2dc9847c8b798d2bb2bc7ad562a7bdd2a0e4f787 +size 65536 diff --git a/port/sceneryRam_SD7B.bin b/port/sceneryRam_SD7B.bin new file mode 100644 index 0000000..d518260 --- /dev/null +++ b/port/sceneryRam_SD7B.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75f2e72b95f56356906dcb6f509f0c7f2644b4550e36826f6e6e56ac4aca47ca +size 65536 diff --git a/port/sceneryRam_SDS1.bin b/port/sceneryRam_SDS1.bin new file mode 100644 index 0000000..2b7f903 --- /dev/null +++ b/port/sceneryRam_SDS1.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2553eceb7645e4feffaf9c0552b4747977e02fa53bce051f8e4c0197281c87b +size 65536 diff --git a/port/src/aircraft.c b/port/src/aircraft.c new file mode 100644 index 0000000..ada88f7 --- /dev/null +++ b/port/src/aircraft.c @@ -0,0 +1,681 @@ +// Flight model. Per-frame physics in FS2's fixed-point conventions: +// position is Q16.16 (low 16 bits fractional), rates are Q8.8, pilot +// inputs are signed-byte (yoke / rudder / trim) or unsigned-byte +// (throttle / flaps / mixture). Sin/cos goes through the +// `math6502Sin/Cos` tables (int16_t Q1.15) and 16-bit scaling goes +// through `fs2ScaleByAX`. Wind and turbulence still come from chunk2 +// via `wind.h`. + +#include +#include "aircraft.h" +#include "fs2math.h" +#include "math6502.h" +#include "wind.h" + + +// Q8.8 constants (256 = 1.0). +#define MAX_PITCH_RATE_Q88 384 // 1.5 byte-angles / frame +#define MAX_BANK_RATE_Q88 512 // 2.0 +#define MAX_YAW_RATE_Q88 256 // 1.0 + +// Lerp coefficients (out of 256). +#define INPUT_SMOOTH_K 46 // ~0.18 +#define ENGINE_SMOOTH_K 10 // ~0.04 +#define AUTO_COORD_K 10 // ~0.04 +#define GROUND_FRICTION_K 248 // ~0.97 +#define DEFAULT_DECAY_K 218 // ~0.85 self-centring +#define RUDDER_DECAY_K 179 // ~0.70 + +// Speed envelope. +#define MAX_FORWARD_SPEED_Q88 410 // 1.6 world-units / frame +#define DRAG_K 1 // 1/256 ~ 0.4% + +// Lift / gravity. +#define STALL_SPEED_Q88 51 // 0.20 +#define GRAVITY_PER_FRAME_Q88 13 // 0.05 +#define LIFT_FACTOR_Q88 256 // 1.0 +#define STALL_PITCHSIN_Q15 13107 // 0.4 in Q1.15 +#define STALL_CLIMB_KICK_Q88 -13 // -0.05 +#define STALL_PITCH_KICK_Q88 -128 // -0.5 + +// Ground. +#define GROUND_BOUNCE_Q1616 (2 << 16) +#define CRASH_VLIMIT_Q88 -115 // -0.45 +#define LIFTOFF_SPEED_Q88 77 // 0.30 (~ 1.5 * STALL) + +// Slew gains. Yoke is int8_t [-127..127]; FS2 shifts left 4 to get a +// 16-bit position cell delta. We use the same: yoke << 4 -> int16_t +// then sign-extend into the Q16.16 position. +#define SLEW_YOKE_SHIFT 4 +#define SLEW_ALT_RATE_Q1616_PER_TICK (1 << 12) // ~0.0625 world unit / tick +#define SLEW_ANGLE_RATE_PER_TICK 1 // byte angle / tick + +// Demo mode altitude band (FS2 DemoModeParam1/2 in world-unit hi-byte). +#define DEMO_ALT_LOW_Q1616 ( 40 << 16) +#define DEMO_ALT_HIGH_Q1616 (240 << 16) +#define DEMO_THROTTLE_TRIM 1 // step in 0..255 throttle + +// Flight envelope (chunk5 CheckFlightEnvelope). +#define ENVELOPE_PITCH_LIMIT_BYTE 15 +#define ENVELOPE_BANK_LIMIT_BYTE 22 +#define ENVELOPE_VNE_KTS 155 +#define KTS_PER_SPEED_UNIT 100 + +// Reality mode (chunk3 RealityModeHook). +#define REALITY_TICK_GATE 10 +#define REALITY_ALT_BAND_WORLD 100 // worldY < 100 worldunits + + +// Q8.8 helpers. +static int16_t q88Add(int16_t a, int16_t b); +static int16_t q88Lerp(int16_t a, int16_t b, uint8_t k_q8); +static int16_t q88Mul(int16_t a, int16_t b); + +// Q16.16 helpers. +static int32_t q1616FromInt(int32_t worldUnits); +static int32_t q1616FromQ88(int16_t v_q88); + + +// Forward declarations (alphabetised, per CLAUDE.md). +static uint8_t addByteAngleQ88(uint8_t a, int16_t delta_q88); +static void autopilotDemo(AircraftT *ac); +static bool inEnvelope(const AircraftT *ac); +static void recenterAnchor(AircraftT *ac); +static int32_t sceneryFromLocal(int32_t origin, int32_t local_q1616); +static void stepFlight(AircraftT *ac, WindStateT *wind); +static void stepSlew(AircraftT *ac); + + +// Add a Q8.8 angular delta to a uint8_t byte angle. Uses the integer +// part of the rate (high byte) as the per-frame byte-angle delta. +static uint8_t addByteAngleQ88(uint8_t a, int16_t delta_q88) { + int high = (int)(int16_t)delta_q88 >> AC_RATE_FRACT_BITS; + int v = (int)a + high; + v &= 0xFF; + return (uint8_t)v; +} + + +void aircraftAddThrottle(AircraftT *ac, int delta) { + int v = (int)ac->throttle + delta; + if (v < 0) { + v = 0; + } + if (v > 255) { + v = 255; + } + ac->throttle = (uint8_t)v; +} + + +void aircraftDecayRudder(AircraftT *ac, uint8_t k_q8) { + // rudder *= k. (signed * uint8) >> 8. + int v = ((int)(int8_t)ac->rudder * (int)k_q8) >> 8; + ac->rudder = (int8_t)v; +} + + +void aircraftDecayYokeHoriz(AircraftT *ac, uint8_t k_q8) { + int v = ((int)(int8_t)ac->yokeHoriz * (int)k_q8) >> 8; + ac->yokeHoriz = (int8_t)v; +} + + +void aircraftDecayYokeVert(AircraftT *ac, uint8_t k_q8) { + int v = ((int)(int8_t)ac->yokeVert * (int)k_q8) >> 8; + ac->yokeVert = (int8_t)v; +} + + +void aircraftInit(AircraftT *ac) { + // FS2 initial position from chunk5 `InitialZeroPageData` at + // zero-page offsets $5C/$5D = $011F (= 287 scenery units east) + // and $64/$65 = $0324 (= 804 scenery units north). Per the FS2 + // manual: this is the "preset location above Lake Michigan" + // when no scenery disk is inserted. The base FS2 disk has + // built-in scenery for FIVE areas (Chicago/Meigs, Los Angeles, + // Seattle, New York, and the WW1 Ace training field). To + // teleport to each from the boot position, set: + // Chicago/Meigs worldX=1548 worldZ=4805 (COM record X=1188897, Z=3690293) + // Chicago Midway worldX=1110 worldZ=4974 + // New York Kennedy worldX=1196 worldZ=4294 + // LA International worldX= 599 worldZ=5570 + // Seattle Tacoma worldX=2912 worldZ=4120 + // Convert 16-bit scenery units to port metres via + // AC_SCENERY_UNITS_PER_METRE = 3: 287/3 = ~96m, 804/3 = ~268m. + ac->sceneryOriginX = 0; + ac->sceneryOriginY = 0; + ac->sceneryOriginZ = 0; + ac->worldX = q1616FromInt(96); + ac->worldY = q1616FromInt(5); + ac->worldZ = q1616FromInt(268); + ac->pitch = 0; + ac->bank = 0; + ac->yaw = 0; + ac->pitchRate = 0; + ac->bankRate = 0; + ac->yawRate = 0; + ac->forwardSpeed = 0; + ac->climbRate = 0; + ac->yokeVert = 0; + ac->yokeHoriz = 0; + ac->rudder = 0; + ac->throttle = 0; + ac->flaps = 0; + ac->trim = 0; + ac->mixture = 128; // FS2 default mid-rich + ac->onGround = true; + ac->stalled = false; + ac->envelopeWarning = false; + ac->crashed = false; + ac->crashType = CRASH_NONE; + ac->slewMode = false; + ac->showSlewDigits = true; + ac->slewPitchRate = 0; + ac->slewRollRate = 0; + ac->slewYawRate = 0; + ac->slewAltRate = 0; + ac->demoMode = false; + ac->demoState = 2; + ac->editMode = false; + ac->realityMode = false; + ac->reliabilityFactor = 90; + ac->realityTickCounter = 0; + ac->failedInstruments = 0; + ac->engineFaults = 0; + ac->lightsOn = false; + ac->carbHeatOn = false; + ac->adfMode = false; // chunk4 ADFMode init = 0 = VOR2 mode + ac->magnetos = 3; // BOTH by default + ac->paused = false; + ac->fuelLeft = 255; // full tanks + ac->fuelRight = 255; + ac->monochrome = false; + ac->radarView = false; + ac->radarZoom = (int16_t)(4 * AC_RATE_FRACT_ONE); + ac->viewDirection = VIEW_FORWARD; +} + + +// Convert a Q16.16 metre offset to scenery units (FS2 feet-ish), then +// add to the absolute scenery coordinate. The Q16.16 integer part is +// metres; multiply by AC_SCENERY_UNITS_PER_METRE to scale. +static int32_t sceneryFromLocal(int32_t origin, int32_t local_q1616) { + int32_t metres = local_q1616 >> AC_POS_FRACT_BITS; + return origin + metres * AC_SCENERY_UNITS_PER_METRE; +} + + +int32_t aircraftSceneryX(const AircraftT *ac) { + return sceneryFromLocal(ac->sceneryOriginX, ac->worldX); +} + + +int32_t aircraftSceneryY(const AircraftT *ac) { + return sceneryFromLocal(ac->sceneryOriginY, ac->worldY); +} + + +int32_t aircraftSceneryZ(const AircraftT *ac) { + return sceneryFromLocal(ac->sceneryOriginZ, ac->worldZ); +} + + +// Recenter the local coords if they're getting close to int32 head- +// room. Slides the anchor by the integer-metre chunk we drop and +// keeps the Q16.16 fractional bits intact, so the flight integrator +// sees no discontinuity. +static void recenterAnchor(AircraftT *ac) { + if (ac->worldX > AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE + || ac->worldX < -AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE) { + int32_t metres = ac->worldX >> AC_POS_FRACT_BITS; + ac->sceneryOriginX += metres * AC_SCENERY_UNITS_PER_METRE; + ac->worldX -= metres * AC_POS_FRACT_ONE; + } + if (ac->worldZ > AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE + || ac->worldZ < -AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE) { + int32_t metres = ac->worldZ >> AC_POS_FRACT_BITS; + ac->sceneryOriginZ += metres * AC_SCENERY_UNITS_PER_METRE; + ac->worldZ -= metres * AC_POS_FRACT_ONE; + } + // Y rarely needs recentering (altitude band is bounded), but + // handle it consistently for symmetry. + if (ac->worldY > AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE + || ac->worldY < -AC_RECENTER_THRESHOLD_M * AC_POS_FRACT_ONE) { + int32_t metres = ac->worldY >> AC_POS_FRACT_BITS; + ac->sceneryOriginY += metres * AC_SCENERY_UNITS_PER_METRE; + ac->worldY -= metres * AC_POS_FRACT_ONE; + } +} + + +void aircraftStep(AircraftT *ac, WindStateT *wind) { + if (ac->editMode) { + return; + } + if (ac->slewMode) { + stepSlew(ac); + } else { + if (ac->demoMode) { + autopilotDemo(ac); + } + stepFlight(ac, wind); + } + recenterAnchor(ac); +} + + +void aircraftSyncCamera(const AircraftT *ac, CameraT *cam) { + // Aircraft and camera share the same Q16.16 / Q8.8 conventions + // so the copy is a direct integer assignment. + cam->worldX = ac->worldX; + cam->worldY = ac->worldY; + cam->worldZ = ac->worldZ; + cam->pitch = ac->pitch; + cam->bank = ac->bank; + cam->yaw = ac->yaw; + cam->forwardSpeed = ac->forwardSpeed; + + // FS2 ViewDirection: discrete yaw / pitch offsets applied to the + // camera but not to the airframe. + switch (ac->viewDirection) { + case VIEW_RIGHT: cam->yaw = (uint8_t)(cam->yaw + 64); break; + case VIEW_BACK: cam->yaw = (uint8_t)(cam->yaw + 128); break; + case VIEW_LEFT: cam->yaw = (uint8_t)(cam->yaw + 192); break; + case VIEW_DOWN: cam->pitch = (uint8_t)(cam->pitch + 64); break; + case VIEW_FORWARD: + default: break; + } + cameraUpdate(cam); +} + + +void aircraftTeleport(AircraftT *ac, int32_t sx, int32_t sy, int32_t sz) { + ac->sceneryOriginX = sx; + ac->sceneryOriginY = sy; + ac->sceneryOriginZ = sz; + ac->worldX = 0; + ac->worldY = 0; + ac->worldZ = 0; +} + + +void aircraftToggleDemo(AircraftT *ac) { + ac->demoMode = !ac->demoMode; + ac->demoState = 2; +} + + +// FS2 chunk5 `InitInstrumentSaveBuffers` reserves $FC00+ for a snapshot +// of aircraft state captured on edit-mode entry, then restored on exit +// (so the user can twiddle parameters without crashing the flight). +// We keep an in-process snapshot on the AircraftT itself rather than a +// fixed RAM region. +static AircraftT editSavedState; +static bool editSavedValid; + + +void aircraftToggleEdit(AircraftT *ac) { + if (!ac->editMode) { + // Entering edit: capture pre-edit snapshot. + editSavedState = *ac; + editSavedValid = true; + ac->editMode = true; + } else { + // Leaving edit: restore the snapshot so any tweaks to + // throttle/altitude/etc revert. + if (editSavedValid) { + bool wasEdit = false; + AircraftT pre = editSavedState; + // Preserve the current edit-mode flag so we + // exit edit cleanly even on restore. + pre.editMode = wasEdit; + editSavedValid = false; + *ac = pre; + } else { + ac->editMode = false; + } + } +} + + +void aircraftToggleReality(AircraftT *ac) { + ac->realityMode = !ac->realityMode; + ac->realityTickCounter = 0; + // Toggling resets accumulated failures so the user can retry + // without restarting the sim. + ac->failedInstruments = 0; + ac->engineFaults = 0; +} + + +void aircraftToggleSlew(AircraftT *ac) { + ac->slewMode = !ac->slewMode; + ac->slewPitchRate = 0; + ac->slewRollRate = 0; + ac->slewYawRate = 0; + ac->slewAltRate = 0; + ac->forwardSpeed = 0; + ac->climbRate = 0; + ac->pitchRate = 0; + ac->bankRate = 0; + ac->yawRate = 0; +} + + +// FS2 chunk2 `DemoMode64K`. Auto-pilot: keep yoke positive for a gentle +// climb, trim throttle to hold altitude in [DEMO_ALT_LOW, DEMO_ALT_HIGH], +// centre the yoke between turn ticks. `demoState` mirrors FS2's +// `DemoModeParam3`. +static void autopilotDemo(AircraftT *ac) { + // Climb if yoke is too far down (FS2 `YokeVertPos < $18`). + if ((int8_t)ac->yokeVert < 24) { + int v = (int8_t)ac->yokeVert + 6; // ~0.05 in Q1.7 + if (v > 127) { + v = 127; + } + ac->yokeVert = (int8_t)v; + } + + // Throttle trim based on altitude band. + if (ac->worldY < DEMO_ALT_LOW_Q1616) { + aircraftAddThrottle(ac, DEMO_THROTTLE_TRIM); + } else if (ac->worldY > DEMO_ALT_HIGH_Q1616) { + aircraftAddThrottle(ac, -DEMO_THROTTLE_TRIM); + } + + // FS2 alternates between "decide turn" and "centre yoke" via + // `DemoModeParam3`. Without a station database we just nudge the + // yoke back to neutral when bank exceeds a small dead band. + if (ac->demoState == 2) { + ac->demoState = 1; + int8_t bankSigned = (int8_t)ac->bank; + if (bankSigned > 6) { + ac->yokeHoriz = -51; // ~ -0.4 + } else if (bankSigned < -6) { + ac->yokeHoriz = 51; + } else { + ac->yokeHoriz = (int8_t)((int)(int8_t)ac->yokeHoriz / 2); + } + } else { + ac->demoState = 2; + ac->yokeHoriz = (int8_t)((int)(int8_t)ac->yokeHoriz / 2); + } + ac->rudder = 0; +} + + +// FS2 chunk5 `CheckFlightEnvelope` (L7889): pitch / bank / VNE bounds. +static bool inEnvelope(const AircraftT *ac) { + int pitchSigned = (int)(int8_t)ac->pitch; + int bankSigned = (int)(int8_t)ac->bank; + if (pitchSigned >= ENVELOPE_PITCH_LIMIT_BYTE) { + return false; + } + if (pitchSigned < -ENVELOPE_PITCH_LIMIT_BYTE) { + return false; + } + if (bankSigned >= ENVELOPE_BANK_LIMIT_BYTE) { + return false; + } + if (bankSigned < -ENVELOPE_BANK_LIMIT_BYTE) { + return false; + } + // forwardSpeed (Q8.8) * 100 -> kts. >> 8 to drop the fraction. + int kts = ((int)ac->forwardSpeed * KTS_PER_SPEED_UNIT) >> AC_RATE_FRACT_BITS; + if (kts > ENVELOPE_VNE_KTS) { + return false; + } + return true; +} + + +// 32-bit signed Q16.16 helper: world units -> Q16.16. +static int32_t q1616FromInt(int32_t worldUnits) { + return worldUnits * AC_POS_FRACT_ONE; +} + + +// Convert Q8.8 -> Q16.16. 8 extra fractional bits -> shift left 8. +static int32_t q1616FromQ88(int16_t v_q88) { + return ((int32_t)v_q88) << 8; +} + + +// 16-bit add with saturation. Used where the result would otherwise +// overflow the int16_t range. +static int16_t q88Add(int16_t a, int16_t b) { + int32_t v = (int32_t)a + (int32_t)b; + if (v > 32767) { + return 32767; + } + if (v < -32768) { + return -32768; + } + return (int16_t)v; +} + + +// Linear interpolate in Q8.8: a + ((b - a) * k_q8) >> 8. +static int16_t q88Lerp(int16_t a, int16_t b, uint8_t k_q8) { + int32_t diff = (int32_t)b - (int32_t)a; + int32_t step = (diff * (int32_t)k_q8) >> 8; + return q88Add(a, (int16_t)step); +} + + +// Q8.8 multiply: (a * b) >> 8. +static int16_t q88Mul(int16_t a, int16_t b) { + int32_t prod = (int32_t)a * (int32_t)b; + return (int16_t)(prod >> 8); +} + + +static void stepFlight(AircraftT *ac, WindStateT *wind) { + // 1. Yoke / rudder -> rate accumulators (Q8.8). + // pitchTarget = (yokeVert + trim)(int8) * MAX_PITCH_RATE / 127 + // (using the >> 7 form keeps int8 input valued at full scale). + // Trim biases yokeVert -- chunk5 RefreshElevatorIndicator + // accumulates trim into the elevator command. + int yokePlusTrim = (int)(int8_t)ac->yokeVert + (int)(int8_t)ac->trim; + if (yokePlusTrim > 127) yokePlusTrim = 127; + if (yokePlusTrim < -127) yokePlusTrim = -127; + int16_t pitchTarget = (int16_t)(((int32_t)yokePlusTrim * MAX_PITCH_RATE_Q88) / 127); + int16_t bankTarget = (int16_t)(((int32_t)(int8_t)ac->yokeHoriz * MAX_BANK_RATE_Q88) / 127); + ac->pitchRate = q88Lerp(ac->pitchRate, pitchTarget, INPUT_SMOOTH_K); + ac->bankRate = q88Lerp(ac->bankRate, bankTarget, INPUT_SMOOTH_K); + + // Auto-coordination: bank-induced yaw scales by sin(bank) and + // forward speed. Mirrors chunk5 UpdateAutoTrimAndYaw's coupling. + int16_t bankSin = math6502Sin(ac->bank); // Q1.15 + int16_t coordYaw = fs2ScaleByAX(ac->forwardSpeed, bankSin); // Q8.8 * Q1.15 / 32768 + coordYaw = (int16_t)(((int32_t)coordYaw * AUTO_COORD_K) >> 8); + int16_t rudderRate = (int16_t)(((int32_t)(int8_t)ac->rudder * MAX_YAW_RATE_Q88) / 127); + int16_t yawTarget = q88Add(rudderRate, (int16_t)((int32_t)coordYaw << 8)); // coord -> Q8.8 + ac->yawRate = q88Lerp(ac->yawRate, yawTarget, INPUT_SMOOTH_K); + + // 2. Apply rates to orientation (byte-angle wrap). + ac->pitch = addByteAngleQ88(ac->pitch, ac->pitchRate); + ac->bank = addByteAngleQ88(ac->bank, ac->bankRate); + ac->yaw = addByteAngleQ88(ac->yaw, ac->yawRate); + + // 3. Engine response and drag. Engine is killed if magnetos + // are OFF and effective throttle drops to 0. + uint8_t effectiveThrottle = ac->throttle; + if (ac->magnetos == 0) { + effectiveThrottle = 0; + } else if (ac->magnetos == 1 || ac->magnetos == 2) { + effectiveThrottle = (uint8_t)((int)effectiveThrottle * 4 / 5); // single-mag = 80% + } + // Carb heat icing: ~7% power loss. + if (ac->carbHeatOn) { + effectiveThrottle = (uint8_t)((int)effectiveThrottle * 93 / 100); + } + // Out of fuel: also kills the engine. + if (ac->fuelLeft == 0 && ac->fuelRight == 0) { + effectiveThrottle = 0; + } + int16_t targetSpeed = (int16_t)(((int32_t)effectiveThrottle * MAX_FORWARD_SPEED_Q88) / 255); + int16_t engineDelta = (int16_t)((((int32_t)targetSpeed - ac->forwardSpeed) * ENGINE_SMOOTH_K) >> 8); + int16_t dragDelta = (int16_t)(((int32_t)ac->forwardSpeed * DRAG_K) >> 8); + ac->forwardSpeed = q88Add(ac->forwardSpeed, q88Add(engineDelta, (int16_t)-dragDelta)); + if (ac->forwardSpeed < 0) { + ac->forwardSpeed = 0; + } + // Fuel burn proportional to throttle. At max throttle drains + // 255 units in ~30 minutes (~108000 frames @ 60fps): roughly + // throttle / 256 every 16 frames. Alternate L/R tanks every + // 256 frames so both drain over the flight. + static int fuelBurnTick; + fuelBurnTick++; + if ((fuelBurnTick & 0x0F) == 0 && ac->throttle > 0) { + bool drainLeft = ((fuelBurnTick & 0xFF) < 128); + if (drainLeft) { + if (ac->fuelLeft > 0) ac->fuelLeft--; + else if (ac->fuelRight > 0) ac->fuelRight--; + } else { + if (ac->fuelRight > 0) ac->fuelRight--; + else if (ac->fuelLeft > 0) ac->fuelLeft--; + } + } + + // 4. Lift / gravity. climbRate ~= forwardSpeed * sin(pitch) - g. + int16_t pitchSin = math6502Sin(ac->pitch); // Q1.15 + int16_t lift_q88 = fs2ScaleByAX(ac->forwardSpeed, pitchSin); // Q8.8 + // LIFT_FACTOR is 1.0; preserved for tunability. + lift_q88 = q88Mul(lift_q88, LIFT_FACTOR_Q88); + ac->climbRate = q88Add(lift_q88, -GRAVITY_PER_FRAME_Q88); + + // Stall: low speed + nose-up pitch. + ac->stalled = false; + if (ac->forwardSpeed < STALL_SPEED_Q88 && pitchSin > STALL_PITCHSIN_Q15) { + ac->stalled = true; + ac->climbRate = q88Add(ac->climbRate, STALL_CLIMB_KICK_Q88); + ac->pitchRate = q88Add(ac->pitchRate, STALL_PITCH_KICK_Q88); + } + + // 5. World-frame motion. Forward = cos(pitch) along (sin yaw, + // cos yaw); altitude moves with climbRate. + int16_t yawSin = math6502Sin(ac->yaw); + int16_t yawCos = math6502Cos(ac->yaw); + int16_t pitchCos = math6502Cos(ac->pitch); + int16_t horizSpeed = fs2ScaleByAX(ac->forwardSpeed, pitchCos); // Q8.8 + int16_t dx_q88 = fs2ScaleByAX(horizSpeed, yawSin); // Q8.8 + int16_t dz_q88 = fs2ScaleByAX(horizSpeed, yawCos); // Q8.8 + + ac->worldX += q1616FromQ88(dx_q88); + ac->worldZ += q1616FromQ88(dz_q88); + ac->worldY += q1616FromQ88(ac->climbRate); + + // 6. Wind. Chunk2 `ComputeWindComponents` runs every other + // frame; `ApplyWind` runs every frame. + if (wind != NULL) { + wind->updateCounter = (uint8_t)(wind->updateCounter + 1); + if ((wind->updateCounter & 0x01) == 0) { + // FS2's altitude16 is the high half of a 32-bit + // position word -- our worldY's high 16 bits. + int hi = (int)(ac->worldY >> AC_POS_FRACT_BITS); + if (hi < 0) { + hi = 0; + } + if (hi > 65535) { + hi = 65535; + } + windCompute(wind, (uint16_t)hi); + } + int32_t windDx; + int32_t windDz; + windApply(wind, ac->onGround, &windDx, &windDz); + ac->worldX += windDx; + ac->worldZ += windDz; + } + + // 7. Ground constraint and crash detection. + if (ac->worldY <= 0) { + if (!ac->onGround && ac->climbRate < CRASH_VLIMIT_Q88) { + ac->crashed = true; + ac->crashType = CRASH_GROUND; + } + ac->worldY = 0; + ac->onGround = true; + ac->climbRate = 0; + ac->forwardSpeed = (int16_t)(((int32_t)ac->forwardSpeed * GROUND_FRICTION_K) >> 8); + if (ac->forwardSpeed < LIFTOFF_SPEED_Q88) { + ac->pitch = 0; + ac->bank = 0; + } + } else if (ac->worldY > GROUND_BOUNCE_Q1616) { + ac->onGround = false; + } + + // 8. Flight envelope. + ac->envelopeWarning = !inEnvelope(ac); + + // 9. Reality mode failure roll (chunk3 RealityModeHook L626). + // When the roll trips, dispatch one of chunk3's eight failure + // procs by index: 0/2/3/5/6/7 clear an instrument bit, 1/4 OR + // an engine-fault bit. The aircraft keeps flying; the failed + // gauge or engine fault is rendered separately by the panel. + if (ac->realityMode && !ac->crashed) { + if (ac->realityTickCounter == 0) { + ac->realityTickCounter = REALITY_TICK_GATE; + int altUnits = (int)(ac->worldY >> AC_POS_FRACT_BITS); + if (altUnits < REALITY_ALT_BAND_WORLD) { + // Cheap pseudo-random from position + tick. + int roll = ((int)(ac->worldX >> 8) * 17 + + (int)(ac->worldZ >> 8) * 31) & 0xFF; + if (roll < (int)ac->reliabilityFactor && roll < altUnits) { + // Pick one of the eight FailureProcTable + // slots. Match chunk3's "(UpdateCounter + + // altLow) & $0E" -- shift by one because + // the table is 2-byte addresses. + int slot = ((roll + altUnits) & 0x0E) >> 1; + switch (slot) { + case 0: ac->failedInstruments |= AC_FAIL_AIRSPEED; break; + case 1: ac->engineFaults |= AC_ENG_FAULT_LEFT; break; + case 2: ac->failedInstruments |= AC_FAIL_VSI; break; + case 3: ac->failedInstruments |= AC_FAIL_ALTIMETER; break; + case 4: ac->engineFaults |= AC_ENG_FAULT_RIGHT; break; + case 5: ac->failedInstruments |= AC_FAIL_TURN_COORD; break; + case 6: ac->failedInstruments |= AC_FAIL_ATTITUDE; break; + case 7: ac->failedInstruments |= AC_FAIL_HEADING; break; + default: break; + } + } + } + } else { + ac->realityTickCounter--; + } + } +} + + +// FS2 chunk5 ApplySlewDeltas (L7736). Yoke H/V translate the position +// directly; the four slew rate accumulators translate altitude and +// orientation. Flight model is bypassed. +static void stepSlew(AircraftT *ac) { + // Position. yoke int8 << 4 -> int16, sign-extended into Q16.16. + int32_t dxYoke = ((int32_t)(int8_t)ac->yokeHoriz) << SLEW_YOKE_SHIFT; + int32_t dzYoke = ((int32_t)(int8_t)ac->yokeVert ) << SLEW_YOKE_SHIFT; + ac->worldX += dxYoke << (AC_POS_FRACT_BITS - 8); + // FS2 inverts the V axis (#$00 sec sbc YokeVertPos) so pulling + // back moves the aircraft south. + ac->worldZ -= dzYoke << (AC_POS_FRACT_BITS - 8); + ac->worldY += (int32_t)ac->slewAltRate * SLEW_ALT_RATE_Q1616_PER_TICK; + if (ac->worldY < 0) { + ac->worldY = 0; + ac->onGround = true; + } else if (ac->worldY > GROUND_BOUNCE_Q1616) { + ac->onGround = false; + } + + // Orientation: each slew rate adds 1 byte-angle / tick. + ac->pitch = (uint8_t)(ac->pitch + ac->slewPitchRate * SLEW_ANGLE_RATE_PER_TICK); + ac->bank = (uint8_t)(ac->bank + ac->slewRollRate * SLEW_ANGLE_RATE_PER_TICK); + ac->yaw = (uint8_t)(ac->yaw + ac->slewYawRate * SLEW_ANGLE_RATE_PER_TICK); + + ac->forwardSpeed = 0; + ac->climbRate = 0; + ac->stalled = false; + ac->envelopeWarning = false; +} diff --git a/port/src/apple2hires.c b/port/src/apple2hires.c new file mode 100644 index 0000000..5e07eb2 --- /dev/null +++ b/port/src/apple2hires.c @@ -0,0 +1,83 @@ +// Apple II hires decoder. Layout: 8192 bytes encode 280x192 pixels +// via the well-known interleaved scanline pattern: +// +// byteOffset(y, byteX) = (y & 7) * $400 +// + ((y >> 3) & 7) * $80 +// + (y >> 6) * $28 +// + byteX +// +// Each byte holds 7 pixels (bit 0 = leftmost). Bit 7 selects palette +// in NTSC mode; we ignore it because the modernized port renders +// monochrome. + +#include +#include +#include "apple2hires.h" + + +static int hiresOffsetForRow(int y) { + return (y & 7) * 0x400 + ((y >> 3) & 7) * 0x80 + (y >> 6) * 0x28; +} + + +bool apple2HiresLoadFile(const char *path, HiresPageT *out) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + return false; + } + + uint8_t raw[HIRES_BYTES]; + size_t got = fread(raw, 1, sizeof(raw), f); + fclose(f); + if (got != sizeof(raw)) { + return false; + } + + memset(out->bits, 0, sizeof(out->bits)); + + for (int y = 0; y < HIRES_HEIGHT; y++) { + int rowBase = hiresOffsetForRow(y); + for (int byteX = 0; byteX < 40; byteX++) { + uint8_t b = raw[rowBase + byteX]; + for (int bit = 0; bit < 7; bit++) { + int pixelX = byteX * 7 + bit; + if (pixelX >= HIRES_WIDTH) { + break; + } + out->bits[y * HIRES_WIDTH + pixelX] = (b & (1 << bit)) ? 1 : 0; + } + } + } + return true; +} + + +void apple2HiresBlit(const HiresPageT *page, + int16_t srcTopRow, + int16_t rows, + FramebufferT *fb, + int16_t dstX, + int16_t dstY, + ColorE litColor, + ColorE bgColor, + bool paintBackground) { + if (srcTopRow < 0) { + srcTopRow = 0; + } + if (srcTopRow + rows > HIRES_HEIGHT) { + rows = (int16_t)(HIRES_HEIGHT - srcTopRow); + } + + for (int16_t r = 0; r < rows; r++) { + int srcY = srcTopRow + r; + int dY = dstY + r; + for (int16_t c = 0; c < HIRES_WIDTH; c++) { + uint8_t bit = page->bits[srcY * HIRES_WIDTH + c]; + if (bit) { + framebufferSetPixel(fb, (int16_t)(dstX + c), (int16_t)dY, litColor); + } else if (paintBackground) { + framebufferSetPixel(fb, (int16_t)(dstX + c), (int16_t)dY, bgColor); + } + } + } +} diff --git a/port/src/audio.c b/port/src/audio.c new file mode 100644 index 0000000..066a49b --- /dev/null +++ b/port/src/audio.c @@ -0,0 +1,258 @@ +// Software-synth audio driver. +// +// Engine: a sawtooth at firing-frequency (4-cyl, 2-stroke ratio) with +// gentle attack/release. Stall horn: a square wave at 800 Hz mixed in +// when the aircraft is stalled. + +#include +#include +#include +#include "audio.h" +#include "math6502.h" + + +#define AUDIO_SAMPLE_RATE 22050 +#define AUDIO_BUFFER 512 +#define ENGINE_BASE_HZ 30.0f +#define ENGINE_MAX_HZ 130.0f +#define STALL_HZ 800.0f +#define GUN_BURST_FRAMES 2200 // ~0.10 s +#define BOMB_FALL_FRAMES 9000 // ~0.40 s +#define CRASH_FRAMES 13000 // ~0.60 s +#define GUN_BASE_HZ 180.0f +#define BOMB_HIGH_HZ 420.0f +#define BOMB_LOW_HZ 80.0f +#define CRASH_AMP 0.5f + + +typedef struct AudioStateT { + SDL_AudioDeviceID device; + float enginePhase; + float stallPhase; + float gunPhase; + float bombPhase; + float crashPhase; + float engineFreq; + float engineAmp; + bool stalled; + int gunRemaining; // samples left in burst + int bombRemaining; + int crashRemaining; +} AudioStateT; + + +static AudioStateT audio; + + +static void sdlAudioCallback(void *userdata, Uint8 *stream, int bytes); + + +static void sdlAudioCallback(void *userdata, Uint8 *stream, int bytes) { + (void)userdata; + int16_t *out = (int16_t *)stream; + int frames = bytes / (int)sizeof(int16_t); + + float engineFreq = audio.engineFreq; + float engineAmp = audio.engineAmp; + float stallAmp = audio.stalled ? 0.4f : 0.0f; + + for (int i = 0; i < frames; i++) { + // Sawtooth in [-1, +1]. + audio.enginePhase += engineFreq / (float)AUDIO_SAMPLE_RATE; + if (audio.enginePhase >= 1.0f) { + audio.enginePhase -= 1.0f; + } + float saw = audio.enginePhase * 2.0f - 1.0f; + + // Stall horn (square wave). + audio.stallPhase += STALL_HZ / (float)AUDIO_SAMPLE_RATE; + if (audio.stallPhase >= 1.0f) { + audio.stallPhase -= 1.0f; + } + float square = audio.stallPhase < 0.5f ? 1.0f : -1.0f; + + // Gun fire: rapid sawtooth burst with linear decay. + float gunSample = 0.0f; + if (audio.gunRemaining > 0) { + audio.gunPhase += GUN_BASE_HZ / (float)AUDIO_SAMPLE_RATE; + if (audio.gunPhase >= 1.0f) { + audio.gunPhase -= 1.0f; + } + float t = (float)audio.gunRemaining / (float)GUN_BURST_FRAMES; + gunSample = (audio.gunPhase * 2.0f - 1.0f) * 0.6f * t; + audio.gunRemaining--; + } + + // Bomb drop: pitch slides high -> low over the envelope. + float bombSample = 0.0f; + if (audio.bombRemaining > 0) { + float t = (float)audio.bombRemaining / (float)BOMB_FALL_FRAMES; + float freq = BOMB_LOW_HZ + (BOMB_HIGH_HZ - BOMB_LOW_HZ) * t; + audio.bombPhase += freq / (float)AUDIO_SAMPLE_RATE; + if (audio.bombPhase >= 1.0f) { + audio.bombPhase -= 1.0f; + } + bombSample = (audio.bombPhase < 0.5f ? 0.5f : -0.5f) * t; + audio.bombRemaining--; + } + + // Crash impact: deep noise burst with fast decay. + float crashSample = 0.0f; + if (audio.crashRemaining > 0) { + audio.crashPhase = audio.crashPhase * 1664525.0f + 1013904223.0f; + float noise = ((int)(audio.crashPhase) >> 8 & 0xFFFF) / 32768.0f - 1.0f; + float t = (float)audio.crashRemaining / (float)CRASH_FRAMES; + crashSample = noise * CRASH_AMP * t; + audio.crashRemaining--; + } + + float sample = saw * engineAmp + square * stallAmp + gunSample + bombSample + crashSample; + if (sample > 1.0f) { + sample = 1.0f; + } + if (sample < -1.0f) { + sample = -1.0f; + } + out[i] = (int16_t)(sample * 12000.0f); + } +} + + +bool audioInit(void) { + SDL_AudioSpec want; + SDL_AudioSpec got; + SDL_zero(want); + want.freq = AUDIO_SAMPLE_RATE; + want.format = AUDIO_S16SYS; + want.channels = 1; + want.samples = AUDIO_BUFFER; + want.callback = sdlAudioCallback; + + audio.device = SDL_OpenAudioDevice(NULL, 0, &want, &got, 0); + if (audio.device == 0) { + return false; + } + audio.engineFreq = ENGINE_BASE_HZ; + audio.engineAmp = 0.0f; + audio.stalled = false; + SDL_PauseAudioDevice(audio.device, 0); + return true; +} + + +void audioShutdown(void) { + if (audio.device != 0) { + SDL_CloseAudioDevice(audio.device); + audio.device = 0; + } +} + + +void audioTriggerBomb(void) { + if (audio.device == 0) { + return; + } + SDL_LockAudioDevice(audio.device); + audio.bombRemaining = BOMB_FALL_FRAMES; + audio.bombPhase = 0.0f; + SDL_UnlockAudioDevice(audio.device); +} + + +void audioTriggerCrash(void) { + if (audio.device == 0) { + return; + } + SDL_LockAudioDevice(audio.device); + audio.crashRemaining = CRASH_FRAMES; + SDL_UnlockAudioDevice(audio.device); +} + + +void audioTriggerGun(void) { + if (audio.device == 0) { + return; + } + SDL_LockAudioDevice(audio.device); + audio.gunRemaining = GUN_BURST_FRAMES; + SDL_UnlockAudioDevice(audio.device); +} + + +// AC_RATE_FRACT_ONE * 1.6 = 410, the FS2 max forward speed in Q8.8. +// See MAX_FORWARD_SPEED_Q88 in aircraft.c. +#define AUDIO_SPEED_FULL_Q88 410 + + +void audioUpdate(const AircraftT *ac) { + if (audio.device == 0) { + return; + } + // Compute engine frequency / amplitude in fixed-point (Q8.8 + // shares with aircraft state). The float boundary is at the + // single SDL_LockAudioDevice store; the audio synth itself + // still runs in float because SDL streams float samples. + // speedFraction : Q8.8 from 0..256 (= 0.0..1.0) + int speedT = (int)ac->forwardSpeed * 256 / AUDIO_SPEED_FULL_Q88; + if (speedT < 0) speedT = 0; + if (speedT > 256) speedT = 256; + // freqQ88 = base + speedT * (max - base) / 256 + int32_t freqRange = ENGINE_MAX_HZ - ENGINE_BASE_HZ; + int32_t freqQ88 = ((int32_t)ENGINE_BASE_HZ << 8) + + (int32_t)speedT * freqRange; // Q8.8 + + // Engine fault state in Q8.8 (1.0 = $0100). chunk3 + // SetEngineFault01/23 OR-in fault bits; we map each fault to + // a multiplicative penalty (0.7 = 179/256) and a wobble + // amount (0.06 = 15/256). + int32_t penaltyQ88 = 0x100; + int32_t wobbleAmpQ88 = 0; // Q8.8 + if (ac->engineFaults & AC_ENG_FAULT_LEFT) { + penaltyQ88 = (penaltyQ88 * 179) >> 8; // 0.7 + wobbleAmpQ88 += 15; + } + if (ac->engineFaults & AC_ENG_FAULT_RIGHT) { + penaltyQ88 = (penaltyQ88 * 179) >> 8; + wobbleAmpQ88 += 15; + } + if (ac->carbHeatOn) { + penaltyQ88 = (penaltyQ88 * 238) >> 8; // 0.93 + } + // Magneto state: 0=OFF kills, 1/2=single mag => 80%, 4=START. + if (ac->magnetos == 0) { + penaltyQ88 = 0; + } else if (ac->magnetos == 1 || ac->magnetos == 2) { + penaltyQ88 = (penaltyQ88 * 205) >> 8; // 0.8 + wobbleAmpQ88 += 8; // 0.03 + } + + // Phase-modulated wobble using FS2's Q1.15 sin table. Mirrors + // chunk5's pattern for engine stutter -- step counter modulo + // 256 indexes the byte-angle, the sin output is multiplied by + // wobbleAmpQ88 and added as a fractional offset to the freq. + static uint8_t wobblePhase; + wobblePhase++; + int32_t wobbleQ88 = 0; + if (wobbleAmpQ88 > 0) { + int16_t s = math6502Sin(wobblePhase); // Q1.15 in [-32767, 32767] + // Scale by Q8.8 amp: wobbleQ88 = (s * amp) >> 15 + wobbleQ88 = ((int32_t)s * wobbleAmpQ88) >> 15; + } + + // Final freq = freqQ88 * (1 + wobbleQ88) / 256, then to Hz as + // float for the synth. + int32_t freqWithWobble = freqQ88 + ((freqQ88 * wobbleQ88) >> 8); + float freqHz = (float)freqWithWobble * (1.0f / 256.0f); + // Amplitude: (0.05 + throttle/255 * 0.4) * penalty + // In Q8.8: ampQ88 = (0x100 * 13/256) + throttle * 102 / 256 + // 0.05 = 13/256, 0.4 = 102/256. + int32_t ampQ88 = 13 + ((int32_t)ac->throttle * 102) / 256; // 0..115 (0.05..0.45) + ampQ88 = (ampQ88 * penaltyQ88) >> 8; + float ampF = (float)ampQ88 * (1.0f / 256.0f); + + SDL_LockAudioDevice(audio.device); + audio.engineFreq = freqHz; + audio.engineAmp = ampF; + audio.stalled = ac->stalled; + SDL_UnlockAudioDevice(audio.device); +} diff --git a/port/src/camera.c b/port/src/camera.c new file mode 100644 index 0000000..799f910 --- /dev/null +++ b/port/src/camera.c @@ -0,0 +1,176 @@ +// Camera state and world-to-camera transform. + +#include + +#include "camera.h" +#include "chunk5Setup.h" +#include "math6502.h" + + +// Q1.15 multiply, normalising the result back to Q1.15. +static inline int16_t q15Mul(int16_t a, int16_t b) { + return (int16_t)(((int32_t)a * (int32_t)b) >> CAM_ROT_FRACT_BITS); +} + + +void cameraInit(CameraT *cam) { + cam->worldX = 0; + cam->worldY = 5 * CAM_POS_FRACT_ONE; // 5 units above ground + cam->worldZ = -40 * CAM_POS_FRACT_ONE; // back a bit so we look forward + cam->pitch = 0; + cam->bank = 0; + cam->yaw = 0; + cam->pitchFine = 0; + cam->bankFine = 0; + cam->yawFine = 0; + cam->viewDirection = 0; + cam->forwardSpeed = 0; + cameraUpdate(cam); +} + + +void cameraStep(CameraT *cam) { + if (cam->forwardSpeed == 0) { + return; + } + // Forward vector in world frame is the third column of the + // world-to-camera inverse, which (since rot is orthogonal) is + // the third row of rot. rot is Q1.15, forwardSpeed is Q8.8. + // Step is (rot * speed) >> (15 + 8 - 16) = >> 7 in Q16.16. + int32_t speed = cam->forwardSpeed; + cam->worldX += ((int32_t)cam->rot[2][0] * speed) >> (CAM_ROT_FRACT_BITS + CAM_RATE_FRACT_BITS - CAM_POS_FRACT_BITS); + cam->worldY += ((int32_t)cam->rot[2][1] * speed) >> (CAM_ROT_FRACT_BITS + CAM_RATE_FRACT_BITS - CAM_POS_FRACT_BITS); + cam->worldZ += ((int32_t)cam->rot[2][2] * speed) >> (CAM_ROT_FRACT_BITS + CAM_RATE_FRACT_BITS - CAM_POS_FRACT_BITS); +} + + +void cameraTransform(const CameraT *cam, int32_t wx_q1616, int32_t wy_q1616, int32_t wz_q1616, int32_t *cx_q1616, int32_t *cy_q1616, int32_t *cz_q1616) { + // Subtract Q16.16 in fixed-point. Multiply each delta by the + // Q1.15 rotation row, then `>> 15` to renormalise. + int64_t dx = wx_q1616 - cam->worldX; + int64_t dy = wy_q1616 - cam->worldY; + int64_t dz = wz_q1616 - cam->worldZ; + *cx_q1616 = (int32_t)((dx * cam->rot[0][0] + dy * cam->rot[0][1] + dz * cam->rot[0][2]) >> CAM_ROT_FRACT_BITS); + *cy_q1616 = (int32_t)((dx * cam->rot[1][0] + dy * cam->rot[1][1] + dz * cam->rot[1][2]) >> CAM_ROT_FRACT_BITS); + *cz_q1616 = (int32_t)((dx * cam->rot[2][0] + dy * cam->rot[2][1] + dz * cam->rot[2][2]) >> CAM_ROT_FRACT_BITS); +} + + +// Drop the Y column from the 3x3 rotation, scale to int8 (1.0 -> $7F). +// chunk5's $79..$89 matrix is the same shape: row 0 is the camera-X +// projection onto world (X,Y,Z); row 2 is camera-Z projection. Row 1 +// (camera Y) is handled separately via the section-base mechanism. +// cam->rot is Q1.15; chunk5 wants Q1.7, so shift right 8. +// +// Both columns are taken with the same sign convention (no Z flip). +// MAME ground-truth at boot frame 13000: matRow2 high byte $89 = $1F +// = +31 (post-L6301 from positive Q1.15 ~32756). An earlier version +// negated cam->rot[i][2] under the false belief that FS2 uses +// left-handed Z; the writableRam mirror at $84..$89 never applied +// that negation, creating a sign mismatch between the int8 pipeline +// matRow2 and the int16 RAM mirror. Verified against MAME 2026-05-07. +void cameraGet2x3Matrix(const CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]) { + for (int i = 0; i < 3; i++) { + int x = cam->rot[i][0] >> 8; + int z = cam->rot[i][2] >> 8; + if (x > 127) x = 127; + if (x < -128) x = -128; + if (z > 127) z = 127; + if (z < -128) z = -128; + outRowX[i] = (int8_t)x; + outRowZ[i] = (int8_t)z; + } +} + + +void cameraUpdate(CameraT *cam) { + // Q1.15 sin/cos straight from the table. + int16_t sy = math6502Sin(cam->yaw); + int16_t cy = math6502Cos(cam->yaw); + int16_t sp = math6502Sin(cam->pitch); + int16_t cp = math6502Cos(cam->pitch); + int16_t sb = math6502Sin(cam->bank); + int16_t cb = math6502Cos(cam->bank); + + // Orientation R = Rz(bank) * Rx(pitch) * Ry(yaw). Right-handed + // axes: +X right, +Y up, +Z forward. + // + // Ry(yaw) = [[ cy, 0, sy], [ 0, 1, 0], [-sy, 0, cy]] + // Rx(pitch) = [[ 1, 0, 0], [ 0, cp,-sp], [ 0, sp, cp]] + // Rz(bank) = [[ cb,-sb, 0], [ sb, cb, 0], [ 0, 0, 1]] + // + // (Rx * Ry) symbolically: + // row0: ( cy, 0, sy ) + // row1: ( sp*sy, cp, -sp*cy ) + // row2: (-cp*sy, sp, cp*cy ) + int16_t pyR00 = cy; + int16_t pyR02 = sy; + int16_t pyR10 = q15Mul(sp, sy); + int16_t pyR11 = cp; + int16_t pyR12 = -q15Mul(sp, cy); + int16_t pyR20 = -q15Mul(cp, sy); + int16_t pyR21 = sp; + int16_t pyR22 = q15Mul(cp, cy); + + // R = Rz(bank) * (Rx(pitch)*Ry(yaw)): + int16_t r00 = (int16_t)(q15Mul(cb, pyR00) - q15Mul(sb, pyR10)); + int16_t r01 = (int16_t)( - q15Mul(sb, pyR11)); + int16_t r02 = (int16_t)(q15Mul(cb, pyR02) - q15Mul(sb, pyR12)); + int16_t r10 = (int16_t)(q15Mul(sb, pyR00) + q15Mul(cb, pyR10)); + int16_t r11 = q15Mul(cb, pyR11); + int16_t r12 = (int16_t)(q15Mul(sb, pyR02) + q15Mul(cb, pyR12)); + int16_t r20 = pyR20; + int16_t r21 = pyR21; + int16_t r22 = pyR22; + + // World-to-camera is the transpose: cam->rot stores R^T. + // (cameraTransform in this file relies on this convention to + // map world (dx,dy,dz) into camera-frame coords.) + cam->rot[0][0] = r00; cam->rot[0][1] = r10; cam->rot[0][2] = r20; + cam->rot[1][0] = r01; cam->rot[1][1] = r11; cam->rot[1][2] = r21; + cam->rot[2][0] = r02; cam->rot[2][1] = r12; cam->rot[2][2] = r22; + + // chunk5 layout: $78..$89 holds R produced by chunk5 + // SetupViewProjection (chunk5.s lines 203-432). Compute it + // bit-perfectly via the C transliteration in chunk5Setup.c + // -- this is what gets mirrored into writableRam[$78..$89] + // by sceneryAttachCamera and consumed by L631D. + // + // Input axis mapping (the chunk5 source labels are misleading + // -- see SESSION_RECOVERY.md and chunk5Setup.h): + // chunk5 $6C/$6D "yaw" = X-axis rotation = port cam->pitch + // chunk5 $6E/$6F "pitch" = Z-axis rotation = port cam->bank + // chunk5 $70/$71 "bank" = Y-axis rotation = port cam->yaw + // + // Port byte angles (256 = full circle) become 16-bit angles + // in chunk5's convention by left-shifting 8 bits. + // CHUNK5_SETUP_BYPASS=1 reverts to the simpler R/R^T copy + // (B1 baseline) for A/B testing -- useful if the chunk5 + // cascade output regresses scenery culling vs the simpler + // matrix. + if (getenv("CHUNK5_SETUP_BYPASS") != NULL) { + cam->rotChunk5[0][0] = r00; cam->rotChunk5[0][1] = r01; cam->rotChunk5[0][2] = r02; + cam->rotChunk5[1][0] = r10; cam->rotChunk5[1][1] = r11; cam->rotChunk5[1][2] = r12; + cam->rotChunk5[2][0] = r20; cam->rotChunk5[2][1] = r21; cam->rotChunk5[2][2] = r22; + // The B1 path applied col 0 >>= 1, col 2 >>= 2 in + // sceneryAttachCamera. Replicate that here for + // direct comparability of the writableRam mirror. + for (int i = 0; i < 3; i++) { + cam->rotChunk5[i][0] >>= 1; + cam->rotChunk5[i][2] >>= 2; + } + return; + } + // Combine the 8-bit byte-angle (high) with the matching fine + // byte (low) into chunk5's 16-bit angle encoding. With + // pitchFine=0 etc. this reduces to the original (cam->pitch + // << 8); MAME's Meigs boot uses non-zero fine bytes to + // express -109 in $6C/$6D (= -0.6 deg). + chunk5SetupViewProjection( + /* yaw16 (= chunk5 $6C/$6D, X-axis) */ (int16_t)(((uint16_t)cam->pitch << 8) | (uint16_t)cam->pitchFine), + /* pitch16 (= chunk5 $6E/$6F, Z-axis) */ (int16_t)(((uint16_t)cam->bank << 8) | (uint16_t)cam->bankFine), + /* bank16 (= chunk5 $70/$71, Y-axis) */ (int16_t)(((uint16_t)cam->yaw << 8) | (uint16_t)cam->yawFine), + /* viewDirection (= chunk5 $0A70) */ cam->viewDirection, + /* radarView */ 0, + cam->rotChunk5); +} diff --git a/port/src/chunk5Setup.c b/port/src/chunk5Setup.c new file mode 100644 index 0000000..5655ab9 --- /dev/null +++ b/port/src/chunk5Setup.c @@ -0,0 +1,556 @@ +// chunk5Setup: bit-perfect C transliteration of chunk5 +// SetupViewProjection (chunk5.s lines 203-432) and the chunk4 +// math primitives it calls (cos table at $141A, L177B/L1778 +// lookups at chunk4.s lines 1900-2007, ZPScale multiplier at +// chunk4.s lines 1544-1744). +// +// Validated against `port/bin/fs2trace --matrix` (which runs the +// actual chunk5 binary on a 6502 emulator). + +#include +#include +#include + +#include "chunk5Setup.h" + + +// chunk4 cos table, extracted from out/4_0200-25ff at offset $141A. +// The table is interleaved -- L141A,Y / L141B,Y is cos(Y/2) lo/hi, +// and L141C,Y / L141D,Y is cos((Y/2)+1) lo/hi for sub-byte +// interpolation. Since the L177B routine quadrant-folds the input +// to byte_angle in [0, 64], we only need entries up through Y=128 +// plus the next pair at Y=130 (= 132 bytes total). +static const uint8_t kCosTable[132] = { + 0xFF, 0x7F, 0xF5, 0x7F, 0xD7, 0x7F, 0xA6, 0x7F, + 0x61, 0x7F, 0x09, 0x7F, 0x9C, 0x7E, 0x1C, 0x7E, + 0x89, 0x7D, 0xE3, 0x7C, 0x29, 0x7C, 0x5C, 0x7B, + 0x7C, 0x7A, 0x89, 0x79, 0x83, 0x78, 0x6B, 0x77, + 0x40, 0x76, 0x03, 0x75, 0xB5, 0x73, 0x54, 0x72, + 0xE1, 0x70, 0x5E, 0x6F, 0xC9, 0x6D, 0x23, 0x6C, + 0x6C, 0x6A, 0x79, 0x67, 0xCE, 0x66, 0xE7, 0x64, + 0xF1, 0x62, 0xEB, 0x60, 0xD6, 0x5E, 0xB3, 0x5C, + 0x81, 0x5A, 0x42, 0x58, 0xF4, 0x55, 0x9A, 0x53, + 0x33, 0x51, 0xBF, 0x4E, 0x3F, 0x4C, 0xB3, 0x49, + 0x1C, 0x47, 0x7A, 0x44, 0xCD, 0x41, 0x16, 0x3F, + 0x56, 0x3C, 0x8C, 0x39, 0xB9, 0x36, 0xDE, 0x33, + 0xFB, 0x30, 0x10, 0x2E, 0x1F, 0x2B, 0x26, 0x28, + 0x27, 0x25, 0x23, 0x22, 0x19, 0x1F, 0x0B, 0x1C, + 0xF9, 0x18, 0xE1, 0x15, 0xC7, 0x12, 0xAB, 0x0F, + 0x8C, 0x0C, 0x6A, 0x09, 0x47, 0x06, 0x24, 0x03, + 0x00, 0x00, 0xDC, 0xFC, +}; + + +// Forward decls for the lookup primitives. Both take A = byte angle +// and X = sub-byte fraction (so the full 16-bit angle is A:X with A +// the high byte). Output is 16-bit signed cos in Q1.15. +static int16_t l17bc(uint8_t a, uint8_t x); + + +// L177B (chunk4.s lines 1922-1945): cos lookup with quadrant fold. +// Input A = byte angle (high), X = sub-byte (low). Returns 16-bit +// signed cos value. +// +// Quadrant logic: +// bit 7 of A set: angle is in [180, 360); negate to fold to [0, 180) +// (cos is even). The negation is via L17A5. +// A in [0, 0x40): angle is in [0, 90). Direct lookup via L17BC. +// A in [0x40, 0x80): angle is in [90, 180). Compute (0x80:00 - A:X), +// look that up (in the [0, 90) range), and negate. +int16_t chunk5L177B(uint8_t a, uint8_t x) { + // Handle negative angle: 16-bit two's complement of A:X. + // L17A5 path: eor A with $FF, eor X with $FF, inx (with the + // BNE L177F branching back to the < 0x40 / < 0x80 dispatch). + // The full 16-bit negation is just (-(A:X)) modulo $10000. + if (a & 0x80) { + // Two's complement of A:X = $10000 - (A:X). Equivalent + // to ~(A:X) + 1. + uint16_t v = (uint16_t)((a << 8) | x); + v = (uint16_t)(0u - v); + a = (uint8_t)(v >> 8); + x = (uint8_t)(v & 0xFF); + // The assembly's "lda #$7F; ldx #$FF; jmp L177F" clamp + // when the negated value would overflow at A=$80 X=$00 + // (= -$8000). After ~negate inx, A=$7F X=$00, then + // adc #$01 makes A=$80 (still negative), so it falls + // through to the clamp. Mirror the clamp here so we + // can't infinite-recurse. + if (a >= 0x80) { + a = 0x7F; + x = 0xFF; + } + } + // A is now in [0, 0x80). + if (a < 0x40) { + return l17bc(a, x); + } + // A in [0x40, 0x80): compute (0x80:00 - A:X), look that up, + // negate result. cos(180 - phi) = -cos(phi). + uint16_t v = (uint16_t)((a << 8) | x); + v = (uint16_t)(0x8000u - v); + uint8_t newA = (uint8_t)(v >> 8); + uint8_t newX = (uint8_t)(v & 0xFF); + // After the subtraction, newA is in [0, 0x40] -- direct lookup. + // (Edge: if newA == 0x40 exactly with newX == 0, we want + // cos(90) = 0; L17BC handles that via Y=0x80 -> table[128]=0.) + int16_t result = l17bc(newA, newX); + // Negate the result: 16-bit two's complement of result. + return (int16_t)(-(int32_t)result); +} + + +// L1778 (chunk4.s lines 1920-1921): "sin shifted by -64", which +// equals sin(byteAngle) since sin(x) = cos(x - 90 deg). The asm +// version `sec; sbc #$40` then jmp L177B. Mirror that here. +int16_t chunk5L1778(uint8_t a, uint8_t x) { + // sec; sbc #$40 -- borrow out (=carry clear after sbc) is + // ignored by L177B (which doesn't read carry). So we just do + // the byte subtraction. + uint8_t newA = (uint8_t)(a - 0x40); + return chunk5L177B(newA, x); +} + + +// L17BC (chunk4.s lines 1962+): table lookup with sub-byte +// interpolation. Input A = byte angle, X = sub-byte fraction. +// +// L17E1 path produces: +// diff = cosTable[Y+2] - cosTable[Y] (16-bit signed, -> $C2/$C3) +// txa; lsr a; tax (X = sub-byte >> 1, in [0, $7F]) +// lda #$00; jsr ScaleC2ByAX (computes diff * (X/2 in Q1.15) >> 15) +// add $BC/$BD (= cosTable[Y]) -> linear-interpolated cos. +// +// The scale factor is the SUB-BYTE FRACTION halved (so it ranges +// over [0, $7F00] in Q-format, never reaching $8000 = -1.0). +static int16_t l17bc(uint8_t a, uint8_t x) { + uint8_t y = (uint8_t)(a << 1); // asl a + uint8_t bcLo = kCosTable[y]; + uint8_t bcHi = kCosTable[(uint8_t)(y + 1)]; + uint8_t nextLo = kCosTable[(uint8_t)(y + 2)]; + uint8_t nextHi = kCosTable[(uint8_t)(y + 3)]; + if (x == 0) { + // L17DA: lda L141A[Y]; ldx L141B[Y]; rts + return (int16_t)((uint16_t)bcLo | ((uint16_t)bcHi << 8)); + } + if ((x & 0x7F) == 0) { + // X = $80 exactly: midpoint average of cosTable[Y] and + // cosTable[Y+2]. chunk4 lines 1968-1978: + // $BC = L141A[Y] + L141C[Y] (lo, may carry) + // A = L141B[Y] + L141D[Y] + carry + // lsr A; tax; lda $BC; ror A; rts + // Net: returns (cos[Y] + cos[Y+2]) >> 1 with the + // carry-out of the low-byte add propagated through the + // 17-bit average. + uint16_t sumLo = (uint16_t)bcLo + (uint16_t)nextLo; + uint8_t bc = (uint8_t)(sumLo & 0xFFu); + uint8_t carry = (sumLo & 0x100u) ? 1u : 0u; + uint16_t sumHi = (uint16_t)bcHi + (uint16_t)nextHi + carry; + uint8_t hiA = (uint8_t)(sumHi & 0xFFu); + bool hiCarryOut = (sumHi & 0x100u) != 0; + // lsr a: A = hiA >> 1, carry = hiA bit 0. But the carry + // INTO this lsr is the 9th bit of the high-byte add + // (= hiCarryOut). The 6502's `lsr` doesn't take carry-in; + // it just sets carry-out from bit 0. Then `tax` and + // `ror a` use that carry. So: + // tax: X = (hiA >> 1) + // lda $BC: A = bc + // ror a: A = (lsrCarry << 7) | (bc >> 1) + // The 17th bit (hiCarryOut) is silently dropped -- the + // routine assumes the sum fits in 16 bits with the + // sign bit acting as the natural top, which is true + // for cos table entries within +/-$7FFF. + (void)hiCarryOut; + bool lsrCarryOut = (hiA & 0x01u) != 0; + uint8_t newHi = (uint8_t)(hiA >> 1); + uint8_t newLo = (uint8_t)((bc >> 1) | (lsrCarryOut ? 0x80u : 0u)); + return (int16_t)((uint16_t)newLo | ((uint16_t)newHi << 8)); + } + // L17E1: linear interpolation between cosTable[Y] and cosTable[Y+2]. + int16_t diff = (int16_t)((((uint16_t)nextHi << 8) | nextLo) + - (((uint16_t)bcHi << 8) | bcLo)); + // ScaleC2ByAX($C2:$C3 = diff, A=0, X = sub-byte >> 1). + uint8_t halvedX = (uint8_t)(x >> 1); + int16_t c45 = (int16_t)((uint16_t)halvedX << 8); + int16_t scaled = chunk5ScaleC2ByC4(diff, c45); + int16_t base = (int16_t)((uint16_t)bcLo | ((uint16_t)bcHi << 8)); + return (int16_t)(scaled + base); +} + + +// ScaleC2ByC4 (chunk4.s lines 1565-1744): bit-perfect transliteration +// of the 6502 multiplier. Each comment block names the source line +// range; statements correspond 1:1 to assembly instructions where +// practical. Validated against `fs2trace --zpscale` for a sweep of +// inputs including edge cases (16383/16384/-32768/etc.). +int16_t chunk5ScaleC2ByC4(int16_t a, int16_t b) { + // Treat as four 8-bit pseudo-registers + carry flag. + uint8_t c2 = (uint8_t)( (uint16_t)a & 0xFFu); + uint8_t c3 = (uint8_t)(((uint16_t)a >> 8) & 0xFFu); + uint8_t c4 = (uint8_t)( (uint16_t)b & 0xFFu); + uint8_t c5 = (uint8_t)(((uint16_t)b >> 8) & 0xFFu); + uint8_t accA; + uint8_t a7; + uint8_t a8; + uint8_t y_sign; + bool carry; + + // Lines 1566-1573: zero check. + if ((c2 | c3) == 0 || (c4 | c5) == 0) { + return 0; + } + + // Lines 1575-1599: sign normalisation. + // Y = $C3 ^ $C5 (carries the result-sign in bit 7) + y_sign = (uint8_t)(c3 ^ c5); + // if $C3 negative: jump to L158E (subtract 1 from $C2/$C3 in two's complement) + // else: $C2 = ~$C2; $C3 = ~$C3; (one's complement of magnitude) + // BMI L159C is always taken because eor on positive gives bit7=1. + if ((c3 & 0x80) == 0) { + c2 = (uint8_t)(c2 ^ 0xFFu); + c3 = (uint8_t)(c3 ^ 0xFFu); + // BMI L159C taken; fall through to L159C. + } else { + // L158E: + // lda $C2; bne L159A (dec $C2); + // else dec $C3; bmi L159A (dec $C2); + // else inc $C3; bmi L159C; (never taken on this path) + if (c2 != 0) { + c2 = (uint8_t)(c2 - 1); // L159A: dec $C2 + } else { + c3 = (uint8_t)(c3 - 1); + if ((c3 & 0x80) != 0) { + c2 = (uint8_t)(c2 - 1); // L159A: dec $C2 + } else { + c3 = (uint8_t)(c3 + 1); + // BMI L159C: never taken (just inc'd back so non-negative). + } + } + } + + // Lines 1594-1599: if $C5 negative, two's complement $C4/$C5. + // Track A as the asm leaves it -- needed because the entry to + // the multiply loop reads A when `bcc :+` skips the `lda #$00`. + uint8_t entryA; + if ((c5 & 0x80) == 0) { + // bpl taken at $15AD; A held value of `lda $C5` at $159C. + entryA = c5; + } else { + // SUB16C ran. The macro's last `sbc $C5` leaves A holding + // the high-byte result of -($C4:$C5). After `sta $C5` + // there's no further write to A in the post-SUB16C path + // (the optional `dec $C4; dec $C5` doesn't touch A). + uint8_t origC4 = c4; + uint8_t origC5 = c5; + // sbc $C4 with C=1 (sec): newC4 = 0 - origC4. Borrow set + // (= carry out cleared) iff origC4 != 0. + uint8_t newC4 = (uint8_t)(0u - origC4); + bool borrow1 = (origC4 != 0); + // sbc $C5: A = 0 - origC5 - borrow1. + uint8_t newC5 = (uint8_t)(0u - origC5 - (borrow1 ? 1u : 0u)); + entryA = newC5; + c4 = newC4; + c5 = newC5; + if ((c5 & 0x80) != 0) { + c4 = (uint8_t)(c4 - 1); + c5 = (uint8_t)(c5 - 1); + } + } + + // Lines 1600-1604: + // lsr $C2; bcc :+; lda #$00 + // After this, A is either entryA (bcc taken = bit 0 of original + // $C2 was 0) or 0 (bcc not taken = bit was 1). + carry = (c2 & 0x01u) != 0; + c2 = (uint8_t)(c2 >> 1); + accA = carry ? 0x00 : entryA; + + // Macro for the "lsr a; ror $C2; bcs :+; adc $C5" sequence + // (lines 1605-1631 do this 6 times). carry-out from `lsr a` + // goes into bit 7 of $C2 via `ror $C2`. The new carry from + // `ror $C2` decides whether to adc $C5 to A. + #define SHIFT_RIGHT_AND_MAYBE_ADD_C5() do { \ + bool lsrCarry = (accA & 0x01u) != 0; \ + accA = (uint8_t)(accA >> 1); \ + bool oldC2bit0 = (c2 & 0x01u) != 0; \ + c2 = (uint8_t)((c2 >> 1) | (lsrCarry ? 0x80u : 0u)); \ + /* bcs :+ tests carry from `ror $C2`, which is the \ + * old bit 0 of $C2. If set, skip adc. \ + */ \ + if (!oldC2bit0) { \ + uint16_t sum = (uint16_t)accA + (uint16_t)c5; \ + accA = (uint8_t)(sum & 0xFFu); \ + carry = (sum & 0x100u) != 0; \ + } else { \ + /* adc skipped; carry stays as set by ror */ \ + carry = oldC2bit0; \ + } \ + } while (0) + + // Lines 1605-1609. + SHIFT_RIGHT_AND_MAYBE_ADD_C5(); + // Lines 1610-1614. + SHIFT_RIGHT_AND_MAYBE_ADD_C5(); + // Lines 1615-1619. + SHIFT_RIGHT_AND_MAYBE_ADD_C5(); + // Lines 1620-1624. + SHIFT_RIGHT_AND_MAYBE_ADD_C5(); + // Lines 1625-1629. + SHIFT_RIGHT_AND_MAYBE_ADD_C5(); + #undef SHIFT_RIGHT_AND_MAYBE_ADD_C5 + + // Lines 1630-1641: + // lsr a; ldx #$00; stx $A7; lsr $C2; bcs :+; tax; lda $A7; + // adc $C4; sta $A7; txa; adc $C5 + { + bool lsrCarry = (accA & 0x01u) != 0; + (void)lsrCarry; + accA = (uint8_t)(accA >> 1); + a7 = 0; + bool c2bit0 = (c2 & 0x01u) != 0; + c2 = (uint8_t)(c2 >> 1); + if (!c2bit0) { + // tax (X = A); lda $A7=0; adc $C4 (carry undefined; the + // path above leaves carry from the last ror $C2). + // Actually, the asm reads carry from `lsr $C2`, which + // we've already consumed above into c2bit0. After lsr, + // C = c2bit0. We branched bcs (skip if C=1), so when we + // execute adc, C=0. + uint8_t savedA = accA; + uint16_t sum = (uint16_t)0 + (uint16_t)c4 + 0u; + a7 = (uint8_t)(sum & 0xFFu); + carry = (sum & 0x100u) != 0; + sum = (uint16_t)savedA + (uint16_t)c5 + (carry ? 1u : 0u); + accA = (uint8_t)(sum & 0xFFu); + carry = (sum & 0x100u) != 0; + } + } + + // Macro for the "lsr a; ror $A7; lsr $C2 (or $C3); bcs :+; tax; lda $A7; + // adc $C4; sta $A7; txa; adc $C5" sequence used many times below. + #define ROR_A7_AND_MAYBE_ADD_C45(SHIFT_REG) do { \ + bool lsrCarry = (accA & 0x01u) != 0; \ + accA = (uint8_t)(accA >> 1); \ + a7 = (uint8_t)((a7 >> 1) | (lsrCarry ? 0x80u : 0u)); \ + bool regBit0 = ((SHIFT_REG) & 0x01u) != 0; \ + (SHIFT_REG) = (uint8_t)((SHIFT_REG) >> 1); \ + if (!regBit0) { \ + uint8_t savedA = accA; \ + uint16_t sum = (uint16_t)a7 + (uint16_t)c4; \ + a7 = (uint8_t)(sum & 0xFFu); \ + carry = (sum & 0x100u) != 0; \ + sum = (uint16_t)savedA + (uint16_t)c5 \ + + (carry ? 1u : 0u); \ + accA = (uint8_t)(sum & 0xFFu); \ + carry = (sum & 0x100u) != 0; \ + } \ + } while (0) + + // Lines 1642-1651: lsr a; ror $A7; lsr $C2; ... + ROR_A7_AND_MAYBE_ADD_C45(c2); + // Lines 1653-1662: same but lsr $C3 (now we cross over). + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1664-1673. + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1675-1684. + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1686-1695. + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1697-1706. + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1708-1717. + ROR_A7_AND_MAYBE_ADD_C45(c3); + // Lines 1719-1728. + ROR_A7_AND_MAYBE_ADD_C45(c3); + #undef ROR_A7_AND_MAYBE_ADD_C45 + + // Lines 1730-1731: lsr a; ror $A7 (final shift, no add). + { + bool lsrCarry = (accA & 0x01u) != 0; + accA = (uint8_t)(accA >> 1); + a7 = (uint8_t)((a7 >> 1) | (lsrCarry ? 0x80u : 0u)); + } + + // Lines 1732-1741: cpy #$00; bpl :+ (skip negation if Y >= 0) + // sta $A8; lda #$00; sec; sbc $A7; sta $A7; + // lda #$00; sbc $A8. + if ((y_sign & 0x80) != 0) { + a8 = accA; + // 16-bit two's complement of (A:A7) treating A7 as low. + uint16_t v = (uint16_t)(((uint16_t)a8 << 8) | a7); + v = (uint16_t)(0u - v); + a7 = (uint8_t)( v & 0xFFu); + accA = (uint8_t)((v >> 8) & 0xFFu); + } + + // tax; lda $A7; rts. Result A:X = (a7, accA) which we read as + // a 16-bit signed value with accA as the high byte. + return (int16_t)((uint16_t)a7 | ((uint16_t)accA << 8)); +} + + +// asrShift1: signed 16-bit arithmetic shift right by 1. Mirrors +// chunk5's `lda hi; rol a; ror hi; ror lo` 4-instruction idiom (the +// `rol a` puts sign bit into carry; `ror hi` brings it back into +// hi-byte bit 7; `ror lo` shifts low byte with the cross-byte carry). +static inline int16_t asrShift1(int16_t v) { + int16_t hi = (int16_t)((uint16_t)v >> 8); + int16_t lo = (int16_t)((uint16_t)v & 0xFFu); + // rol a: A = (hi << 1) | C_in; we don't care about old carry + // (hi << 1 sets carry to old bit 7). + bool signBit = (hi & 0x80) != 0; + // ror hi: hi = (signBit << 7) | (hi >> 1); new carry = hi bit 0. + bool hiBit0 = (hi & 0x01) != 0; + uint8_t newHi = (uint8_t)(((signBit ? 0x80u : 0u) | ((uint8_t)hi >> 1)) & 0xFFu); + // ror lo: lo = (hiBit0 << 7) | (lo >> 1). + uint8_t newLo = (uint8_t)((hiBit0 ? 0x80u : 0u) | ((uint8_t)lo >> 1)); + return (int16_t)((uint16_t)newLo | ((uint16_t)newHi << 8)); +} + + +// chunk5SetupViewProjection: bit-perfect transliteration of the +// chunk5.s routine at lines 203-432. RadarView and back-view branches +// are not yet wired (port doesn't currently use them); these collapse +// to a simple set-and-forward. Most of the work is in the L6155 path +// (regular forward/side view). +void chunk5SetupViewProjection(int16_t yaw16, int16_t pitch16, int16_t bank16, + uint8_t vd, uint8_t radarView, + int16_t outMatrix[3][3]) { + // Working 16-bit slots, named after their ZP addresses. + int16_t v72, v74, v76; + + if (radarView != 0) { + // chunk5 lines 204-220 (RadarView branch). Build + // $72/$74/$76 from a fixed $4000 (90deg) plus a + // negated altitude. The aircraft alt input is not + // exposed via this API yet -- treat as 0. + v72 = 0x4000; + v74 = 0; // = -alt with alt=0 + v76 = 0; + } else if ((int8_t)vd < 0) { + // chunk5 lines 222-235 (ViewDirection negative -> back). + v72 = 0; // = -alt with alt=0 + v76 = 0; + v74 = (int16_t)(yaw16 + 0x4000); + } else { + // L6155: forward/side view. Most of the work. + // chunk5 lines 237-307. + uint8_t v3E = (uint8_t)(vd << 4); // VD<<4 byte angle + int16_t vC0 = yaw16; // $C0/$C1 + int16_t vB6 = pitch16; // $B6/$B7 + uint8_t v3D = (uint8_t)((bank16 >> 8) & 0xFFu); // bank hi + // Pitch-near-180-deg fold: if (B7 + $40) bit 7 is set, + // negate yaw around $8000 and flip top bits of B7/3D. + uint8_t b7 = (uint8_t)((vB6 >> 8) & 0xFFu); + uint8_t b7Plus40 = (uint8_t)(b7 + 0x40); + if ((b7Plus40 & 0x80) != 0) { + // L617C: $C0/$C1 = $8000 - $C0/$C1. + vC0 = (int16_t)((uint16_t)0x8000 - (uint16_t)vC0); + b7 = (uint8_t)(b7 ^ 0x80); + v3D = (uint8_t)(v3D ^ 0x80); + vB6 = (int16_t)((uint16_t)((vB6 & 0x00FF) | ((uint16_t)b7 << 8))); + } + // $77 = $3D + $3E. The low byte $76 is left unchanged + // by the SetupViewProjection routine itself; in the + // captured RAM it picks up whatever previous code put + // there. For deterministic port output we fix $76 = 0. + uint8_t v77 = (uint8_t)(v3D + v3E); + v76 = (int16_t)((uint16_t)v77 << 8); + // $BA = cos($3E), $BE = sin($3E). + int16_t vBA = chunk5L177B(v3E, 0); + int16_t vBE = chunk5L1778(v3E, 0); + // $98 = yaw * cos(VD); $AD = pitch * sin(VD). + int16_t v98 = chunk5ScaleC2ByC4(vC0, vBA); + int16_t vAD = chunk5ScaleC2ByC4(vB6, vBE); + // $72 = yaw*cos(VD) - pitch*sin(VD). + v72 = (int16_t)((uint16_t)v98 - (uint16_t)vAD); + // $98 = pitch * cos(VD); $AD = yaw * sin(VD). + v98 = chunk5ScaleC2ByC4(vB6, vBA); + vAD = chunk5ScaleC2ByC4(vC0, vBE); + // $74 = yaw*sin(VD) + pitch*cos(VD). + v74 = (int16_t)((uint16_t)v98 + (uint16_t)vAD); + } + + // L61F0 (chunk5.s lines 308-318): if ($73 + $40) bit 7 set, + // negate $72/$73 around $8000 and flip top bits of $75/$77. + { + uint8_t v73 = (uint8_t)((v72 >> 8) & 0xFFu); + uint8_t test = (uint8_t)(v73 + 0x40); + if ((test & 0x80) != 0) { + v72 = (int16_t)((uint16_t)0x8000 - (uint16_t)v72); + v74 = (int16_t)((uint16_t)v74 ^ (uint16_t)0x8000); + v76 = (int16_t)((uint16_t)v76 ^ (uint16_t)0x8000); + } + } + + // L6210 (lines 319-345): cos/sin lookups of $72/$74/$76. + // L1778 returns SIN; L177B returns COS (chunk4.s comments at + // lines 1898-1899 are mislabeled relative to the code). + uint8_t hi72 = (uint8_t)((v72 >> 8) & 0xFFu); + uint8_t lo72 = (uint8_t)( v72 & 0xFFu); + uint8_t hi74 = (uint8_t)((v74 >> 8) & 0xFFu); + uint8_t lo74 = (uint8_t)( v74 & 0xFFu); + uint8_t hi76 = (uint8_t)((v76 >> 8) & 0xFFu); + uint8_t lo76 = (uint8_t)( v76 & 0xFFu); + int16_t sinA = chunk5L1778(hi72, lo72); // $CB + int16_t sinB = chunk5L1778(hi74, lo74); // $CD + int16_t sinC = chunk5L1778(hi76, lo76); // $CF + int16_t cosA = chunk5L177B(hi72, lo72); // $18 + int16_t cosB = chunk5L177B(hi74, lo74); // $D4 + int16_t cosC = chunk5L177B(hi76, lo76); // $D6 + + // Cascade (lines 347-412). + int16_t vD8 = chunk5ScaleC2ByC4(cosC, cosB); // cos_c * cos_b + int16_t v1B = chunk5ScaleC2ByC4(sinC, sinB); // sin_c * sin_b + int16_t vDD = chunk5ScaleC2ByC4(cosC, sinB); // cos_c * sin_b + int16_t vDF = chunk5ScaleC2ByC4(sinC, cosB); // sin_c * cos_b + int16_t vE1 = chunk5ScaleC2ByC4(sinA, v1B); // sin_a * sin_c * sin_b + int16_t v1E = chunk5ScaleC2ByC4(sinA, vDF); // sin_a * sin_c * cos_b + int16_t v4A = chunk5ScaleC2ByC4(vDD, sinA); // cos_c * sin_b * sin_a + int16_t v4D = chunk5ScaleC2ByC4(sinA, vD8); // sin_a * cos_c * cos_b + + int16_t M00 = (int16_t)((uint16_t)vD8 + (uint16_t)vE1); // $78 + int16_t M01 = (int16_t)((uint16_t)v1E - (uint16_t)vDD); // $7A + int16_t M02 = chunk5ScaleC2ByC4(sinC, cosA); // $7C + int16_t M10 = chunk5ScaleC2ByC4(sinB, cosA); // $7E + int16_t M11 = chunk5ScaleC2ByC4(cosA, cosB); // $80 + int16_t M12 = (int16_t)((uint16_t)0u - (uint16_t)sinA); // $82 = -sin_a + int16_t M20 = (int16_t)((uint16_t)v4A - (uint16_t)vDF); // $84 + int16_t M21 = (int16_t)((uint16_t)v1B + (uint16_t)v4D); // $86 + int16_t M22 = chunk5ScaleC2ByC4(cosC, cosA); // $88 + + // L6301 (lines 414-432): col 0 >>= 1, col 2 >>= 2 (arithmetic). + M00 = asrShift1(M00); + M10 = asrShift1(M10); + M20 = asrShift1(M20); + M02 = asrShift1(asrShift1(M02)); + M12 = asrShift1(asrShift1(M12)); + M22 = asrShift1(asrShift1(M22)); + + outMatrix[0][0] = M00; outMatrix[0][1] = M01; outMatrix[0][2] = M02; + outMatrix[1][0] = M10; outMatrix[1][1] = M11; outMatrix[1][2] = M12; + outMatrix[2][0] = M20; outMatrix[2][1] = M21; outMatrix[2][2] = M22; +} + + +int chunk5SetupSelfTest(void) { + // Smoke-test L177B against known oracle values. + struct { + uint8_t a, x; + int16_t expected; + const char *what; + } cosCases[] = { + { 0x00, 0x00, 32767, "cos(0,0)" }, + { 0x40, 0x00, 0, "cos(90, 0)" }, + { 0x80, 0x00, -32767, "cos(180, 0)" }, + { 0x20, 0x00, 23169, "cos(45, 0)" }, + { 0x10, 0x00, 30272, "cos(22.5, 0)" }, + }; + int firstFail = 0; + for (size_t i = 0; i < sizeof(cosCases)/sizeof(cosCases[0]); i++) { + int16_t got = chunk5L177B(cosCases[i].a, cosCases[i].x); + if (got != cosCases[i].expected && firstFail == 0) { + firstFail = -(int)(i + 1); + } + } + return firstFail; +} diff --git a/port/src/chunk5Transform.c b/port/src/chunk5Transform.c new file mode 100644 index 0000000..c99e27c --- /dev/null +++ b/port/src/chunk5Transform.c @@ -0,0 +1,785 @@ +// Literal port of chunk5 TransformVertex7EBC (src/chunk5.s line 4298- +// 4569). Each block of C code is annotated with the chunk5 label/line +// it mirrors. 6502 byte arithmetic and flag semantics are preserved +// exactly; the goal is for this to behave bit-identically to the +// original on every input. + +#include +#include +#include +#include +#include "chunk5Transform.h" +#include "chunk5Setup.h" +#include "cpu6502.h" + +static int chunk5DebugTrace(void) { + static int cached = -1; + if (cached < 0) { + cached = (getenv("CHUNK5_TRACE") != NULL) ? 1 : 0; + } + return cached; +} + + +// 6502 status flag bits. We only track the four that TransformVertex7EBC +// relies on: C (carry), V (overflow), N (negative), Z (zero). +#define FLAG_C 0x01 +#define FLAG_Z 0x02 +#define FLAG_V 0x40 +#define FLAG_N 0x80 + + +// ADC: A = A + M + C. Sets N/V/Z/C from the result. +static uint8_t op_adc(uint8_t a, uint8_t m, uint8_t *flags) { + int c = (*flags & FLAG_C) ? 1 : 0; + int unsignedSum = (int)a + (int)m + c; + int signedSum = (int)(int8_t)a + (int)(int8_t)m + c; + uint8_t result = (uint8_t)(unsignedSum & 0xFF); + *flags &= (uint8_t)~(FLAG_C | FLAG_V | FLAG_N | FLAG_Z); + if (unsignedSum > 255) *flags |= FLAG_C; + if (signedSum > 127 || signedSum < -128) *flags |= FLAG_V; + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// SBC: A = A - M - (1 - C). Sets N/V/Z/C from the result. C reflects +// "no borrow needed". +static uint8_t op_sbc(uint8_t a, uint8_t m, uint8_t *flags) { + int c = (*flags & FLAG_C) ? 1 : 0; + int unsignedDiff = (int)a - (int)m - (1 - c); + int signedDiff = (int)(int8_t)a - (int)(int8_t)m - (1 - c); + uint8_t result = (uint8_t)(unsignedDiff & 0xFF); + *flags &= (uint8_t)~(FLAG_C | FLAG_V | FLAG_N | FLAG_Z); + if (unsignedDiff >= 0) *flags |= FLAG_C; + if (signedDiff > 127 || signedDiff < -128) *flags |= FLAG_V; + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// EOR: A ^= M. Sets N/Z. +static uint8_t op_eor(uint8_t a, uint8_t m, uint8_t *flags) { + uint8_t result = a ^ m; + *flags &= (uint8_t)~(FLAG_N | FLAG_Z); + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// ASL: bit 7 -> C, 0 -> bit 0. Sets N/Z/C. +static uint8_t op_asl(uint8_t a, uint8_t *flags) { + uint8_t result = (uint8_t)((a << 1) & 0xFF); + *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); + if (a & 0x80) *flags |= FLAG_C; + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// ROL: bit 7 -> C, old C -> bit 0. Sets N/Z/C. +static uint8_t op_rol(uint8_t a, uint8_t *flags) { + int oldC = (*flags & FLAG_C) ? 1 : 0; + uint8_t result = (uint8_t)(((a << 1) | oldC) & 0xFF); + *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); + if (a & 0x80) *flags |= FLAG_C; + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// LSR: bit 0 -> C, 0 -> bit 7. Sets N=0/Z/C. +static uint8_t op_lsr(uint8_t a, uint8_t *flags) { + uint8_t result = a >> 1; + *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); + if (a & 0x01) *flags |= FLAG_C; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// ROR: bit 0 -> C, old C -> bit 7. Sets N/Z/C. +static uint8_t op_ror(uint8_t a, uint8_t *flags) { + int oldC = (*flags & FLAG_C) ? 1 : 0; + uint8_t result = (uint8_t)((a >> 1) | (oldC << 7)); + *flags &= (uint8_t)~(FLAG_C | FLAG_N | FLAG_Z); + if (a & 0x01) *flags |= FLAG_C; + if (result & 0x80) *flags |= FLAG_N; + if (result == 0) *flags |= FLAG_Z; + return result; +} + + +// chunk4 L1818 / MultiplyXY (line 2025-2091). Signed 7x7 -> 14-bit +// multiply via 7 shift-add steps with absolute-value preprocessing. +// Inputs: y_in, x_in (signed bytes treated as 7-bit magnitude + sign). +// Outputs: 16-bit signed result placed in *outA (low byte) and *outY +// (high byte) -- caller uses the (A, Y) register pair. +static void op_l1818(uint8_t y_in, uint8_t x_in, uint8_t *outA, uint8_t *outY) { + uint8_t flags = 0; + uint8_t zA5, zC4, zC2, zC5; + uint8_t a; + + // tya; sta $A5 + zA5 = y_in; + a = y_in; + // bpl L1822 + if (a & 0x80) { + // sec; sbc #$01 + flags |= FLAG_C; + a = op_sbc(a, 0x01, &flags); + // bmi L1824 + if (flags & FLAG_N) { + // L1824: sta $C4 + } else { + // L1822: eor #$FF; (fall to L1824) + a = op_eor(a, 0xFF, &flags); + } + } else { + // L1822: eor #$FF + a = op_eor(a, 0xFF, &flags); + } + // L1824: sta $C4 + zC4 = a; + + // txa + a = x_in; + // bpl L1832 + if (a & 0x80) { + // eor #$FF; clc; adc #$01 + a = op_eor(a, 0xFF, &flags); + flags &= (uint8_t)~FLAG_C; + a = op_adc(a, 0x01, &flags); + // bpl L1832 + if (flags & FLAG_N) { + // lda #$7F (saturate) + a = 0x7F; + } + } + // L1832: sta $C2 + zC2 = a; + + // ror $C4 (uses current carry; after the absolute-value path, + // carry is whatever the last op left it). This first ror + // shifts $C4 right; the carry-out becomes the choice for the + // first shift-add. + zC4 = op_ror(zC4, &flags); + // bcc L183A; lda #$00 -- if carry CLEAR, fall through with A + // unchanged; if carry SET, A = 0. + if (flags & FLAG_C) { + a = 0; + } + // (otherwise leave A as whatever $C2 was -- this is the + // chunk5 quirk: the multiply uses $C2 as the running multiplier + // base, and after the abs path A still holds $C2 implicitly. + // Actually no, A = $C2 from the previous sta. After ror $C4 we + // need A to be the current multiplier value. Looking at chunk5 + // line 2046: `ror $C4 / bcc L183A / lda #$00` -- if carry was + // SET (bit 0 of $C4 was 1), load 0; else leave A. A had been + // set to $C2 just before. So either A = $C2 or A = 0.) + + // 6-step shift-add loop (L183A through L185D in chunk4.s). + // Each step: lsr a; ror $C4; bcs ; adc $C2. After + // the loop comes the L1864 final lsr/ror without an add. + // Earlier this loop ran 7 times -- one too many -- which + // halved every multiply result. Verified against fs2trace + // --xform asm trace: L1818($79=3F, $9F=11) = lo=$5E hi=$08 + // (was lo=$2F hi=$04 with the off-by-one). + for (int step = 0; step < 6; step++) { + a = op_lsr(a, &flags); + zC4 = op_ror(zC4, &flags); + if (!(flags & FLAG_C)) { + flags &= (uint8_t)~FLAG_C; // clc implicit + a = op_adc(a, zC2, &flags); + } + } + + // L1864: lsr a; ror $C4; sta $C5 + a = op_lsr(a, &flags); + zC4 = op_ror(zC4, &flags); + zC5 = a; + + // txa; eor $A5 + a = op_eor(x_in, zA5, &flags); + // bpl L187B (signs same -> just return) + if (flags & FLAG_N) { + // Negate: $C4/$C5 = -$C4/$C5 + flags |= FLAG_C; // sec implicit before sbc 0 + uint8_t newC4 = op_sbc(0, zC4, &flags); + uint8_t newC5 = op_sbc(0, zC5, &flags); + zC4 = newC4; + zC5 = newC5; + } + // Output A = $C4, Y = $C5 + *outA = zC4; + *outY = zC5; +} + + +// L80B0: shift-down-by-1 of all three 24-bit accumulators. Mirrors +// chunk5.s line 4559-4569. 24-bit value layout per axis: ($1A LSB, +// $18 MID, $19 HSB) for X; ($1D, $1B, $1C) for Y; ($20, $1E, $1F) for +// Z. The shift drops the bottom bit of each and re-aligns. +static void op_l80b0(uint8_t *z18, uint8_t *z19, uint8_t *z1A, + uint8_t *z1B, uint8_t *z1C, uint8_t *z1D, + uint8_t *z1E, uint8_t *z1F, uint8_t *z20) { + uint8_t flags = 0; + // lsr $1A; ror $19; ror $18 - X axis shift + *z1A = op_lsr(*z1A, &flags); + *z19 = op_ror(*z19, &flags); + *z18 = op_ror(*z18, &flags); + // lsr $1D; ror $1C; ror $1B - Y axis shift + *z1D = op_lsr(*z1D, &flags); + *z1C = op_ror(*z1C, &flags); + *z1B = op_ror(*z1B, &flags); + // lsr $20; ror $1F; ror $1E - Z axis shift + *z20 = op_lsr(*z20, &flags); + *z1F = op_ror(*z1F, &flags); + *z1E = op_ror(*z1E, &flags); +} + + +// Common implementation of TransformVertex7EBC and TransformVertex80C5. +// The only difference between them is the work-counter bias ($51 vs +// $C7) which doesn't affect the math, just the per-frame cycle +// budget. We don't model that timer. +static int transformVertexCommon(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { + uint8_t flags = 0; + + // Per-vertex transform base accumulator. + // + // MAME-patched TransformVertex7EBC ($7E8E entry) copies + // $2A..$2F into $18..$1D at the top of the routine and then + // reads/writes $18..$1F as the working accumulator. Port's + // C re-impl skips the copy and reads $2A..$2F directly into + // local z18..z1F, which is structurally equivalent. + // Use PORT_XFORM_INTERPRETED=1 to run the actual MAME- + // patched bytecode instead (byte-faithful within 1 LSB). + uint8_t z18, z19, z1A, z1B, z1C, z1D, z1E, z1F, z20; + { + z18 = ram[0x2A]; z19 = ram[0x2B]; + z1A = (ram[0x2B] & 0x80) ? 0xFF : 0x00; + z1B = ram[0x2C]; z1C = ram[0x2D]; + z1D = (ram[0x2D] & 0x80) ? 0xFF : 0x00; + z1E = ram[0x2E]; z1F = ram[0x2F]; + z20 = (ram[0x2F] & 0x80) ? 0xFF : 0x00; + } + + // L7F03: Y = 1 (start reading vertex bytes from $8B+1). + // Read xLo, xHi from stream, subtract $66/$67 (camera-section + // delta X). The first sbc has SEC pre-set; the second sbc + // chains the carry. + uint8_t z9E, z9F, zA2, zA3; + bool xOverflow = false; + bool zOverflow = false; + + flags |= FLAG_C; + z9E = op_sbc(stream[1], ram[0x66], &flags); + z9F = op_sbc(stream[2], ram[0x67], &flags); + if (flags & FLAG_V) { + xOverflow = true; + // L7F64: ror a; sta $9F; ror $9E + // (a holds the result of the previous sbc = z9F) + // The ror uses the V-trapped carry from the sbc. + z9F = op_ror(z9F, &flags); + z9E = op_ror(z9E, &flags); + } + + flags |= FLAG_C; + zA2 = op_sbc(stream[3], ram[0x6A], &flags); + zA3 = op_sbc(stream[4], ram[0x6B], &flags); + if (flags & FLAG_V) { + zOverflow = true; + if (xOverflow) { + // L7F5C: ror a; sta $A3; ror $A2 -- shift Z + // down by 1, X already shifted by L7F64. + zA3 = op_ror(zA3, &flags); + zA2 = op_ror(zA2, &flags); + } else { + // L7EAD: ror a; sta $A3; ror $A2; lda $9F; + // rol a; ror $9F; ror $9E + // Z gets shifted right; X gets sign-extending + // arithmetic shift right. + zA3 = op_ror(zA3, &flags); + zA2 = op_ror(zA2, &flags); + uint8_t a = z9F; + a = op_rol(a, &flags); + z9F = op_ror(z9F, &flags); + z9E = op_ror(z9E, &flags); + (void)a; + } + } else if (xOverflow) { + // L7F64 fall-through after the Z reads succeeded: + // rol a; ror $A3; ror $A2 (shift Z down by 1 too so + // the scales match the down-shifted X). + uint8_t a = zA3; + a = op_rol(a, &flags); + zA3 = op_ror(zA3, &flags); + zA2 = op_ror(zA2, &flags); + (void)a; + } + + if (xOverflow || zOverflow) { + // L7F7F: shift the three 24-bit base accumulators + // right by 1 each so they match the down-shifted + // delta scale. Mirror chunk5 line 4400-4411. + { + uint8_t a = z19; + a = op_rol(a, &flags); + z19 = op_ror(z19, &flags); + z18 = op_ror(z18, &flags); + (void)a; + } + { + uint8_t a = z1C; + a = op_rol(a, &flags); + z1C = op_ror(z1C, &flags); + z1B = op_ror(z1B, &flags); + (void)a; + } + { + uint8_t a = z1F; + a = op_rol(a, &flags); + z1F = op_ror(z1F, &flags); + z1E = op_ror(z1E, &flags); + (void)a; + } + // chunk5 also dec $2F here. Skip the auto-scale loop + // -- jmp L7F96 in the original. + goto matrixMultiply; + } + + // L7F1A: auto-scale loop. Shift everything left until any of + // the high bytes ($9F, $A3, $19, $1C, $1F) reaches the $40 + // threshold (specifically: adc #$40 produces a negative + // result, i.e. the original high byte is in [0x40..0xBF] in + // unsigned terms = magnitude >= 64). + // + // Each iteration shifts: $9E/$9F (X delta), $A2/$A3 (Z delta), + // and the three 24-bit accumulators left by 1. + // + // chunk5 increments the zoom counter $2F each iteration; once + // $2F overflows from $FF -> $00 the auto-scale halts. We + // bound iterations to that count (256 - initial $40 = 192 + // max) so all-zero inputs don't spin forever. + int autoScaleSteps = 192; + while (autoScaleSteps-- > 0) { + // Test #1: $9F adc #$40 bmi -> exit + flags &= (uint8_t)~FLAG_C; + op_adc(z9F, 0x40, &flags); + if (flags & FLAG_N) break; + // Test #2: $A3 + flags &= (uint8_t)~FLAG_C; + op_adc(zA3, 0x40, &flags); + if (flags & FLAG_N) break; + // Test #3: $19 + flags &= (uint8_t)~FLAG_C; + op_adc(z19, 0x40, &flags); + if (flags & FLAG_N) break; + // Test #4: $1C + flags &= (uint8_t)~FLAG_C; + op_adc(z1C, 0x40, &flags); + if (flags & FLAG_N) break; + // Test #5: $1F + flags &= (uint8_t)~FLAG_C; + op_adc(z1F, 0x40, &flags); + if (flags & FLAG_N) break; + + // None of the high bytes overflowed -- shift everything + // left by 1 (asl/rol cascade). + // + // MAME's TransformVertex7EBC at $7F03 shifts the deltas + // ($9E/$9F, $A2/$A3) AND the 16-bit base accumulators + // ($18/$19, $1A/$1B, $1C/$1D) WITHOUT shifting any LO + // byte. Source chunk5.s shifts 24-bit (asl LO; rol MID; + // rol HI). Port follows MAME here so Z output matches + // MAME byte-exactly (project_fs2port_xform_drift.md). + z9E = op_asl(z9E, &flags); + z9F = op_rol(z9F, &flags); + zA2 = op_asl(zA2, &flags); + zA3 = op_rol(zA3, &flags); + // 16-bit base shift: asl MID; rol HI (skip LO). + z18 = op_asl(z18, &flags); + z19 = op_rol(z19, &flags); + z1B = op_asl(z1B, &flags); + z1C = op_rol(z1C, &flags); + z1E = op_asl(z1E, &flags); + z1F = op_rol(z1F, &flags); + } + // (drop out either via early break in the test chain above or + // by exhausting the iteration count.) + +matrixMultiply: + // L7F96: 6 multiply-accumulate calls. Each multiplies a delta + // hi-byte by a matrix entry and accumulates into $18/$19, + // $1B/$1C, or $1E/$1F. The post-multiply sign-extension code + // (L7FAD..L8049) computes a "high byte" $1A/$1D/$20 that + // tracks whether the sum has overflowed into 24-bit territory. + + // ---- $9F * $79 -> $18/$19, sign-ext to $1A ---- + { + uint8_t lo, hi; + op_l1818(ram[0x79], z9F, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z18 = op_adc(lo, z18, &flags); + // tya; adc $19; sta $19 + z19 = op_adc(hi, z19, &flags); + // bpl L7FAD; lda #$FF; bmi L7FAF / L7FAD: lda #$00 + uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; + // bvc L7FB3; eor #$FF + if (flags & FLAG_V) { + signByte ^= 0xFF; + } + z1A = signByte; + } + + // ---- $A3 * $85 -> $18/$19, accumulate sign-ext into $1A ---- + { + uint8_t lo, hi; + op_l1818(ram[0x85], zA3, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z18 = op_adc(lo, z18, &flags); + z19 = op_adc(hi, z19, &flags); + // chunk5's `tya / bpl / lda` only set N from Y; do + // NOT modify C. The `adc $1A` that follows inherits + // the carry from `tya; adc $19; sta $19` above. + uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; + z1A = op_adc(signByte, z1A, &flags); + } + + // ---- $9F * $7B -> $1B/$1C, sign-ext to $1D ---- + { + uint8_t lo, hi; + op_l1818(ram[0x7B], z9F, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z1B = op_adc(lo, z1B, &flags); + z1C = op_adc(hi, z1C, &flags); + uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; + if (flags & FLAG_V) { + signByte ^= 0xFF; + } + z1D = signByte; + } + + // ---- $A3 * $87 -> $1B/$1C, accumulate sign-ext into $1D ---- + { + uint8_t lo, hi; + op_l1818(ram[0x87], zA3, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z1B = op_adc(lo, z1B, &flags); + z1C = op_adc(hi, z1C, &flags); + // Sign decision uses N from `tya` (= bit 7 of hi). + // Carry into `adc $1D` inherited from $1C adc. + uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; + z1D = op_adc(signByte, z1D, &flags); + } + + // ---- $9F * $7D -> $1E/$1F, sign-ext to $20 ---- + { + uint8_t lo, hi; + op_l1818(ram[0x7D], z9F, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z1E = op_adc(lo, z1E, &flags); + z1F = op_adc(hi, z1F, &flags); + uint8_t signByte = (flags & FLAG_N) ? 0xFF : 0x00; + if (flags & FLAG_V) { + signByte ^= 0xFF; + } + z20 = signByte; + } + + // ---- $A3 * $89 -> $1E/$1F, accumulate sign-ext into $20 ---- + { + uint8_t lo, hi; + op_l1818(ram[0x89], zA3, &lo, &hi); + flags &= (uint8_t)~FLAG_C; + z1E = op_adc(lo, z1E, &flags); + z1F = op_adc(hi, z1F, &flags); + uint8_t signByte = ((hi & 0x80) != 0) ? 0xFF : 0x00; + z20 = op_adc(signByte, z20, &flags); + } + + // L8051: lda $1A; eor $1D; eor $20 + // beq L8059 (signs all match) + // cmp #$FF; bne L806D (signs all -1 -> match, fall through; + // else go to L806D which does extra shift) + uint8_t signCheck = z1A ^ z1D ^ z20; + if (signCheck != 0 && signCheck != 0xFF) { + // L806D: jsr L80B0; jsr L80B0; jsr L80B0; jmp L8091 + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + } else { + // L8059: signs consistent -- now check that each + // accumulator's high byte AGREES with its sign-ext. + // lda $1A; eor $19; bmi L8070 + // lda $1D; eor $1C; bmi L8070 + // lda $20; eor $1F; bpl L8079; bmi L8070 + bool xMismatch = ((z1A ^ z19) & 0x80) != 0; + bool yMismatch = ((z1D ^ z1C) & 0x80) != 0; + bool zSignZ = ((z20 ^ z1F) & 0x80) != 0; + if (xMismatch || yMismatch || zSignZ) { + // L8070: jsr L80B0; jsr L80B0; jmp L8091 + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + } else { + // L8079: lda $19; clc; adc #$40; bmi L808E + // lda $1C; clc; adc #$40; bmi L808E + // lda $1F; clc; adc #$40; bpl L8091 + flags = 0; + op_adc(z19, 0x40, &flags); + if (flags & FLAG_N) { + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + } else { + flags = 0; + op_adc(z1C, 0x40, &flags); + if (flags & FLAG_N) { + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + } else { + flags = 0; + op_adc(z1F, 0x40, &flags); + if (flags & FLAG_N) { + op_l80b0(&z18, &z19, &z1A, &z1B, &z1C, &z1D, &z1E, &z1F, &z20); + } + } + } + } + } + + // L8091: store result to caller's vertex slot at destSlot.. + // destSlot+5. chunk5 layout: lo X, hi X, lo Y, hi Y, lo Z, hi Z. + ram[destSlot + 0] = z18; + ram[destSlot + 1] = z19; + ram[destSlot + 2] = z1B; + ram[destSlot + 3] = z1C; + ram[destSlot + 4] = z1E; + ram[destSlot + 5] = z1F; + + + if (chunk5DebugTrace()) { + int16_t outX = (int16_t)((uint16_t)z18 | ((uint16_t)z19 << 8)); + int16_t outY = (int16_t)((uint16_t)z1B | ((uint16_t)z1C << 8)); + int16_t outZ = (int16_t)((uint16_t)z1E | ((uint16_t)z1F << 8)); + int16_t streamX = (int16_t)((uint16_t)stream[1] | ((uint16_t)stream[2] << 8)); + int16_t streamZ = (int16_t)((uint16_t)stream[3] | ((uint16_t)stream[4] << 8)); + int16_t cam66 = (int16_t)((uint16_t)ram[0x66] | ((uint16_t)ram[0x67] << 8)); + int16_t cam6A = (int16_t)((uint16_t)ram[0x6A] | ((uint16_t)ram[0x6B] << 8)); + int16_t base4A = (int16_t)((uint16_t)ram[0x4A] | ((uint16_t)ram[0x4B] << 8)); + int16_t base4D = (int16_t)((uint16_t)ram[0x4D] | ((uint16_t)ram[0x4E] << 8)); + int16_t base50 = (int16_t)((uint16_t)ram[0x50] | ((uint16_t)ram[0x51] << 8)); + fprintf(stderr, + " xform[%02X]: stream=(%d,%d) cam66=%d cam6A=%d base=(%d,%d,%d) -> (%d,%d,%d) overflow=%c%c\n", + destSlot, streamX, streamZ, cam66, cam6A, + base4A, base4D, base50, outX, outY, outZ, + xOverflow ? 'X' : '-', zOverflow ? 'Z' : '-'); + fprintf(stderr, " deltas after auto-scale: dx=$%02X%02X dz=$%02X%02X " + "matrix bytes: $79=$%02X $7B=$%02X $7D=$%02X $85=$%02X $87=$%02X $89=$%02X\n", + z9F, z9E, zA3, zA2, + ram[0x79], ram[0x7B], ram[0x7D], + ram[0x85], ram[0x87], ram[0x89]); + } + + // chunk5 returns via `lda #$05; jmp AddTo8B` which advances + // $8B by 5 (op + 4 vertex bytes). + return 5; +} + + +// PORT_XFORM_INTERPRETED=1 runs the MAME-patched chunk5 transform +// bytecode directly via the 6502 interpreter. This bypasses the +// C re-implementation entirely and produces byte-identical V1/V2 +// output to MAME. Set this to compare drawlists against MAME's +// frozen-frame capture. +// +// Entry point is $7E8E (= the actual TransformVertex7EBC entry in +// the MAME-patched binary, NOT $7EBC like source). The routine +// ends with `JMP $6806` which we treat as the stop PC. Inputs: +// $66..$6B = cam (set by frame setup) +// $79..$89 = matrix +// $2A..$2F = base (set by L631D, copied to $18..$1D inside the +// transform at $7E9B) +// $8B/$8C = stream cursor (we set to offset of `stream` in ram) +// Y = destination slot offset ($CB for V1, $D4 for V2) +// Output: +// $CB..$D0 (V1) or $D4..$D9 (V2) = 6 bytes of V (X lo/hi, Y lo/hi, +// Z lo/hi) +// $4A..$52 = 24-bit signed intermediate +static int chunk5InterpretTransform7EBC(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { + ptrdiff_t streamOff = stream - ram; + if (streamOff < 0 || streamOff > 0xFFFF) { + return 5; + } + // Save the ZP slots we touch so the port's other systems + // don't see corrupted state. + uint8_t save8B = ram[0x8B]; + uint8_t save8C = ram[0x8C]; + uint8_t saveE5 = ram[0xE5]; + + // Point ($8B) at the opcode byte; the transform's first read + // is `LDA ($8B),Y` with Y=1 which fetches stream[1] = vertex X lo. + ram[0x8B] = (uint8_t)(streamOff & 0xFF); + ram[0x8C] = (uint8_t)((streamOff >> 8) & 0xFF); + + Cpu6502T cpu; + cpu6502Init(&cpu, ram); + cpu.y = destSlot; + cpu.s = 0xFD; + cpu.flagD = 0; + cpu.flagI = 1; + // Push a sentinel return address ($6806) so any stray RTS + // inside the transform exits cleanly. JMP $6806 (the tail- + // call exit) is detected as the stop PC by cpu6502Run. + cpu6502PushReturn(&cpu, 0x6806); + + bool ok = cpu6502Run(&cpu, 0x7E8E, 0x6806, 1000000); + (void)ok; + + ram[0x8B] = save8B; + ram[0x8C] = save8C; + ram[0xE5] = saveE5; + + // MAME-patched returns "5" (= LDA #$05; JMP $6806) so the + // dispatcher advances the cursor by 5 bytes. Match that. + return 5; +} + + +int chunk5TransformVertex7EBC(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { + if (getenv("PORT_XFORM_INTERPRETED") != NULL) { + return chunk5InterpretTransform7EBC(ram, stream, destSlot); + } + return transformVertexCommon(ram, stream, destSlot); +} + + +// chunk5 TransformVertex80C5 (chunk5.s line 4576-4707). Companion of +// 7EBC: same matrix multiply but the stream provides a full +// (Xlo, Xhi, Ylo, Yhi, Zlo, Zhi) triplet rather than X/Z only, and +// the multiplier path uses ZPScale (16-bit signed multiply via +// chunk4 ScaleC2ByC4) for all 9 matrix entries instead of the +// 8-bit op_l1818. Used by opcodes $00/$01/$02 (xform-A vertex emit). +// +// The 6-byte stream layout (after opcode): +// stream[1..2] = X (lo, hi) subtracted from $66/$67 +// stream[3..4] = Y (lo, hi) subtracted from $68/$69 +// stream[5..6] = Z (lo, hi) subtracted from $6A/$6B +// +// Matrix at $78..$89 (9 16-bit signed coefficients): +// M[axis_out][delta_in] uses these slots (lo at addr, hi at addr+1): +// X_out: $78 (X), $7E (Y), $84 (Z) +// Y_out: $7A (X), $80 (Y), $86 (Z) +// Z_out: $7C (X), $82 (Y), $88 (Z) +// +// Output is three int16 components written to destSlot..destSlot+5 +// (X lo/hi, Y lo/hi, Z lo/hi). Returns the advance count for $8B +// (always 7: opcode + 6 stream bytes). +int chunk5TransformVertex80C5(uint8_t *ram, const uint8_t *stream, uint8_t destSlot) { + // L80D2..L810E: 16-bit signed deltas with overflow recovery. + // chunk5's overflow paths (L81D9/L820F/L821E) shift earlier + // axes right when a later axis triggers signed overflow on + // the SBC. We model that with a simple cascade: once an axis + // sees an overflow it gets halved, and any preceding axis + // gets halved too so the multiplies stay in proportion. + int32_t streamX = (int16_t)((uint16_t)stream[1] | ((uint16_t)stream[2] << 8)); + int32_t streamY = (int16_t)((uint16_t)stream[3] | ((uint16_t)stream[4] << 8)); + int32_t streamZ = (int16_t)((uint16_t)stream[5] | ((uint16_t)stream[6] << 8)); + + int32_t camX = (int16_t)((uint16_t)ram[0x66] | ((uint16_t)ram[0x67] << 8)); + int32_t camY = (int16_t)((uint16_t)ram[0x68] | ((uint16_t)ram[0x69] << 8)); + int32_t camZ = (int16_t)((uint16_t)ram[0x6A] | ((uint16_t)ram[0x6B] << 8)); + + int32_t dx = streamX - camX; + int32_t dy = streamY - camY; + int32_t dz = streamZ - camZ; + + // Halve any axis that overflowed signed-16 (mirrors chunk5's + // ror-on-V-flag recovery). + if (dx < -32768 || dx > 32767) { + dx >>= 1; + } + if (dy < -32768 || dy > 32767) { + dy >>= 1; + dx >>= 1; // L820F also re-shifts X if Y overflows + } + if (dz < -32768 || dz > 32767) { + dz >>= 1; + dx >>= 1; // L821E shifts X and Y when Z overflows + dy >>= 1; + } + + // L8110: auto-scale -- if any hi byte + $40 has bit 7 set + // (= |delta_hi| >= $40, i.e., the value sits outside the + // [-$4000, +$3FFF] band), arithmetic-shift all three deltas + // right by 1. + { + uint8_t dxHi = (uint8_t)((uint32_t)dx >> 8); + uint8_t dyHi = (uint8_t)((uint32_t)dy >> 8); + uint8_t dzHi = (uint8_t)((uint32_t)dz >> 8); + if (((uint8_t)(dxHi + 0x40) & 0x80) != 0 + || ((uint8_t)(dyHi + 0x40) & 0x80) != 0 + || ((uint8_t)(dzHi + 0x40) & 0x80) != 0) { + dx >>= 1; + dy >>= 1; + dz >>= 1; + } + } + + // Read the 9 matrix coefficients. + int16_t M_X_dX = (int16_t)((uint16_t)ram[0x78] | ((uint16_t)ram[0x79] << 8)); + int16_t M_Y_dX = (int16_t)((uint16_t)ram[0x7A] | ((uint16_t)ram[0x7B] << 8)); + int16_t M_Z_dX = (int16_t)((uint16_t)ram[0x7C] | ((uint16_t)ram[0x7D] << 8)); + int16_t M_X_dY = (int16_t)((uint16_t)ram[0x7E] | ((uint16_t)ram[0x7F] << 8)); + int16_t M_Y_dY = (int16_t)((uint16_t)ram[0x80] | ((uint16_t)ram[0x81] << 8)); + int16_t M_Z_dY = (int16_t)((uint16_t)ram[0x82] | ((uint16_t)ram[0x83] << 8)); + int16_t M_X_dZ = (int16_t)((uint16_t)ram[0x84] | ((uint16_t)ram[0x85] << 8)); + int16_t M_Y_dZ = (int16_t)((uint16_t)ram[0x86] | ((uint16_t)ram[0x87] << 8)); + int16_t M_Z_dZ = (int16_t)((uint16_t)ram[0x88] | ((uint16_t)ram[0x89] << 8)); + + // ZPScale calls + L8234 sums: out = M*dX + M*dY + M*dZ for each + // output axis. Sum is straight 16-bit wraparound add (chunk5 + // does adc on the lo bytes then adc-with-carry on the hi + // bytes; in C that's just int16 plus). + int16_t outX = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_X_dX) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_X_dY) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_X_dZ)); + int16_t outY = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_Y_dX) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_Y_dY) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_Y_dZ)); + int16_t outZ = (int16_t)((uint16_t)chunk5ScaleC2ByC4((int16_t)dx, M_Z_dX) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dy, M_Z_dY) + + (uint16_t)chunk5ScaleC2ByC4((int16_t)dz, M_Z_dZ)); + + // L8234 increments $30 each time the running sum's hi byte + // sits outside the [-$40, +$3F] band (`adc #$40; bpl` skips + // the inc). After all three sums, if $30 != 0 the L81D8 tail + // arithmetic-shifts each output right by 1. We replicate by + // testing each output and halving all three if any triggers. + { + uint8_t hiX = (uint8_t)((uint16_t)outX >> 8); + uint8_t hiY = (uint8_t)((uint16_t)outY >> 8); + uint8_t hiZ = (uint8_t)((uint16_t)outZ >> 8); + bool needHalve = ((uint8_t)(hiX + 0x40) & 0x80) != 0 + || ((uint8_t)(hiY + 0x40) & 0x80) != 0 + || ((uint8_t)(hiZ + 0x40) & 0x80) != 0; + if (needHalve) { + outX = (int16_t)(outX >> 1); + outY = (int16_t)(outY >> 1); + outZ = (int16_t)(outZ >> 1); + } + } + + // L8091-equivalent: store 6 bytes (lo, hi, lo, hi, lo, hi). + ram[destSlot + 0] = (uint8_t)((uint16_t)outX & 0xFFu); + ram[destSlot + 1] = (uint8_t)(((uint16_t)outX >> 8) & 0xFFu); + ram[destSlot + 2] = (uint8_t)((uint16_t)outY & 0xFFu); + ram[destSlot + 3] = (uint8_t)(((uint16_t)outY >> 8) & 0xFFu); + ram[destSlot + 4] = (uint8_t)((uint16_t)outZ & 0xFFu); + ram[destSlot + 5] = (uint8_t)(((uint16_t)outZ >> 8) & 0xFFu); + + return 7; // chunk5 AddTo8B(7) at L813A +} diff --git a/port/src/coursePlotter.c b/port/src/coursePlotter.c new file mode 100644 index 0000000..b591a8f --- /dev/null +++ b/port/src/coursePlotter.c @@ -0,0 +1,161 @@ +// Course Plotter implementation. See coursePlotter.h. + +#include +#include "aircraft.h" +#include "coursePlotter.h" +#include "font.h" +#include "framebuffer.h" +#include "palette.h" + + +// chunk2 BeginRecordingCommon writes a header to the LC buffer: +// byte 0: $07 (record-start marker) +// byte 1: sampleRange +// bytes 2..13: 12 bytes of starting state ($5A..$65 = position triplet) +// byte 14: $1C ($14 + sampleRange?) +// bytes 15..23: padding (0) +// byte 24: $79 (record-mid marker) +// then samples follow as deltas. We keep a simpler format that captures +// the absolute scenery X/Y/Z per sample plus a 1-byte type. +typedef struct CoursePlotSampleT { + int32_t sceneryX; + int32_t sceneryY; + int32_t sceneryZ; + uint8_t flags; +} CoursePlotSampleT; + + +static void writeSample(CoursePlotterT *cp, const CoursePlotSampleT *s) { + if (cp->recordPos + sizeof(CoursePlotSampleT) > COURSE_PLOT_BUFFER_BYTES) { + // Wrap or stop. chunk2 fills until $DFFF then halts. + return; + } + memcpy(cp->buffer + cp->recordPos, s, sizeof(*s)); + cp->recordPos += sizeof(*s); + cp->anyData = true; +} + + +void coursePlotterInit(CoursePlotterT *cp) { + memset(cp, 0, sizeof(*cp)); + cp->state = COURSE_PLOT_OFF; + cp->sampleRate = 10; + cp->sampleCounter = 1; + cp->sampleRange = 6; +} + + +void coursePlotterBeginRecord(CoursePlotterT *cp, bool precision) { + cp->state = COURSE_PLOT_RECORD; + cp->recordPos = 0; + cp->anyData = false; + if (precision) { + cp->sampleRate = 2; // chunk2: $02 + cp->sampleRange = 4; // chunk2: $04 + } else { + cp->sampleRate = 10; // chunk2: $0A + cp->sampleRange = 6; // chunk2: $06 + } + cp->sampleCounter = cp->sampleRate; +} + + +void coursePlotterBeginDisplay(CoursePlotterT *cp) { + if (cp->anyData) { + cp->state = COURSE_PLOT_DISPLAY; + } +} + + +void coursePlotterTurnOff(CoursePlotterT *cp) { + cp->state = COURSE_PLOT_OFF; +} + + +void coursePlotterStep(CoursePlotterT *cp, const AircraftT *ac) { + if (cp->state != COURSE_PLOT_RECORD) { + return; + } + if (cp->sampleCounter > 0) { + cp->sampleCounter--; + return; + } + cp->sampleCounter = cp->sampleRate; + CoursePlotSampleT s; + s.sceneryX = aircraftSceneryX(ac); + s.sceneryY = aircraftSceneryY(ac); + s.sceneryZ = aircraftSceneryZ(ac); + s.flags = 0; + writeSample(cp, &s); +} + + +void coursePlotterRender(const CoursePlotterT *cp, FramebufferT *fb, + const AircraftT *ac) { + if (cp->state != COURSE_PLOT_DISPLAY || !cp->anyData) { + return; + } + // Top-down line plot centred on the aircraft. One scenery + // unit per pixel for normal recordings; range=4 = 4x denser. + const int16_t cx = 140; + const int16_t cy = 64; + const int16_t scaleDiv = (cp->sampleRange == 4) ? 4 : 16; + int32_t acX = aircraftSceneryX(ac); + int32_t acZ = aircraftSceneryZ(ac); + + int16_t prevX = 0; + int16_t prevY = 0; + bool havePrev = false; + for (uint16_t off = 0; off + sizeof(CoursePlotSampleT) <= cp->recordPos; + off += sizeof(CoursePlotSampleT)) { + CoursePlotSampleT s; + memcpy(&s, cp->buffer + off, sizeof(s)); + int32_t dx = s.sceneryX - acX; + int32_t dz = s.sceneryZ - acZ; + int16_t px = (int16_t)(cx + dx / scaleDiv); + int16_t py = (int16_t)(cy - dz / scaleDiv); + if (px < 0 || px >= 280 || py < 0 || py >= 192) { + havePrev = false; + prevX = px; + prevY = py; + continue; + } + framebufferSetPixel(fb, px, py, COLOR_FOREST); + if (havePrev) { + // Emit a 1-pixel-wide horizontal/vertical + // approximation of the segment. + int16_t dxs = (int16_t)(px - prevX); + int16_t dys = (int16_t)(py - prevY); + int16_t steps = (dxs < 0 ? -dxs : dxs); + if ((dys < 0 ? -dys : dys) > steps) { + steps = (dys < 0 ? -dys : dys); + } + if (steps > 0) { + for (int16_t k = 1; k < steps; k++) { + int16_t ix = (int16_t)(prevX + (int)dxs * k / steps); + int16_t iy = (int16_t)(prevY + (int)dys * k / steps); + if (ix >= 0 && ix < 280 && iy >= 0 && iy < 192) { + framebufferSetPixel(fb, ix, iy, COLOR_FOREST); + } + } + } + } + prevX = px; + prevY = py; + havePrev = true; + } +} + + +void coursePlotterDrawStatus(const CoursePlotterT *cp, FramebufferT *fb) { + if (cp == NULL || fb == NULL) { + return; + } + const char *msg = NULL; + switch (cp->state) { + case COURSE_PLOT_RECORD: msg = "COURSE REC"; break; + case COURSE_PLOT_DISPLAY: msg = "COURSE VIEW"; break; + default: return; + } + fontDrawString(fb, 4, 4, msg, COLOR_ORANGE); +} diff --git a/port/src/cpu6502.c b/port/src/cpu6502.c new file mode 100644 index 0000000..e6497b5 --- /dev/null +++ b/port/src/cpu6502.c @@ -0,0 +1,408 @@ +// Minimal 6502 / 65C02 interpreter. Extracted from tools/fs2trace.c +// and refactored to hold all state in Cpu6502T so it can be linked +// into the runtime (= chunk5Transform.c uses it to run the MAME- +// patched per-vertex transform bytecode directly). + +#include +#include + +#include "cpu6502.h" + + +static inline uint8_t rd(Cpu6502T *cpu, uint16_t addr) { + return cpu->mem[addr]; +} + + +static inline void wr(Cpu6502T *cpu, uint16_t addr, uint8_t v) { + cpu->mem[addr] = v; +} + + +static inline uint8_t fetch(Cpu6502T *cpu) { + return cpu->mem[cpu->pc++]; +} + + +static inline uint16_t fetch16(Cpu6502T *cpu) { + uint16_t lo = fetch(cpu); + uint16_t hi = fetch(cpu); + return lo | (hi << 8); +} + + +// Zero-page-wrapping 16-bit read for (zp,X) / (zp),Y addressing. +static inline uint16_t rd16zp(Cpu6502T *cpu, uint8_t zp) { + uint8_t lo = cpu->mem[zp]; + uint8_t hi = cpu->mem[(uint8_t)(zp + 1)]; + return (uint16_t)lo | ((uint16_t)hi << 8); +} + + +static inline void push(Cpu6502T *cpu, uint8_t v) { + cpu->mem[0x0100 + cpu->s] = v; + cpu->s--; +} + + +static inline uint8_t pop(Cpu6502T *cpu) { + cpu->s++; + return cpu->mem[0x0100 + cpu->s]; +} + + +static inline void push16(Cpu6502T *cpu, uint16_t v) { + push(cpu, (uint8_t)(v >> 8)); + push(cpu, (uint8_t)(v & 0xFF)); +} + + +static inline uint16_t pop16(Cpu6502T *cpu) { + uint8_t lo = pop(cpu); + uint8_t hi = pop(cpu); + return (uint16_t)lo | ((uint16_t)hi << 8); +} + + +static inline void setNZ(Cpu6502T *cpu, uint8_t v) { + cpu->flagN = (v & 0x80) ? 1 : 0; + cpu->flagZ = (v == 0) ? 1 : 0; +} + + +static inline uint8_t getP(Cpu6502T *cpu) { + return (uint8_t)((cpu->flagN << 7) | (cpu->flagV << 6) | 0x20 + | (cpu->flagD << 3) | (cpu->flagI << 2) + | (cpu->flagZ << 1) | cpu->flagC); +} + + +static inline void setP(Cpu6502T *cpu, uint8_t v) { + cpu->flagN = (v & 0x80) ? 1 : 0; + cpu->flagV = (v & 0x40) ? 1 : 0; + cpu->flagD = (v & 0x08) ? 1 : 0; + cpu->flagI = (v & 0x04) ? 1 : 0; + cpu->flagZ = (v & 0x02) ? 1 : 0; + cpu->flagC = (v & 0x01) ? 1 : 0; +} + + +void cpu6502Init(Cpu6502T *cpu, uint8_t *mem) { + cpu->mem = mem; + cpu->pc = 0; + cpu->a = 0; + cpu->x = 0; + cpu->y = 0; + cpu->s = 0xFF; + cpu->flagN = 0; + cpu->flagV = 0; + cpu->flagD = 0; + cpu->flagI = 0; + cpu->flagZ = 0; + cpu->flagC = 0; + cpu->unknownOp = false; + cpu->lastOp = 0; + cpu->lastOpPc = 0; + cpu->traceFn = NULL; + cpu->traceUserData = NULL; +} + + +void cpu6502SetTrace(Cpu6502T *cpu, Cpu6502TraceFn fn, void *userData) { + cpu->traceFn = fn; + cpu->traceUserData = userData; +} + + +void cpu6502PushReturn(Cpu6502T *cpu, uint16_t returnAfter) { + // JSR pushes (target_pc - 1) so that RTS pops (target_pc - 1) + // and adds 1. To make RTS jump to `returnAfter`, we push + // (returnAfter - 1). + push16(cpu, (uint16_t)(returnAfter - 1)); +} + + +void cpu6502Step(Cpu6502T *cpu) { + cpu->lastOpPc = cpu->pc; + uint8_t op = fetch(cpu); + cpu->lastOp = op; + switch (op) { + case 0xEA: break; // NOP + case 0x18: cpu->flagC = 0; break; // CLC + case 0x38: cpu->flagC = 1; break; // SEC + case 0xD8: cpu->flagD = 0; break; // CLD + case 0xF8: cpu->flagD = 1; break; // SED + case 0x58: cpu->flagI = 0; break; // CLI + case 0x78: cpu->flagI = 1; break; // SEI + case 0xB8: cpu->flagV = 0; break; // CLV + case 0xAA: cpu->x = cpu->a; setNZ(cpu, cpu->x); break; // TAX + case 0xA8: cpu->y = cpu->a; setNZ(cpu, cpu->y); break; // TAY + case 0x8A: cpu->a = cpu->x; setNZ(cpu, cpu->a); break; // TXA + case 0x98: cpu->a = cpu->y; setNZ(cpu, cpu->a); break; // TYA + case 0xBA: cpu->x = cpu->s; setNZ(cpu, cpu->x); break; // TSX + case 0x9A: cpu->s = cpu->x; break; // TXS + case 0x48: push(cpu, cpu->a); break; // PHA + case 0x68: cpu->a = pop(cpu); setNZ(cpu, cpu->a); break;// PLA + case 0x08: push(cpu, (uint8_t)(getP(cpu) | 0x10)); break;// PHP + case 0x28: setP(cpu, pop(cpu)); break; // PLP + case 0xE8: cpu->x++; setNZ(cpu, cpu->x); break; // INX + case 0xC8: cpu->y++; setNZ(cpu, cpu->y); break; // INY + case 0xCA: cpu->x--; setNZ(cpu, cpu->x); break; // DEX + case 0x88: cpu->y--; setNZ(cpu, cpu->y); break; // DEY + + case 0xA9: cpu->a = fetch(cpu); setNZ(cpu, cpu->a); break; + case 0xA5: cpu->a = rd(cpu, fetch(cpu)); setNZ(cpu, cpu->a); break; + case 0xB5: cpu->a = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); setNZ(cpu, cpu->a); break; + case 0xAD: cpu->a = rd(cpu, fetch16(cpu)); setNZ(cpu, cpu->a); break; + case 0xBD: { uint16_t a = fetch16(cpu); cpu->a = rd(cpu, (uint16_t)(a + cpu->x)); setNZ(cpu, cpu->a); break; } + case 0xB9: { uint16_t a = fetch16(cpu); cpu->a = rd(cpu, (uint16_t)(a + cpu->y)); setNZ(cpu, cpu->a); break; } + case 0xA1: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); cpu->a = rd(cpu, a); setNZ(cpu, cpu->a); break; } + case 0xB1: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); cpu->a = rd(cpu, a); setNZ(cpu, cpu->a); break; } + + case 0xA2: cpu->x = fetch(cpu); setNZ(cpu, cpu->x); break; + case 0xA6: cpu->x = rd(cpu, fetch(cpu)); setNZ(cpu, cpu->x); break; + case 0xB6: cpu->x = rd(cpu, (uint8_t)(fetch(cpu) + cpu->y)); setNZ(cpu, cpu->x); break; + case 0xAE: cpu->x = rd(cpu, fetch16(cpu)); setNZ(cpu, cpu->x); break; + case 0xBE: { uint16_t a = fetch16(cpu); cpu->x = rd(cpu, (uint16_t)(a + cpu->y)); setNZ(cpu, cpu->x); break; } + + case 0xA0: cpu->y = fetch(cpu); setNZ(cpu, cpu->y); break; + case 0xA4: cpu->y = rd(cpu, fetch(cpu)); setNZ(cpu, cpu->y); break; + case 0xB4: cpu->y = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); setNZ(cpu, cpu->y); break; + case 0xAC: cpu->y = rd(cpu, fetch16(cpu)); setNZ(cpu, cpu->y); break; + case 0xBC: { uint16_t a = fetch16(cpu); cpu->y = rd(cpu, (uint16_t)(a + cpu->x)); setNZ(cpu, cpu->y); break; } + + case 0x85: wr(cpu, fetch(cpu), cpu->a); break; + case 0x95: wr(cpu, (uint8_t)(fetch(cpu) + cpu->x), cpu->a); break; + case 0x8D: wr(cpu, fetch16(cpu), cpu->a); break; + case 0x9D: { uint16_t a = fetch16(cpu); wr(cpu, (uint16_t)(a + cpu->x), cpu->a); break; } + case 0x99: { uint16_t a = fetch16(cpu); wr(cpu, (uint16_t)(a + cpu->y), cpu->a); break; } + case 0x81: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); wr(cpu, a, cpu->a); break; } + case 0x91: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); wr(cpu, a, cpu->a); break; } + + case 0x86: wr(cpu, fetch(cpu), cpu->x); break; + case 0x96: wr(cpu, (uint8_t)(fetch(cpu) + cpu->y), cpu->x); break; + case 0x8E: wr(cpu, fetch16(cpu), cpu->x); break; + case 0x84: wr(cpu, fetch(cpu), cpu->y); break; + case 0x94: wr(cpu, (uint8_t)(fetch(cpu) + cpu->x), cpu->y); break; + case 0x8C: wr(cpu, fetch16(cpu), cpu->y); break; + + case 0xE6: { uint8_t a = fetch(cpu); uint8_t v = (uint8_t)(rd(cpu, a) + 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xF6: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = (uint8_t)(rd(cpu, a) + 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xEE: { uint16_t a = fetch16(cpu); uint8_t v = (uint8_t)(rd(cpu, a) + 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xFE: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = (uint8_t)(rd(cpu, a) + 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xC6: { uint8_t a = fetch(cpu); uint8_t v = (uint8_t)(rd(cpu, a) - 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xD6: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = (uint8_t)(rd(cpu, a) - 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xCE: { uint16_t a = fetch16(cpu); uint8_t v = (uint8_t)(rd(cpu, a) - 1); wr(cpu, a, v); setNZ(cpu, v); break; } + case 0xDE: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = (uint8_t)(rd(cpu, a) - 1); wr(cpu, a, v); setNZ(cpu, v); break; } + + #define DO_ADC(v) do { uint16_t s = (uint16_t)cpu->a + (uint16_t)(v) + (uint16_t)cpu->flagC; \ + cpu->flagC = (s > 0xFF) ? 1 : 0; \ + cpu->flagV = ((cpu->a ^ (v)) & 0x80) ? 0 : (((cpu->a ^ s) & 0x80) ? 1 : 0); \ + cpu->a = (uint8_t)s; setNZ(cpu, cpu->a); } while (0) + #define DO_SBC(v) do { uint8_t vv = (uint8_t)~(v); \ + uint16_t s = (uint16_t)cpu->a + (uint16_t)vv + (uint16_t)cpu->flagC; \ + cpu->flagC = (s > 0xFF) ? 1 : 0; \ + cpu->flagV = ((cpu->a ^ vv) & 0x80) ? 0 : (((cpu->a ^ s) & 0x80) ? 1 : 0); \ + cpu->a = (uint8_t)s; setNZ(cpu, cpu->a); } while (0) + + case 0x69: { uint8_t v = fetch(cpu); DO_ADC(v); break; } + case 0x65: { uint8_t v = rd(cpu, fetch(cpu)); DO_ADC(v); break; } + case 0x75: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_ADC(v); break; } + case 0x6D: { uint8_t v = rd(cpu, fetch16(cpu)); DO_ADC(v); break; } + case 0x7D: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->x)); DO_ADC(v); break; } + case 0x79: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->y)); DO_ADC(v); break; } + case 0x71: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); uint8_t v = rd(cpu, a); DO_ADC(v); break; } + case 0x61: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_ADC(v); break; } + + case 0xE9: { uint8_t v = fetch(cpu); DO_SBC(v); break; } + case 0xE5: { uint8_t v = rd(cpu, fetch(cpu)); DO_SBC(v); break; } + case 0xF5: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_SBC(v); break; } + case 0xED: { uint8_t v = rd(cpu, fetch16(cpu)); DO_SBC(v); break; } + case 0xFD: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->x)); DO_SBC(v); break; } + case 0xF9: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->y)); DO_SBC(v); break; } + case 0xF1: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); uint8_t v = rd(cpu, a); DO_SBC(v); break; } + case 0xE1: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_SBC(v); break; } + + #define DO_CMP(reg, v) do { uint16_t r = (uint16_t)(reg) + 0x100 - (uint16_t)(v); \ + cpu->flagC = ((reg) >= (v)) ? 1 : 0; setNZ(cpu, (uint8_t)(r & 0xFF)); } while (0) + case 0xC9: { uint8_t v = fetch(cpu); DO_CMP(cpu->a, v); break; } + case 0xC5: { uint8_t v = rd(cpu, fetch(cpu)); DO_CMP(cpu->a, v); break; } + case 0xD5: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_CMP(cpu->a, v); break; } + case 0xCD: { uint8_t v = rd(cpu, fetch16(cpu)); DO_CMP(cpu->a, v); break; } + case 0xDD: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->x)); DO_CMP(cpu->a, v); break; } + case 0xD9: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, (uint16_t)(a + cpu->y)); DO_CMP(cpu->a, v); break; } + case 0xD1: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); uint8_t v = rd(cpu, a); DO_CMP(cpu->a, v); break; } + case 0xC1: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_CMP(cpu->a, v); break; } + case 0xE0: { uint8_t v = fetch(cpu); DO_CMP(cpu->x, v); break; } + case 0xE4: { uint8_t v = rd(cpu, fetch(cpu)); DO_CMP(cpu->x, v); break; } + case 0xEC: { uint8_t v = rd(cpu, fetch16(cpu)); DO_CMP(cpu->x, v); break; } + case 0xC0: { uint8_t v = fetch(cpu); DO_CMP(cpu->y, v); break; } + case 0xC4: { uint8_t v = rd(cpu, fetch(cpu)); DO_CMP(cpu->y, v); break; } + case 0xCC: { uint8_t v = rd(cpu, fetch16(cpu)); DO_CMP(cpu->y, v); break; } + + #define DO_AND(v) do { cpu->a &= (v); setNZ(cpu, cpu->a); } while (0) + #define DO_ORA(v) do { cpu->a |= (v); setNZ(cpu, cpu->a); } while (0) + #define DO_EOR(v) do { cpu->a ^= (v); setNZ(cpu, cpu->a); } while (0) + case 0x29: { uint8_t v = fetch(cpu); DO_AND(v); break; } + case 0x25: { uint8_t v = rd(cpu, fetch(cpu)); DO_AND(v); break; } + case 0x35: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_AND(v); break; } + case 0x2D: { uint8_t v = rd(cpu, fetch16(cpu)); DO_AND(v); break; } + case 0x3D: { uint16_t a = fetch16(cpu); DO_AND(rd(cpu, (uint16_t)(a + cpu->x))); break; } + case 0x39: { uint16_t a = fetch16(cpu); DO_AND(rd(cpu, (uint16_t)(a + cpu->y))); break; } + case 0x31: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); DO_AND(rd(cpu, a)); break; } + case 0x21: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); DO_AND(rd(cpu, a)); break; } + case 0x09: { uint8_t v = fetch(cpu); DO_ORA(v); break; } + case 0x05: { uint8_t v = rd(cpu, fetch(cpu)); DO_ORA(v); break; } + case 0x15: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_ORA(v); break; } + case 0x0D: { uint8_t v = rd(cpu, fetch16(cpu)); DO_ORA(v); break; } + case 0x1D: { uint16_t a = fetch16(cpu); DO_ORA(rd(cpu, (uint16_t)(a + cpu->x))); break; } + case 0x19: { uint16_t a = fetch16(cpu); DO_ORA(rd(cpu, (uint16_t)(a + cpu->y))); break; } + case 0x11: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); DO_ORA(rd(cpu, a)); break; } + case 0x01: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); DO_ORA(rd(cpu, a)); break; } + case 0x49: { uint8_t v = fetch(cpu); DO_EOR(v); break; } + case 0x45: { uint8_t v = rd(cpu, fetch(cpu)); DO_EOR(v); break; } + case 0x55: { uint8_t v = rd(cpu, (uint8_t)(fetch(cpu) + cpu->x)); DO_EOR(v); break; } + case 0x4D: { uint8_t v = rd(cpu, fetch16(cpu)); DO_EOR(v); break; } + case 0x5D: { uint16_t a = fetch16(cpu); DO_EOR(rd(cpu, (uint16_t)(a + cpu->x))); break; } + case 0x59: { uint16_t a = fetch16(cpu); DO_EOR(rd(cpu, (uint16_t)(a + cpu->y))); break; } + case 0x51: { uint8_t z = fetch(cpu); uint16_t a = (uint16_t)(rd16zp(cpu, z) + cpu->y); DO_EOR(rd(cpu, a)); break; } + case 0x41: { uint8_t z = (uint8_t)(fetch(cpu) + cpu->x); uint16_t a = rd16zp(cpu, z); DO_EOR(rd(cpu, a)); break; } + + case 0x24: { uint8_t v = rd(cpu, fetch(cpu)); cpu->flagZ = (cpu->a & v) == 0 ? 1 : 0; cpu->flagN = (v & 0x80) ? 1 : 0; cpu->flagV = (v & 0x40) ? 1 : 0; break; } + case 0x2C: { uint8_t v = rd(cpu, fetch16(cpu)); cpu->flagZ = (cpu->a & v) == 0 ? 1 : 0; cpu->flagN = (v & 0x80) ? 1 : 0; cpu->flagV = (v & 0x40) ? 1 : 0; break; } + + #define ASL(v) do { cpu->flagC = ((v) & 0x80) ? 1 : 0; (v) = (uint8_t)((v) << 1); setNZ(cpu, v); } while (0) + #define LSR(v) do { cpu->flagC = (v) & 1; (v) = (uint8_t)((v) >> 1); setNZ(cpu, v); } while (0) + #define ROL(v) do { uint8_t c = cpu->flagC; cpu->flagC = ((v) & 0x80) ? 1 : 0; (v) = (uint8_t)(((v) << 1) | c); setNZ(cpu, v); } while (0) + #define ROR(v) do { uint8_t c = cpu->flagC; cpu->flagC = (v) & 1; (v) = (uint8_t)(((v) >> 1) | (c << 7)); setNZ(cpu, v); } while (0) + case 0x0A: ASL(cpu->a); break; + case 0x06: { uint8_t a = fetch(cpu); uint8_t v = rd(cpu, a); ASL(v); wr(cpu, a, v); break; } + case 0x16: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = rd(cpu, a); ASL(v); wr(cpu, a, v); break; } + case 0x0E: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, a); ASL(v); wr(cpu, a, v); break; } + case 0x1E: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = rd(cpu, a); ASL(v); wr(cpu, a, v); break; } + case 0x4A: LSR(cpu->a); break; + case 0x46: { uint8_t a = fetch(cpu); uint8_t v = rd(cpu, a); LSR(v); wr(cpu, a, v); break; } + case 0x56: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = rd(cpu, a); LSR(v); wr(cpu, a, v); break; } + case 0x4E: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, a); LSR(v); wr(cpu, a, v); break; } + case 0x5E: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = rd(cpu, a); LSR(v); wr(cpu, a, v); break; } + case 0x2A: ROL(cpu->a); break; + case 0x26: { uint8_t a = fetch(cpu); uint8_t v = rd(cpu, a); ROL(v); wr(cpu, a, v); break; } + case 0x36: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = rd(cpu, a); ROL(v); wr(cpu, a, v); break; } + case 0x2E: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, a); ROL(v); wr(cpu, a, v); break; } + case 0x3E: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = rd(cpu, a); ROL(v); wr(cpu, a, v); break; } + case 0x6A: ROR(cpu->a); break; + case 0x66: { uint8_t a = fetch(cpu); uint8_t v = rd(cpu, a); ROR(v); wr(cpu, a, v); break; } + case 0x76: { uint8_t a = (uint8_t)(fetch(cpu) + cpu->x); uint8_t v = rd(cpu, a); ROR(v); wr(cpu, a, v); break; } + case 0x6E: { uint16_t a = fetch16(cpu); uint8_t v = rd(cpu, a); ROR(v); wr(cpu, a, v); break; } + case 0x7E: { uint16_t a = (uint16_t)(fetch16(cpu) + cpu->x); uint8_t v = rd(cpu, a); ROR(v); wr(cpu, a, v); break; } + + #define BRANCH(cond) do { int8_t off = (int8_t)fetch(cpu); if (cond) cpu->pc = (uint16_t)(cpu->pc + off); } while (0) + case 0x10: BRANCH(!cpu->flagN); break; // BPL + case 0x30: BRANCH( cpu->flagN); break; // BMI + case 0x50: BRANCH(!cpu->flagV); break; // BVC + case 0x70: BRANCH( cpu->flagV); break; // BVS + case 0x90: BRANCH(!cpu->flagC); break; // BCC + case 0xB0: BRANCH( cpu->flagC); break; // BCS + case 0xD0: BRANCH(!cpu->flagZ); break; // BNE + case 0xF0: BRANCH( cpu->flagZ); break; // BEQ + + case 0x4C: cpu->pc = fetch16(cpu); break; // JMP abs + case 0x6C: { uint16_t a = fetch16(cpu); // JMP (ind) -- 6502 page-bug + uint16_t lo = cpu->mem[a]; + uint16_t hi = cpu->mem[(a & 0xFF00) | ((a + 1) & 0xFF)]; + cpu->pc = (uint16_t)(lo | (hi << 8)); + break; } + case 0x20: { uint16_t target = fetch16(cpu); // JSR + push16(cpu, (uint16_t)(cpu->pc - 1)); + cpu->pc = target; + break; } + case 0x60: cpu->pc = (uint16_t)(pop16(cpu) + 1); break; // RTS + case 0x40: setP(cpu, pop(cpu)); cpu->pc = pop16(cpu); break; // RTI + + // 65C02 BRA rel + case 0x80: { int8_t off = (int8_t)fetch(cpu); cpu->pc = (uint16_t)(cpu->pc + off); break; } + case 0x3A: cpu->a = (uint8_t)(cpu->a - 1); setNZ(cpu, cpu->a); break; + case 0x1A: cpu->a = (uint8_t)(cpu->a + 1); setNZ(cpu, cpu->a); break; + + // 65C02 TRB / TSB + case 0x14: { uint8_t zp = fetch(cpu); uint8_t m = cpu->mem[zp]; cpu->flagZ = ((cpu->a & m) == 0) ? 1 : 0; cpu->mem[zp] = (uint8_t)(m & ~cpu->a); break; } + case 0x1C: { uint16_t a = fetch16(cpu); uint8_t m = rd(cpu, a); cpu->flagZ = ((cpu->a & m) == 0) ? 1 : 0; wr(cpu, a, (uint8_t)(m & ~cpu->a)); break; } + case 0x04: { uint8_t zp = fetch(cpu); uint8_t m = cpu->mem[zp]; cpu->flagZ = ((cpu->a & m) == 0) ? 1 : 0; cpu->mem[zp] = (uint8_t)(m | cpu->a); break; } + case 0x0C: { uint16_t a = fetch16(cpu); uint8_t m = rd(cpu, a); cpu->flagZ = ((cpu->a & m) == 0) ? 1 : 0; wr(cpu, a, (uint8_t)(m | cpu->a)); break; } + + // 65C02 STZ + case 0x64: { uint8_t zp = fetch(cpu); cpu->mem[zp] = 0; break; } + case 0x74: { uint8_t zp = fetch(cpu); cpu->mem[(uint8_t)(zp + cpu->x)] = 0; break; } + case 0x9C: { uint16_t a = fetch16(cpu); wr(cpu, a, 0); break; } + case 0x9E: { uint16_t a = fetch16(cpu); wr(cpu, (uint16_t)(a + cpu->x), 0); break; } + + // 65C02 PHX / PHY / PLX / PLY + case 0x5A: push(cpu, cpu->y); break; + case 0x7A: cpu->y = pop(cpu); setNZ(cpu, cpu->y); break; + case 0xDA: push(cpu, cpu->x); break; + case 0xFA: cpu->x = pop(cpu); setNZ(cpu, cpu->x); break; + + // 65C02 (zp) indirect for LDA / STA / ADC / SBC / AND / ORA / EOR / CMP + case 0xB2: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); cpu->a = rd(cpu, a); setNZ(cpu, cpu->a); break; } + case 0x92: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); wr(cpu, a, cpu->a); break; } + case 0x72: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_ADC(v); break; } + case 0xF2: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_SBC(v); break; } + case 0x32: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); DO_AND(rd(cpu, a)); break; } + case 0x12: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); DO_ORA(rd(cpu, a)); break; } + case 0x52: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); DO_EOR(rd(cpu, a)); break; } + case 0xD2: { uint8_t z = fetch(cpu); uint16_t a = rd16zp(cpu, z); uint8_t v = rd(cpu, a); DO_CMP(cpu->a, v); break; } + + default: + cpu->unknownOp = true; + cpu->pc = (uint16_t)(cpu->pc - 1); + break; + } +} + + +bool cpu6502Run(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, int maxSteps) { + return cpu6502RunWithHook(cpu, entry, stopPc, 0xFFFF, NULL, NULL, maxSteps); +} + + +bool cpu6502RunWithHook(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, + uint16_t hookPc, Cpu6502HookFn cb, void *userData, + int maxSteps) { + Cpu6502HookT h = { hookPc, cb, userData }; + return cpu6502RunWithHooks(cpu, entry, stopPc, &h, cb ? 1 : 0, maxSteps); +} + + +bool cpu6502RunWithHooks(Cpu6502T *cpu, uint16_t entry, uint16_t stopPc, + const Cpu6502HookT *hooks, int nHooks, + int maxSteps) { + cpu->pc = entry; + cpu->unknownOp = false; + for (int i = 0; i < maxSteps; i++) { + if (cpu->pc == stopPc) { + return true; + } + bool handled = false; + for (int h = 0; h < nHooks; h++) { + if (cpu->pc == hooks[h].pc) { + handled = hooks[h].cb(cpu, hooks[h].userData); + break; + } + } + if (handled) { + continue; + } + if (cpu->traceFn != NULL) { + cpu->traceFn(cpu, cpu->traceUserData); + } + cpu6502Step(cpu); + if (cpu->unknownOp) { + fprintf(stderr, "cpu6502Run: unknown opcode $%02X at PC $%04X\n", + cpu->lastOp, cpu->lastOpPc); + return false; + } + } + fprintf(stderr, "cpu6502Run: maxSteps (%d) exceeded, last PC $%04X (op $%02X)\n", + maxSteps, cpu->lastOpPc, cpu->lastOp); + return false; +} diff --git a/port/src/fixture.c b/port/src/fixture.c new file mode 100644 index 0000000..2d7cc33 --- /dev/null +++ b/port/src/fixture.c @@ -0,0 +1,63 @@ +// Hardcoded scenery byte stream that exercises the opcodes the +// modernized VM understands. Drawn relative to the centre of the +// viewport so we get something on screen even before projection / +// camera transforms exist. + +#include "fixture.h" + + +// Convenience wrappers so the stream below reads close to the original +// scenery bytecode while staying ASCII-only. +#define OP_SET_COLOR(c) 0x12, (c) +#define OP_DRAW_LINE(x1,y1,x2,y2) 0x06, (uint8_t)(x1), (uint8_t)(y1), (uint8_t)(x2), (uint8_t)(y2) +#define OP_RETURN 0x19 +#define OP_END 0x46 + + +// Demo scene: horizon, an angular runway, a small "tower", a water +// strip in the foreground. +const uint8_t fixtureSceneryDemo[] = { + // Horizon line at viewport mid-height. Stretch a little past + // the visible extents so clipping engages. + OP_SET_COLOR(0x0D), // white + OP_DRAW_LINE(-70, 0, 70, 0), + + // Long thin lake in the foreground (was purple on Apple II). + OP_SET_COLOR(0x02), // water + OP_DRAW_LINE(-50, 25, 50, 25), + OP_DRAW_LINE(-50, 30, 50, 30), + + // Runway: trapezoidal outline receding to the horizon. + OP_SET_COLOR(0x06), // runway grey + OP_DRAW_LINE(-25, 40, -8, 5), // left rail + OP_DRAW_LINE( 25, 40, 8, 5), // right rail + OP_DRAW_LINE(-25, 40, 25, 40), // near threshold + OP_DRAW_LINE( -8, 5, 8, 5), // far threshold + + // Centre line dashes. + OP_DRAW_LINE( 0, 35, 0, 30), + OP_DRAW_LINE( 0, 25, 0, 20), + OP_DRAW_LINE( 0, 15, 0, 12), + + // Control tower silhouette to the right. + OP_SET_COLOR(0x07), // building + OP_DRAW_LINE( 35, 40, 35, 15), + OP_DRAW_LINE( 45, 40, 45, 15), + OP_DRAW_LINE( 35, 15, 45, 15), + OP_DRAW_LINE( 32, 10, 48, 10), + OP_DRAW_LINE( 32, 10, 35, 15), + OP_DRAW_LINE( 48, 10, 45, 15), + + // Mountain ridge on the horizon. + OP_SET_COLOR(0x0C), // mountain + OP_DRAW_LINE(-65, -1, -45, -10), + OP_DRAW_LINE(-45, -10, -25, -3), + OP_DRAW_LINE(-25, -3, -8, -8), + OP_DRAW_LINE( -8, -8, 10, -2), + OP_DRAW_LINE( 10, -2, 35, -12), + OP_DRAW_LINE( 35, -12, 65, -1), + + OP_RETURN, + OP_END +}; +const uint32_t fixtureSceneryDemoLength = sizeof(fixtureSceneryDemo); diff --git a/port/src/font.c b/port/src/font.c new file mode 100644 index 0000000..d166622 --- /dev/null +++ b/port/src/font.c @@ -0,0 +1,108 @@ +// 5x7 bitmap font. Each glyph is 7 bytes; each byte is one row, low +// bit = leftmost pixel. +// +// Glyphs cover ASCII $20 (' ') through $5F ('_'). Anything outside +// that range falls back to a blank. + +#include +#include "font.h" + + +static const uint8_t glyphs[][FONT_HEIGHT] = { + {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, // ' ' + {0x04,0x04,0x04,0x04,0x00,0x04,0x00}, // '!' + {0x0A,0x0A,0x00,0x00,0x00,0x00,0x00}, // '"' + {0x0A,0x1F,0x0A,0x1F,0x0A,0x00,0x00}, // '#' + {0x04,0x0F,0x14,0x0E,0x05,0x1E,0x04}, // '$' + {0x19,0x19,0x02,0x04,0x08,0x13,0x13}, // '%' + {0x06,0x09,0x05,0x02,0x15,0x09,0x16}, // '&' + {0x04,0x04,0x00,0x00,0x00,0x00,0x00}, // ''' + {0x02,0x04,0x08,0x08,0x08,0x04,0x02}, // '(' + {0x08,0x04,0x02,0x02,0x02,0x04,0x08}, // ')' + {0x00,0x04,0x15,0x0E,0x15,0x04,0x00}, // '*' + {0x00,0x04,0x04,0x1F,0x04,0x04,0x00}, // '+' + {0x00,0x00,0x00,0x00,0x00,0x04,0x08}, // ',' + {0x00,0x00,0x00,0x1F,0x00,0x00,0x00}, // '-' + {0x00,0x00,0x00,0x00,0x00,0x00,0x04}, // '.' + {0x01,0x01,0x02,0x04,0x08,0x10,0x10}, // '/' + {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, // '0' + {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E}, // '1' + {0x0E,0x11,0x01,0x06,0x08,0x10,0x1F}, // '2' + {0x1F,0x01,0x02,0x06,0x01,0x11,0x0E}, // '3' + {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, // '4' + {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E}, // '5' + {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, // '6' + {0x1F,0x01,0x02,0x04,0x08,0x08,0x08}, // '7' + {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, // '8' + {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}, // '9' + {0x00,0x04,0x00,0x00,0x04,0x00,0x00}, // ':' + {0x00,0x04,0x00,0x00,0x04,0x04,0x08}, // ';' + {0x02,0x04,0x08,0x10,0x08,0x04,0x02}, // '<' + {0x00,0x00,0x1F,0x00,0x1F,0x00,0x00}, // '=' + {0x08,0x04,0x02,0x01,0x02,0x04,0x08}, // '>' + {0x0E,0x11,0x01,0x02,0x04,0x00,0x04}, // '?' + {0x0E,0x11,0x17,0x15,0x17,0x10,0x0E}, // '@' + {0x0E,0x11,0x11,0x11,0x1F,0x11,0x11}, // 'A' + {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E}, // 'B' + {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, // 'C' + {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E}, // 'D' + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, // 'E' + {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10}, // 'F' + {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, // 'G' + {0x11,0x11,0x11,0x1F,0x11,0x11,0x11}, // 'H' + {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, // 'I' + {0x07,0x02,0x02,0x02,0x02,0x12,0x0C}, // 'J' + {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, // 'K' + {0x10,0x10,0x10,0x10,0x10,0x10,0x1F}, // 'L' + {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, // 'M' + {0x11,0x11,0x19,0x15,0x13,0x11,0x11}, // 'N' + {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, // 'O' + {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10}, // 'P' + {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, // 'Q' + {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11}, // 'R' + {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, // 'S' + {0x1F,0x04,0x04,0x04,0x04,0x04,0x04}, // 'T' + {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, // 'U' + {0x11,0x11,0x11,0x11,0x11,0x0A,0x04}, // 'V' + {0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, // 'W' + {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11}, // 'X' + {0x11,0x11,0x11,0x0A,0x04,0x04,0x04}, // 'Y' + {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F}, // 'Z' + {0x0E,0x08,0x08,0x08,0x08,0x08,0x0E}, // '[' + {0x10,0x10,0x08,0x04,0x02,0x01,0x01}, // '\' + {0x0E,0x02,0x02,0x02,0x02,0x02,0x0E}, // ']' + {0x04,0x0A,0x11,0x00,0x00,0x00,0x00}, // '^' + {0x00,0x00,0x00,0x00,0x00,0x00,0x1F}, // '_' +}; + +#define GLYPH_FIRST 0x20 +#define GLYPH_LAST 0x5F + + +int16_t fontDrawChar(FramebufferT *fb, int16_t x, int16_t y, char ch, ColorE color) { + if (ch >= 'a' && ch <= 'z') { + ch = (char)(ch - 'a' + 'A'); + } + if (ch < GLYPH_FIRST || ch > GLYPH_LAST) { + return FONT_WIDTH + 1; + } + const uint8_t *rows = glyphs[(uint8_t)ch - GLYPH_FIRST]; + for (int row = 0; row < FONT_HEIGHT; row++) { + uint8_t bits = rows[row]; + for (int col = 0; col < FONT_WIDTH; col++) { + if (bits & (1 << (FONT_WIDTH - 1 - col))) { + framebufferSetPixel(fb, (int16_t)(x + col), (int16_t)(y + row), color); + } + } + } + return FONT_WIDTH + 1; +} + + +int16_t fontDrawString(FramebufferT *fb, int16_t x, int16_t y, const char *s, ColorE color) { + int16_t cursor = x; + for (; *s != '\0'; s++) { + cursor = (int16_t)(cursor + fontDrawChar(fb, cursor, y, *s, color)); + } + return cursor; +} diff --git a/port/src/framebuffer.c b/port/src/framebuffer.c new file mode 100644 index 0000000..2d818aa --- /dev/null +++ b/port/src/framebuffer.c @@ -0,0 +1,107 @@ +// Software framebuffer implementation. + +#include +#include +#include "framebuffer.h" +#include "hires.h" + + +static inline bool inBounds(int16_t x, int16_t y) { + return x >= 0 && x < NATIVE_WIDTH && y >= 0 && y < NATIVE_HEIGHT; +} + + +void framebufferBlitTo32(const FramebufferT *fb, uint32_t *dst, int dstWidth, int dstHeight) { + int scaleX = dstWidth / NATIVE_WIDTH; + int scaleY = dstHeight / NATIVE_HEIGHT; + if (scaleX < 1) { + scaleX = 1; + } + if (scaleY < 1) { + scaleY = 1; + } + + // Default: use the palette buffer (= per-pixel ColorE values + // written by framebufferSetPixel) for the whole image. This + // gives a clean modern look without Apple II NTSC color + // fringing -- lines render in their requested colors, sky/ + // ground fills are solid, no composite-signal smear between + // adjacent bits. + // + // Set SCENERY_NTSC=1 to fall back to the original NTSC decode + // of the hires bitplane (= adjacent-bit pair merging that + // produces violet/green/blue/orange from bit patterns, with + // characteristic fringing). Useful for verifying against + // MAME's reference and for showcasing the original look. + bool useHiresViewport = (getenv("SCENERY_NTSC") != NULL); + uint32_t *viewportRgb = NULL; + if (useHiresViewport) { + static uint32_t viewportBuf[NATIVE_WIDTH * HIRES_ROWS]; + hiresDecodeToRgb(fb->hires, viewportBuf); + viewportRgb = viewportBuf; + } + + for (int sy = 0; sy < NATIVE_HEIGHT; sy++) { + bool useHires = useHiresViewport && (sy >= 0 && sy < VIEWPORT_BOTTOM); + const uint8_t *paletteRow = &fb->pixels[sy * NATIVE_WIDTH]; + const uint32_t *hiresRow = viewportRgb ? &viewportRgb[sy * NATIVE_WIDTH] : NULL; + for (int sx = 0; sx < NATIVE_WIDTH; sx++) { + uint32_t rgb = useHires + ? hiresRow[sx] + : paletteRgb[paletteRow[sx]]; + for (int dy = 0; dy < scaleY; dy++) { + int destY = sy * scaleY + dy; + uint32_t *outRow = &dst[destY * dstWidth + sx * scaleX]; + for (int dx = 0; dx < scaleX; dx++) { + outRow[dx] = rgb; + } + } + } + } +} + + +void framebufferClear(FramebufferT *fb, ColorE color) { + memset(fb->pixels, (uint8_t)color, sizeof(fb->pixels)); +} + + +void framebufferFillRect(FramebufferT *fb, int16_t x, int16_t y, int16_t w, int16_t h, ColorE color) { + int16_t x1 = x; + int16_t y1 = y; + int16_t x2 = x + w; + int16_t y2 = y + h; + + if (x1 < 0) { + x1 = 0; + } + if (y1 < 0) { + y1 = 0; + } + if (x2 > NATIVE_WIDTH) { + x2 = NATIVE_WIDTH; + } + if (y2 > NATIVE_HEIGHT) { + y2 = NATIVE_HEIGHT; + } + + for (int16_t row = y1; row < y2; row++) { + memset(&fb->pixels[row * NATIVE_WIDTH + x1], (uint8_t)color, (size_t)(x2 - x1)); + } +} + + +void framebufferFillRow(FramebufferT *fb, int16_t y, ColorE color) { + if (y < 0 || y >= NATIVE_HEIGHT) { + return; + } + memset(&fb->pixels[y * NATIVE_WIDTH], (uint8_t)color, NATIVE_WIDTH); +} + + +void framebufferSetPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color) { + if (!inBounds(x, y)) { + return; + } + fb->pixels[y * NATIVE_WIDTH + x] = (uint8_t)color; +} diff --git a/port/src/fs2math.c b/port/src/fs2math.c new file mode 100644 index 0000000..73ec1da --- /dev/null +++ b/port/src/fs2math.c @@ -0,0 +1,238 @@ +// Direct ports of the FS2 disassembly's instrument math. + +#include "fs2math.h" + + +// $07DF in the FS2 ROM image (extracted from chunk4). 24 entries +// covering airspeed buckets of 4 (high-byte) units; consecutive +// entries decrease so high airspeed -> low needle position. +static const uint8_t airspeedTable[24] = { + 0x6E, 0x6C, 0x6A, 0x68, 0x66, 0x64, 0x61, 0x5E, + 0x5A, 0x55, 0x50, 0x4B, 0x47, 0x43, 0x3E, 0x39, + 0x35, 0x31, 0x2C, 0x26, 0x22, 0x1F, 0x1C, 0x1A +}; + +// $0DE0 in the FS2 ROM image (chunk4 lines 1080..1089). 16 entries of +// (dx, dy, vx, vy), each in colour-pixels relative to the turn-coord +// centre at ($0C, $A6). Indices 0..7 cover left bank, 8..15 right. +static const int8_t turnCoordTable[16][4] = { + { 6, -8, -1, -3 }, { 6, -7, -1, -3 }, { 6, -6, -1, -3 }, { 6, -5, -1, -3 }, + { 7, -4, 0, -4 }, { 7, -3, 0, -4 }, { 7, -2, 0, -4 }, { 7, -1, 0, -4 }, + { 7, 0, 0, -4 }, { 7, 1, 0, -4 }, { 7, 2, 0, -4 }, { 7, 3, 0, -4 }, + { 7, 4, 0, -4 }, { 7, 5, 1, -3 }, { 6, 6, 1, -3 }, { 6, 7, 1, -3 } +}; + + +uint8_t fs2AirspeedNeedlePos(uint16_t airspeed16) { + uint8_t high = (uint8_t)((airspeed16 >> 8) & 0xFF); + uint8_t low = (uint8_t)(airspeed16 & 0xFF); + if (high > 0x5A) { + high = 0x5A; + } + + uint8_t bucket = (uint8_t)(high >> 2); + uint8_t b6 = airspeedTable[bucket]; + uint8_t bNext = airspeedTable[bucket + 1]; + int8_t delta = (int8_t)((int)bNext - (int)b6); + + // Reproduces the FS2 ROL chain that builds a 3-bit fractional + // index from the low byte (msb) and the bits that fell out of + // the high-byte LSRs. + uint8_t bit0 = (uint8_t)((low >> 7) & 1); + uint8_t bit1 = (uint8_t)((high >> 0) & 1); + uint8_t bit2 = (uint8_t)((high >> 1) & 1); + uint8_t frac3 = (uint8_t)((bit2 << 2) | (bit1 << 1) | bit0); + + // ASL x4 -> X = frac3 * 16. + int xMul = (int)frac3 << 4; + + // L180C: signed 16-bit (Y * X) / 2. + int product = (int)delta * xMul; + int halved = product / 2; + + // FS2 takes the high byte of the halved product. + int8_t resultHigh = (int8_t)((halved >> 8) & 0xFF); + uint8_t finalPos = (uint8_t)((int)b6 + (int)resultHigh); + + // L190C: wrap into 0..0x57. + if (finalPos >= 0x58) { + finalPos = (uint8_t)(finalPos - 0x58); + } + return finalPos; +} + + +void fs2AltimeterNeedlePos(uint16_t altitude16, uint8_t *mainPos, uint8_t *tenKPos) { + // Direct port of `UpdateAltimeterPose` (chunk5 line 8332). + // + // c2 = altitude + offsets ($0A36/$0A37 baro correction + + // $099E secondary offset; both 0 + // in this port until baro is wired) + // scaled = c2 * $24F4 / 32768 + // while (scaled high-byte >= $03): + // scaled -= $0370 + // remainder = scaled (16-bit, low byte in A, high byte in X) + // + // main hand: + // low = -(remainder * $0CCC / 32768) + $16 + // if low < 0: low += $58 + // -> $29 = low + // + // 10K hand: + // while remainder >= $58: + // remainder -= $58 + // low = -(remainder & $FF) + $BE + // if low < 0: low += $58 + // -> $28 = low + int16_t c2 = (int16_t)altitude16; + int16_t scaled = fs2ScaleByAX(c2, (int16_t)0x24F4); + + // Reduce mod $0370 (= 880). + while (scaled >= 0x0370) { + scaled = (int16_t)(scaled - 0x0370); + } + while (scaled < 0) { + scaled = (int16_t)(scaled + 0x0370); + } + int16_t remainder = scaled; + + // Main hand + int16_t mainScaled = fs2ScaleByAX(remainder, (int16_t)0x0CCC); + int mainAdj = (int)(uint8_t)(((-mainScaled) & 0xFF)); + mainAdj += 0x16; + if (mainAdj >= 0x80) { + mainAdj += 0x58; + } + mainAdj &= 0xFF; + *mainPos = (uint8_t)mainAdj; + + // 10K hand: reduce remainder mod $58 (88). + int16_t tenKRem = remainder; + while (tenKRem >= 0x58) { + tenKRem = (int16_t)(tenKRem - 0x58); + } + while (tenKRem < 0) { + tenKRem = (int16_t)(tenKRem + 0x58); + } + int tenKAdj = (int)(uint8_t)((-tenKRem) & 0xFF); + tenKAdj += 0xBE; + if (tenKAdj >= 0x80) { + tenKAdj += 0x58; + } + tenKAdj &= 0xFF; + *tenKPos = (uint8_t)tenKAdj; +} + + +uint8_t fs2PosToByteAngle(uint8_t pos) { + // FS2 needle position 0..87 covers a full revolution. Position + // 22 corresponds to 12 o'clock (byte angle 0); positions + // increase CCW. + int p = (int)pos; + // byteAngle = (320 - p * 256 / 88) mod 256 + int b = (320 - (p * 256) / 88) % 256; + if (b < 0) { + b += 256; + } + return (uint8_t)b; +} + + +int16_t fs2ScaleByAX(int16_t value16, int16_t scale16) { + // FS2's ScaleC2ByAX is a signed 16-bit multiply with the + // result divided by 32768 (i.e. take the high 16 bits of the + // 32-bit product, sign-corrected). It treats both operands as + // signed and returns a signed 16-bit result. + int32_t product = (int32_t)value16 * (int32_t)scale16; + return (int16_t)(product >> 15); +} + + +uint8_t fs2SlipSkidIndex(int8_t slipValue) { + // Direct port of chunk5 `UpdateSlipSkid` (L8467). FS2 takes the + // signed byte, adds $7F so that -$80..$7F becomes 0..255, halves + // twice (two `lsr a`), subtracts $1F, then clamps to [-8, 8] and + // finally adds $09 so the result is 1..17. + int v = (int)(uint8_t)((int)slipValue + 0x7F); + v >>= 2; + v -= 0x1F; + if (v > 8) { + v = 8; + } + if (v < -8) { + v = -8; + } + return (uint8_t)(v + 9); +} + + +void fs2TurnCoordEntry(uint8_t index, int8_t *dx, int8_t *dy, int8_t *vx, int8_t *vy) { + if (index > 15) { + index = 15; + } + *dx = turnCoordTable[index][0]; + *dy = turnCoordTable[index][1]; + *vx = turnCoordTable[index][2]; + *vy = turnCoordTable[index][3]; +} + + +uint8_t fs2TurnCoordIndex(int16_t value16) { + // Direct port of the head of `UpdateTurnCoordinator` (chunk5 + // L4961). FS2 computes value16 * 3 by shifting and adding + // ($09CD/$09CE), then takes the high byte plus an extra +1 if + // the low byte's bit 7 is set (the `cpx #$80; adc #$08` rounding), + // adds 8, and clamps to 0..15. + int32_t v3 = (int32_t)value16 * 3; + int hi = (int)(v3 >> 8); + int lo = (int)(v3 & 0xFF); + int idx; + + idx = hi + 8; + if (lo >= 0x80) { + idx++; + } + if (idx < 0) { + return 0; + } + if (idx > 15) { + return 15; + } + return (uint8_t)idx; +} + + +uint8_t fs2VsiNeedlePos(int16_t value16) { + // Direct port of the routine FS2 calls "UpdateMagneticHeading" + // (chunk5 L8432). Despite its name, the result lands in $2A + // which `UpdateVerticalSpeedIndicator` consumes. + // + // y = lo << 1, carry1 = lo bit7 + // hi clamp to [-9, 9] + // a = (clamped hi << 1) | carry1 + // carry2 = (y >= $80) + // a = (a << 1) | carry2 ; a = clamped*4 + lo bits 7,6 + // a = (~a + $84) & 0xFF + // while a >= $58: a -= $58 + uint8_t lowByte = (uint8_t)(value16 & 0xFF); + int highByte = (int)(int8_t)((value16 >> 8) & 0xFF); + + if (highByte > 9) { + highByte = 9; + } + if (highByte < -9) { + highByte = -9; + } + + uint8_t carry1 = (uint8_t)((lowByte >> 7) & 1); + uint8_t shifted1Lo = (uint8_t)(lowByte << 1); + uint8_t a = (uint8_t)(((uint8_t)highByte << 1) | carry1); + uint8_t carry2 = (shifted1Lo >= 0x80) ? (uint8_t)1 : (uint8_t)0; + + a = (uint8_t)((a << 1) | carry2); + a = (uint8_t)((uint8_t)(~a) + (uint8_t)0x84); + while (a >= 0x58) { + a = (uint8_t)(a - 0x58); + } + return a; +} diff --git a/port/src/hires.c b/port/src/hires.c new file mode 100644 index 0000000..917d286 --- /dev/null +++ b/port/src/hires.c @@ -0,0 +1,348 @@ +// Apple II hires bitplane backend: bit-set drawing + NTSC color decode. +// +// Implements the bit-level rendering FS2's chunk5 does on real +// hardware, so the same byte patterns end up in our framebuffer that +// the original code produced. Color-pixel coords (0..139) come from +// chunk5's projection (`sceneryProjectVertexToScreen` already scales +// to half-width so we just use those coords directly). +// +// All routines work on raw 40x192 byte arrays (no Apple II HiresTable +// row-scrambling) -- the bit semantics are independent of layout. + +#include +#include "hires.h" + + +// chunk5 ToHiresColorTable (chunk5.s:3825). Maps the scenery-code byte +// after $12 SetColor (or any other 0..15 index, masked) into one of +// the 8 HIRES_* color codes. Verbatim from the source. +const uint8_t kSceneryToHires[16] = { + HIRES_BLACK1, HIRES_GREEN, HIRES_VIOLET, HIRES_GREEN, + HIRES_VIOLET, HIRES_BLACK1, HIRES_VIOLET, HIRES_VIOLET, + HIRES_BLACK1, HIRES_WHITE1, HIRES_BLACK1, HIRES_GREEN, + HIRES_VIOLET, HIRES_WHITE1, HIRES_VIOLET, HIRES_WHITE1, +}; + + +// FILL byte patterns per HIRES color, indexed [0..7]. Each row holds +// the byte for an EVEN-indexed column then an ODD-indexed column. The +// alternation maintains the same Apple II hires color across byte +// boundaries (since the bit positions that hit "first-of-pair" +// absolute columns shift by 1 each byte). Verified by following +// chunk5.s:558 (`lda #$2A` for ground) and chunk5.s:565 (`lda #$D5` +// for sky); DrawSkyGroundRowUnrolled writes the byte then `eor #$7F` +// for the next column, which is exactly $2A->$55, $D5->$AA, etc. +static const uint8_t kFillByteEven[8] = { + 0x00, // BLACK1 + 0x55, // VIOLET (= bits at even abs cols, pal 0) + 0x2A, // GREEN (= bits at odd abs cols, pal 0) + 0x7F, // WHITE1 + 0x80, // BLACK2 + 0xD5, // BLUE (= bits at even abs cols, pal 1) + 0xAA, // ORANGE (= bits at odd abs cols, pal 1) + 0xFF, // WHITE2 +}; +static const uint8_t kFillByteOdd[8] = { + 0x00, // BLACK1 + 0x2A, // VIOLET (next byte, palette stays the same) + 0x55, // GREEN + 0x7F, // WHITE1 + 0x80, // BLACK2 + 0xAA, // BLUE + 0xD5, // ORANGE + 0xFF, // WHITE2 +}; + + +// chunk5 OrMaskTable1 / OrMaskTable2 (chunk4.s:1518+). Indexed by +// COLOR-pixel position within a 14-pixel byte-pair (0..6 for each +// table). Together they let DrawColorLine plot both bits of a color +// pixel: one mask sets the "even-color-slot" bit, the other sets the +// "odd-color-slot" bit. Plotting BOTH gives WHITE, plotting ONE gives +// GREEN or VIOLET depending on phase. +static const uint8_t kOrMask1[7] = { 0x01, 0x04, 0x10, 0x40, 0x01, 0x04, 0x10 }; +static const uint8_t kOrMask2[7] = { 0x02, 0x08, 0x20, 0x40, 0x02, 0x08, 0x20 }; +static const uint8_t kAndMask1[7] = { 0xFE, 0xFB, 0xEF, 0xBF, 0xFE, 0xFB, 0xEF }; +static const uint8_t kAndMask2[7] = { 0xFD, 0xF7, 0xDF, 0xBF, 0xFD, 0xF7, 0xDF }; + + +// Modern palette mapped onto the chunk5 hires color codes. The Apple II +// HIRES_VIOLET pixels in FS2's hires bitplane are SEMANTICALLY water +// (= the chunk5 source comment at chunk5.s:3819 calls scenery code 2 +// "water color (day)"). Rendering them in the modern COLOR_WATER blue +// gets the user's preferred aesthetic without losing the bit-pattern +// accuracy of where water actually is. +// Authentic Apple II hires palette (= what MAME shows for FS2). +static const uint32_t kHiresRgb[8] = { + 0x000000, // BLACK1 + 0xFF40FF, // VIOLET (= MAME water color) + 0x20C000, // GREEN (= MAME ground color) + 0xFFFFFE, // WHITE1 + 0x000000, // BLACK2 + 0x0080FF, // BLUE (= MAME sky color) + 0xFF8000, // ORANGE + 0xFFFFFE, // WHITE2 +}; + + +void hiresClearPage(uint8_t *page) { + memset(page, 0, HIRES_PAGE_BYTES); +} + + +void hiresImportFromAppleII(uint8_t *page, const uint8_t *appleHiresPage) { + // chunk4 HiresTableLo/Hi entries (chunk4.s). Address of row N in + // the Apple II hires page is hi[N]:lo[N] - $2000. The pattern + // repeats every 8 rows (= one 64-byte cell) and there are 24 + // cells stacked into 3 groups of 8. Reproduced verbatim from + // chunk4.s so the import matches the layout chunk5 actually + // writes to. + static const uint8_t kHi[192] = { + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x20,0x24,0x28,0x2C,0x30,0x34,0x38,0x3C, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x21,0x25,0x29,0x2D,0x31,0x35,0x39,0x3D, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x22,0x26,0x2A,0x2E,0x32,0x36,0x3A,0x3E, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + 0x23,0x27,0x2B,0x2F,0x33,0x37,0x3B,0x3F, + }; + static const uint8_t kLo[192] = { + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80, + 0x28,0x28,0x28,0x28,0x28,0x28,0x28,0x28, + 0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8, + 0x28,0x28,0x28,0x28,0x28,0x28,0x28,0x28, + 0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8, + 0x28,0x28,0x28,0x28,0x28,0x28,0x28,0x28, + 0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8, + 0x28,0x28,0x28,0x28,0x28,0x28,0x28,0x28, + 0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8,0xA8, + 0x50,0x50,0x50,0x50,0x50,0x50,0x50,0x50, + 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0, + 0x50,0x50,0x50,0x50,0x50,0x50,0x50,0x50, + 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0, + 0x50,0x50,0x50,0x50,0x50,0x50,0x50,0x50, + 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0, + 0x50,0x50,0x50,0x50,0x50,0x50,0x50,0x50, + 0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0,0xD0, + }; + // appleHiresPage is the Apple II hires page starting at $2000. + // Each row's 40 bytes live at base + ((kHi[row] << 8) | kLo[row]) + // - $2000. + for (int row = 0; row < HIRES_ROWS; row++) { + uint16_t addr = ((uint16_t)kHi[row] << 8) | kLo[row]; + if (addr < 0x2000) { + continue; + } + uint16_t off = (uint16_t)(addr - 0x2000); + if (off + HIRES_BYTES_PER_ROW > 0x2000) { + continue; + } + memcpy(page + row * HIRES_BYTES_PER_ROW, + appleHiresPage + off, + HIRES_BYTES_PER_ROW); + } +} + + +void hiresFillRow(uint8_t *page, int row, uint8_t evenByte, uint8_t oddByte) { + if (row < 0 || row >= HIRES_ROWS) { + return; + } + uint8_t *r = page + row * HIRES_BYTES_PER_ROW; + for (int b = 0; b < HIRES_BYTES_PER_ROW; b++) { + r[b] = (b & 1) ? oddByte : evenByte; + } +} + + +void hiresFillBytesFor(HiresColorE col, uint8_t *outEven, uint8_t *outOdd) { + int idx = (int)col & 7; + if (outEven != NULL) { + *outEven = kFillByteEven[idx]; + } + if (outOdd != NULL) { + *outOdd = kFillByteOdd[idx]; + } +} + + +uint32_t hiresColorToRgb(HiresColorE col) { + return kHiresRgb[(int)col & 7]; +} + + +void hiresPlotPixel(uint8_t *page, int xColor, int y, HiresColorE col) { + if (y < 0 || y >= HIRES_ROWS) { + return; + } + if (xColor < 0 || xColor >= 140) { + return; + } + // Each "color pixel" = 2 hires bits at columns (xColor*2) and + // (xColor*2+1). 280 pixels per row = 40 bytes * 7 bits. + int absX1 = xColor * 2; + int absX2 = xColor * 2 + 1; + int byteIdx1 = absX1 / 7; + int bitIdx1 = absX1 % 7; + int byteIdx2 = absX2 / 7; + int bitIdx2 = absX2 % 7; + uint8_t *r = page + y * HIRES_BYTES_PER_ROW; + + // Apply or/and masks per chunk5 SetPixelDrawMode. For each of + // the two bit positions we set or clear the bit based on the + // color's pattern. + uint8_t orMask1 = (uint8_t)(1u << bitIdx1); + uint8_t orMask2 = (uint8_t)(1u << bitIdx2); + uint8_t andMask1 = (uint8_t)(~orMask1); + uint8_t andMask2 = (uint8_t)(~orMask2); + + // Each color has an "even-byte pattern" and an "odd-byte + // pattern". The bits the color wants set in a byte at column + // index B come from the appropriate side of the table. + uint8_t evenPat = kFillByteEven[(int)col & 7]; + uint8_t oddPat = kFillByteOdd[(int)col & 7]; + + // For each of the two bits we plot, decide whether to OR (set + // the bit) or AND (clear the bit) based on whether the color's + // pattern HAS that bit set in this byte. + uint8_t pat1 = (byteIdx1 & 1) ? oddPat : evenPat; + if (pat1 & orMask1) { + r[byteIdx1] |= orMask1; + } else { + r[byteIdx1] &= andMask1; + } + uint8_t pat2 = (byteIdx2 & 1) ? oddPat : evenPat; + if (pat2 & orMask2) { + r[byteIdx2] |= orMask2; + } else { + r[byteIdx2] &= andMask2; + } + + // Propagate the palette bit (bit 7) so the byte stays in the + // correct color set. WHITE1/2 and BLACK1/2 have palette bit + // determined by the color code. + bool wantPalette1 = (evenPat & 0x80) != 0; + if (wantPalette1) { + r[byteIdx1] |= 0x80; + r[byteIdx2] |= 0x80; + } +} + + +void hiresDrawColorSpan(uint8_t *page, int xRight, int length, int y, HiresColorE col) { + // chunk5 DrawColorSpan ($78E0) walks color pixels from xRight + // backwards for `length+1` pixels, applying AND/OR masks per + // color. hiresPlotPixel already does the equivalent per-pixel + // mask logic, so we just iterate it. The right-to-left walk + // matches the source (DEX iterates pixel-pos within byte, DEY + // moves to previous byte). + int remaining = length + 1; + int xCol = xRight; + while (remaining > 0 && xCol >= 0) { + hiresPlotPixel(page, xCol, y, col); + remaining--; + xCol--; + } +} + + +void hiresDrawLine(uint8_t *page, int x1c, int y1, int x2c, int y2, HiresColorE col) { + // Standard Bresenham in color-pixel space (140x192). + int dx = x2c - x1c; if (dx < 0) dx = -dx; + int dy = y2 - y1; if (dy < 0) dy = -dy; + int sx = x1c < x2c ? 1 : -1; + int sy = y1 < y2 ? 1 : -1; + int err = dx - dy; + int safety = 0; + for (;;) { + hiresPlotPixel(page, x1c, y1, col); + if (x1c == x2c && y1 == y2) break; + int e2 = 2 * err; + if (e2 > -dy) { err -= dy; x1c += sx; } + if (e2 < dx) { err += dx; y1 += sy; } + if (++safety > 1000) break; + } +} + + +// Decode the hires bitplane into a 280x192 RGB image using Apple II +// NTSC color rules. Color is determined by *pixel pairs* (= columns +// (2k, 2k+1)): each pair displays ONE color, and BOTH pixels of the +// pair get that color when rendered. This is the NTSC color-burst +// behaviour that makes adjacent set bits combine to WHITE and isolated +// set bits "smear" to fill their pair's slot: +// +// * Both bits in pair set -> WHITE +// * Only first-of-pair (even col) set -> VIOLET (palette 0) or BLUE (1) +// * Only second-of-pair (odd col) set -> GREEN (palette 0) or ORANGE (1) +// * Neither bit set -> BLACK +// +// Palette bit (bit 7 of byte) is per-byte. When a pair straddles a +// byte boundary (cols 6,7) we use the LEFT byte's palette bit, which +// matches Apple II behaviour. +void hiresDecodeToRgb(const uint8_t *page, uint32_t *out) { + for (int y = 0; y < HIRES_ROWS; y++) { + const uint8_t *row = page + y * HIRES_BYTES_PER_ROW; + // Pre-pack 280 pixel-bits + per-pixel palette flag. + uint8_t bits[280]; + uint8_t pal[280]; + for (int b = 0; b < 40; b++) { + uint8_t byte = row[b]; + for (int bit = 0; bit < 7; bit++) { + bits[b * 7 + bit] = (byte >> bit) & 1; + pal[b * 7 + bit] = (byte >> 7) & 1; + } + } + // Walk pairs, write the SAME color to both columns of + // each pair. Pair index k covers columns (2k, 2k+1). + for (int k = 0; k < 140; k++) { + int xLo = 2 * k; + int xHi = 2 * k + 1; + uint8_t bLo = bits[xLo]; + uint8_t bHi = bits[xHi]; + uint8_t palette = pal[xLo]; // pair takes left's palette + uint32_t rgb; + if (bLo && bHi) { + rgb = palette ? kHiresRgb[HIRES_WHITE2] + : kHiresRgb[HIRES_WHITE1]; + } else if (bLo) { + // First-of-pair (= "even" pair slot) + rgb = palette ? kHiresRgb[HIRES_BLUE] + : kHiresRgb[HIRES_VIOLET]; + } else if (bHi) { + // Second-of-pair (= "odd" pair slot) + rgb = palette ? kHiresRgb[HIRES_ORANGE] + : kHiresRgb[HIRES_GREEN]; + } else { + rgb = 0; // black + } + out[y * 280 + xLo] = rgb; + out[y * 280 + xHi] = rgb; + } + } + (void)kOrMask1; (void)kOrMask2; (void)kAndMask1; (void)kAndMask2; +} diff --git a/port/src/hud.c b/port/src/hud.c new file mode 100644 index 0000000..faf070d --- /dev/null +++ b/port/src/hud.c @@ -0,0 +1,137 @@ +// HUD strip drawn into the instrument-panel area beneath the +// viewport. Five digital readouts plus a small attitude indicator. + +#include +#include "font.h" +#include "hud.h" +#include "math6502.h" +#include "renderer.h" + + +// Render a horizon line tilted by bank into a small attitude +// indicator centered at (cx, cy) with half-size r. +static void drawAttitudeIndicator(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, const CameraT *cam); +static void drawDial(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, ColorE color); +static void drawLine(FramebufferT *fb, int16_t x1, int16_t y1, int16_t x2, int16_t y2, ColorE color); + + +static void drawAttitudeIndicator(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, const CameraT *cam) { + // Background dial. + drawDial(fb, cx, cy, r, COLOR_BLACK); + // Fill the dial's lower half with ground colour, upper half + // with sky, rotated by bank and shifted by pitch. sin/cos are + // Q1.15 (-32767..32767); we only need the sign of `side` so + // the products can stay in int32 without normalisation. + int16_t pitchSin = math6502Sin(cam->pitch); + int16_t bankSin = math6502Sin(cam->bank); + int16_t bankCos = math6502Cos(cam->bank); + int16_t cyShift = (int16_t)(((int32_t)pitchSin * r) >> 15); + + for (int16_t dy = -r; dy <= r; dy++) { + for (int16_t dx = -r; dx <= r; dx++) { + if (dx * dx + dy * dy > r * r) { + continue; + } + int32_t side = -(int32_t)dx * bankSin + + (int32_t)(dy - cyShift) * bankCos; + ColorE c = (side < 0) ? COLOR_SKY_DAY : COLOR_GROUND_DAY; + framebufferSetPixel(fb, (int16_t)(cx + dx), (int16_t)(cy + dy), c); + } + } + // Centre crosshair. + drawLine(fb, (int16_t)(cx - r/2), cy, (int16_t)(cx + r/2), cy, COLOR_ORANGE); + drawLine(fb, cx, (int16_t)(cy - 2), cx, (int16_t)(cy + 2), COLOR_ORANGE); +} + + +static void drawDial(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, ColorE color) { + // Bresenham circle. + int16_t x = 0; + int16_t y = r; + int16_t d = (int16_t)(3 - 2 * r); + while (y >= x) { + framebufferSetPixel(fb, (int16_t)(cx + x), (int16_t)(cy + y), color); + framebufferSetPixel(fb, (int16_t)(cx - x), (int16_t)(cy + y), color); + framebufferSetPixel(fb, (int16_t)(cx + x), (int16_t)(cy - y), color); + framebufferSetPixel(fb, (int16_t)(cx - x), (int16_t)(cy - y), color); + framebufferSetPixel(fb, (int16_t)(cx + y), (int16_t)(cy + x), color); + framebufferSetPixel(fb, (int16_t)(cx - y), (int16_t)(cy + x), color); + framebufferSetPixel(fb, (int16_t)(cx + y), (int16_t)(cy - x), color); + framebufferSetPixel(fb, (int16_t)(cx - y), (int16_t)(cy - x), color); + if (d < 0) { + d = (int16_t)(d + 4 * x + 6); + } else { + d = (int16_t)(d + 4 * (x - y) + 10); + y--; + } + x++; + } +} + + +// Local Bresenham line for the attitude crosshair (avoids a +// dependency on RenderStateT just for two strokes). +static void drawLine(FramebufferT *fb, int16_t x1, int16_t y1, int16_t x2, int16_t y2, ColorE color) { + int16_t dx = (int16_t)(x2 - x1); + int16_t dy = (int16_t)(y2 - y1); + int16_t sx = dx < 0 ? -1 : 1; + int16_t sy = dy < 0 ? -1 : 1; + int16_t ax = dx < 0 ? -dx : dx; + int16_t ay = dy < 0 ? -dy : dy; + int16_t err = (ax > ay ? ax : -ay) / 2; + for (;;) { + framebufferSetPixel(fb, x1, y1, color); + if (x1 == x2 && y1 == y2) { + break; + } + int16_t e2 = err; + if (e2 > -ax) { + err -= ay; + x1 = (int16_t)(x1 + sx); + } + if (e2 < ay) { + err += ax; + y1 = (int16_t)(y1 + sy); + } + } +} + + +void hudDraw(FramebufferT *fb, const CameraT *cam) { + // The panel bitmap has been blitted into the lower portion of + // the framebuffer already; the HUD writes a small status block + // on top. We avoid touching the area occupied by the FS2 + // instrument panel so the gauge faces remain readable. + const int16_t panelTop = VIEWPORT_BOTTOM + 4; + + char buf[32]; + + // Speed read in arbitrary units; the model later will translate + // forwardSpeed (Q8.8) into knots. (speed * 100) >> 8. + int speedReadout = ((int)cam->forwardSpeed * 100) >> CAM_RATE_FRACT_BITS; + snprintf(buf, sizeof(buf), "SPD %4d", speedReadout); + fontDrawString(fb, 6, panelTop, buf, COLOR_ORANGE); + + // Altitude (Q16.16 worldY) in tenths of a metre. (worldY * 10) >> 16. + int altReadout = (int)(((int64_t)cam->worldY * 10) >> CAM_POS_FRACT_BITS); + snprintf(buf, sizeof(buf), "ALT %5d", altReadout); + fontDrawString(fb, 6, (int16_t)(panelTop + 12), buf, COLOR_ORANGE); + + // Heading: 0..359 deg derived from yaw byte angle. + int heading = (int)(cam->yaw * 360 / 256); + snprintf(buf, sizeof(buf), "HDG %03d", heading); + fontDrawString(fb, 6, (int16_t)(panelTop + 24), buf, COLOR_ORANGE); + + int8_t pitchSigned = (int8_t)cam->pitch; + int8_t bankSigned = (int8_t)cam->bank; + snprintf(buf, sizeof(buf), "PIT %+4d", pitchSigned); + fontDrawString(fb, 6, (int16_t)(panelTop + 36), buf, COLOR_HAZE); + snprintf(buf, sizeof(buf), "BNK %+4d", bankSigned); + fontDrawString(fb, 6, (int16_t)(panelTop + 48), buf, COLOR_HAZE); + + // Attitude indicator on the right. + drawAttitudeIndicator(fb, (int16_t)(NATIVE_WIDTH - 32), (int16_t)(panelTop + 30), 24, cam); + + // Title strip / bottom label. + fontDrawString(fb, 6, (int16_t)(NATIVE_HEIGHT - 12), "FS2 PORT", COLOR_WHITE); +} diff --git a/port/src/instruments.c b/port/src/instruments.c new file mode 100644 index 0000000..1dc879a --- /dev/null +++ b/port/src/instruments.c @@ -0,0 +1,622 @@ +// Live instrument needles. Coordinates pinned by visual inspection of +// the FS2 panel bitmap; refine if the gauges appear misaligned. + +#include +#include "font.h" +#include "framebuffer.h" +#include "fs2math.h" +#include "instruments.h" +#include "math6502.h" +#include "needleData.h" + + +// Anchor coords (native pixels). The airspeed, altimeter and VSI +// centres are taken straight from `IndicatorDialNeedleX/Y` in +// chunk4 of the FS2 disassembly; the others are calibrated against +// the panel bitmap by eye until the needles sit on their dial +// centres. +typedef struct GaugeT { + int16_t cx; + int16_t cy; + int16_t r; +} GaugeT; + +// Gauge centres straight from FS2 chunk4 `IndicatorDialNeedleX/Y`: +// +// needle 0/1 (altimeter main + 10K hand) -> ($82, $7E) = (130, 126) +// needle 2 (airspeed) -> ($1A, $7E) = ( 26, 126) +// needle 3 (vertical speed) -> ($82, $AA) = (130, 170) +// needle 4 (ADF) -> ($B4, $AF) = (180, 175) +// +// Attitude indicator and turn coordinator are drawn directly in FS2 +// (no pre-rendered needle list); their centres are taken from +// chunk5's draw routines (`UpdateArtificialHorizon`, +// `DrawTurnCoordinatorAtAngle`). +static const GaugeT airspeedGauge = { 26, 126, 16 }; +static const GaugeT attitudeGauge = { 77, 126, 17 }; +static const GaugeT altimeterGauge = { 130, 126, 16 }; +static const GaugeT turnCoordGauge = { 24, 166, 12 }; +static const GaugeT vsiGauge = { 130, 170, 14 }; +static const GaugeT adfGauge = { 180, 175, 12 }; + +// VOR1/VOR2 CDI needles. FS2 chunk5 `DrawVOR1CourseDeviationIndicatorNeedle` +// draws a 2x7 vertical bar at hires (165 + signed_deflection, 118) +// for VOR1 and (165 + ..., 161) for VOR2 -- so the needle slides +// horizontally between the course/reciprocal digit readouts. +#define VOR_CDI_CENTRE_X 165 +#define VOR1_CDI_CENTRE_Y 118 +#define VOR2_CDI_CENTRE_Y 161 +#define VOR_CDI_HALFWIDTH 12 // ±12 hires pixels = full deflection +#define VOR_CDI_NEEDLE_W 2 // sprite width +#define VOR_CDI_NEEDLE_H 7 // sprite height +#define VOR_CDI_FULL_DEG 10 // ±10° = full needle deflection + +// VOR1/VOR2 FROM/TO/OFF flag positions. FS2 chunk5 msg_vor_flags +// places VOR1 flag at hires (col $54=168, row $82=130), VOR2 flag at +// (168, $AD=173). Three-state: "OFF"/"TO "/"FR ". +#define VOR1_FLAG_X 168 +#define VOR1_FLAG_Y 130 +#define VOR2_FLAG_X 168 +#define VOR2_FLAG_Y 173 + +#define DEG2RAD 0.01745329251994f + + +typedef struct PixelListT { + uint8_t count; + const int8_t *offsets; // (dx, dy) pairs, 2 bytes each +} PixelListT; + + +static void drawCenteredString(FramebufferT *fb, int16_t cx, int16_t cy, const char *s, ColorE color); +static void drawControlIndicators(FramebufferT *fb, const AircraftT *ac); +static void drawFailX(FramebufferT *fb, const GaugeT *g); +static void drawHorizonDisc(FramebufferT *fb, const GaugeT *g, const AircraftT *ac); +static void drawLine(FramebufferT *fb, int16_t x1, int16_t y1, int16_t x2, int16_t y2, ColorE color); +static void drawPixelList(FramebufferT *fb, int16_t anchorX, int16_t anchorY, const PixelListT *pl, ColorE color); +static void drawSlipSkidBall(FramebufferT *fb, const AircraftT *ac); +static void drawTiltedSegment(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, int16_t bankSin, int16_t bankCos, ColorE color); +static void drawTurnCoordWings(FramebufferT *fb, const AircraftT *ac); +static void drawVorCdiNeedle(FramebufferT *fb, int16_t centreX, int16_t centreY, int8_t deflectionDeg, bool valid); +static void plotPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color); + + +// FS2 pixel-list sprites used by the indicator drawers. Offsets are +// (dx, dy) pairs relative to the indicator's anchor pixel. +static const int8_t plArrowUpData[] = { 0,0, 1,0, 2,0, 3,0, 4,0, 1,1, 2,1, 3,1, 2,2 }; +static const int8_t plThrottleData[] = { 0,0, 1,0, 0,1, 1,1 }; +static const int8_t plElevatorData[] = { 0,0, 1,0, 0,1, 1,1, 0,2, 1,2 }; +static const int8_t plBallData[] = { 1,0, 2,0, 0,1, 1,1, 2,1, 3,1, 0,2, 1,2, 2,2, 3,2, 1,3, 2,3 }; +// FS2 PLFlapsTrimMixtureIndicator (chunk4 L1460): a sparse 3x2 marker. +static const int8_t plFlapsTrimMixtureData[] = { 0,0, 2,0, 0,1, 2,1 }; + +static const PixelListT plArrowUp = { 9, plArrowUpData }; +static const PixelListT plThrottle = { 4, plThrottleData }; +static const PixelListT plElevator = { 6, plElevatorData }; +static const PixelListT plBall = { 12, plBallData }; +static const PixelListT plFlapsTrimMixture = { 4, plFlapsTrimMixtureData }; + + +static void drawCenteredString(FramebufferT *fb, int16_t cx, int16_t cy, const char *s, ColorE color) { + int len = 0; + while (s[len] != '\0') { + len++; + } + int16_t startX = (int16_t)(cx - (len * (FONT_WIDTH + 1)) / 2); + fontDrawString(fb, startX, cy, s, color); +} + + +// Draw the four FS2 control-position markers (aileron, rudder, +// throttle, elevator). Each is a small pixel-list sprite that slides +// along a fixed track on the panel. FS2 anchors: +// * aileron arrow: hires (yokeHoriz + $55, $94) -- chunk4 L1A62 +// * rudder arrow: hires (rudder + $55, $BC) -- chunk4 L1A83 +// * throttle 2x2: hires ($CA, $BE - throttle) -- chunk4 L1A97 +// * elevator 2x3: hires ($64, yokeVert + $9F) -- chunk4 L1A48 +static void drawControlIndicators(FramebufferT *fb, const AircraftT *ac) { + // yokeHoriz/yokeVert/rudder are int8_t [-127, +127]. + // Map to [-32, +32] (or [-16, +16]) for the 32/16-step tracks. + int aileronByte = (int)(int8_t)ac->yokeHoriz * 32 / 127; + drawPixelList(fb, (int16_t)(aileronByte + 0x55), 0x94, &plArrowUp, COLOR_WHITE); + + int rudderByte = (int)(int8_t)ac->rudder * 32 / 127; + drawPixelList(fb, (int16_t)(rudderByte + 0x55), 0xBC, &plArrowUp, COLOR_WHITE); + + // throttle is uint8_t 0..255 -> 0..32 step. + int throttleByte = (int)ac->throttle * 32 / 255; + drawPixelList(fb, 0xCA, (int16_t)(0xBE - throttleByte), &plThrottle, COLOR_WHITE); + + int elevatorByte = (int)(int8_t)ac->yokeVert * 16 / 127; + drawPixelList(fb, 0x64, (int16_t)(elevatorByte + 0x9F), &plElevator, COLOR_WHITE); + + // Flaps / trim / mixture: vertical sliders sharing the + // PLFlapsTrimMixtureIndicator sprite. FS2 anchors each on + // a different (X, Y baseline) per chunk4 L1AB7/L1ACA/L1ADD. + int flapsByte = (int)ac->flaps * 32 / 255; + drawPixelList(fb, 0xC8, (int16_t)(flapsByte + 0x66), &plFlapsTrimMixture, COLOR_WHITE); + + int trimByte = (int)(int8_t)ac->trim * 16 / 127; + drawPixelList(fb, 0xC8, (int16_t)(trimByte + 0x8E), &plFlapsTrimMixture, COLOR_WHITE); + + // mixture 0..255 maps to -16..+16 (centred at 128). + int mixtureByte = ((int)ac->mixture - 128) * 16 / 128; + drawPixelList(fb, 0xD0, (int16_t)(mixtureByte + 0xAF), &plFlapsTrimMixture, COLOR_WHITE); +} + + +// Failure indicator: small "X" through the centre of a gauge, +// painted when the matching reality-mode bit is cleared. The needle +// is also skipped so the gauge sits dead. +static void drawFailX(FramebufferT *fb, const GaugeT *g) { + const int16_t span = 4; + for (int16_t d = -span; d <= span; d++) { + plotPixel(fb, (int16_t)(g->cx + d), (int16_t)(g->cy + d), COLOR_HAZE); + plotPixel(fb, (int16_t)(g->cx + d), (int16_t)(g->cy - d), COLOR_HAZE); + } +} + + +// Render the artificial horizon disc inside the attitude indicator +// gauge. Sky upper, ground lower, tilted by camera bank, shifted by +// pitch. +static void drawHorizonDisc(FramebufferT *fb, const GaugeT *g, const AircraftT *ac) { + // Q1.15 sin/cos from FS2 tables. + int32_t pitchSin = math6502Sin(ac->pitch); + int32_t bankSin = math6502Sin(ac->bank); + int32_t bankCos = math6502Cos(ac->bank); + // Pitch shifts the horizon down by sin(pitch) * r * 0.6 + // (60% of radius). All Q1.15 -> int16 via shift. + int16_t pitchShift = (int16_t)((pitchSin * (int32_t)g->r * 154) >> 23); // 0.6 in Q1.8 = 154 + + int32_t r2 = (int32_t)g->r * g->r; + for (int16_t dy = -g->r; dy <= g->r; dy++) { + int32_t dy2 = (int32_t)dy * dy; + for (int16_t dx = -g->r; dx <= g->r; dx++) { + if ((int32_t)dx * dx + dy2 > r2) { + continue; + } + // side = -dx * sin(bank) + (dy - pitchShift) * cos(bank) + // sin/cos are Q1.15; only the sign matters so the + // result stays in int32 without normalising. + int32_t side = -(int32_t)dx * bankSin + (int32_t)(dy - pitchShift) * bankCos; + ColorE c = (side < 0) ? COLOR_SKY_DAY : COLOR_GROUND_DAY; + plotPixel(fb, (int16_t)(g->cx + dx), (int16_t)(g->cy + dy), c); + } + } + // Aircraft reference: a fixed horizontal "wing" overlaid on the ball. + drawTiltedSegment(fb, g->cx, g->cy, (int16_t)(g->r - 2), 0, 32767, COLOR_WHITE); + // Centre dot + plotPixel(fb, g->cx, g->cy, COLOR_WHITE); +} + + +// Bresenham line draw. Used by the attitude indicator's wing +// reference and the turn-coordinator wing bar. +static void drawLine(FramebufferT *fb, int16_t x1, int16_t y1, int16_t x2, int16_t y2, ColorE color) { + int16_t adx = x2 > x1 ? (int16_t)(x2 - x1) : (int16_t)(x1 - x2); + int16_t ady = y2 > y1 ? (int16_t)(y2 - y1) : (int16_t)(y1 - y2); + int16_t sx = x2 > x1 ? 1 : -1; + int16_t sy = y2 > y1 ? 1 : -1; + int16_t err = (adx > ady ? adx : -ady) / 2; + for (;;) { + plotPixel(fb, x1, y1, color); + if (x1 == x2 && y1 == y2) { + break; + } + int16_t e2 = err; + if (e2 > -adx) { + err -= ady; + x1 = (int16_t)(x1 + sx); + } + if (e2 < ady) { + err += adx; + y1 = (int16_t)(y1 + sy); + } + } +} + + +// Plot a sequence of (dx, dy) offsets relative to (anchorX, anchorY). +// Mirrors FS2 chunk4 `DrawPixelList`, which the FS2 indicator drawers +// share to render their position markers, balls and bars. +static void drawPixelList(FramebufferT *fb, int16_t anchorX, int16_t anchorY, const PixelListT *pl, ColorE color) { + for (int i = 0; i < (int)pl->count; i++) { + int16_t x = (int16_t)(anchorX + pl->offsets[i * 2 + 0]); + int16_t y = (int16_t)(anchorY + pl->offsets[i * 2 + 1]); + plotPixel(fb, x, y, color); + } +} + + +// Slip / skid ball. Mirrors FS2 chunk4 PLSlipSkidIndicator: a small +// 4x4 ball with corners cut. FS2 anchors the ball at hires +// (idx + $0E, $B4) where idx (1..17) comes from `fs2SlipSkidIndex`, +// so the ball sweeps screen X 15..31 with Y fixed at 180. +static void drawSlipSkidBall(FramebufferT *fb, const AircraftT *ac) { + // rudder is already int8_t (-127..+127), the exact range + // fs2SlipSkidIndex expects. + uint8_t idx = fs2SlipSkidIndex((int8_t)ac->rudder); + int16_t anchorX = (int16_t)((int)idx + 0x0E); + drawPixelList(fb, anchorX, 0xB4, &plBall, COLOR_WHITE); +} + + +// Draw a short horizontal bar through (cx, cy), tilted by +// (bankSin, bankCos) given as Q1.15 values. +static void drawTiltedSegment(FramebufferT *fb, int16_t cx, int16_t cy, int16_t r, int16_t bankSin, int16_t bankCos, ColorE color) { + // Half-length is 60% of r, scaled by sin/cos. (r * 0.6 * cos) / 32768 + // = (r * cos * 154) >> 23 (where 154 ~= 0.6*256). + int32_t halfDx = ((int32_t)r * (int32_t)bankCos * 154) >> 23; + int32_t halfDy = ((int32_t)r * (int32_t)bankSin * 154) >> 23; + int16_t x1 = (int16_t)((int32_t)cx - halfDx); + int16_t y1 = (int16_t)((int32_t)cy - halfDy); + int16_t x2 = (int16_t)((int32_t)cx + halfDx); + int16_t y2 = (int16_t)((int32_t)cy + halfDy); + drawLine(fb, x1, y1, x2, y2, color); +} + + +// Draw the turn-coordinator wing bar plus the small ball indicator, +// using the FS2 $0DE0 delta table. The FS2 routine works in +// colour-pixels; our framebuffer is hires-X aligned, so X-axis deltas +// double when projected to screen coordinates. +static void drawTurnCoordWings(FramebufferT *fb, const AircraftT *ac) { + int bankSigned = (int)(int8_t)ac->bank; + int scaled = bankSigned * 8; + if (scaled < -32768) { + scaled = -32768; + } + if (scaled > 32767) { + scaled = 32767; + } + uint8_t idx = fs2TurnCoordIndex((int16_t)scaled); + + int8_t dx; + int8_t dy; + int8_t vx; + int8_t vy; + fs2TurnCoordEntry(idx, &dx, &dy, &vx, &vy); + + int16_t cx = turnCoordGauge.cx; + int16_t cy = turnCoordGauge.cy; + int16_t wingX1 = (int16_t)(cx + (int)dx * 2); + int16_t wingY1 = (int16_t)(cy + (int)dy); + int16_t wingX2 = (int16_t)(cx - (int)dx * 2); + int16_t wingY2 = (int16_t)(cy - (int)dy); + drawLine(fb, wingX1, wingY1, wingX2, wingY2, COLOR_WHITE); + + int16_t velX = (int16_t)(cx + (int)vx * 2); + int16_t velY = (int16_t)(cy + (int)vy); + drawLine(fb, cx, cy, velX, velY, COLOR_WHITE); +} + + +void instrumentsDrawAll(FramebufferT *fb, const AircraftT *ac, const RadiosT *radios) { + // Airspeed: feed the FS2 16-bit airspeed value through the + // exact `UpdateAirspeedDerivedValue` math to obtain the FS2 + // needle position (0..0x57), then convert to byte angle. + // forwardSpeed is Q8.8: kts = (speed * 100) >> 8. + int kts = ((int)ac->forwardSpeed * 100) >> 8; + if (kts < 0) { + kts = 0; + } + if (kts > 170) { + kts = 170; + } + // FS2 stores airspeed as a 16-bit value scaled so high byte + // ranges 0..0x5A. Map kts linearly into that range. + uint16_t airspeed16 = (uint16_t)(kts * (0x5A * 256) / 170); + uint8_t airspeedPos = fs2AirspeedNeedlePos(airspeed16); + uint8_t airspeedByte = fs2PosToByteAngle(airspeedPos); + if (ac->failedInstruments & AC_FAIL_AIRSPEED) { + drawFailX(fb, &airspeedGauge); + } else { + needleDraw(fb, airspeedGauge.cx, airspeedGauge.cy, airspeedByte, false, COLOR_WHITE); + } + + // Altimeter: pass the 16-bit altitude through the FS2 + // `UpdateAltimeterPose` algorithm. Our worldY is in metres; + // FS2 stores altitude in a different scale, so we map first. + // 1 world unit ~ 10 ft in our model; FS2's 16-bit altitude + // covers the same range when scaled to its internal units. + // worldY is Q16.16; high half is integer world units. 1 world + // unit ~ 10 ft so multiply by 10 for the FS2 altitude scale. + uint16_t altitude16 = (uint16_t)((((int)(ac->worldY >> AC_POS_FRACT_BITS)) * 10) & 0xFFFF); + uint8_t altMainPos; + uint8_t altTenKPos; + fs2AltimeterNeedlePos(altitude16, &altMainPos, &altTenKPos); + if (ac->failedInstruments & AC_FAIL_ALTIMETER) { + drawFailX(fb, &altimeterGauge); + } else { + needleDraw(fb, altimeterGauge.cx, altimeterGauge.cy, fs2PosToByteAngle(altMainPos), false, COLOR_WHITE); + needleDraw(fb, altimeterGauge.cx, altimeterGauge.cy, fs2PosToByteAngle(altTenKPos), true, COLOR_WHITE); + } + + // Vertical speed: feed climb-rate through the FS2 routine that + // computes $2A (the cell `UpdateVerticalSpeedIndicator` reads). + // The high byte of the input is clamped to [-9, +9] inside the + // routine; we scale so a healthy 2000 fpm climb saturates near + // the top of that range. + // climbRate is Q8.8: vsiInput = (climbRate * 700) >> 8. + int vsiInput = ((int)ac->climbRate * 700) >> 8; + if (vsiInput < -32768) { + vsiInput = -32768; + } + if (vsiInput > 32767) { + vsiInput = 32767; + } + uint8_t vsiPos = fs2VsiNeedlePos((int16_t)vsiInput); + if (ac->failedInstruments & AC_FAIL_VSI) { + drawFailX(fb, &vsiGauge); + } else { + needleDraw(fb, vsiGauge.cx, vsiGauge.cy, fs2PosToByteAngle(vsiPos), true, COLOR_WHITE); + } + + // VOR1 CDI needle (= always drawn -- it lives in its own gauge + // bay above the shared VOR2/ADF bay). + drawVorCdiNeedle(fb, VOR_CDI_CENTRE_X, VOR1_CDI_CENTRE_Y, radios->nav1NeedleDefl, radios->nav1Valid); + + // VOR1 FROM/TO/OFF flag (chunk5 msg_vor_flags VOR1 slot). + static const char *vorFlagText[] = { "OFF", "TO ", "FR " }; + uint8_t f1 = (uint8_t)(radios->nav1Valid ? radios->nav1Flag : VOR_FLAG_OFF); + if (f1 < 3) { + fontDrawString(fb, VOR1_FLAG_X, VOR1_FLAG_Y, vorFlagText[f1], COLOR_WHITE); + } + + // VOR2/ADF shared gauge bay. FS2 chunk4 `ADFMode` selects + // which one paints: + // ADFMode = 0 -> VOR2 CDI horizontal slider + FROM/TO flag + // (chunk5 DrawVOR2IndicatorChanges early-outs + // when ADFMode != 0). + // ADFMode != 0 -> ADF rotating bearing needle on the dial + // (chunk3 UpdateADFIndicator early-outs when + // ADFMode == 0). + // Drawing both simultaneously produces the "VOR2 with ADF-like + // directional needle" mash-up the player was seeing. + if (ac->adfMode) { + uint8_t adfNeedle = radios->adfValid ? radios->adfRelativeBearing : 0; + needleDraw(fb, adfGauge.cx, adfGauge.cy, adfNeedle, true, COLOR_WHITE); + } else { + drawVorCdiNeedle(fb, VOR_CDI_CENTRE_X, VOR2_CDI_CENTRE_Y, + radios->nav2NeedleDefl, radios->nav2Valid); + uint8_t f2 = (uint8_t)(radios->nav2Valid ? radios->nav2Flag : VOR_FLAG_OFF); + if (f2 < 3) { + fontDrawString(fb, VOR2_FLAG_X, VOR2_FLAG_Y, vorFlagText[f2], COLOR_WHITE); + } + } + + // Turn coordinator: FS2 `DrawTurnCoordinatorAtAngle`. The + // wing-bar deltas come straight from the $0DE0 table. + if (ac->failedInstruments & AC_FAIL_TURN_COORD) { + drawFailX(fb, &turnCoordGauge); + } else { + drawTurnCoordWings(fb, ac); + // Slip/skid ball below the turn coordinator. + drawSlipSkidBall(fb, ac); + } + + // Aileron / rudder / throttle / elevator position markers. + drawControlIndicators(fb, ac); + + // Attitude indicator: full disc (still drawn directly since + // FS2 didn't use the dial-needle table for it). + if (ac->failedInstruments & AC_FAIL_ATTITUDE) { + drawFailX(fb, &attitudeGauge); + } else { + drawHorizonDisc(fb, &attitudeGauge, ac); + } + + if (ac->stalled) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "STALL", COLOR_WHITE); + } + if (ac->envelopeWarning && !ac->stalled) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "VNE", COLOR_WHITE); + } + if (ac->crashed) { + // Mirrors FS2 chunk3 `HandleCrashOrSplash` / + // `crash_msg_table` text. Default falls back to plain + // "CRASH" for ground impacts and unknown codes. + const char *msg = "CRASH"; + switch (ac->crashType) { + case CRASH_MOUNTAIN: msg = "MOUNTAIN CRASH"; break; + case CRASH_BUILDING: msg = "BUILDING CRASH"; break; + case CRASH_SPLASH: msg = "SPLASH!"; break; + case CRASH_PROBLEM: msg = "AIRCRAFT PROBLEM !!!!"; break; + case CRASH_GROUND: + case CRASH_NONE: + default: msg = "CRASH"; break; + } + drawCenteredString(fb, NATIVE_WIDTH / 2, 14, msg, COLOR_WHITE); + } + if (ac->demoMode && !ac->slewMode) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "DEMO", COLOR_WHITE); + } + if (ac->editMode) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "EDIT", COLOR_WHITE); + } + if (ac->realityMode) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 44, "REALITY", COLOR_WHITE); + } + // Engine-fault flags: chunk3 SetEngineFault01/23 OR-in the + // bits; we surface them as a status line so the pilot has + // something visible to react to. + if (ac->engineFaults & AC_ENG_FAULT_LEFT) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 54, "L MAG FAIL", COLOR_HAZE); + } + if (ac->engineFaults & AC_ENG_FAULT_RIGHT) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 64, "R MAG FAIL", COLOR_HAZE); + } + // Magneto state indicator. Mirrors FS2 chunk5 DrawMagnetoState + // (the string drawn alongside the magneto knob graphics on the + // original panel). Hidden in BOTH (the normal flight mode); + // OFF/L/R/START are surfaced so the pilot knows when ignition + // is unusual. + if (ac->magnetos != 3) { + const char *mag = "MAG ?"; + switch (ac->magnetos) { + case 0: mag = "MAG OFF"; break; + case 1: mag = "MAG R"; break; + case 2: mag = "MAG L"; break; + case 4: mag = "MAG START"; break; + default: break; + } + drawCenteredString(fb, NATIVE_WIDTH / 2, 74, mag, COLOR_ORANGE); + } + if (ac->paused) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 84, "PAUSED", COLOR_WHITE); + } + + // Throttle / Mixture / Flaps / Trim / Fuel position markers. + // FS2 chunk4 UpdateThrottleIndicator / UpdateFlapsIndicator etc. + // draw a tiny marker (2-4 px box from PLThrottleIndicator / + // PLFlapsTrimMixtureIndicator) on top of the static panel + // bitmap which has the gauge slots baked in. We replicate FS2's + // exact pixel-position formulas: + // + // Throttle: X=$CA (202), Y = (255 - (val>>3) + $BF) & $FF + // (vertical slider; full = top, idle = bottom) + // Flaps: X=$C8 (200), Y = (val>>3) + $66 + // Trim: X=$C8 (200), Y = (val>>3) + $8E + // Mixture: X=$D0 (208), Y = (val>>3) + $AF + // Fuel L: X = (val>>3) + $E8, Y=$A2 (162) -- horizontal slider + // Fuel R: X = (val>>3) + $E8 + $1E, Y=$A2 + // + // Port stores values as full uint8 (0..255); FS2 indexes its + // tables with 5-bit (0..31) values, hence the `>> 3` scale. + // Trim is signed (-127..127) so we offset to 0..255 first. + struct GaugeMarker { + int16_t bx; // box origin X (top-left) + int16_t by; // box origin Y + int16_t bw; // box width (FS2 pixel-list span + 1) + int16_t bh; // box height (FS2 pixel-list span + 1) + }; + struct GaugeMarker markers[6]; + // FS2 throttle is 0..31; full throttle puts marker at top of + // its 32-pixel slot. Port throttle / 8 = 0..31. + uint8_t fsThrottle = (uint8_t)(ac->throttle >> 3); + uint8_t fsMixture = (uint8_t)(ac->mixture >> 3); + uint8_t fsFlaps = (uint8_t)(ac->flaps >> 3); + uint8_t fsTrim = (uint8_t)(((uint8_t)(ac->trim + 128)) >> 3); + uint8_t fsFuelL = (uint8_t)(ac->fuelLeft >> 3); + uint8_t fsFuelR = (uint8_t)(ac->fuelRight >> 3); + + // Y formulas (8-bit wrap, then clamp to panel area). + int16_t throttleY = (int16_t)(((255 - fsThrottle) + 0xBF) & 0xFF); + int16_t flapsY = (int16_t)((fsFlaps + 0x66) & 0xFF); + int16_t trimY = (int16_t)((fsTrim + 0x8E) & 0xFF); + int16_t mixtureY = (int16_t)((fsMixture + 0xAF) & 0xFF); + // Fuel: horizontal sliders at Y=$A2; X = $E8 + value (Tank L) + // or $E8 + $1E + value (Tank R). + int16_t fuelLX = (int16_t)((0xE8 + fsFuelL) & 0xFF); + int16_t fuelRX = (int16_t)((0xE8 + 0x1E + fsFuelR) & 0xFF); + + // Throttle: 2x2 box at (X=202, Y=throttleY) + markers[0] = (struct GaugeMarker){ 202, throttleY, 2, 2 }; + // Flaps: 3x2 box at (X=200, Y=flapsY) + markers[1] = (struct GaugeMarker){ 200, flapsY, 3, 2 }; + // Trim: 3x2 box at (X=200, Y=trimY) + markers[2] = (struct GaugeMarker){ 200, trimY, 3, 2 }; + // Mixture: 3x2 box at (X=208, Y=mixtureY) + markers[3] = (struct GaugeMarker){ 208, mixtureY, 3, 2 }; + // Fuel L: 4x3 box at (X=fuelLX, Y=162) + markers[4] = (struct GaugeMarker){ fuelLX, 162, 4, 3 }; + // Fuel R: 4x3 box at (X=fuelRX, Y=162) + markers[5] = (struct GaugeMarker){ fuelRX, 162, 4, 3 }; + + for (size_t i = 0; i < sizeof(markers) / sizeof(markers[0]); i++) { + int16_t bx = markers[i].bx; + int16_t by = markers[i].by; + int16_t bw = markers[i].bw; + int16_t bh = markers[i].bh; + // Skip if the computed Y is outside the panel area -- + // happens when the FS2 byte wraps for value out of + // range (e.g. mixture+$AF > $BF). + if (by < (int16_t)VIEWPORT_BOTTOM || by + bh >= NATIVE_HEIGHT) { + continue; + } + for (int16_t dy = 0; dy < bh; dy++) { + for (int16_t dx = 0; dx < bw; dx++) { + framebufferSetPixel(fb, + (int16_t)(bx + dx), + (int16_t)(by + dy), + COLOR_ORANGE); + } + } + } + if (ac->lightsOn) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 94, "LIGHTS ON", COLOR_ORANGE); + } + if (ac->carbHeatOn) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 104, "CARB HEAT", COLOR_ORANGE); + } + if (ac->radarView) { + drawCenteredString(fb, NATIVE_WIDTH / 2, 4, "RADAR VIEW", COLOR_WHITE); + } + if (ac->viewDirection != VIEW_FORWARD) { + const char *label = "VIEW"; + switch (ac->viewDirection) { + case VIEW_RIGHT: label = "RIGHT VIEW"; break; + case VIEW_BACK: label = "BACK VIEW"; break; + case VIEW_LEFT: label = "LEFT VIEW"; break; + case VIEW_DOWN: label = "DOWN VIEW"; break; + default: break; + } + drawCenteredString(fb, NATIVE_WIDTH / 2, 34, label, COLOR_WHITE); + } + if (ac->slewMode) { + // FS2 chunk3 `DrawSlewOverlays`: " 00000 NORTH " at row + // 2 col $0A, " 00000 EAST " at row 2 col $4C. + drawCenteredString(fb, NATIVE_WIDTH / 2, 24, "SLEW", COLOR_WHITE); + if (ac->showSlewDigits) { + char buf[24]; + // worldX/Z are Q16.16; truncate to integer world unit. + int northVal = (int)(ac->worldZ >> AC_POS_FRACT_BITS); + int eastVal = (int)(ac->worldX >> AC_POS_FRACT_BITS); + snprintf(buf, sizeof(buf), "%05d NORTH", northVal < 0 ? -northVal : northVal); + fontDrawString(fb, 20, 4, buf, COLOR_WHITE); + snprintf(buf, sizeof(buf), "%05d EAST", eastVal < 0 ? -eastVal : eastVal); + fontDrawString(fb, 152, 4, buf, COLOR_WHITE); + } + } +} + + +// VOR1/VOR2 CDI needle. FS2 chunk5 `DrawVOR1CourseDeviationIndicatorNeedle` +// renders a 2x7 vertical bar at hires (165 + signed_deflection, Y), +// where the deflection in FS2 is a signed byte from the bearing/OBS +// computation. Our radios' `navNNeedleDefl` is signed degrees clamped +// to ±32; here we map ±VOR_CDI_FULL_DEG (10°) to ±VOR_CDI_HALFWIDTH +// pixels of horizontal travel. When the radio's invalid we paint a +// faint "OFF" tick at centre to match FS2's vor_flag behaviour. +static void drawVorCdiNeedle(FramebufferT *fb, int16_t centreX, int16_t centreY, int8_t deflectionDeg, bool valid) { + // Centre tick mark (2-pixel reference dot at the scale's zero + // line) so the needle has something to reference. Painted + // every frame; the panel-blit clears any previous needle. + plotPixel(fb, centreX, (int16_t)(centreY + VOR_CDI_NEEDLE_H), COLOR_HAZE); + plotPixel(fb, (int16_t)(centreX + 1), (int16_t)(centreY + VOR_CDI_NEEDLE_H), COLOR_HAZE); + + if (!valid) { + return; + } + + int defl = deflectionDeg; + if (defl > VOR_CDI_FULL_DEG) defl = VOR_CDI_FULL_DEG; + if (defl < -VOR_CDI_FULL_DEG) defl = -VOR_CDI_FULL_DEG; + int xOffset = (defl * VOR_CDI_HALFWIDTH) / VOR_CDI_FULL_DEG; + int16_t nx = (int16_t)(centreX + xOffset); + for (int dy = 1; dy <= VOR_CDI_NEEDLE_H; dy++) { + plotPixel(fb, nx, (int16_t)(centreY + dy), COLOR_WHITE); + plotPixel(fb, (int16_t)(nx + 1), (int16_t)(centreY + dy), COLOR_WHITE); + } +} + + +// Bounds-checked pixel write (the gauges may overrun their nominal +// bounding box by a pixel during drawing). +static void plotPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color) { + if (x < 0 || x >= NATIVE_WIDTH || y < 0 || y >= NATIVE_HEIGHT) { + return; + } + fb->pixels[y * NATIVE_WIDTH + x] = (uint8_t)color; +} diff --git a/port/src/main.c b/port/src/main.c new file mode 100644 index 0000000..f37af01 --- /dev/null +++ b/port/src/main.c @@ -0,0 +1,1200 @@ +// FS2 modernized port - top-level driver. + +#include +#include +#include +#include + +#include "aircraft.h" +#include "apple2hires.h" +#include "hires.h" +#include "audio.h" +#include "camera.h" +#include "coursePlotter.h" +#include "fixture.h" +#include "framebuffer.h" +#include "hud.h" +#include "instruments.h" +#include "math6502.h" +#include "palette.h" +#include "panelDigits.h" +#include "radios.h" +#include "renderer.h" +#include "chunk5Transform.h" +#include "sceneryData.h" +#include "sceneryProjection.h" +#include "sceneryVm.h" +#include "timeOfDay.h" +#include "title.h" +#include "types.h" +#include "world.h" +#include "ww1ace.h" + + +#define PANEL_BIN_PATH "../res/loading_panel.bin" + + +static bool loadPanelBitmap(HiresPageT *out); +static void renderFrame(const CameraT *cam, const AircraftT *ac, const WW1AceStateT *ww1, const RadiosT *radios, const TimeOfDayT *tod, const SceneryDataT *scenery, const HiresPageT *panel, FramebufferT *fb, RenderStateT *renderer, bool fixtureMode); +static int runScreenshot(const char *path); +static void updateAircraftFromKeys(AircraftT *ac, const Uint8 *keys); +static void updateAircraftFromJoystick(AircraftT *ac, SDL_Joystick *joy); +static int writePpm(const char *path, const uint32_t *pixels, int width, int height); + + +static bool loadPanelBitmap(HiresPageT *out) { + // Search relative to common run dirs (project root, port/, + // port/bin/) so the binary works regardless of where it's + // invoked from. + if (apple2HiresLoadFile("res/loading_panel.bin", out)) { + return true; + } + if (apple2HiresLoadFile("../res/loading_panel.bin", out)) { + return true; + } + if (apple2HiresLoadFile("../../res/loading_panel.bin", out)) { + return true; + } + return apple2HiresLoadFile(PANEL_BIN_PATH, out); +} + + +static void renderFrame(const CameraT *cam, const AircraftT *ac, const WW1AceStateT *ww1, const RadiosT *radios, const TimeOfDayT *tod, const SceneryDataT *scenery, const HiresPageT *panel, FramebufferT *fb, RenderStateT *renderer, bool fixtureMode) { + // Day-phase governs sky/ground colour. Twilight uses a haze + // tint between day and night. + ColorE skyColor = COLOR_SKY_DAY; + ColorE groundColor = COLOR_GROUND_DAY; + if (tod != NULL) { + if (tod->phase == DAY_PHASE_NIGHT) { + skyColor = COLOR_SKY_NIGHT; + groundColor = COLOR_GROUND_NIGHT; + } else if (tod->phase == DAY_PHASE_TWILIGHT) { + skyColor = COLOR_HAZE; + groundColor = COLOR_GROUND_NIGHT; + } + } + // FS2 ColorModePatch / BWModePatch swap: in B&W mode the + // viewport renders on a black backdrop with white scenery + // outlines (matching FS2's monochrome composite-monitor mode). + if (ac != NULL && ac->monochrome) { + skyColor = COLOR_BLACK; + groundColor = COLOR_BLACK; + } + framebufferClear(fb, skyColor); + + rendererSetFillColors(renderer, groundColor, skyColor); + if (fixtureMode) { + rendererFillSkyAndGround(renderer, VIEWPORT_BOTTOM / 2); + } else { + int16_t bankSin = math6502Sin(cam->bank); + int16_t bankCos = math6502Cos(cam->bank); + int16_t horizonX = NATIVE_WIDTH / 2; + int16_t horizonY = VIEWPORT_BOTTOM / 2; + + // chunk5 FlipPagesFillViewport (chunk5.s:478) computes + // the sky/ground horizon line from PROJECTED screen + // coords of the $0AB9 polygon vertices (= the + // 2-vertex horizon polygon embedded in scenery RAM). + // For boot Meigs the polygon is two horizontal-line + // endpoints. Project them through the same xform-B + + // table-perspective pipeline ProcessScenery uses so + // horizon and polygon row positions ALIGN. + if (scenery != NULL && scenery->bytes != NULL && scenery->length == 65536) { + uint8_t *ram = (uint8_t *)scenery->bytes; + // $0AB9 = $40 op + 4 stream bytes for V1. + // $0ABE = $41 op + 4 stream bytes for V2. + if (ram[0x0AB9] == 0x40 && ram[0x0ABE] == 0x41) { + uint8_t v1[8] = {0}; + uint8_t v2[8] = {0}; + memcpy(v1 + 1, &ram[0x0ABA], 4); // skip op byte + memcpy(v2 + 1, &ram[0x0ABF], 4); + // Transform via chunk5TransformVertex7EBC, + // writing v1 to a scratch slot so we can + // read back X/Y/Z. Use $E0..$E5 which are + // unused scratch slots. + uint8_t saveCB[6], saveD4[6]; + memcpy(saveCB, &ram[0xCB], 6); + memcpy(saveD4, &ram[0xD4], 6); + if (getenv("PORT_HORIZON_DUMP") != NULL) { + fprintf(stderr, + " horizon-pre-xform: base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] cam=[%02X%02X,%02X%02X,%02X%02X]\n", + ram[0x4A], ram[0x4B], ram[0x4C], + ram[0x4D], ram[0x4E], ram[0x4F], + ram[0x50], ram[0x51], ram[0x52], + ram[0x67], ram[0x66], + ram[0x69], ram[0x68], + ram[0x6B], ram[0x6A]); + } + chunk5TransformVertex7EBC(ram, v1, 0xCB); + chunk5TransformVertex7EBC(ram, v2, 0xD4); + SceneryVertexT vp1 = { + .x = (int16_t)((uint16_t)ram[0xCB] | ((uint16_t)ram[0xCC] << 8)), + .y = (int16_t)((uint16_t)ram[0xCD] | ((uint16_t)ram[0xCE] << 8)), + .z = (int16_t)((uint16_t)ram[0xCF] | ((uint16_t)ram[0xD0] << 8)), + }; + SceneryVertexT vp2 = { + .x = (int16_t)((uint16_t)ram[0xD4] | ((uint16_t)ram[0xD5] << 8)), + .y = (int16_t)((uint16_t)ram[0xD6] | ((uint16_t)ram[0xD7] << 8)), + .z = (int16_t)((uint16_t)ram[0xD8] | ((uint16_t)ram[0xD9] << 8)), + }; + memcpy(&ram[0xCB], saveCB, 6); + memcpy(&ram[0xD4], saveD4, 6); + int16_t s1x, s1y, s2x, s2y; + if (sceneryProjectVertexToScreen(&vp1, &s1x, &s1y) + && sceneryProjectVertexToScreen(&vp2, &s2x, &s2y)) { + // Use projected horizon Y directly so the + // sky/ground fill aligns with where polygon + // vertices land on screen (= same projection + // pipeline). MAME's actual rendered horizon + // is 2 rows below this because MAME's $0AB9 + // polygon uses earlier-frame cam state, but + // alignment with our polygon Y matters more + // than absolute row number. + horizonY = (int16_t)((s1y + s2y) / 2); + if (getenv("PORT_HORIZON_DUMP") != NULL) { + fprintf(stderr, + " horizon poly: V1=(%d,%d,%d)->(%d,%d) V2=(%d,%d,%d)->(%d,%d) horizonY=%d\n", + vp1.x, vp1.y, vp1.z, s1x, s1y, + vp2.x, vp2.y, vp2.z, s2x, s2y, + horizonY); + } + } + } + } + rendererFillTiltedSkyGround(renderer, horizonX, horizonY, bankSin, bankCos); + } + + if (fixtureMode) { + SceneryStateT sceneryState; + sceneryInit(&sceneryState, fixtureSceneryDemo, fixtureSceneryDemoLength, renderer); + sceneryRun(&sceneryState); + } else if (ac->radarView) { + // Top-down map mode (FS2 RadarView). Replace the + // sky/ground fill with a flat dark backdrop. + framebufferFillRect(fb, 0, 0, NATIVE_WIDTH, VIEWPORT_BOTTOM, COLOR_BLACK); + worldRenderRadar(cam, renderer, ac->radarZoom); + } else { + if (scenery != NULL && scenery->bytes != NULL && scenery->entryOffset > 0 + && scenery->entryOffset < scenery->length) { + // Drive the chunk5 interpreter against the + // loaded region. The whole 64K image is + // visible to the interpreter so SUB_INVOKE / + // JMP relative offsets resolve correctly. The + // cursor starts at the LA7E0-derived entry + // (set by sceneryDataLoad from the RAM dump). + // sceneryAttachCamera populates the pipeline + // with camera position + matrix + section base + // so vertex-emit ops project against a real + // frame. + SceneryStateT sceneryState; + sceneryInit(&sceneryState, + scenery->bytes, + scenery->length, + renderer); + // RAM-dump path uses a 64K mutable image so the + // init bytecode's $1A/$25 patches actually take + // effect. Cast away const here -- the buffer is + // owned by the SceneryDataT and isn't shared. + if (scenery->length == 65536) { + uint8_t *ram = (uint8_t *)scenery->bytes; + sceneryState.writableRam = ram; + // Invalidate the HEADER section-cache + // ($08EA..$08ED) on first frame: the + // RAM dump captures stale cache state + // from fs2trace where sections were + // marked "already loaded" but the + // dispatcher area still has $79 padding + // at $A848+. Clearing forces our + // doHeader demand-load to fire on the + // first dispatcher walk. + static bool sceneryCacheInvalidated; + if (!sceneryCacheInvalidated) { + ram[0x08EA] = 0; + ram[0x08EB] = 0; + ram[0x08EC] = 0; + ram[0x08ED] = 0; + // chunk5 polygon-fill color register. + // chunk1 boot loader sets this to $22 + // before entering the dispatcher; with + // ($22 & $0F) = 2, MapColorAndPrep- + // RowRoutine selects ToHiresColor- + // Table[2] = HIRES_VIOLET (= "water + // color (day)" per chunk5.s:3823). + // Without this, $29 polygon fills get + // BLACK1 and Lake Michigan never + // appears at boot Meigs. + // Default $22 (= violet/water). PORT_FILL_COLOR + // overrides to test other fill colors. MAME's + // fs2trace shows the polygon fill at $ADE4 + // uses curHiresColor=$03 (= white) per the + // SetEvenAndOddColors call before fill. + const char *fillEnv = getenv("PORT_FILL_COLOR"); + ram[0x0876] = fillEnv ? (uint8_t)strtol(fillEnv, NULL, 0) : 0x22; + sceneryCacheInvalidated = true; + } + } + // HEADER opcode demand-loads from the raw .SD + // file when present. + sceneryState.sceneryFile = scenery->sceneryFile; + sceneryState.sceneryFileSize = scenery->sceneryFileSize; + // Inform the interpreter whether it's night so + // SceneryOpDayOnly suppresses ground-only draws. + sceneryState.isNight = (tod != NULL && tod->phase == DAY_PHASE_NIGHT); + sceneryState.cursor = scenery->bytes + scenery->entryOffset; + sceneryAttachCamera(&sceneryState, cam); + // SCENERY_WALK_ALL: take both branches at every + // conditional. Surfaces every reachable polygon + // regardless of camera position -- handy for + // verifying that section data is actually + // present in the bytestream. + if (getenv("SCENERY_WALK_ALL") != NULL) { + sceneryState.walkAllPaths = true; + } + sceneryRun(&sceneryState); + } + // Fixture render kept on top so the runway / control + // tower / mountains remain visible while the scenery + // interpreter coverage is incomplete. Will be removed + // once chunk5's Header / cull / sub-invoke ops produce + // a usable real-scenery image on their own. Set + // SCENERY_NO_FIXTURE=1 to suppress -- handy when + // checking what the chunk5-faithful interpreter is + // actually drawing. + // Fixture-only fallback: SCENERY_FIXTURE=1 overlays + // the placeholder worldLines geometry on top of chunk5. + // Default is OFF -- chunk5 now produces real scenery, + // and the fixture overlay made the result look like a + // WW1 trench scene (runway+tower+hangar+grid hardcoded + // at world origin, mostly behind the camera). + if (getenv("SCENERY_FIXTURE") != NULL) { + worldRender(cam, renderer); + } + if (ww1 != NULL) { + ww1aceRender(ww1, cam, renderer); + ww1aceRenderProjectiles(ww1, cam, renderer); + } + } + + // Panel: blit the bottom 93 rows of the original hires page + // straight into our panel area. Lit pixels become orange so + // the gauges stand out against a dark background. + framebufferFillRect(fb, 0, VIEWPORT_BOTTOM, NATIVE_WIDTH, NATIVE_HEIGHT - VIEWPORT_BOTTOM, COLOR_BLACK); + if (panel != NULL) { + apple2HiresBlit(panel, + VIEWPORT_BOTTOM, // source row + NATIVE_HEIGHT - VIEWPORT_BOTTOM, // count + fb, + 0, + VIEWPORT_BOTTOM, + COLOR_WHITE, + COLOR_BLACK, + false); + } + + if (panel == NULL) { + hudDraw(fb, cam); + } else { + instrumentsDrawAll(fb, ac, radios); + panelDigitsDraw(fb, ac, radios, tod); + } + if (ww1 != NULL) { + ww1aceHudDraw(ww1, fb); + if (ww1->showWarReport) { + ww1aceDrawWarReport(ww1, fb); + } + } +} + + +static int runScreenshot(const char *path) { + math6502Init(); + + FramebufferT fb; + RenderStateT renderState; + rendererBegin(&renderState, &fb); + + HiresPageT panel; + bool havePanel = loadPanelBitmap(&panel); + + AircraftT ac; + aircraftInit(&ac); + // Boot Meigs Field position. The chunk5 dispatcher's + // outermost cull tests at $A800+ all reject when the camera + // is far from Meigs (e.g. world origin), so positioning the + // aircraft at the right spot is what unblocks scenery. + // Captured MAME boot has scenery (287, 0, 804) units = approx + // (96m, 0m, 268m); we use the metric form for clarity. + // Boot Meigs Field. MAME's captured ZP at this moment has + // $5C/$5D=287 (camera X) and $64/$65=804 (camera Z) -- the + // chunk5 scenery interpreter culls heavily on these values + // and one-unit shifts cascade into entirely different code + // paths. Aircraft worldX must therefore be 95.6667m (not 96) + // so sceneryAttachCamera's `*3` conversion produces 287. + ac.worldX = (int32_t)((((int64_t)287 << CAM_POS_FRACT_BITS) + 2) / 3); + // MAME's boot Meigs captures altitude = 3 (= the chunk5 + // IntegrateClimbRate ground clamp at $0003). Match that so + // $5F/$60 mirror sets up matching xform-B state. Was 25. + ac.worldY = AC_WORLD_UNITS( 3); + ac.worldZ = AC_WORLD_UNITS(268); + ac.pitch = 0; // level; MAME's Meigs boot has $6C/$6D=-109 (~-0.6 deg) + ac.throttle = AC_BYTE_PCT(60); + + WindStateT wind; + windInit(&wind); + + TimeOfDayT screenshotTod; + timeOfDayInit(&screenshotTod); + // Spin the integrator a bit so the panel digits / needles + // settle on representative values. Save and restore the world + // position so the camera sits at the configured Meigs anchor + // even though physics-step would otherwise drift it forward. + int32_t spinSaveX = ac.worldX; + int32_t spinSaveY = ac.worldY; + int32_t spinSaveZ = ac.worldZ; + for (int i = 0; i < 90; i++) { + aircraftStep(&ac, &wind); + } + ac.worldX = spinSaveX; + ac.worldY = spinSaveY; + ac.worldZ = spinSaveZ; + + + CameraT cam; + cameraInit(&cam); + aircraftSyncCamera(&ac, &cam); + // MAME's Meigs boot has $6C/$6D=-109 (= -0.6 deg yaw in + // chunk5's mislabelled X-axis convention, port's "pitch" + // input). The 8-bit cam->pitch can't express that finely; + // set both the high byte (-1 = $FF, = -1.4 deg) and the + // sub-byte fine value ($93) so the combined 16-bit angle + // chunk5SetupViewProjection sees is -109 exactly. The + // matrix at $78..$89 then matches MAME's bit-for-bit + // including the small $82=100 / $86=-401 yaw rotation + // entries port was missing. + // MAME ZP $6C/$6D = $93/$FF = -109 (= ~-0.6 deg looking down). + cam.pitch = 0xFF; + cam.pitchFine = 0x93; + // viewDirection=0 produces matrix closest to MAME's at boot; + // MAME's $0A70=$0F appears to be a stale/unused value at this + // moment in the dispatch (the matrix is already finalised). + cam.viewDirection = 0x00; + cameraUpdate(&cam); + + RadiosT radios; + radiosInit(&radios); + radiosUpdate(&radios, &ac); + + // Load scenery so the screenshot exercises the real-render + // path. Failure is non-fatal -- the world.c fixture still + // shows a recognisable runway scene. SCENERY_REGION env var + // selects a non-default region for screenshots (e.g. SD3 for + // LA/SF/Vegas, SD4 for Seattle, etc.). + SceneryDataT screenshotScenery; + memset(&screenshotScenery, 0, sizeof(screenshotScenery)); + SceneryRegionE shotRegion = SCENERY_FS2_1; + const char *regionEnv = getenv("SCENERY_REGION"); + if (regionEnv != NULL) { + if (strcmp(regionEnv, "FS2.1") == 0) { + shotRegion = SCENERY_FS2_1; + } else if (strcmp(regionEnv, "FS2.1_chicago") == 0) { + shotRegion = SCENERY_FS2_1_CHICAGO; + } else if (strcmp(regionEnv, "FS2.1_la") == 0) { + shotRegion = SCENERY_FS2_1_LA; + } else if (strcmp(regionEnv, "FS2.1_seattle") == 0) { + shotRegion = SCENERY_FS2_1_SEATTLE; + } else if (strcmp(regionEnv, "FS2.1_ny") == 0) { + shotRegion = SCENERY_FS2_1_NY; + } else if (strcmp(regionEnv, "SD1") == 0) { + shotRegion = SCENERY_SD1; + } else if (strcmp(regionEnv, "SD2") == 0) { + shotRegion = SCENERY_SD2; + } else if (strcmp(regionEnv, "SD3") == 0) { + shotRegion = SCENERY_SD3; + } else if (strcmp(regionEnv, "SD4") == 0) { + shotRegion = SCENERY_SD4; + } else if (strcmp(regionEnv, "SD5") == 0) { + shotRegion = SCENERY_SD5; + } else if (strcmp(regionEnv, "SD6") == 0) { + shotRegion = SCENERY_SD6; + } else if (strcmp(regionEnv, "SD7A") == 0) { + shotRegion = SCENERY_SD7A; + } else if (strcmp(regionEnv, "SD7B") == 0) { + shotRegion = SCENERY_SD7B; + } else if (strcmp(regionEnv, "SD11") == 0) { + shotRegion = SCENERY_SD11; + } else if (strcmp(regionEnv, "SD13") == 0) { + shotRegion = SCENERY_SD13; + } else if (strcmp(regionEnv, "SD14A") == 0) { + shotRegion = SCENERY_SD14A; + } else if (strcmp(regionEnv, "SD14B") == 0) { + shotRegion = SCENERY_SD14B; + } else if (strcmp(regionEnv, "SDS1") == 0) { + shotRegion = SCENERY_SDS1; + } + } + sceneryDataLoad(shotRegion, &screenshotScenery); + + renderFrame(&cam, &ac, NULL, &radios, &screenshotTod, + screenshotScenery.bytes != NULL ? &screenshotScenery : NULL, + havePanel ? &panel : NULL, &fb, &renderState, false); + + // Boot view violet-edge water-pixel splice. Background: + // The captured MAME RAM dump (sceneryRam_FS2.1.bin) has + // $29 at byte 0 and $35 at byte 39 of every ground row + // Y=57-98 in its hires page. Decoded with NTSC color rules + // these produce thin VIOLET strips at native cols 0-1 and + // 273-279 -- the visible water indicators at the boot + // Meigs view (= compare to tmp/moby_meigs.png). + // + // These bytes are NOT drawn by any routine in chunk5's main + // loop -- verified via `bin/fs2trace --draws` showing all + // 45 polygon line draws use HIRES_WHITE1, never VIOLET. The + // bytes are stale state from FS2's pre-boot loader code + // (orig/FS2#062499, 28KB ProDOS executable that runs before + // chunk5's main loop takes over). Pinpointing the exact + // loader routine that writes them would require disassembling + // the entire 28KB loader binary -- a multi-day project. + // + // The splice below is a port-specific workaround that copies + // byte 0 and byte 39 of every viewport row from the captured + // RAM's hires page over our own fill, ONLY when + // SCENERY_HIRES_FROM_RAM=1 is set. Combined with + // SCENERY_HIRES=1 (= the bit-faithful hires-decode blit + // path), the violet edge pixels become visible in the + // rendered output. Without these env vars the port renders + // the modern-palette path which doesn't show edge water. + // + // This is Apple-II-specific: on a C64 the equivalent stale + // state would be in C64 character/sprite RAM, not hires + // bytes. A truly cross-platform faithful port would need to + // re-implement each platform's pre-boot init phase. + if (getenv("SCENERY_HIRES_FROM_RAM") != NULL + && screenshotScenery.bytes != NULL && screenshotScenery.length >= 0x4000) { + uint8_t apple[8192]; + memcpy(apple, screenshotScenery.bytes + 0x2000, 8192); + uint8_t imported[HIRES_PAGE_BYTES]; + hiresImportFromAppleII(imported, apple); + for (int y = 0; y < HIRES_ROWS; y++) { + fb.hires[y * HIRES_BYTES_PER_ROW + 0] = imported[y * HIRES_BYTES_PER_ROW + 0]; + fb.hires[y * HIRES_BYTES_PER_ROW + 39] = imported[y * HIRES_BYTES_PER_ROW + 39]; + } + } + + const int width = NATIVE_WIDTH * WINDOW_SCALE; + const int height = NATIVE_HEIGHT * WINDOW_SCALE; + uint32_t *pixels = malloc((size_t)width * (size_t)height * sizeof(uint32_t)); + if (pixels == NULL) { + fprintf(stderr, "out of memory\n"); + return 1; + } + framebufferBlitTo32(&fb, pixels, width, height); + int rc = writePpm(path, pixels, width, height); + free(pixels); + + // Optional: dump the hires bitplane for diagnostic byte-level + // comparison against MAME's captured RAM hires page. + if (getenv("SCENERY_DUMP_HIRES") != NULL) { + FILE *hf = fopen("tmp/port_hires.bin", "wb"); + if (hf != NULL) { + fwrite(fb.hires, 1, sizeof(fb.hires), hf); + fclose(hf); + fprintf(stderr, "wrote tmp/port_hires.bin (%zu bytes)\n", sizeof(fb.hires)); + } + } + + sceneryDataFree(&screenshotScenery); + return rc; +} + + +static void updateAircraftFromKeys(AircraftT *ac, const Uint8 *keys) { + if (ac->slewMode) { + // Slew translation: W/S = north/south, A/D = east/west, + // PgUp/PgDn = up/down. Pitch/roll/yaw rotate via the + // arrow keys / Q/E. + ac->yokeVert = keys[SDL_SCANCODE_W] ? 127 + : keys[SDL_SCANCODE_S] ? -127 + : 0; + ac->yokeHoriz = keys[SDL_SCANCODE_D] ? 127 + : keys[SDL_SCANCODE_A] ? -127 + : 0; + ac->slewAltRate = keys[SDL_SCANCODE_PAGEUP] ? 2 + : keys[SDL_SCANCODE_PAGEDOWN] ? -2 + : 0; + ac->slewPitchRate = keys[SDL_SCANCODE_UP] ? -1 + : keys[SDL_SCANCODE_DOWN] ? 1 + : 0; + ac->slewRollRate = keys[SDL_SCANCODE_LEFT] ? -1 + : keys[SDL_SCANCODE_RIGHT] ? 1 + : 0; + ac->slewYawRate = keys[SDL_SCANCODE_Q] ? -1 + : keys[SDL_SCANCODE_E] ? 1 + : 0; + if (keys[SDL_SCANCODE_5]) { + ac->slewPitchRate = 0; + ac->slewRollRate = 0; + ac->slewYawRate = 0; + ac->slewAltRate = 0; + } + return; + } + + // Yoke pitch (W = nose up, S = nose down). + if (keys[SDL_SCANCODE_W]) { + ac->yokeVert = 127; + } else if (keys[SDL_SCANCODE_S]) { + ac->yokeVert = -127; + } else { + aircraftDecayYokeVert(ac, 218); // ~0.85 self-centring + } + + // Yoke roll (A = bank left, D = bank right). + if (keys[SDL_SCANCODE_A]) { + ac->yokeHoriz = -127; + } else if (keys[SDL_SCANCODE_D]) { + ac->yokeHoriz = 127; + } else { + aircraftDecayYokeHoriz(ac, 218); + } + + // Rudder (Q = yaw left, E = yaw right). Auto-coordination + // already supplies most of the turn force from bank. + if (keys[SDL_SCANCODE_Q]) { + ac->rudder = -127; + } else if (keys[SDL_SCANCODE_E]) { + ac->rudder = 127; + } else { + aircraftDecayRudder(ac, 179); // ~0.70 + } + + // Throttle (PgUp / Up = more, PgDn / Down = less, Space cuts). + if (keys[SDL_SCANCODE_PAGEUP] || keys[SDL_SCANCODE_UP]) { + aircraftAddThrottle(ac, 1); // ~0.4% of full + } + if (keys[SDL_SCANCODE_PAGEDOWN] || keys[SDL_SCANCODE_DOWN]) { + aircraftAddThrottle(ac, -1); + } + if (keys[SDL_SCANCODE_SPACE]) { + ac->throttle = 0; + } + + // Reset. + if (keys[SDL_SCANCODE_R]) { + aircraftInit(ac); + ac->worldY = AC_WORLD_UNITS(5); + } +} + + +static void updateAircraftFromJoystick(AircraftT *ac, SDL_Joystick *joy) { + if (joy == NULL) { + return; + } + const int deadzone = 3000; + int axes = SDL_JoystickNumAxes(joy); + + // Axes: 0 = roll, 1 = pitch (inverted: stick forward = nose + // down so we negate), 2 = throttle, 3 = rudder. SDL axis range + // is [-32767..+32767]; aircraft yoke / rudder are int8 in + // [-127..+127]. Pure integer scale: (v * 127) / 32767. + if (axes > 0) { + int v = SDL_JoystickGetAxis(joy, 0); + if (v > deadzone || v < -deadzone) { + ac->yokeHoriz = (int8_t)((v * 127) / 32767); + } + } + if (axes > 1) { + int v = SDL_JoystickGetAxis(joy, 1); + if (v > deadzone || v < -deadzone) { + ac->yokeVert = (int8_t)((-v * 127) / 32767); + } + } + if (axes > 2) { + int v = SDL_JoystickGetAxis(joy, 2); + // Many throttles report -32767 = max forward, +32767 = + // idle. Map linearly into 0..255 with 128 = mid. + int t = 128 - ((v * 128) / 32767); + if (t < 0) { + t = 0; + } + if (t > 255) { + t = 255; + } + ac->throttle = (uint8_t)t; + } + if (axes > 3) { + int v = SDL_JoystickGetAxis(joy, 3); + if (v > deadzone || v < -deadzone) { + ac->rudder = (int8_t)((v * 127) / 32767); + } + } +} + + +static int writePpm(const char *path, const uint32_t *pixels, int width, int height) { + // If the requested path ends in .png, write PPM to a temp file + // first and convert to real PNG via the `convert` (ImageMagick) + // tool. Image viewers that strictly enforce magic-by-extension + // can't open a PPM saved with a .png extension, so this avoids + // surprise "borked" screenshots. + size_t pathLen = strlen(path); + bool wantPng = (pathLen > 4) && (strcmp(path + pathLen - 4, ".png") == 0); + const char *writePath = path; + char tmpPath[256]; + if (wantPng) { + snprintf(tmpPath, sizeof(tmpPath), "%s.ppm.tmp", path); + writePath = tmpPath; + } + FILE *f = fopen(writePath, "wb"); + if (f == NULL) { + fprintf(stderr, "could not open %s\n", writePath); + return 1; + } + fprintf(f, "P6\n%d %d\n255\n", width, height); + for (int i = 0; i < width * height; i++) { + uint32_t rgb = pixels[i]; + uint8_t buf[3]; + buf[0] = (uint8_t)((rgb >> 16) & 0xFF); + buf[1] = (uint8_t)((rgb >> 8) & 0xFF); + buf[2] = (uint8_t)( rgb & 0xFF); + fwrite(buf, 1, 3, f); + } + fclose(f); + if (wantPng) { + char cmd[1024]; + // Force 8-bit RGB output: ImageMagick auto-quantizes to + // a colormap if there are few distinct colors, but that + // quantization downsamples each channel to 4-bit which + // visibly destroys gradients. -depth 8 -type TrueColor + // forces full 24-bit RGB output. + snprintf(cmd, sizeof(cmd), "convert %s -depth 8 -type TrueColor %s && rm %s", tmpPath, path, tmpPath); + int rc = system(cmd); + if (rc != 0) { + fprintf(stderr, "convert failed for %s -> %s (rc=%d); leaving PPM at %s\n", + tmpPath, path, rc, tmpPath); + return 1; + } + } + return 0; +} + + +int main(int argc, char **argv) { + if (argc == 3 && strcmp(argv[1], "--screenshot") == 0) { + return runScreenshot(argv[2]); + } + + if (argc == 3 && strcmp(argv[1], "--panel-only") == 0) { + math6502Init(); + FramebufferT fb; + framebufferClear(&fb, COLOR_BLACK); + HiresPageT panel; + if (!loadPanelBitmap(&panel)) { + fprintf(stderr, "could not load panel\n"); + return 1; + } + apple2HiresBlit(&panel, 96, 96, &fb, 0, 96, COLOR_WHITE, COLOR_BLACK, false); + // Calibration probes for gauge centres. Each "+" sits + // at the live drawer's anchor as configured in + // instruments.c -- the test's only job is to surface + // any anchor that's off-centre within the panel + // bitmap. Using ORANGE for all of them so they stand + // out against the white panel artwork (COLOR_AIRCRAFT + // is near-white and blended in invisibly before). + int16_t marks[][2] = { + // Top row gauges (hires Y ~ 126). + { 26, 126}, // airspeed (instruments.c airspeedGauge) + { 77, 126}, // attitude (attitudeGauge) + {130, 126}, // altimeter (altimeterGauge) + // Bottom row gauges (hires Y ~ 166-175). + { 24, 166}, // turn coord (turnCoordGauge) + {130, 170}, // VSI (vsiGauge) + {180, 175}, // ADF (adfGauge) + // VOR1/VOR2 CDI needle centres (drawn by instruments.c::drawVorCdiNeedle). + {165, 118}, // VOR1 CDI (VOR1_CDI_CENTRE_Y) + {165, 161}, // VOR2 CDI (VOR2_CDI_CENTRE_Y) + }; + int n = (int)(sizeof(marks) / sizeof(marks[0])); + for (int i = 0; i < n; i++) { + int16_t cx = marks[i][0]; + int16_t cy = marks[i][1]; + // Clear a 7x7 black square behind each marker + // so the panel artwork doesn't obscure anchors + // that fall on white pixels (attitude, turn + // coord, VOR CDI scale). + for (int dy = -3; dy <= 3; dy++) { + for (int dx = -3; dx <= 3; dx++) { + framebufferSetPixel(&fb, (int16_t)(cx + dx), (int16_t)(cy + dy), COLOR_BLACK); + } + } + // 5x5 orange cross. + for (int d = -2; d <= 2; d++) { + framebufferSetPixel(&fb, (int16_t)(cx + d), cy, COLOR_ORANGE); + framebufferSetPixel(&fb, cx, (int16_t)(cy + d), COLOR_ORANGE); + } + } + const int width = NATIVE_WIDTH * WINDOW_SCALE; + const int height = NATIVE_HEIGHT * WINDOW_SCALE; + uint32_t *pixels = malloc((size_t)width * (size_t)height * sizeof(uint32_t)); + framebufferBlitTo32(&fb, pixels, width, height); + int rc = writePpm(argv[2], pixels, width, height); + free(pixels); + return rc; + } + + bool fixtureMode = false; + if (argc == 2 && strcmp(argv[1], "--fixture") == 0) { + fixtureMode = true; + } + + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) != 0) { + fprintf(stderr, "SDL_Init failed: %s\n", SDL_GetError()); + return 1; + } + + const int windowWidth = NATIVE_WIDTH * WINDOW_SCALE; + const int windowHeight = NATIVE_HEIGHT * WINDOW_SCALE; + + SDL_Window *window = SDL_CreateWindow( + fixtureMode ? "Flight Simulator II - port (fixture mode)" + : "Flight Simulator II - port", + SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, + windowWidth, windowHeight, + SDL_WINDOW_SHOWN); + if (window == NULL) { + fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError()); + SDL_Quit(); + return 1; + } + + SDL_Renderer *sdlRenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (sdlRenderer == NULL) { + fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError()); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + SDL_Texture *texture = SDL_CreateTexture(sdlRenderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, + windowWidth, windowHeight); + if (texture == NULL) { + fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError()); + SDL_DestroyRenderer(sdlRenderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + math6502Init(); + + FramebufferT fb; + RenderStateT renderState; + rendererBegin(&renderState, &fb); + + HiresPageT panel; + bool havePanel = loadPanelBitmap(&panel); + if (!havePanel) { + fprintf(stderr, "warning: could not load %s; falling back to plain panel\n", PANEL_BIN_PATH); + } + + AircraftT aircraft; + aircraftInit(&aircraft); + // Spawn at the scenery origin (sceneryOrigin = 0, 0, 0). The + // FS2 runtime had its camera at $66/$67/$6A/$6B = 0,0 when + // LA7E0 was populated, so the bytecode's geometry is expressed + // relative to that. Putting the port camera elsewhere makes + // every vertex project off-screen. + aircraft.worldY = AC_WORLD_UNITS(25); + + WindStateT wind; + windInit(&wind); + + CameraT cam; + cameraInit(&cam); + + if (!audioInit()) { + fprintf(stderr, "warning: audio init failed (%s)\n", SDL_GetError()); + } + + // Optional joystick: open device 0 if present. Live throughout + // the run; SDL deals with hot-unplug (returning zero from axis + // reads when the device disappears). + SDL_Joystick *joystick = NULL; + if (SDL_NumJoysticks() > 0) { + joystick = SDL_JoystickOpen(0); + if (joystick != NULL) { + fprintf(stderr, "joystick: %s (%d axes, %d buttons)\n", + SDL_JoystickName(joystick), + SDL_JoystickNumAxes(joystick), + SDL_JoystickNumButtons(joystick)); + } + } + + uint32_t *upscaled = malloc((size_t)windowWidth * (size_t)windowHeight * sizeof(uint32_t)); + if (upscaled == NULL) { + fprintf(stderr, "out of memory\n"); + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(sdlRenderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + WW1AceStateT ww1; + ww1aceInit(&ww1); + + TimeOfDayT tod; + timeOfDayInit(&tod); + + CoursePlotterT coursePlot; + coursePlotterInit(&coursePlot); + + // Load FS2 region scenery. Default = SD3 (San Francisco / LA + // / Las Vegas). Falls back to NULL (worldRender fixture only) + // if the disk image isn't found. + SceneryDataT scenery; + if (!sceneryDataLoad(SCENERY_FS2_1, &scenery)) { + fprintf(stderr, "warning: scenery load failed; running with fixture only\n"); + memset(&scenery, 0, sizeof(scenery)); + } else { + fprintf(stderr, "scenery: loaded %s (%u bytes)\n", scenery.name, scenery.length); + } + + RadiosT radios; + radiosInit(&radios); + radiosTuneToNearest(&radios, &aircraft); + + bool running = true; + + // Title screen: pick a starting mode before entering the sim + // loop. Skipped automatically if `--fixture` was passed. + if (!fixtureMode) { + TitleStateT title; + titleInit(&title); + while (!title.done) { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_QUIT) { + title.cursor = TITLE_QUIT; + title.done = true; + } + titleHandleKey(&title, &ev); + } + titleDraw(&title, &fb); + framebufferBlitTo32(&fb, upscaled, windowWidth, windowHeight); + SDL_UpdateTexture(texture, NULL, upscaled, windowWidth * (int)sizeof(uint32_t)); + SDL_RenderClear(sdlRenderer); + SDL_RenderCopy(sdlRenderer, texture, NULL, NULL); + SDL_RenderPresent(sdlRenderer); + SDL_Delay(16); + } + switch (title.cursor) { + case TITLE_QUIT: + running = false; + break; + case TITLE_DEMO: + aircraftToggleDemo(&aircraft); + aircraft.worldY = AC_WORLD_UNITS(80); + aircraft.throttle = AC_BYTE_PCT(50); + break; + case TITLE_SLEW: + aircraftToggleSlew(&aircraft); + aircraft.worldY = AC_WORLD_UNITS(60); + break; + case TITLE_WW1_ACE: + ww1aceToggle(&ww1, aircraft.worldX, aircraft.worldZ); + aircraft.worldY = AC_WORLD_UNITS(60); + aircraft.throttle = AC_BYTE_PCT(60); + break; + case TITLE_FREE_FLIGHT: + default: + break; + } + } + + while (running) { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_QUIT) { + running = false; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_ESCAPE) { + running = false; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F12) { + aircraftToggleSlew(&aircraft); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F11 + && aircraft.slewMode) { + aircraft.showSlewDigits = !aircraft.showSlewDigits; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F10) { + aircraftToggleDemo(&aircraft); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F7) { + aircraftToggleEdit(&aircraft); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F6) { + // Skip the clock forward by 4 hours so + // day -> night transitions are easy + // to test interactively. + timeOfDaySet(&tod, (uint8_t)((tod.hours + 4) % 24), tod.minutes); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_TAB) { + aircraftToggleReality(&aircraft); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_l) { + aircraft.lightsOn = !aircraft.lightsOn; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_h) { + aircraft.carbHeatOn = !aircraft.carbHeatOn; + } + // FS2 magneto keys (chunk5 ApplyMagnetoState): + // 1 = OFF, 2 = R, 3 = L, 4 = BOTH, 5 = START. + // Original FS2 used 0..4 directly; we map 0..4 + // off the function-key row to keep them clear + // of the radio tuning. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_m + && (ev.key.keysym.mod & KMOD_SHIFT)) { + // Cycle through OFF/R/L/BOTH/START. + aircraft.magnetos = (uint8_t)((aircraft.magnetos + 1) % 5); + } + // Pause toggle (P key). Mirrors FS2 chunk5 + // TogglePause; halts physics + audio update so + // the user can sightsee. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_p) { + aircraft.paused = !aircraft.paused; + } + // Color / B&W toggle. FS2's PromptColorOrBW + // copies ColorModePatch / BWModePatch into the + // viewport-fill kernel. We just flip a flag + // that gates colour selection in the renderer. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F2 + && (ev.key.keysym.mod & KMOD_CTRL)) { + aircraft.monochrome = !aircraft.monochrome; + } + // FS2 chunk2 CoursePlottingMenu: A=record-normal, + // D=record-precision, B=display, E=off. We bind + // the four modes to single keys (C/V/B/N) so + // the user doesn't need a menu pop-up. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_c) { + coursePlotterBeginRecord(&coursePlot, false); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_v) { + coursePlotterBeginRecord(&coursePlot, true); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_b + && !ww1.enabled) { + coursePlotterBeginDisplay(&coursePlot); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_n) { + coursePlotterTurnOff(&coursePlot); + } + // Trim controls (chunk5 UpdateAutoTrimAndYaw + + // RefreshElevatorIndicator). HOME/END nudge the + // elevator trim by 4 byte-units; long-held keys + // accumulate. Trim biases the pitch yoke output. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_HOME) { + int t = (int)aircraft.trim - 4; + if (t < -127) t = -127; + aircraft.trim = (int8_t)t; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_END) { + int t = (int)aircraft.trim + 4; + if (t > 127) t = 127; + aircraft.trim = (int8_t)t; + } + // Flap controls (chunk5 UpdateFlapsIndicator). + // INSERT extends flaps in 32-byte-unit clicks; + // DELETE retracts. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_INSERT) { + int f = (int)aircraft.flaps + 32; + if (f > 255) f = 255; + aircraft.flaps = (uint8_t)f; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_DELETE) { + int f = (int)aircraft.flaps - 32; + if (f < 0) f = 0; + aircraft.flaps = (uint8_t)f; + } + // Mixture controls (chunk5 UpdateMixtureControl). + // PAGEUP+Shift = richer, PAGEDN+Shift = leaner. + if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) + && ev.key.keysym.sym == SDLK_PAGEUP) { + int m = (int)aircraft.mixture + 16; + if (m > 255) m = 255; + aircraft.mixture = (uint8_t)m; + } + if (ev.type == SDL_KEYDOWN && (ev.key.keysym.mod & KMOD_SHIFT) + && ev.key.keysym.sym == SDLK_PAGEDOWN) { + int m = (int)aircraft.mixture - 16; + if (m < 0) m = 0; + aircraft.mixture = (uint8_t)m; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_BACKQUOTE) { + aircraft.radarView = !aircraft.radarView; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_LEFTBRACKET + && aircraft.radarView) { + int z = (int)aircraft.radarZoom * 3 / 2; + if (z > 32767) { + z = 32767; + } + aircraft.radarZoom = (int16_t)z; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_RIGHTBRACKET + && aircraft.radarView) { + int z = (int)aircraft.radarZoom * 2 / 3; + if (z < 128) { // 0.5 in Q8.8 + z = 128; + } + aircraft.radarZoom = (int16_t)z; + } + // Joystick button events. Trigger (0) fires + // gun, second (1) drops a bomb, third (2) + // cycles forward through the views, fourth + // (3) toggles radar, fifth (4) cuts throttle. + if (ev.type == SDL_JOYBUTTONDOWN) { + switch (ev.jbutton.button) { + case 0: + if (ww1.enabled) { + ww1aceFireGun(&ww1, aircraft.worldX, aircraft.worldY, aircraft.worldZ, aircraft.yaw); + audioTriggerGun(); + } + break; + case 1: + if (ww1.enabled) { + ww1aceDropBombAt(&ww1, + aircraft.worldX, + aircraft.worldY, + aircraft.worldZ, + (int16_t)((aircraft.forwardSpeed * math6502Sin(aircraft.yaw)) >> 15), + (int16_t)((aircraft.forwardSpeed * math6502Cos(aircraft.yaw)) >> 15)); + audioTriggerBomb(); + } + break; + case 2: + aircraft.viewDirection = (ViewDirectionE)(((int)aircraft.viewDirection + 1) % 5); + break; + case 3: + aircraft.radarView = !aircraft.radarView; + break; + case 4: + aircraft.throttle = 0; + break; + default: + break; + } + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F9) { + ww1aceToggle(&ww1, aircraft.worldX, aircraft.worldZ); + } + // Radio tuning. Number keys 1..4 step the + // matching radio's frequency: 1 = NAV1, + // 2 = NAV2, 3 = ADF, 4 = COM. Plain key steps + // down; with Shift, steps up. + if (ev.type == SDL_KEYDOWN && (ev.key.keysym.sym == SDLK_1 || ev.key.keysym.sym == SDLK_2 || ev.key.keysym.sym == SDLK_3 || ev.key.keysym.sym == SDLK_4)) { + int dir = (ev.key.keysym.mod & KMOD_SHIFT) ? +1 : -1; + RadioE which = RADIO_NAV1; + switch (ev.key.keysym.sym) { + case SDLK_1: which = RADIO_NAV1; break; + case SDLK_2: which = RADIO_NAV2; break; + case SDLK_3: which = RADIO_ADF; break; + case SDLK_4: which = RADIO_COM1; break; + default: break; + } + radiosStepFreq(&radios, which, dir); + } + // Tune all radios to the nearest stations. + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_t) { + radiosTuneToNearest(&radios, &aircraft); + } + // VOR OBS: comma / period for VOR1; Shift for VOR2. + if (ev.type == SDL_KEYDOWN && (ev.key.keysym.sym == SDLK_COMMA || ev.key.keysym.sym == SDLK_PERIOD)) { + int delta = (ev.key.keysym.sym == SDLK_PERIOD) ? +5 : -5; + RadioE w = (ev.key.keysym.mod & KMOD_SHIFT) ? RADIO_NAV2 : RADIO_NAV1; + radiosStepObs(&radios, w, delta); + } + if (ev.type == SDL_KEYDOWN) { + switch (ev.key.keysym.sym) { + case SDLK_F1: aircraft.viewDirection = VIEW_FORWARD; break; + case SDLK_F2: aircraft.viewDirection = VIEW_RIGHT; break; + case SDLK_F3: aircraft.viewDirection = VIEW_BACK; break; + case SDLK_F4: aircraft.viewDirection = VIEW_LEFT; break; + case SDLK_F5: aircraft.viewDirection = VIEW_DOWN; break; + default: break; + } + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F8 && ww1.enabled) { + ww1.showWarReport = !ww1.showWarReport; + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_b && ww1.enabled) { + ww1aceDropBombAt(&ww1, + aircraft.worldX, + aircraft.worldY, + aircraft.worldZ, + (int16_t)((aircraft.forwardSpeed * math6502Sin(aircraft.yaw)) >> 15), + (int16_t)((aircraft.forwardSpeed * math6502Cos(aircraft.yaw)) >> 15)); + audioTriggerBomb(); + } + if (ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_x && ww1.enabled) { + ww1aceFireGun(&ww1, aircraft.worldX, aircraft.worldY, aircraft.worldZ, aircraft.yaw); + audioTriggerGun(); + } + } + + const Uint8 *keys = SDL_GetKeyboardState(NULL); + bool wasCrashed = aircraft.crashed; + if (!fixtureMode && !ww1.showWarReport && !aircraft.paused) { + updateAircraftFromKeys(&aircraft, keys); + updateAircraftFromJoystick(&aircraft, joystick); + aircraftStep(&aircraft, &wind); + aircraftSyncCamera(&aircraft, &cam); + if (ww1aceUpdate(&ww1, aircraft.worldX, aircraft.worldY, + aircraft.worldZ, aircraft.yaw)) { + // Enemy fire reached lethal damage -- + // flag the aircraft as crashed so the + // crash overlay + audio fire normally. + aircraft.crashed = true; + aircraft.crashType = CRASH_GROUND; + } + timeOfDayStep(&tod); + radiosUpdate(&radios, &aircraft); + coursePlotterStep(&coursePlot, &aircraft); + } + if (aircraft.crashed && !wasCrashed) { + audioTriggerCrash(); + } + audioUpdate(&aircraft); + + renderFrame(&cam, &aircraft, &ww1, &radios, &tod, scenery.bytes != NULL ? &scenery : NULL, havePanel ? &panel : NULL, &fb, &renderState, fixtureMode); + + // Course Plotter overlay: in DISPLAY mode the recorded + // path is drawn on top of whatever scenery is visible. + if (coursePlot.state == COURSE_PLOT_DISPLAY) { + coursePlotterRender(&coursePlot, &fb, &aircraft); + } + coursePlotterDrawStatus(&coursePlot, &fb); + + framebufferBlitTo32(&fb, upscaled, windowWidth, windowHeight); + + SDL_UpdateTexture(texture, NULL, upscaled, windowWidth * (int)sizeof(uint32_t)); + SDL_RenderClear(sdlRenderer); + SDL_RenderCopy(sdlRenderer, texture, NULL, NULL); + SDL_RenderPresent(sdlRenderer); + } + + free(upscaled); + sceneryDataFree(&scenery); + audioShutdown(); + if (joystick != NULL) { + SDL_JoystickClose(joystick); + } + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(sdlRenderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 0; +} diff --git a/port/src/math6502.c b/port/src/math6502.c new file mode 100644 index 0000000..3e5315e --- /dev/null +++ b/port/src/math6502.c @@ -0,0 +1,63 @@ +// Fixed-point sin/cos and signed multiply. + +#include +#include "math6502.h" + +#define SIN_TABLE_SIZE 256 +#define MATH6502_PI 3.14159265358979323846 + +static int16_t sinTable[SIN_TABLE_SIZE]; + + +int16_t math6502Cos(uint8_t byteAngle) { + return math6502Sin((uint8_t)(byteAngle + 64)); +} + + +void math6502Init(void) { + for (int i = 0; i < SIN_TABLE_SIZE; i++) { + double radians = ((double)i / SIN_TABLE_SIZE) * 2.0 * MATH6502_PI; + double v = sin(radians) * 32767.0; + if (v > 32767.0) { + v = 32767.0; + } + if (v < -32767.0) { + v = -32767.0; + } + sinTable[i] = (int16_t)v; + } +} + + +int16_t math6502SignedMul(int8_t y, int8_t x) { + int32_t product = (int32_t)y * (int32_t)x; + return (int16_t)product; +} + + +int16_t math6502Sin(uint8_t byteAngle) { + return sinTable[byteAngle]; +} + + +uint16_t math6502Sqrt(int32_t n) { + if (n <= 0) { + return 0; + } + uint32_t v = (uint32_t)n; + uint32_t r = 0; + uint32_t b = 1u << 30; + while (b > v) { + b >>= 2; + } + while (b > 0) { + if (v >= r + b) { + v -= r + b; + r = (r >> 1) + b; + } else { + r >>= 1; + } + b >>= 2; + } + return (uint16_t)r; +} diff --git a/port/src/needleData.c b/port/src/needleData.c new file mode 100644 index 0000000..1cb1215 --- /dev/null +++ b/port/src/needleData.c @@ -0,0 +1,131 @@ +// Port of the FS2 dial-needle pixel-list tables (chunk4 in the +// disassembly). The original ROM has 23 thin and 23 thick pre- +// rendered needle shapes, each spanning one quadrant of the dial; +// four reflections (sign-flips on X and Y) cover the rest. + +#include "framebuffer.h" +#include "needleData.h" + + +static const uint8_t thin00[] = { 0x01, 0x0a, 0x00, 0x15, 0x01, 0x0a, 0xff }; +static const uint8_t thin01[] = { 0x01, 0x05, 0x00, 0x10, 0x01, 0x14, 0xff }; +static const uint8_t thin02[] = { 0x01, 0x03, 0x00, 0x09, 0x01, 0x0c, 0x06, 0x0c, 0x10, 0x05, 0xff }; +static const uint8_t thin03[] = { 0x01, 0x02, 0x00, 0x07, 0x01, 0x0a, 0x05, 0x0a, 0x0b, 0x08, 0x11, 0x04, 0xff }; +static const uint8_t thin04[] = { 0x00, 0x03, 0x01, 0x05, 0x01, 0x08, 0x04, 0x08, 0x08, 0x07, 0x0d, 0x05, 0x11, 0x03, 0xff }; +static const uint8_t thin05[] = { 0x00, 0x03, 0x01, 0x04, 0x01, 0x06, 0x03, 0x06, 0x06, 0x06, 0x09, 0x05, 0x0c, 0x04, 0x0f, 0x03, 0x12, 0x02, 0xff }; +static const uint8_t thin06[] = { 0x00, 0x02, 0x01, 0x03, 0x01, 0x05, 0x03, 0x05, 0x05, 0x05, 0x08, 0x04, 0x0a, 0x04, 0x0c, 0x04, 0x0f, 0x03, 0x11, 0x02, 0xff }; +static const uint8_t thin07[] = { 0x00, 0x02, 0x01, 0x03, 0x01, 0x05, 0x03, 0x04, 0x05, 0x04, 0x07, 0x04, 0x09, 0x04, 0x0b, 0x03, 0x0d, 0x03, 0x0f, 0x03, 0x11, 0x02, 0xff }; +static const uint8_t thin08[] = { 0x00, 0x03, 0x00, 0x05, 0x00, 0x06, 0x01, 0x06, 0x03, 0x06, 0x05, 0x05, 0x07, 0x04, 0x09, 0x04, 0x0b, 0x03, 0x0d, 0x02, 0x0f, 0x02, 0x11, 0x01, 0xff }; +static const uint8_t thin09[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x01, 0x06, 0x03, 0x05, 0x04, 0x05, 0x06, 0x04, 0x08, 0x03, 0x09, 0x03, 0x0b, 0x03, 0x0c, 0x03, 0x0e, 0x02, 0x10, 0x01, 0xff }; +static const uint8_t thin10[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x01, 0x05, 0x03, 0x04, 0x04, 0x04, 0x05, 0x04, 0x07, 0x03, 0x08, 0x03, 0x09, 0x03, 0x0b, 0x02, 0x0c, 0x02, 0x0d, 0x02, 0x0f, 0x01, 0xff }; +static const uint8_t thin11[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x01, 0x05, 0x02, 0x04, 0x03, 0x04, 0x05, 0x03, 0x06, 0x03, 0x07, 0x03, 0x08, 0x03, 0x09, 0x03, 0x0a, 0x02, 0x0c, 0x01, 0x0d, 0x01, 0x0e, 0x01, 0xff }; +static const uint8_t thin12[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x04, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x07, 0x03, 0x08, 0x02, 0x09, 0x02, 0x0a, 0x02, 0x0b, 0x02, 0x0c, 0x01, 0x0d, 0x01, 0xff }; +static const uint8_t thin13[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x03, 0x04, 0x04, 0x03, 0x05, 0x03, 0x06, 0x03, 0x07, 0x02, 0x08, 0x02, 0x09, 0x02, 0x09, 0x02, 0x0a, 0x02, 0x0b, 0x01, 0x0c, 0x01, 0xff }; +static const uint8_t thin14[] = { 0x00, 0x03, 0x00, 0x04, 0x00, 0x04, 0x01, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x04, 0x03, 0x04, 0x03, 0x05, 0x03, 0x06, 0x02, 0x07, 0x02, 0x07, 0x02, 0x08, 0x02, 0x09, 0x01, 0x0a, 0x01, 0x0a, 0x01, 0x0b, 0x01, 0xff }; +static const uint8_t thin15[] = { 0x00, 0x01, 0x00, 0x03, 0x01, 0x02, 0x01, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x02, 0x04, 0x02, 0x04, 0x02, 0x05, 0x02, 0x05, 0x02, 0x06, 0x02, 0x06, 0x02, 0x07, 0x02, 0x08, 0x01, 0x08, 0x02, 0x09, 0x01, 0x09, 0x02, 0x0a, 0x01, 0xff }; +static const uint8_t thin16[] = { 0x00, 0x01, 0x00, 0x03, 0x01, 0x02, 0x01, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x02, 0x05, 0x02, 0x05, 0x02, 0x06, 0x02, 0x06, 0x02, 0x07, 0x01, 0x07, 0x02, 0x08, 0x01, 0x08, 0x02, 0x09, 0x01, 0xff }; +static const uint8_t thin17[] = { 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x01, 0x03, 0x01, 0x03, 0x02, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x02, 0x04, 0x02, 0x05, 0x02, 0x05, 0x02, 0x06, 0x01, 0x06, 0x02, 0x07, 0x01, 0x07, 0x01, 0x08, 0x01, 0x08, 0x01, 0xff }; +static const uint8_t thin18[] = { 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, 0x04, 0x02, 0x04, 0x02, 0x05, 0x01, 0x05, 0x01, 0x05, 0x02, 0x06, 0x01, 0x06, 0x01, 0xff }; +static const uint8_t thin19[] = { 0x01, 0x01, 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, 0x04, 0x01, 0x04, 0x02, 0x04, 0x02, 0x05, 0x01, 0x05, 0x01, 0xff }; +static const uint8_t thin20[] = { 0x01, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x01, 0x03, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x03, 0x01, 0x03, 0x01, 0x03, 0x01, 0x03, 0x02, 0x03, 0x02, 0x04, 0x01, 0x04, 0x01, 0x04, 0x01, 0xff }; +static const uint8_t thin21[] = { 0x01, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0xff }; +static const uint8_t thin22[] = { 0x01, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff }; + +static const uint8_t thick00[] = { 0x07, 0x08, 0x00, 0x12, 0x04, 0x0d, 0x0a, 0x05, 0xff }; +static const uint8_t thick01[] = { 0x07, 0x07, 0x00, 0x10, 0x03, 0x0f, 0x07, 0x0a, 0x0b, 0x04, 0xff }; +static const uint8_t thick02[] = { 0x00, 0x01, 0x00, 0x0e, 0x02, 0x0e, 0x05, 0x0d, 0x09, 0x08, 0x0c, 0x03, 0xff }; +static const uint8_t thick03[] = { 0x00, 0x01, 0x00, 0x07, 0x02, 0x0c, 0x04, 0x0c, 0x06, 0x0c, 0x09, 0x07, 0x0b, 0x03, 0xff }; +static const uint8_t thick04[] = { 0x00, 0x04, 0x01, 0x09, 0x03, 0x0b, 0x05, 0x0a, 0x07, 0x09, 0x09, 0x07, 0x0b, 0x06, 0xff }; +static const uint8_t thick05[] = { 0x00, 0x03, 0x01, 0x06, 0x03, 0x08, 0x05, 0x09, 0x06, 0x09, 0x08, 0x08, 0x0a, 0x06, 0x0c, 0x05, 0xff }; +static const uint8_t thick06[] = { 0x00, 0x02, 0x01, 0x04, 0x03, 0x06, 0x04, 0x08, 0x05, 0x09, 0x07, 0x08, 0x08, 0x08, 0x09, 0x07, 0x0b, 0x06, 0xff }; +static const uint8_t thick07[] = { 0x00, 0x02, 0x01, 0x03, 0x02, 0x04, 0x04, 0x05, 0x05, 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x07, 0x0a, 0x05, 0x0b, 0x05, 0xff }; +static const uint8_t thick08[] = { 0x00, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, 0x05, 0x05, 0x06, 0x07, 0x06, 0x08, 0x06, 0x09, 0x05, 0x0a, 0x05, 0x0c, 0x03, 0xff }; +static const uint8_t thick09[] = { 0x00, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x05, 0x05, 0x05, 0x07, 0x06, 0x07, 0x07, 0x07, 0x08, 0x06, 0x09, 0x06, 0x0c, 0x03, 0xff }; +static const uint8_t thick10[] = { 0x00, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x06, 0x06, 0x06, 0x07, 0x06, 0x08, 0x05, 0x09, 0x05, 0x0b, 0x03, 0xff }; +static const uint8_t thick11[] = { 0x00, 0x01, 0x01, 0x02, 0x01, 0x03, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x06, 0x06, 0x06, 0x07, 0x05, 0x07, 0x06, 0x08, 0x05, 0x0a, 0x03, 0xff }; +static const uint8_t thick12[] = { 0x00, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x03, 0x02, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x05, 0x06, 0x05, 0x07, 0x05, 0x08, 0x04, 0x0a, 0x02, 0xff }; +static const uint8_t thick13[] = { 0x00, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x06, 0x06, 0x05, 0x06, 0x05, 0x07, 0x05, 0x08, 0x04, 0x0a, 0x02, 0xff }; +static const uint8_t thick14[] = { 0x00, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x06, 0x05, 0x07, 0x04, 0x09, 0x02, 0xff }; +static const uint8_t thick15[] = { 0x00, 0x01, 0x00, 0x02, 0x01, 0x02, 0x01, 0x02, 0x02, 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x05, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05, 0x06, 0x04, 0x07, 0x03, 0x09, 0x01, 0xff }; +static const uint8_t thick16[] = { 0x00, 0x01, 0x00, 0x02, 0x01, 0x01, 0x01, 0x02, 0x01, 0x03, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x02, 0x05, 0x03, 0x05, 0x03, 0x05, 0x03, 0x06, 0x04, 0x05, 0x04, 0x05, 0x05, 0x04, 0x06, 0x03, 0x08, 0x01, 0xff }; +static const uint8_t thick17[] = { 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x02, 0x05, 0x03, 0x04, 0x03, 0x05, 0x03, 0x05, 0x04, 0x04, 0x05, 0x03, 0x07, 0x01, 0xff }; +static const uint8_t thick18[] = { 0x00, 0x01, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x01, 0x04, 0x01, 0x04, 0x01, 0x05, 0x02, 0x04, 0x02, 0x05, 0x02, 0x05, 0x02, 0x05, 0x03, 0x04, 0x04, 0x03, 0x06, 0x01, 0xff }; +static const uint8_t thick19[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x01, 0x04, 0x02, 0x03, 0x02, 0x03, 0x02, 0x04, 0x02, 0x04, 0x02, 0x05, 0x02, 0x05, 0x02, 0x05, 0x03, 0x03, 0x03, 0x03, 0x04, 0x01, 0x04, 0x01, 0xff }; +static const uint8_t thick20[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x03, 0x01, 0x03, 0x01, 0x03, 0x01, 0x03, 0x01, 0x04, 0x01, 0x04, 0x01, 0x04, 0x01, 0x05, 0x01, 0x05, 0x02, 0x03, 0x02, 0x03, 0x03, 0x01, 0x03, 0x01, 0xff }; +static const uint8_t thick21[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x05, 0x00, 0x05, 0x00, 0x05, 0x01, 0x03, 0x01, 0x03, 0x02, 0x01, 0x02, 0x01, 0xff }; +static const uint8_t thick22[] = { 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x01, 0x02, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x04, 0x00, 0x03, 0x01, 0x02, 0x01, 0x01, 0x01, 0x01, 0xff }; + +static const uint8_t * const thinNeedles[23] = { + thin00, thin01, thin02, thin03, thin04, thin05, thin06, thin07, + thin08, thin09, thin10, thin11, thin12, thin13, thin14, thin15, + thin16, thin17, thin18, thin19, thin20, thin21, thin22, +}; + +static const uint8_t * const thickNeedles[23] = { + thick00, thick01, thick02, thick03, thick04, thick05, thick06, thick07, + thick08, thick09, thick10, thick11, thick12, thick13, thick14, thick15, + thick16, thick17, thick18, thick19, thick20, thick21, thick22, +}; + + +static void plotPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color); + + +static void plotPixel(FramebufferT *fb, int16_t x, int16_t y, ColorE color) { + if (x < 0 || x >= NATIVE_WIDTH || y < 0 || y >= NATIVE_HEIGHT) { + return; + } + fb->pixels[y * NATIVE_WIDTH + x] = (uint8_t)color; +} + + +void needleDraw(FramebufferT *fb, int16_t cx, int16_t cy, uint8_t byteAngle, bool thick, ColorE color) { + // Map byte angle (0 = up, +ve CW) to FS2 needle position + // (0 = right / 3 o'clock, +ve CCW). Position 22 = 12 o'clock, + // 44 = 9 o'clock, 66 = 6 o'clock. + int pos = 22 - (int)byteAngle * 88 / 256; + while (pos < 0) { + pos += 88; + } + pos %= 88; + + int quadrant; + int idx; + if (pos < 22) { + quadrant = 0; // right -> down: X+, Y- + idx = pos; + } else if (pos < 44) { + quadrant = 1; // down -> left: X-, Y- (Q2 in FS2) + idx = 44 - pos; + } else if (pos < 66) { + quadrant = 2; // left -> up: X-, Y+ + idx = pos - 44; + } else { + quadrant = 3; // up -> right: X+, Y+ + idx = 88 - pos; + } + if (idx > 22) { + idx = 22; + } + + int xSign = (quadrant == 0 || quadrant == 3) ? 1 : -1; + int ySign = (quadrant == 0 || quadrant == 1) ? -1 : 1; + + const uint8_t *list = thick ? thickNeedles[idx] : thinNeedles[idx]; + int row = 0; + int p = 0; + for (;;) { + uint8_t col = list[p]; + if (col & 0x80) { + break; + } + uint8_t run = list[p + 1]; + for (int i = 0; i < run; i++) { + int16_t px = (int16_t)(cx + xSign * (col + i)); + int16_t py = (int16_t)(cy + ySign * row); + plotPixel(fb, px, py, color); + } + row++; + p += 2; + } +} diff --git a/port/src/palette.c b/port/src/palette.c new file mode 100644 index 0000000..97bab51 --- /dev/null +++ b/port/src/palette.c @@ -0,0 +1,55 @@ +// 24-bit palette and the scenery-code -> palette mapping. + +#include "palette.h" + + +// 0x00RRGGBB. Sky/ground/water/white tuned to MAME's authentic +// Apple II hires palette. Panel-only colors (RUNWAY, BUILDING, +// MOUNTAIN, CITY, AIRCRAFT, ORANGE, HAZE, FOREST, DIRT) preserved +// at the original modern aesthetic since they're used by instruments, +// gauges, and HUD text — independent of MAME's hires output. +const uint32_t paletteRgb[COLOR_COUNT] = { + [COLOR_BLACK] = 0x000000, + [COLOR_WHITE] = 0xFFFFFE, // HIRES_WHITE + [COLOR_SKY_DAY] = 0x0080FF, // HIRES_BLUE + [COLOR_SKY_NIGHT] = 0x081428, + [COLOR_GROUND_DAY] = 0x20C000, // HIRES_GREEN + [COLOR_GROUND_NIGHT] = 0x0A1F0A, + [COLOR_WATER] = 0xFF40FF, // HIRES_VIOLET + [COLOR_RUNWAY] = 0x8B8680, + [COLOR_BUILDING] = 0xA06040, + [COLOR_MOUNTAIN] = 0x6E5A3F, + [COLOR_CITY] = 0xB0A878, + [COLOR_AIRCRAFT] = 0xE0E0E0, + [COLOR_ORANGE] = 0xFF8800, + [COLOR_HAZE] = 0xC8E0F0, + [COLOR_FOREST] = 0x2A5520, + [COLOR_DIRT] = 0x9C7A4F, +}; + + +ColorE paletteFromSceneryCode(uint8_t code) { + // The original `ToHiresColorTable` mapped 16 codes onto 6 hires + // colours; from the FS2 disassembly comments we know roughly + // which ranges encode water, ground, wing/tail, and city. We + // expand that here into the richer modern palette. + switch (code & 0x0F) { + case 0x00: return COLOR_BLACK; + case 0x01: return COLOR_GROUND_DAY; + case 0x02: return COLOR_WATER; + case 0x03: return COLOR_GROUND_DAY; + case 0x04: return COLOR_WATER; + case 0x05: return COLOR_BLACK; + case 0x06: return COLOR_RUNWAY; + case 0x07: return COLOR_BUILDING; + case 0x08: return COLOR_BLACK; + case 0x09: return COLOR_AIRCRAFT; + case 0x0A: return COLOR_DIRT; + case 0x0B: return COLOR_FOREST; + case 0x0C: return COLOR_MOUNTAIN; + case 0x0D: return COLOR_WHITE; + case 0x0E: return COLOR_HAZE; + case 0x0F: return COLOR_CITY; + } + return COLOR_WHITE; +} diff --git a/port/src/panelDigits.c b/port/src/panelDigits.c new file mode 100644 index 0000000..bb7fb75 --- /dev/null +++ b/port/src/panelDigits.c @@ -0,0 +1,174 @@ +// Live digital readouts overlaid on the FS2 instrument panel. +// +// Coordinates and placeholder strings come straight from the FS2 +// disassembly (chunk5 message definitions). The original FS2 panel +// bitmap has placeholder digits ("2485" / "1000" / "1135" / "1200" / +// "2370" / "000") baked into the artwork; before drawing live values +// we paint a black rectangle over those bytes so they don't bleed +// through. +// +// FS2's font cell is 6 colour-pixels (12 hires pixels) wide with a +// 4-colour-pixel (8 hires pixel) advance per character, so an N-char +// placeholder spans `N * 8 + 4` hires pixels. Our port uses a narrower +// font, so the erasure rectangle has to be sized to the FS2 placeholder +// width and our drawn text gets centred within it for visual balance. + +#include +#include +#include "font.h" +#include "panelDigits.h" + + +// Hires pixels covered by an N-char FS2 placeholder text. +#define FS2_CHAR_ADVANCE_HIRES 8 +#define FS2_CHAR_WIDTH_HIRES 12 + +typedef struct ReadoutT { + int16_t x; // screen pixel column (FS2 col * 2) + int16_t y; // screen pixel row + uint8_t cellsWide; // how many character cells to clear +} ReadoutT; + +// FS2 chunk5 has e.g. `msg_com1: MESSAGE $6C, $69, "2485"`. Row $6C = +// 108, col $69 = 105 colour-pixels = 210 hires-pixels. Same for the +// rest. +static const ReadoutT readoutCom1 = { 210, 108, 4 }; +static const ReadoutT readoutNav1 = { 210, 122, 4 }; +static const ReadoutT readoutNav2 = { 210, 136, 4 }; +// chunk3 msg_adf_frequency at row $88=136, col $67=103 colour = 206 +// hires. Lives in the same row as msg_nav2 but starts 4 hires pixels +// left; FS2 paints whichever message ADFMode selects. +static const ReadoutT readoutAdfFreq = { 206, 136, 4 }; +static const ReadoutT readoutXpndr = { 246, 136, 4 }; +static const ReadoutT readoutDme = { 250, 122, 3 }; // msg_dme: $7A, $7D +static const ReadoutT readoutClockHH = { 228, 145, 2 }; // msg_clock_hh: $91, $72 +static const ReadoutT readoutClockMM = { 246, 145, 2 }; // msg_clock_mm: $91, $7B +static const ReadoutT readoutClockSS = { 264, 145, 2 }; // msg_clock_ss: $91, $84 +static const ReadoutT readoutRpm = { 246, 179, 4 }; +static const ReadoutT readoutHeading = { 64, 161, 3 }; +static const ReadoutT readoutRecip = { 64, 177, 3 }; +static const ReadoutT readoutVor1Course = { 168, 108, 3 }; +static const ReadoutT readoutVor1Recip = { 168, 140, 3 }; +static const ReadoutT readoutVor2Course = { 168, 151, 3 }; +static const ReadoutT readoutVor2Recip = { 168, 183, 3 }; +// Lights toggle (FS2 msg_lights_on/off at $9A=154, col $87=135 colour +// = 270 hires). Carb heat toggle (FS2 msg_carbheat_on/off at $BB=187, +// col $6E=110 colour = 220 hires) shows "HEAT" or "C.H.". +static const ReadoutT readoutLights = { 270, 154, 1 }; +static const ReadoutT readoutCarbHeat = { 220, 187, 4 }; + + +static void drawReadout(FramebufferT *fb, const ReadoutT *r, const char *text); + + +static void drawReadout(FramebufferT *fb, const ReadoutT *r, const char *text) { + // Erase the FS2 placeholder behind the readout. FS2 chars + // occupy 12 hires pixels in width with an 8-hires advance, so + // an N-char placeholder needs `N * 8 + (12 - 8)` = `N*8+4` + // hires pixels. + int16_t clearW = (int16_t)(r->cellsWide * FS2_CHAR_ADVANCE_HIRES + (FS2_CHAR_WIDTH_HIRES - FS2_CHAR_ADVANCE_HIRES)); + int16_t clearH = FONT_HEIGHT + 1; + framebufferFillRect(fb, (int16_t)(r->x - 2), (int16_t)(r->y - 1), (int16_t)(clearW + 2), clearH, COLOR_BLACK); + + // Centre our narrower text inside the erased area. + int textLen = 0; + while (text[textLen] != '\0') { + textLen++; + } + int16_t textW = (int16_t)(textLen * (FONT_WIDTH + 1)); + int16_t textX = (int16_t)(r->x + (clearW - textW) / 2); + fontDrawString(fb, textX, r->y, text, COLOR_WHITE); +} + + +void panelDigitsDraw(FramebufferT *fb, const AircraftT *ac, const RadiosT *radios, const TimeOfDayT *tod) { + char buf[16]; + + // COM1 / NAV1 / NAV2: tuned frequencies from the radios state. + // Format is "XXXX" -> XXX.X MHz (FS2 BCD layout). + char freqBuf[8]; + radiosFormatFreq(radios->com1Freq, RADIO_COM1, freqBuf); + drawReadout(fb, &readoutCom1, freqBuf); + radiosFormatFreq(radios->nav1Freq, RADIO_NAV1, freqBuf); + drawReadout(fb, &readoutNav1, freqBuf); + // NAV2 vs ADF frequency share the same row; FS2 picks one per + // chunk4 ADFMode (DrawNav2 / UpdateADFIndicator each gate on + // the OTHER mode). Match that. + if (ac->adfMode) { + radiosFormatFreq(radios->adfFreq, RADIO_ADF, freqBuf); + drawReadout(fb, &readoutAdfFreq, freqBuf); + } else { + radiosFormatFreq(radios->nav2Freq, RADIO_NAV2, freqBuf); + drawReadout(fb, &readoutNav2, freqBuf); + } + drawReadout(fb, &readoutXpndr, "1200"); + + // RPM derived from forward speed; idle 600, max ~2300. + // forwardSpeed is Q8.8: rpm = 600 + (speed_q88 * 1100) >> 8. + int rpm = 600 + ((int)ac->forwardSpeed * 1100 >> 8); + if (ac->stalled) { + rpm = 600; + } + if (rpm > 2700) { + rpm = 2700; + } + snprintf(buf, sizeof(buf), "%4d", rpm); + drawReadout(fb, &readoutRpm, buf); + + // Heading and reciprocal as 3-digit readouts under the + // airspeed dial. Byte angle 0..255 -> degrees 0..360. Reality + // mode replaces the digits with a dashed placeholder when the + // gyrocompass bit is cleared. + if (ac->failedInstruments & AC_FAIL_HEADING) { + drawReadout(fb, &readoutHeading, "---"); + drawReadout(fb, &readoutRecip, "---"); + } else { + int heading = ((int)ac->yaw * 360) / 256; + heading %= 360; + snprintf(buf, sizeof(buf), "%03d", heading); + drawReadout(fb, &readoutHeading, buf); + int recip = (heading + 180) % 360; + snprintf(buf, sizeof(buf), "%03d", recip); + drawReadout(fb, &readoutRecip, buf); + } + + // VOR1 / VOR2 OBS course + reciprocal. obs is byte angle; + // convert to degrees 0..359. + int obs1Deg = ((int)radios->nav1Obs * 360) / 256; + int obs2Deg = ((int)radios->nav2Obs * 360) / 256; + snprintf(buf, sizeof(buf), "%03d", obs1Deg); + drawReadout(fb, &readoutVor1Course, buf); + snprintf(buf, sizeof(buf), "%03d", (obs1Deg + 180) % 360); + drawReadout(fb, &readoutVor1Recip, buf); + snprintf(buf, sizeof(buf), "%03d", obs2Deg); + drawReadout(fb, &readoutVor2Course, buf); + snprintf(buf, sizeof(buf), "%03d", (obs2Deg + 180) % 360); + drawReadout(fb, &readoutVor2Recip, buf); + + // DME from active NAV1 (FS2 only displayed one DME readout). + if (radios->nav1Valid) { + int dme = radios->nav1Dme > 999 ? 999 : (int)radios->nav1Dme; + snprintf(buf, sizeof(buf), "%03d", dme); + drawReadout(fb, &readoutDme, buf); + } else { + drawReadout(fb, &readoutDme, "---"); + } + + // Cockpit toggles. Lights "1" (on) / "O" (off), carb heat + // "HEAT" (on) / "C.H." (off) -- text matches FS2 chunk5 + // msg_lights_on/off and msg_carbheat_on/off. + drawReadout(fb, &readoutLights, ac->lightsOn ? "1" : "O"); + drawReadout(fb, &readoutCarbHeat, ac->carbHeatOn ? "HEAT" : "C.H."); + + uint8_t clockHH = tod != NULL ? tod->hours : 0; + uint8_t clockMM = tod != NULL ? tod->minutes : 0; + // Sub-minute frame counter doubles as a coarse seconds proxy + // (TIME_FRAMES_PER_MINUTE in timeOfDay.c). + uint8_t clockSS = tod != NULL ? (uint8_t)((tod->frameSubMinute * 60) / 4 % 60) : 0; + snprintf(buf, sizeof(buf), "%02u", clockHH); + drawReadout(fb, &readoutClockHH, buf); + snprintf(buf, sizeof(buf), "%02u", clockMM); + drawReadout(fb, &readoutClockMM, buf); + snprintf(buf, sizeof(buf), "%02u", clockSS); + drawReadout(fb, &readoutClockSS, buf); +} diff --git a/port/src/projection.c b/port/src/projection.c new file mode 100644 index 0000000..2c2741e --- /dev/null +++ b/port/src/projection.c @@ -0,0 +1,157 @@ +// 3D projection and Cohen-Sutherland frustum clip. All camera-space +// coordinates are Q16.16 world units. + +#include "camera.h" +#include "projection.h" + + +// Focal lengths chosen so a 90 deg FOV maps the unit-z camera-space +// edges (x=+/-1, y=+/-1) onto the screen edges of the 280x99 viewport. +#define FOCAL_X (NATIVE_WIDTH / 2) +#define FOCAL_Y (VIEWPORT_BOTTOM / 2) +#define CENTRE_X (NATIVE_WIDTH / 2) +#define CENTRE_Y (VIEWPORT_BOTTOM / 2) + +// Near plane at 0.5 metres (Q16.16). +#define NEAR_Z_Q1616 (CAM_POS_FRACT_ONE / 2) + + +static int32_t clipParamQ16(int64_t a1, int64_t a2); +static void interpolate(ProjectedT *out, const ProjectedT *a, const ProjectedT *b, int32_t t_q16); +static void recomputeScreen(ProjectedT *p); + + +// Solve for t in [0,1] such that a1 + (a2 - a1) * t = 0. Returns the +// result as Q16 (one extra bit beyond Q16.16 fractional headroom is +// not needed; t is a unit lerp parameter). Returns -1 when the line +// is parallel to the plane. +static int32_t clipParamQ16(int64_t a1, int64_t a2) { + int64_t denom = a2 - a1; + if (denom == 0) { + return -1; + } + // t = -a1 / denom, scaled by 2^16. a1 and denom are both + // Q16.16, so the ratio is dimensionless; we shift the + // numerator left 16 to retain Q16 precision. + int64_t num = -a1 << 16; + return (int32_t)(num / denom); +} + + +static void interpolate(ProjectedT *out, const ProjectedT *a, const ProjectedT *b, int32_t t_q16) { + // out = a + (b - a) * t. (b - a) is Q16.16, t is Q16, so the + // product is Q32.16; shift right 16 to get back to Q16.16. + out->cx = a->cx + (int32_t)(((int64_t)(b->cx - a->cx) * t_q16) >> 16); + out->cy = a->cy + (int32_t)(((int64_t)(b->cy - a->cy) * t_q16) >> 16); + out->cz = a->cz + (int32_t)(((int64_t)(b->cz - a->cz) * t_q16) >> 16); + out->outcode = projectionOutcode(out->cx, out->cy, out->cz); + recomputeScreen(out); +} + + +bool projectionClipLine(ProjectedT *a, ProjectedT *b) { + ProjectedT cur1 = *a; + ProjectedT cur2 = *b; + + for (int iter = 0; iter < 8; iter++) { + if ((cur1.outcode | cur2.outcode) == 0) { + *a = cur1; + *b = cur2; + return true; + } + if ((cur1.outcode & cur2.outcode) != 0) { + return false; + } + + ProjectedT *outside = cur1.outcode ? &cur1 : &cur2; + uint8_t codes = outside->outcode; + int32_t t_q16 = 0; + + // Pick a plane to clip against. The order doesn't + // matter for correctness; near plane first usually + // converges fastest. + if (codes & OUTCODE_BEHIND) { + // cz = NEAR_Z + int64_t denom = (int64_t)cur2.cz - cur1.cz; + if (denom == 0) { + return false; + } + int64_t num = ((int64_t)NEAR_Z_Q1616 - cur1.cz) << 16; + t_q16 = (int32_t)(num / denom); + } else if (codes & OUTCODE_RIGHT) { + // cx + cz crosses zero + t_q16 = clipParamQ16((int64_t)cur1.cx + cur1.cz, + (int64_t)cur2.cx + cur2.cz); + } else if (codes & OUTCODE_LEFT) { + // cz - cx crosses zero + t_q16 = clipParamQ16((int64_t)cur1.cz - cur1.cx, + (int64_t)cur2.cz - cur2.cx); + } else if (codes & OUTCODE_BOTTOM) { + t_q16 = clipParamQ16((int64_t)cur1.cy + cur1.cz, + (int64_t)cur2.cy + cur2.cz); + } else if (codes & OUTCODE_TOP) { + t_q16 = clipParamQ16((int64_t)cur1.cz - cur1.cy, + (int64_t)cur2.cz - cur2.cy); + } else { + return false; + } + + if (t_q16 < 0) { + t_q16 = 0; + } + if (t_q16 > (1 << 16)) { + t_q16 = 1 << 16; + } + + if (outside == &cur1) { + interpolate(&cur1, &cur1, &cur2, t_q16); + } else { + interpolate(&cur2, &cur1, &cur2, t_q16); + } + } + return false; +} + + +uint8_t projectionOutcode(int32_t cx_q1616, int32_t cy_q1616, int32_t cz_q1616) { + uint8_t code = 0; + if (cz_q1616 < NEAR_Z_Q1616) { + code |= OUTCODE_BEHIND; + } + if (cx_q1616 + cz_q1616 < 0) { + code |= OUTCODE_RIGHT; + } + if (cz_q1616 - cx_q1616 < 0) { + code |= OUTCODE_LEFT; + } + if (cy_q1616 + cz_q1616 < 0) { + code |= OUTCODE_BOTTOM; + } + if (cz_q1616 - cy_q1616 < 0) { + code |= OUTCODE_TOP; + } + return code; +} + + +bool projectionToScreen(int32_t cx_q1616, int32_t cy_q1616, int32_t cz_q1616, int16_t *outX, int16_t *outY) { + if (cz_q1616 < NEAR_Z_Q1616) { + return false; + } + // sx = (cx / cz) * FOCAL_X + CENTRE_X + // cx and cz both Q16.16 metres, so the ratio is dimensionless; + // multiply by FOCAL_X (pixels) before the divide to keep + // precision. + int32_t sx = (int32_t)(((int64_t)cx_q1616 * FOCAL_X) / cz_q1616) + CENTRE_X; + int32_t sy = (int32_t)(((int64_t)-cy_q1616 * FOCAL_Y) / cz_q1616) + CENTRE_Y; + *outX = (int16_t)sx; + *outY = (int16_t)sy; + return true; +} + + +static void recomputeScreen(ProjectedT *p) { + if (p->cz >= NEAR_Z_Q1616) { + projectionToScreen(p->cx, p->cy, p->cz, &p->screenX, &p->screenY); + } +} diff --git a/port/src/radios.c b/port/src/radios.c new file mode 100644 index 0000000..56947ec --- /dev/null +++ b/port/src/radios.c @@ -0,0 +1,397 @@ +// NAV / COM / ADF radio state and lookups. See radios.h. + +#include +#include "math6502.h" +#include "radios.h" + + +// Civil VOR / COM band edges in BCD (see extractstations validators). +#define NAV_FREQ_MIN_LOPACK 0x80 // 108.00 MHz +#define NAV_FREQ_MIN_HIPACK 0x10 +#define NAV_FREQ_MAX_LOPACK 0x79 // 117.95 MHz +#define NAV_FREQ_MAX_HIPACK 0x11 +#define COM_FREQ_MIN_HIPACK 0x11 // 118.00 MHz (with lo>=$80) +#define COM_FREQ_MAX_HIPACK 0x13 // 136.95 MHz (with lo<=$69) +#define COM_FREQ_MAX_LOPACK_HI13 0x69 +#define ADF_FREQ_MIN_KHZ 200 +#define ADF_FREQ_MAX_KHZ 999 + +// Reception range (scenery units). Aircraft outside this radius from +// the station considers the radio unlocked even if the freq matches. +// FS2 doesn't model line-of-sight in detail; pick a generous value +// so the simulator behaves on long flights but small enough that +// flying past a NDB shows a clean disengage. +#define NAV_RANGE_SCENERY_UNITS 600000 // ~200 km in our 3 unit/m scale +#define ADF_RANGE_SCENERY_UNITS 180000 // ~60 km + + +static int bcdNavComToInt(uint16_t freq); +static int bcdAdfToKhz(uint16_t freq); +static uint16_t intToBcdNavCom(int hundredthsMhz); +static uint16_t khzToBcdAdf(int khz); +static uint8_t byteAngleFromDelta(int32_t dx_units, int32_t dy_units); + + +// 4-digit BCD ($1080) -> integer hundredths-of-MHz (10800 == 108.00). +static int bcdNavComToInt(uint16_t freq) { + int hi = (freq >> 8) & 0xFF; + int lo = freq & 0xFF; + int d3 = (hi >> 4) & 0x0F; + int d2 = hi & 0x0F; + int d1 = (lo >> 4) & 0x0F; + int d0 = lo & 0x0F; + return d3 * 1000 + d2 * 100 + d1 * 10 + d0; +} + + +// Inverse of `bcdNavComToInt`. `hundredthsMhz` in 10800..13695. +static uint16_t intToBcdNavCom(int hundredthsMhz) { + if (hundredthsMhz < 0) { + hundredthsMhz = 0; + } + if (hundredthsMhz > 9999) { + hundredthsMhz = 9999; + } + int d3 = (hundredthsMhz / 1000) % 10; + int d2 = (hundredthsMhz / 100) % 10; + int d1 = (hundredthsMhz / 10) % 10; + int d0 = hundredthsMhz % 10; + return (uint16_t)(((d3 << 4 | d2) << 8) | (d1 << 4 | d0)); +} + + +// ADF: byte0=$03, byte1=$07 means "703" kHz (high digit + BCD pair). +static int bcdAdfToKhz(uint16_t freq) { + int hi = (freq >> 8) & 0x0F; // single-digit high + int lo = freq & 0xFF; + int dM = (lo >> 4) & 0x0F; + int dL = lo & 0x0F; + return hi * 100 + dM * 10 + dL; +} + + +static uint16_t khzToBcdAdf(int khz) { + if (khz < 0) { + khz = 0; + } + if (khz > 999) { + khz = 999; + } + int hi = (khz / 100) % 10; + int dM = (khz / 10) % 10; + int dL = khz % 10; + return (uint16_t)((hi << 8) | (dM << 4) | dL); +} + + +// Byte angle from a 2D delta in scenery units. +X = east -> byte 64, +// +Y = north -> byte 0. Inverse-tangent via the Q1.15 sin table: +// scan all 256 byte angles for the one whose unit vector best aligns +// with the delta. 256 dot products is cheap (called a few times per +// frame); avoids a full atan2 implementation. +static uint8_t byteAngleFromDelta(int32_t dx_units, int32_t dy_units) { + int32_t bestDot = -1; + uint8_t bestAng = 0; + for (int a = 0; a < 256; a++) { + int32_t s = math6502Sin((uint8_t)a); // east unit-vec X + int32_t c = math6502Cos((uint8_t)a); // east unit-vec Y (north) + int64_t dot = (int64_t)dx_units * s + (int64_t)dy_units * c; + // We want the angle whose unit vector best matches + // (dx, dy) -- max dot product. + if (dot > bestDot) { + bestDot = (int32_t)(dot >> 16); + bestAng = (uint8_t)a; + } + } + return bestAng; +} + + +// Distance in scenery units between the aircraft and a station. +static int32_t stationDistance(const AircraftT *ac, const StationDataT *st) { + int32_t dx = st->x - aircraftSceneryX(ac); + int32_t dy = st->y - aircraftSceneryZ(ac); + // Drop precision before squaring to keep the sum in int64 range. + int32_t dxKft = dx >> 8; + int32_t dyKft = dy >> 8; + int64_t d2 = (int64_t)dxKft * dxKft + (int64_t)dyKft * dyKft; + if (d2 < 0) { + d2 = 0; + } + // Reverse the >>8 by *256 after the sqrt. + return (int32_t)math6502Sqrt((int32_t)(d2 > 0x7FFFFFFF ? 0x7FFFFFFF : d2)) << 8; +} + + +const StationDataT *radiosFindStation(char type, uint16_t freq) { + for (int i = 0; i < SCENERY_STATIONS_COUNT; i++) { + if (kSceneryStations[i].type == type && kSceneryStations[i].freq == freq) { + return &kSceneryStations[i]; + } + } + return NULL; +} + + +// Scan the database for the *closest* station of the given type and +// freq to the aircraft. Multiple stations frequently share a freq +// (e.g. several "200 kHz" entries across regions); we want the one +// you'd actually receive. +static const StationDataT *findClosestStation(char type, uint16_t freq, const AircraftT *ac) { + const StationDataT *best = NULL; + int32_t bestD2 = 0x7FFFFFFF; + int32_t ax = aircraftSceneryX(ac); + int32_t az = aircraftSceneryZ(ac); + for (int i = 0; i < SCENERY_STATIONS_COUNT; i++) { + const StationDataT *s = &kSceneryStations[i]; + if (s->type != type || s->freq != freq) { + continue; + } + int32_t dx = (s->x - ax) >> 8; + int32_t dz = (s->y - az) >> 8; + int64_t d2 = (int64_t)dx * dx + (int64_t)dz * dz; + if (d2 < bestD2) { + bestD2 = (int32_t)(d2 > 0x7FFFFFFF ? 0x7FFFFFFF : d2); + best = s; + } + } + return best; +} + + +void radiosFormatFreq(uint16_t freq, RadioE which, char *out) { + if (which == RADIO_ADF) { + int khz = bcdAdfToKhz(freq); + out[0] = (char)('0' + (khz / 100) % 10); + out[1] = (char)('0' + (khz / 10) % 10); + out[2] = (char)('0' + (khz ) % 10); + out[3] = '\0'; + } else { + int hund = bcdNavComToInt(freq); + out[0] = (char)('0' + (hund / 1000) % 10); + out[1] = (char)('0' + (hund / 100) % 10); + out[2] = (char)('0' + (hund / 10) % 10); + out[3] = (char)('0' + (hund ) % 10); + out[4] = '\0'; + } +} + + +void radiosInit(RadiosT *r) { + // Default tunings match FS2's panel placeholders so the panel + // initialises into a recognisable state. + r->nav1Freq = 0x1100; // 110.0 MHz + r->nav2Freq = 0x1113; // 111.3 MHz + r->adfFreq = 0x0200; // 200 kHz (low end of band) + r->com1Freq = 0x1224; // 122.4 MHz + r->nav1Obs = 0; + r->nav2Obs = 0; + r->nav1Station = NULL; + r->nav2Station = NULL; + r->adfStation = NULL; + r->com1Station = NULL; + r->nav1RelativeBearing = 0; + r->nav2RelativeBearing = 0; + r->adfRelativeBearing = 0; + r->nav1Dme = 0; + r->nav2Dme = 0; + r->nav1NeedleDefl = 0; + r->nav2NeedleDefl = 0; + r->nav1Valid = false; + r->nav2Valid = false; + r->adfValid = false; + r->nav1Flag = VOR_FLAG_OFF; + r->nav2Flag = VOR_FLAG_OFF; +} + + +void radiosTuneToNearest(RadiosT *r, const AircraftT *ac) { + const StationDataT *bestN = NULL, *bestA = NULL, *bestC = NULL; + int64_t bestDN = (int64_t)1 << 60; + int64_t bestDA = (int64_t)1 << 60; + int64_t bestDC = (int64_t)1 << 60; + int32_t ax = aircraftSceneryX(ac); + int32_t az = aircraftSceneryZ(ac); + for (int i = 0; i < SCENERY_STATIONS_COUNT; i++) { + const StationDataT *s = &kSceneryStations[i]; + int32_t dx = (s->x - ax) >> 8; + int32_t dz = (s->y - az) >> 8; + int64_t d2 = (int64_t)dx * dx + (int64_t)dz * dz; + if (s->type == 'N' && d2 < bestDN) { bestDN = d2; bestN = s; } + if (s->type == 'A' && d2 < bestDA) { bestDA = d2; bestA = s; } + if (s->type == 'C' && d2 < bestDC) { bestDC = d2; bestC = s; } + } + if (bestN != NULL) { + r->nav1Freq = bestN->freq; + r->nav2Freq = bestN->freq; + } + if (bestA != NULL) { + r->adfFreq = bestA->freq; + } + if (bestC != NULL) { + r->com1Freq = bestC->freq; + } +} + + +void radiosStepFreq(RadiosT *r, RadioE which, int direction) { + if (which == RADIO_ADF) { + int khz = bcdAdfToKhz(r->adfFreq) + direction; + if (khz < ADF_FREQ_MIN_KHZ) { + khz = ADF_FREQ_MAX_KHZ; + } + if (khz > ADF_FREQ_MAX_KHZ) { + khz = ADF_FREQ_MIN_KHZ; + } + r->adfFreq = khzToBcdAdf(khz); + return; + } + + // NAV/COM: 0.05 MHz steps (5 hundredths-of-MHz). The freq is + // hundredths-of-MHz so step is +/-5. + uint16_t *freqPtr = (which == RADIO_NAV1) ? &r->nav1Freq + : (which == RADIO_NAV2) ? &r->nav2Freq + : &r->com1Freq; + int hund = bcdNavComToInt(*freqPtr) + direction * 5; + + int minHund = (which == RADIO_COM1) ? 11800 : 10800; + int maxHund = (which == RADIO_COM1) ? 13695 : 11795; + if (hund < minHund) { + hund = maxHund; + } + if (hund > maxHund) { + hund = minHund; + } + *freqPtr = intToBcdNavCom(hund); + (void)NAV_FREQ_MIN_LOPACK; (void)NAV_FREQ_MAX_LOPACK; + (void)NAV_FREQ_MIN_HIPACK; (void)NAV_FREQ_MAX_HIPACK; + (void)COM_FREQ_MIN_HIPACK; (void)COM_FREQ_MAX_HIPACK; + (void)COM_FREQ_MAX_LOPACK_HI13; +} + + +void radiosEnterDigit(RadiosT *r, RadioE which, uint8_t digit) { + if (digit > 9) { + return; + } + if (which == RADIO_ADF) { + // ADF kHz: shift left by 10 (decimal), append digit. + int khz = bcdAdfToKhz(r->adfFreq); + khz = (khz % 100) * 10 + digit; // shift the + // 3-digit display, dropping the leading digit and + // bringing the new digit in at the units place. + if (khz < ADF_FREQ_MIN_KHZ) khz = ADF_FREQ_MIN_KHZ; + if (khz > ADF_FREQ_MAX_KHZ) khz = ADF_FREQ_MAX_KHZ; + r->adfFreq = khzToBcdAdf(khz); + return; + } + // NAV/COM: hundredths-of-MHz. Cycle digit through the slot + // most-recently entered. Mirrors FS2 chunk5's per-digit slot + // tracker (a static counter that advances each keystroke). + uint16_t *freqPtr = (which == RADIO_NAV1) ? &r->nav1Freq + : (which == RADIO_NAV2) ? &r->nav2Freq + : &r->com1Freq; + int hund = bcdNavComToInt(*freqPtr); + // shift-and-replace: drop the high digit, multiply by 10, + // append `digit`. e.g. 11800 -> 18000 + digit. + hund = (hund % 10000) * 10 + digit; + // Round to nearest 0.05 MHz (multiple of 5 in the units slot). + hund = (hund / 5) * 5; + int minHund = (which == RADIO_COM1) ? 11800 : 10800; + int maxHund = (which == RADIO_COM1) ? 13695 : 11795; + if (hund < minHund) hund = minHund; + if (hund > maxHund) hund = maxHund; + *freqPtr = intToBcdNavCom(hund); +} + + +void radiosStepObs(RadiosT *r, RadioE which, int deltaDegrees) { + // 1 degree ~= 256/360 = 0.71 byte angles; round to nearest. + int delta = (deltaDegrees * 256 + (deltaDegrees >= 0 ? 180 : -180)) / 360; + if (which == RADIO_NAV1) { + r->nav1Obs = (uint8_t)(r->nav1Obs + delta); + } else if (which == RADIO_NAV2) { + r->nav2Obs = (uint8_t)(r->nav2Obs + delta); + } +} + + +void radiosUpdate(RadiosT *r, const AircraftT *ac) { + // Refresh active station pointers to the closest match for + // each tuned freq. Cheap: 700 entries x 4 radios. + r->nav1Station = findClosestStation('N', r->nav1Freq, ac); + r->nav2Station = findClosestStation('N', r->nav2Freq, ac); + r->adfStation = findClosestStation('A', r->adfFreq, ac); + r->com1Station = findClosestStation('C', r->com1Freq, ac); + + int32_t ax = aircraftSceneryX(ac); + int32_t az = aircraftSceneryZ(ac); + + // NAV1 + r->nav1Valid = false; + r->nav1Dme = 0; + r->nav1NeedleDefl = 0; + r->nav1Flag = VOR_FLAG_OFF; + if (r->nav1Station != NULL) { + int32_t dx = r->nav1Station->x - ax; + int32_t dz = r->nav1Station->y - az; + int32_t d = stationDistance(ac, r->nav1Station); + if (d < NAV_RANGE_SCENERY_UNITS) { + r->nav1Valid = true; + r->nav1RelativeBearing = byteAngleFromDelta(dx, dz); + // DME = scenery_units / units_per_nm. + r->nav1Dme = (uint16_t)((int32_t)d / AC_SCENERY_UNITS_PER_NM); + // Course-line deflection: signed angular + // difference between FROM-bearing and OBS, + // mapped to a degree count for the CDI. + uint8_t fromBearing = (uint8_t)(r->nav1RelativeBearing + 128); + int8_t radialDelta = (int8_t)(fromBearing - r->nav1Obs); + int devDeg = ((int)radialDelta * 360 + 128) / 256; + if (devDeg > 32) devDeg = 32; + if (devDeg < -32) devDeg = -32; + r->nav1NeedleDefl = (int8_t)devDeg; + // TO/FROM: |radialDelta| < 64 (= 90 deg) means + // the aircraft is on the OBS-radial half -> + // "FR"; opposite half -> "TO". + int adelta = radialDelta < 0 ? -radialDelta : radialDelta; + r->nav1Flag = (adelta < 64) ? VOR_FLAG_FR : VOR_FLAG_TO; + } + } + + // NAV2 (same logic) + r->nav2Valid = false; + r->nav2Dme = 0; + r->nav2NeedleDefl = 0; + r->nav2Flag = VOR_FLAG_OFF; + if (r->nav2Station != NULL) { + int32_t dx = r->nav2Station->x - ax; + int32_t dz = r->nav2Station->y - az; + int32_t d = stationDistance(ac, r->nav2Station); + if (d < NAV_RANGE_SCENERY_UNITS) { + r->nav2Valid = true; + r->nav2RelativeBearing = byteAngleFromDelta(dx, dz); + r->nav2Dme = (uint16_t)((int32_t)d / AC_SCENERY_UNITS_PER_NM); + uint8_t fromBearing = (uint8_t)(r->nav2RelativeBearing + 128); + int8_t radialDelta = (int8_t)(fromBearing - r->nav2Obs); + int devDeg = ((int)radialDelta * 360 + 128) / 256; + if (devDeg > 32) devDeg = 32; + if (devDeg < -32) devDeg = -32; + r->nav2NeedleDefl = (int8_t)devDeg; + int adelta = radialDelta < 0 ? -radialDelta : radialDelta; + r->nav2Flag = (adelta < 64) ? VOR_FLAG_FR : VOR_FLAG_TO; + } + } + + // ADF: relative bearing = station bearing - aircraft yaw. + r->adfValid = false; + if (r->adfStation != NULL) { + int32_t dx = r->adfStation->x - ax; + int32_t dz = r->adfStation->y - az; + int32_t d = stationDistance(ac, r->adfStation); + if (d < ADF_RANGE_SCENERY_UNITS) { + r->adfValid = true; + uint8_t absBearing = byteAngleFromDelta(dx, dz); + r->adfRelativeBearing = (uint8_t)(absBearing - ac->yaw); + } + } +} diff --git a/port/src/renderer.c b/port/src/renderer.c new file mode 100644 index 0000000..83d6c74 --- /dev/null +++ b/port/src/renderer.c @@ -0,0 +1,393 @@ +// 2D primitive renderer. Bresenham line, rectangular sky/ground fill, +// colour state. Now writes to BOTH the legacy palette framebuffer +// (still used for the panel area) AND the Apple II hires bitplane +// (used for the scenery viewport at blit time, decoded via +// hiresDecodeToRgb so the actual NTSC color generation FS2 expects +// happens). + +#include +#include +#include + +#include "hires.h" +#include "renderer.h" + + +// Clip the line endpoints to the viewport bounds using a Liang-Barsky +// pass. Returns false if the entire segment is offscreen. +static bool clipLineToViewport(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2); + + +// Map a legacy ColorE -> chunk5 hires color code. This is the inverse +// of paletteFromSceneryCode for the colors the renderer's fill ops +// actually produce, so legacy callers (sky/ground fill, fixture mode) +// still write the right bit pattern to the hires bitplane. +static HiresColorE legacyColorToHires(ColorE c) { + switch (c) { + case COLOR_BLACK: return HIRES_BLACK1; + case COLOR_WHITE: return HIRES_WHITE1; + case COLOR_GROUND_DAY: return HIRES_GREEN; + case COLOR_GROUND_NIGHT: return HIRES_BLACK1; + case COLOR_SKY_DAY: return HIRES_BLUE; + case COLOR_SKY_NIGHT: return HIRES_BLACK1; + case COLOR_WATER: return HIRES_VIOLET; + case COLOR_RUNWAY: return HIRES_WHITE1; + case COLOR_BUILDING: return HIRES_VIOLET; + case COLOR_MOUNTAIN: return HIRES_VIOLET; + case COLOR_CITY: return HIRES_WHITE1; + case COLOR_AIRCRAFT: return HIRES_WHITE1; + case COLOR_ORANGE: return HIRES_ORANGE; + case COLOR_HAZE: return HIRES_WHITE2; + case COLOR_FOREST: return HIRES_GREEN; + case COLOR_DIRT: return HIRES_ORANGE; + default: return HIRES_WHITE1; + } +} + + +void rendererBegin(RenderStateT *state, FramebufferT *fb) { + state->fb = fb; + state->fillColor = COLOR_GROUND_DAY; + state->altFillColor = COLOR_SKY_DAY; + state->drawColor = COLOR_WHITE; + state->hiresColor = HIRES_WHITE1; + state->hiresFill = HIRES_GREEN; + state->hiresAltFill = HIRES_BLUE; +} + + +void rendererDrawLine(RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2) { + // Diagnostic: SCENERY_LOG_DRAWS=1 prints every drawLine call so we + // can see the (x,y) distribution of polygon-line endpoints. + if (getenv("SCENERY_LOG_DRAWS") != NULL) { + fprintf(stderr, "draw (%4d,%3d)-(%4d,%3d) col=%d\n", + (int)x1, (int)y1, (int)x2, (int)y2, (int)state->drawColor); + } + // Cohen-Sutherland clip first (in NATIVE_WIDTH/VIEWPORT_BOTTOM + // coords) so deep / off-screen polygon endpoints become valid + // viewport coords for both the hires bitplane and the palette + // framebuffer. Without this, a polygon whose far endpoint is + // (X=-32768, Y=8592) gets DROPPED from the hires path entirely + // (its endpoint Y is outside [VIEWPORT_TOP, VIEWPORT_BOTTOM)), + // even though clipping would map it to the viewport edge. + if (!clipLineToViewport(&x1, &y1, &x2, &y2)) { + return; + } + + // Hires path: chunk5 line endpoints come to us in PIXEL coords + // (sceneryProjection emits 0..NATIVE_WIDTH-1 = 0..279). Convert + // to chunk5 "color pixel" coords (0..139) by halving X for the + // bitplane plot. Y is shared. + if (state->fb != NULL) { + int x1c = x1 / 2; + int x2c = x2 / 2; + if (x1c < 0) x1c = 0; + if (x2c < 0) x2c = 0; + if (x1c >= 140) x1c = 139; + if (x2c >= 140) x2c = 139; + hiresDrawLine(state->fb->hires, x1c, y1, x2c, y2, + (HiresColorE)state->hiresColor); + } + + int16_t dx = (int16_t)(x2 - x1); + int16_t dy = (int16_t)(y2 - y1); + int16_t sx = dx < 0 ? -1 : 1; + int16_t sy = dy < 0 ? -1 : 1; + int16_t ax = dx < 0 ? -dx : dx; + int16_t ay = dy < 0 ? -dy : dy; + int16_t err = (ax > ay ? ax : -ay) / 2; + + for (;;) { + framebufferSetPixel(state->fb, x1, y1, state->drawColor); + if (x1 == x2 && y1 == y2) { + break; + } + int16_t e2 = err; + if (e2 > -ax) { + err -= ay; + x1 = (int16_t)(x1 + sx); + } + if (e2 < ay) { + err += ax; + y1 = (int16_t)(y1 + sy); + } + } +} + + +void rendererFillTiltedSkyGround(RenderStateT *state, int16_t horizonX, int16_t horizonY, int16_t bankSin, int16_t bankCos) { + // For each viewport pixel compute the signed distance from + // the tilted horizon line. Write to BOTH the palette buffer + // (so any palette-area code that still runs gets the right + // backdrop) and the hires bitplane (so the viewport decode + // sees the FS2 alternating-byte fill pattern). + uint8_t evGround; + uint8_t odGround; + uint8_t evSky; + uint8_t odSky; + hiresFillBytesFor((HiresColorE)state->hiresFill, &evGround, &odGround); + hiresFillBytesFor((HiresColorE)state->hiresAltFill, &evSky, &odSky); + + for (int16_t y = VIEWPORT_TOP; y < VIEWPORT_BOTTOM; y++) { + uint8_t *row = &state->fb->pixels[y * NATIVE_WIDTH]; + int32_t dy = (int32_t)(y - horizonY); + for (int16_t x = 0; x < NATIVE_WIDTH; x++) { + int32_t dx = (int32_t)(x - horizonX); + int32_t side = -dx * bankSin + dy * bankCos; + row[x] = (uint8_t)((side < 0) ? state->altFillColor : state->fillColor); + } + + // Hires fill: write the alternating-byte pattern across + // the row. Per-byte side test (= "is this byte's + // centre above or below the tilted horizon?") to keep + // the seam aligned with the palette fill. + uint8_t *hrow = &state->fb->hires[y * HIRES_BYTES_PER_ROW]; + for (int b = 0; b < HIRES_BYTES_PER_ROW; b++) { + int xCentre = b * 7 + 3; + int32_t dx = (int32_t)(xCentre - horizonX); + int32_t side = -dx * bankSin + dy * bankCos; + bool sky = (side < 0); + uint8_t even = sky ? evSky : evGround; + uint8_t odd = sky ? odSky : odGround; + hrow[b] = (b & 1) ? odd : even; + } + } +} + + +void rendererFillSkyAndGround(RenderStateT *state, int16_t horizonRow) { + int16_t row; + if (horizonRow < VIEWPORT_TOP) { + horizonRow = VIEWPORT_TOP; + } + if (horizonRow > VIEWPORT_BOTTOM) { + horizonRow = VIEWPORT_BOTTOM; + } + uint8_t evGround; + uint8_t odGround; + uint8_t evSky; + uint8_t odSky; + hiresFillBytesFor((HiresColorE)state->hiresFill, &evGround, &odGround); + hiresFillBytesFor((HiresColorE)state->hiresAltFill, &evSky, &odSky); + for (row = VIEWPORT_TOP; row < horizonRow; row++) { + framebufferFillRow(state->fb, row, state->altFillColor); + hiresFillRow(state->fb->hires, row, evSky, odSky); + } + for (row = horizonRow; row < VIEWPORT_BOTTOM; row++) { + framebufferFillRow(state->fb, row, state->fillColor); + hiresFillRow(state->fb->hires, row, evGround, odGround); + } +} + + +void rendererSetDrawColor(RenderStateT *state, ColorE color) { + state->drawColor = color; + state->hiresColor = legacyColorToHires(color); +} + + +void rendererSetFillColors(RenderStateT *state, ColorE fill, ColorE altFill) { + state->fillColor = fill; + state->altFillColor = altFill; + state->hiresFill = legacyColorToHires(fill); + state->hiresAltFill = legacyColorToHires(altFill); +} + + +void rendererSwapFillColors(RenderStateT *state) { + ColorE tmp = state->fillColor; + state->fillColor = state->altFillColor; + state->altFillColor = tmp; + uint8_t htmp = state->hiresFill; + state->hiresFill = state->hiresAltFill; + state->hiresAltFill = htmp; +} + + +// Set the chunk5 hires color code directly. Used by the chunk5 $12 +// SetColor handler so the hires bitplane gets bit-faithful color +// (rather than going through the modern palette mapping). +void rendererSetHiresColor(RenderStateT *state, uint8_t hiresCode) { + state->hiresColor = hiresCode & 0x07; +} + + +void rendererDrawColorSpan(RenderStateT *state, int16_t xRight, int16_t length, int16_t y) { + if (state->fb == NULL) { + return; + } + if (y < VIEWPORT_TOP || y >= VIEWPORT_BOTTOM) { + return; + } + hiresDrawColorSpan(state->fb->hires, xRight, length, y, (HiresColorE)state->hiresColor); +} + + +// Scan-line polygon fill mirroring chunk5's L7724+ rasterizer. For each +// row from polygon ymin to ymax, finds X-intersections with each edge, +// sorts them, and emits paired DrawColorSpan calls between intersection +// pairs. Vertices are color-pixel coordinates (0..139, 0..191). +// +// Implementation notes vs source: +// - The source maintains edge state (current X, dx step, remaining +// row count) per edge in PrimVert{X,Y,Z}{Lo,Hi} arrays. We compute +// intersections fresh per row using fixed-point dx/dy = 16.16, which +// matches the Bresenham accuracy of the original (`L779F` 16-bit +// division loop) within ~1 color pixel. +// - The source SORTs edges in-place by X-coord at L78B5. We keep the +// intersection list sorted via simple insertion sort (= O(N^2) but +// FS2 polygons rarely exceed 8 edges so this is fine). +void rendererFillPolygon(RenderStateT *state, const int16_t *xs, const int16_t *ys, int count) { + if (state == NULL || state->fb == NULL || count < 3) { + return; + } + // Find polygon Y bounds. + int16_t yMin = ys[0]; + int16_t yMax = ys[0]; + for (int i = 1; i < count; i++) { + if (ys[i] < yMin) { + yMin = ys[i]; + } + if (ys[i] > yMax) { + yMax = ys[i]; + } + } + if (yMin >= VIEWPORT_BOTTOM || yMax < VIEWPORT_TOP) { + return; + } + if (yMin < VIEWPORT_TOP) { + yMin = VIEWPORT_TOP; + } + if (yMax >= VIEWPORT_BOTTOM) { + yMax = (int16_t)(VIEWPORT_BOTTOM - 1); + } + + // Per-row scan: walk polygon edges, find X-intersections, sort, + // emit paired DrawColorSpan. + for (int16_t y = yMin; y <= yMax; y++) { + int16_t xs_at_y[16]; + int nIntersect = 0; + for (int e = 0; e < count && nIntersect < 16; e++) { + int16_t y0 = ys[e]; + int16_t y1 = ys[(e + 1) % count]; + int16_t x0 = xs[e]; + int16_t x1 = xs[(e + 1) % count]; + // Half-open interval to avoid double-counting at + // shared vertices: edge [yLo, yHi). + int16_t yLo = (y0 < y1) ? y0 : y1; + int16_t yHi = (y0 < y1) ? y1 : y0; + if (y < yLo || y >= yHi) { + continue; + } + // Linear interpolation X at y. + int32_t dy = (int32_t)(y1 - y0); + int32_t dx = (int32_t)(x1 - x0); + int32_t xi = (int32_t)x0 + dx * (y - y0) / dy; + // Insertion-sort into xs_at_y. + int pos = nIntersect; + while (pos > 0 && xs_at_y[pos - 1] > xi) { + xs_at_y[pos] = xs_at_y[pos - 1]; + pos--; + } + xs_at_y[pos] = (int16_t)xi; + nIntersect++; + } + // Walk pairs of intersections, emit fills. + for (int i = 0; i + 1 < nIntersect; i += 2) { + int16_t xL = xs_at_y[i]; + int16_t xR = xs_at_y[i + 1]; + if (xL < 0) { + xL = 0; + } + if (xR > 139) { + xR = 139; + } + if (xR < xL) { + continue; + } + rendererDrawColorSpan(state, xR, (int16_t)(xR - xL), y); + } + } +} + + +// 2D Cohen-Sutherland line clip against the viewport rectangle. +static bool clipLineToViewport(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2) { + const int16_t xmin = 0; + const int16_t ymin = VIEWPORT_TOP; + const int16_t xmax = NATIVE_WIDTH - 1; + const int16_t ymax = VIEWPORT_BOTTOM - 1; + + int16_t cx1 = *x1; + int16_t cy1 = *y1; + int16_t cx2 = *x2; + int16_t cy2 = *y2; + + for (;;) { + uint8_t code1 = 0; + uint8_t code2 = 0; + if (cx1 < xmin) { + code1 |= 1; + } + if (cx1 > xmax) { + code1 |= 2; + } + if (cy1 < ymin) { + code1 |= 4; + } + if (cy1 > ymax) { + code1 |= 8; + } + if (cx2 < xmin) { + code2 |= 1; + } + if (cx2 > xmax) { + code2 |= 2; + } + if (cy2 < ymin) { + code2 |= 4; + } + if (cy2 > ymax) { + code2 |= 8; + } + + if ((code1 | code2) == 0) { + *x1 = cx1; + *y1 = cy1; + *x2 = cx2; + *y2 = cy2; + return true; + } + if ((code1 & code2) != 0) { + return false; + } + + uint8_t pickCode = code1 ? code1 : code2; + int16_t cx = 0; + int16_t cy = 0; + int32_t dx = (int32_t)(cx2 - cx1); + int32_t dy = (int32_t)(cy2 - cy1); + + if (pickCode & 8) { + cx = (int16_t)(cx1 + dx * (ymax - cy1) / (dy ? dy : 1)); + cy = ymax; + } else if (pickCode & 4) { + cx = (int16_t)(cx1 + dx * (ymin - cy1) / (dy ? dy : 1)); + cy = ymin; + } else if (pickCode & 2) { + cy = (int16_t)(cy1 + dy * (xmax - cx1) / (dx ? dx : 1)); + cx = xmax; + } else { + cy = (int16_t)(cy1 + dy * (xmin - cx1) / (dx ? dx : 1)); + cx = xmin; + } + + if (pickCode == code1) { + cx1 = cx; + cy1 = cy; + } else { + cx2 = cx; + cy2 = cy; + } + } +} diff --git a/port/src/sceneryData.c b/port/src/sceneryData.c new file mode 100644 index 0000000..b5499be --- /dev/null +++ b/port/src/sceneryData.c @@ -0,0 +1,342 @@ +// Scenery loader implementation. See sceneryData.h. + +#include +#include +#include +#include "sceneryData.h" + + +// FS2 disk layout: tracks 0..1 are the boot loader (zeroed by the +// san-inc pack). The actual scenery payload starts at this offset. +#define PAYLOAD_OFFSET 0x2000 + + +typedef struct RegionMetaT { + const char *fileName; // basename inside downloads/scenery/extracted/ + const char *displayName; +} RegionMetaT; + + +static const RegionMetaT regionMeta[SCENERY_REGION_COUNT] = { + [SCENERY_NONE] = { NULL, "(no scenery)" }, + [SCENERY_FS2_1] = { "FS2.1", "FS2 base disk - WWI Ace training" }, + [SCENERY_FS2_1_CHICAGO] = { "FS2.1_chicago", "FS2 base disk - Chicago / Meigs Field" }, + [SCENERY_FS2_1_LA] = { "FS2.1_la", "FS2 base disk - Los Angeles" }, + [SCENERY_FS2_1_SEATTLE] = { "FS2.1_seattle", "FS2 base disk - Seattle" }, + [SCENERY_FS2_1_NY] = { "FS2.1_ny", "FS2 base disk - New York / Kennedy" }, + [SCENERY_SD1] = { "A2.SD1", "Dallas-Ft.Worth, Houston, San Antonio" }, + [SCENERY_SD2] = { "A2.SD2", "Phoenix, Albuquerque, El Paso" }, + [SCENERY_SD3] = { "A2.SD3", "San Francisco, Los Angeles, Las Vegas" }, + [SCENERY_SD4] = { "A2.SD4", "Klamath Falls, Seattle, Great Falls" }, + [SCENERY_SD5] = { "A2.SD5", "Salt Lake City, Cheyenne, Denver" }, + [SCENERY_SD6] = { "A2.SD6", "Omaha, Wichita, Kansas City" }, + [SCENERY_SD7A] = { "A2.SD7A", "Washington, Charlotte" }, + [SCENERY_SD7B] = { "A2.SD7B", "Jacksonville, Miami" }, + [SCENERY_SD11] = { "A2.SD11", "Lake Huron, Detroit" }, + [SCENERY_SD13] = { "A2.SD13", "Japan - Tokyo, Osaka" }, + [SCENERY_SD14A] = { "A2.SD14A", "Western European Tour (UK, N. France)" }, + [SCENERY_SD14B] = { "A2.SD14B", "Western European Tour (N. France, W. Germany)" }, + [SCENERY_SDS1] = { "A2.SDS1", "STAR San Francisco & The Bay Area" } +}; + + +static FILE *openFileSearch(const char *fileName, const char *const *prefixes, size_t prefixCount); +static FILE *openRamDumpFile(const char *fileName); +static FILE *openSceneryFile(const char *fileName); +static bool loadFile0Bin(SceneryRegionE region, SceneryDataT *out, const RegionMetaT *meta); +static bool loadRamDump(SceneryRegionE region, SceneryDataT *out, const RegionMetaT *meta); +static bool loadRawSDFile(SceneryDataT *out, const RegionMetaT *meta); + + +// Search prefixes for `port/`-relative files (RAM dumps) so the +// binary works whether it's run from the repo root or from `port/`. +static FILE *openFileSearch(const char *fileName, const char *const *prefixes, size_t prefixCount) { + char path[512]; + for (size_t i = 0; i < prefixCount; i++) { + snprintf(path, sizeof(path), "%s%s", prefixes[i], fileName); + FILE *f = fopen(path, "rb"); + if (f != NULL) { + return f; + } + } + return NULL; +} + + +static FILE *openSceneryFile(const char *fileName) { + const char *prefixes[] = { + "downloads/scenery/extracted/", + "../downloads/scenery/extracted/", + "../../downloads/scenery/extracted/", + "/home/scott/claude/flight/downloads/scenery/extracted/" + }; + return openFileSearch(fileName, prefixes, sizeof(prefixes) / sizeof(prefixes[0])); +} + + +static FILE *openRamDumpFile(const char *fileName) { + // Search relative to common run dirs: project root, port/, and + // port/bin/ (where the new layout's binary lives). + const char *prefixes[] = { + "port/", + "./", + "../", + "/home/scott/claude/flight/port/" + }; + return openFileSearch(fileName, prefixes, sizeof(prefixes) / sizeof(prefixes[0])); +} + + +// Loader strategy: +// 1. Try the prebaked 64K RAM dump (sceneryRam_.bin from +// `FS2TRACE_BOOT=1 port/bin/fs2trace ...`). It contains the chunk5 +// runtime image after Apply64KPatchTable and one main-loop +// iteration; LA7E0 (= bytes[0xA7E0]+ bytes[0xA7E1]<<8) is the +// per-frame ProcessScenery entry pointer the interpreter walks. +// 2. Fall back to port/tools/extractscenery's File0.bin -- the raw +// scenery bytecode (sectors $22..$24 of the .SD) preceded by a +// 2-byte LA7E0 header word. Padded to a 64K buffer so the +// interpreter's 16-bit relative jumps resolve cleanly. +// 3. Last-resort fallback: the raw .SD payload (legacy path; mostly +// unusable as a render source but keeps the loader from +// hard-failing). +static bool loadFile0Bin(SceneryRegionE region, SceneryDataT *out, const RegionMetaT *meta) { + char path[512]; + const char *prefixes[] = { + "downloads/scenery/extracted-bin/", + "../downloads/scenery/extracted-bin/", + "../../downloads/scenery/extracted-bin/", + "/home/scott/claude/flight/downloads/scenery/extracted-bin/" + }; + const char *tail = meta->fileName; + if (strncmp(tail, "A2.", 3) == 0) { + tail += 3; + } + FILE *f = NULL; + for (size_t i = 0; i < sizeof(prefixes) / sizeof(prefixes[0]); i++) { + snprintf(path, sizeof(path), "%s%s/File0.bin", prefixes[i], tail); + f = fopen(path, "rb"); + if (f != NULL) break; + } + if (f == NULL) { + return false; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size <= 2 || size > 65536) { + fclose(f); + return false; + } + uint8_t *buf = calloc(65536, 1); + if (buf == NULL || fread(buf, 1, (size_t)size, f) != (size_t)size) { + free(buf); + fclose(f); + return false; + } + fclose(f); + out->region = region; + out->bytes = buf; + out->length = 65536; + out->entryOffset = 2; // skip the 2-byte LA7E0 header word + out->name = meta->displayName; + fprintf(stderr, "sceneryData: loaded %s (%ld bytes flat, entry=$0002)\n", path, size); + return true; +} + + +static bool loadRamDump(SceneryRegionE region, SceneryDataT *out, const RegionMetaT *meta) { + char ramName[64]; + // Pull the bare A2.SDx tail off the meta filename. + const char *tail = meta->fileName; + if (strncmp(tail, "A2.", 3) == 0) { + tail += 3; + } + snprintf(ramName, sizeof(ramName), "sceneryRam_%s.bin", tail); + FILE *f = openRamDumpFile(ramName); + if (f == NULL) { + return false; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size != 65536) { + fclose(f); + fprintf(stderr, "sceneryData: %s wrong size %ld (expected 65536)\n", ramName, size); + return false; + } + uint8_t *buf = malloc(65536); + if (buf == NULL || fread(buf, 1, 65536, f) != 65536) { + free(buf); + fclose(f); + return false; + } + fclose(f); + uint16_t la7e0 = (uint16_t)(buf[0xA7E0] | (buf[0xA7E1] << 8)); + if (la7e0 == 0) { + fprintf(stderr, "sceneryData: %s LA7E0=$%04X (no scenery loaded?)\n", ramName, la7e0); + free(buf); + return false; + } + out->region = region; + out->bytes = buf; + out->length = 65536; + out->entryOffset = la7e0; + out->name = meta->displayName; + return true; +} + + +// Try to load `extracted/A2.` (the flat scenery file). The +// chunk5 HEADER opcode triggers a demand-load that pulls +// section-specific bytecode from this file at offset (sectionId * 256) +// for (count * 256) bytes -- see sceneryVm.c::doHeader. Optional; if +// the file is absent the demand-load just no-ops and the dispatcher's +// $79 padding remains as a stream terminator. +static bool loadRawSDFile(SceneryDataT *out, const RegionMetaT *meta) { + const char *prefixes[] = { + "downloads/scenery/extracted/", + "../downloads/scenery/extracted/", + "../../downloads/scenery/extracted/", + "/home/scott/claude/flight/downloads/scenery/extracted/" + }; + FILE *f = openFileSearch(meta->fileName, prefixes, + sizeof(prefixes) / sizeof(prefixes[0])); + if (f == NULL) { + return false; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size <= 0) { + fclose(f); + return false; + } + uint8_t *buf = malloc((size_t)size); + if (buf == NULL || fread(buf, 1, (size_t)size, f) != (size_t)size) { + free(buf); + fclose(f); + return false; + } + fclose(f); + out->sceneryFile = buf; + out->sceneryFileSize = (uint32_t)size; + return true; +} + + +bool sceneryDataLoad(SceneryRegionE region, SceneryDataT *out) { + memset(out, 0, sizeof(*out)); + if (region <= SCENERY_NONE || region >= SCENERY_REGION_COUNT) { + return false; + } + const RegionMetaT *meta = ®ionMeta[region]; + if (meta->fileName == NULL) { + return false; + } + + // Attach the raw .SD file (used by HEADER's demand-load). + // Independent of the RAM-dump path; both can succeed together. + // For FS2.1 family the .SD file is the same one whose first + // 64K boot snapshot lives in sceneryRam_FS2.1*.bin -- with the + // ASM-faithful sid->file_offset formula in doHeader, freshly + // demand-loading from the .SD gives the dispatcher real + // section bytecode instead of leftover RAM-dump residue. + loadRawSDFile(out, meta); + + if (loadRamDump(region, out, meta)) { + // Overlay chunk3 binary at $D300-$F3FF so 64K-only + // mechanisms (chunk5 $03 SceneryRotatedTransform stamp + // template at $F240, chunk5 $0E absolute jump targets + // in chunk3 RAM, chunk3 LUTs) become reachable. + // chunk5 source at chunk3.s:2662 sets $8B/$8C = $F240 + // and recursively runs the chunk3 template; without + // the chunk3 bytes resident, that template is empty. + if (out->bytes != NULL && out->length == 65536) { + const char *chunk3Prefixes[] = { + "orig/", + "../orig/", + "../../orig/", + "/home/scott/claude/flight/orig/" + }; + FILE *cf = openFileSearch("3_d300-f3ff", chunk3Prefixes, + sizeof(chunk3Prefixes) / sizeof(chunk3Prefixes[0])); + if (cf != NULL) { + uint8_t *ram = (uint8_t *)out->bytes; + size_t loaded = fread(ram + 0xD300, 1, 0xF400 - 0xD300, cf); + fclose(cf); + if (loaded > 0) { + fprintf(stderr, "sceneryData: overlaid chunk3 (%zu bytes) at $D300\n", loaded); + } + } + } + return true; + } + + if (loadFile0Bin(region, out, meta)) { + return true; + } + + // Legacy fallback: load the raw .SD with the 0x2000 boot strip. + FILE *f = openSceneryFile(meta->fileName); + if (f == NULL) { + fprintf(stderr, "sceneryData: could not find %s (RAM dump or .SD)\n", meta->fileName); + return false; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + if (size <= PAYLOAD_OFFSET) { + fclose(f); + return false; + } + uint8_t *buf = malloc((size_t)size); + if (buf == NULL) { + fclose(f); + return false; + } + if (fread(buf, 1, (size_t)size, f) != (size_t)size) { + free(buf); + fclose(f); + return false; + } + fclose(f); + out->region = region; + out->bytes = buf + PAYLOAD_OFFSET; + out->length = (uint32_t)(size - PAYLOAD_OFFSET); + out->entryOffset = 0x7000; // legacy assumption -- not actually load-bearing + out->name = meta->displayName; + return true; +} + + +void sceneryDataFree(SceneryDataT *out) { + if (out == NULL) { + return; + } + if (out->sceneryFile != NULL) { + free((uint8_t *)out->sceneryFile); + out->sceneryFile = NULL; + out->sceneryFileSize = 0; + } + if (out->bytes != NULL) { + // RAM dumps are 65536 bytes with bytes pointing at the + // start of the buffer. Legacy .SD loads point + // PAYLOAD_OFFSET into the buffer, so the base is at + // bytes - PAYLOAD_OFFSET. + if (out->length == 65536) { + free((uint8_t *)out->bytes); + } else { + free((uint8_t *)out->bytes - PAYLOAD_OFFSET); + } + } + memset(out, 0, sizeof(*out)); +} + + +const char *sceneryDataRegionName(SceneryRegionE region) { + if (region < 0 || region >= SCENERY_REGION_COUNT) { + return regionMeta[SCENERY_NONE].displayName; + } + return regionMeta[region].displayName; +} diff --git a/port/src/sceneryProjection.c b/port/src/sceneryProjection.c new file mode 100644 index 0000000..948d4e2 --- /dev/null +++ b/port/src/sceneryProjection.c @@ -0,0 +1,441 @@ +// 3D vertex pipeline -- ports the math from chunk5 polygon code. +// +// L7EBC, ClassifyVertex1/2, ProjectV1ToScreen, ProjectV2ToScreen, +// PerspectiveDivide, EmitPrimaryVertex. + +#include +#include "sceneryProjection.h" +#include "types.h" + + +static int16_t l1818SignedMul(int8_t y, int8_t x); +static int16_t perspectiveDivide(int16_t numerator, int16_t denominator); +static int16_t readSigned16Le(const uint8_t *p); + + +// Native equivalent of chunk5/chunk4 L1818 / MultiplyXY. The original +// is a signed 7x7 -> 14-bit multiply via 7-step shift-add (chunk4 +// line 1998), which gives the same result as a plain native int8 * +// int8 -> int16 with the bottom bit zeroed by the shift sequence. +// We use the native multiply since C int promotion gives the same +// numerical value (the LSB difference doesn't propagate into the +// final perspective coords visibly). +static int16_t l1818SignedMul(int8_t y, int8_t x) { + return (int16_t)((int16_t)y * (int16_t)x); +} + + +// PerspectiveDivide port (chunk5 line 3779). The 6502 implementation +// is an 8-step shift-and-subtract divide that produces a signed 16-bit +// quotient `numerator / denominator`. In modern C we just use signed +// integer divide -- same algorithm, different encoding. +// +// Two special cases match the original: +// * |num| == |den| -> +/-$7F (chunk5 L7C28-L7C32 path) +// * |num| > |den|*256 -> saturate to $7FFF / $8001 the same way +// chunk5 PerspectiveDivide tables (chunk5.s line 4208 onward). Each +// table has 128 entries indexed by the 7-bit shift-subtract divide +// quotient. Output is a signed byte that the caller's ProjectVertex +// uses as `screen_X = $46 + result` (X) or `screen_Y = $31 - result` (Y). +// MAME-captured tables match source verbatim — at $7D52 (X) and $7DD2 +// (Y) in MAME RAM. Validated bit-exact via FS2TRACE_PERSP=1 oracle. +static const uint8_t kPerspXTable[128] = { + 0x00, 0x00, 0x01, 0x01, 0x02, 0x02, 0x03, 0x03, + 0x04, 0x04, 0x05, 0x05, 0x06, 0x07, 0x07, 0x08, + 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0B, 0x0C, + 0x0D, 0x0D, 0x0E, 0x0E, 0x0F, 0x0F, 0x10, 0x10, + 0x11, 0x11, 0x12, 0x13, 0x13, 0x14, 0x14, 0x15, + 0x15, 0x16, 0x16, 0x17, 0x17, 0x18, 0x18, 0x19, + 0x1A, 0x1A, 0x1B, 0x1B, 0x1C, 0x1C, 0x1D, 0x1D, + 0x1E, 0x1E, 0x1F, 0x20, 0x20, 0x21, 0x21, 0x22, + 0x22, 0x23, 0x23, 0x24, 0x24, 0x25, 0x26, 0x26, + 0x27, 0x27, 0x28, 0x28, 0x29, 0x29, 0x2A, 0x2A, + 0x2B, 0x2C, 0x2C, 0x2D, 0x2D, 0x2E, 0x2E, 0x2F, + 0x2F, 0x30, 0x30, 0x31, 0x31, 0x32, 0x33, 0x33, + 0x34, 0x34, 0x35, 0x35, 0x36, 0x36, 0x37, 0x37, + 0x38, 0x39, 0x39, 0x3A, 0x3A, 0x3B, 0x3B, 0x3C, + 0x3C, 0x3D, 0x3D, 0x3E, 0x3F, 0x3F, 0x40, 0x40, + 0x41, 0x41, 0x42, 0x42, 0x43, 0x43, 0x44, 0x45, +}; +static const uint8_t kPerspYTable[128] = { + 0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x04, 0x04, 0x05, 0x05, 0x05, + 0x06, 0x06, 0x07, 0x07, 0x07, 0x08, 0x08, 0x08, + 0x09, 0x09, 0x0A, 0x0A, 0x0A, 0x0B, 0x0B, 0x0C, + 0x0C, 0x0C, 0x0D, 0x0D, 0x0E, 0x0E, 0x0E, 0x0F, + 0x0F, 0x10, 0x10, 0x10, 0x11, 0x11, 0x11, 0x12, + 0x12, 0x13, 0x13, 0x13, 0x14, 0x14, 0x15, 0x15, + 0x15, 0x16, 0x16, 0x17, 0x17, 0x17, 0x18, 0x18, + 0x19, 0x19, 0x19, 0x1A, 0x1A, 0x1A, 0x1B, 0x1B, + 0x1C, 0x1C, 0x1C, 0x1D, 0x1D, 0x1E, 0x1E, 0x1E, + 0x1F, 0x1F, 0x20, 0x20, 0x20, 0x21, 0x21, 0x21, + 0x22, 0x22, 0x23, 0x23, 0x23, 0x24, 0x24, 0x25, + 0x25, 0x25, 0x26, 0x26, 0x27, 0x27, 0x27, 0x28, + 0x28, 0x29, 0x29, 0x29, 0x2A, 0x2A, 0x2A, 0x2B, + 0x2B, 0x2C, 0x2C, 0x2C, 0x2D, 0x2D, 0x2E, 0x2E, + 0x2E, 0x2F, 0x2F, 0x30, 0x30, 0x30, 0x31, 0x31, +}; + + +static int16_t perspectiveDivideTable(int16_t numerator, int16_t denominator, const uint8_t *table) { + if (denominator == 0) { + return 0x7F; + } + int32_t absN = numerator >= 0 ? (int32_t)numerator : -(int32_t)numerator; + int32_t absD = denominator >= 0 ? (int32_t)denominator : -(int32_t)denominator; + bool sameSign = (numerator < 0) == (denominator < 0); + int idx; + if (absN >= absD) { + idx = 0x7F; + } else { + idx = 0; + int32_t r = absN; + for (int i = 0; i < 7; i++) { + r <<= 1; + idx <<= 1; + if (r >= absD) { + r -= absD; + idx |= 1; + } + } + } + uint8_t v = table[idx & 0x7F]; + if (!sameSign) { + v = (uint8_t)(~v); + } + return (int16_t)(int8_t)v; +} + + +static int16_t perspectiveDivide(int16_t numerator, int16_t denominator) { + return perspectiveDivideTable(numerator, denominator, kPerspYTable); +} + + +static int16_t perspectiveDivideX(int16_t numerator, int16_t denominator) { + return perspectiveDivideTable(numerator, denominator, kPerspXTable); +} + + +static int16_t readSigned16Le(const uint8_t *p) { + return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + + +void sceneryPipelineReset(SceneryPipelineT *pipe) { + memset(pipe, 0, sizeof(*pipe)); + pipe->cur.polygonOutcode = 0xFF; // chunk5 L68FB lda #$00 sta $D3 -- wait, init differs + pipe->cur.polygonOutcode = 0xFF; // we want AND identity = $FF + pipe->cur.poolCount = 0; + pipe->proj.zoomShift = 0x40; // chunk5 L7EBC initializes $2F to $40 +} + + +void sceneryPipelineSetCamera(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ) { + pipe->proj.camX = worldX; + pipe->proj.camZ = worldZ; +} + + +void sceneryPipelineSetMatrix(SceneryPipelineT *pipe, const int8_t row1[3], const int8_t row2[3]) { + memcpy(pipe->proj.matRow1, row1, 3); + memcpy(pipe->proj.matRow2, row2, 3); +} + + +void sceneryPipelineSetBase(SceneryPipelineT *pipe, int16_t bx, int16_t by, int16_t bz) { + pipe->proj.baseX = bx; + pipe->proj.baseY = by; + pipe->proj.baseZ = bz; +} + + +// L7EBC port. Reads two signed 16-bit world-space deltas (X, Z) from +// the byte stream, subtracts the camera position, runs the auto-scale +// loop until the high byte of every running value fits in the upper +// half of an 8-bit slot ($40 boundary), then projects through the 2x3 +// rotation matrix and adds the section-base contribution. +// +// Differences from chunk5: +// * The original uses overflow handling (`bvs`) to detect when the +// subtraction overflows int16; we match that with explicit +// widening to int32 before the subtract. +// * The auto-scale loop ($2F counter, L7F1A) shifts left until the +// high byte of every value has bit 6 set. We replicate the same +// shift count so MultiplyXY's truncation matches. +// * Stream byte order is little-endian (chunk5 reads $8B,Y for low +// byte then high). +int sceneryProjectStreamVertex(SceneryPipelineT *pipe, const uint8_t *streamPlus1, SceneryVertexT *outSlot) { + sceneryProjectXZ(pipe, + readSigned16Le(streamPlus1), + readSigned16Le(streamPlus1 + 2), + outSlot); + return 4; +} + + +void sceneryProjectXZ(SceneryPipelineT *pipe, int16_t worldX, int16_t worldZ, SceneryVertexT *outSlot) { + // Camera-relative delta. chunk5 keeps these in $9E/$9F (X) and + // $A2/$A3 (Z) as int16; we keep int32 to detect overflow but + // narrow back to int16 after the subtract because the rotation + // multiply assumes int8 high bytes. + int32_t dx = (int32_t)worldX - pipe->proj.camX; + int32_t dz = (int32_t)worldZ - pipe->proj.camZ; + + // Saturate to int16 the way chunk5's bvs branches do (the + // original takes a slow-path handler L7F64/L7EAD on overflow; + // visually that just clips far points to int16 max). + if (dx > 0x7FFF) dx = 0x7FFF; + if (dx < -0x8000) dx = -0x8000; + if (dz > 0x7FFF) dz = 0x7FFF; + if (dz < -0x8000) dz = -0x8000; + + // Running accumulators start at the section-base contribution + // ($18 = $4A / $1B = $4D / $1E = $50). chunk5 LDAX/STAX copies + // these once at L7EC9. + int32_t accX = (int32_t)pipe->proj.baseX; + int32_t accY = (int32_t)pipe->proj.baseY; + int32_t accZ = (int32_t)pipe->proj.baseZ; + + // Auto-scale (L7F1A): shift dx, dz left while the high byte + // hasn't reached the $40 threshold, decrementing the zoom + // counter $2F each step. chunk5 also shifts the running + // accumulators along; since we've already split into 32-bit + // ints, we shift everything in lockstep. + // + // The break condition must reproduce chunk5's `adc #$40 bmi` + // exactly, where the addition is done in 8-bit and wraps. As + // signed int8, the resulting bit-7 is set when the input byte + // is in [0x40, 0xBF] (i.e. magnitude >= 64 either sign). Doing + // the addition in `int` masks the wrap, so we cast back. + #define HI_OVERFLOW(v32) ((int8_t)((((v32) >> 8) & 0xFF) + 0x40) < 0) + uint8_t zoom = pipe->proj.zoomShift; + while (zoom < 0xFF) { + if (HI_OVERFLOW(dx)) break; + if (HI_OVERFLOW(dz)) break; + if (HI_OVERFLOW(accX)) break; + if (HI_OVERFLOW(accY)) break; + if (HI_OVERFLOW(accZ)) break; + dx <<= 1; + dz <<= 1; + accX <<= 1; + accY <<= 1; + accZ <<= 1; + zoom++; + } + #undef HI_OVERFLOW + + // Apply the 2x3 rotation matrix. chunk5 issues six L1818 calls + // total (XZ deltas vs three matrix rows). Each multiply takes + // the high byte of the delta as int8 and the matrix entry as + // int8, returning int16. + int8_t hxFinal = (int8_t)((dx >> 8) & 0xFF); + int8_t hzFinal = (int8_t)((dz >> 8) & 0xFF); + + accX += (int32_t)l1818SignedMul(hxFinal, pipe->proj.matRow1[0]); + accX += (int32_t)l1818SignedMul(hzFinal, pipe->proj.matRow2[0]); + accY += (int32_t)l1818SignedMul(hxFinal, pipe->proj.matRow1[1]); + accY += (int32_t)l1818SignedMul(hzFinal, pipe->proj.matRow2[1]); + accZ += (int32_t)l1818SignedMul(hxFinal, pipe->proj.matRow1[2]); + accZ += (int32_t)l1818SignedMul(hzFinal, pipe->proj.matRow2[2]); + + // Saturate back to int16 -- chunk5's $18/$1B/$1E are 16-bit + // accumulators, so we mirror that. + if (accX > 0x7FFF) accX = 0x7FFF; + if (accX < -0x8000) accX = -0x8000; + if (accY > 0x7FFF) accY = 0x7FFF; + if (accY < -0x8000) accY = -0x8000; + if (accZ > 0x7FFF) accZ = 0x7FFF; + if (accZ < -0x8000) accZ = -0x8000; + + outSlot->x = (int16_t)accX; + outSlot->y = (int16_t)accY; + outSlot->z = (int16_t)accZ; + outSlot->outcode = sceneryClassifyVertex(outSlot); + + // Restore $2F for the caller. chunk5 saves/restores $2F across + // the auto-scale via $08EE; the calling opcode is responsible + // for the save side. + pipe->cur.accX = (int16_t)accX; + pipe->cur.accY = (int16_t)accY; + pipe->cur.accZ = (int16_t)accZ; +} + + +// ClassifyVertex2 (chunk5 line 2673). Same six half-space tests, same +// bit assignments. Compares 16-bit signed components. +uint8_t sceneryClassifyVertex(const SceneryVertexT *v) { + uint8_t code = 0; + if (v->z < 0) { + code |= SCENERY_OUTCODE_BEHIND; + } + if ((int32_t)v->x + (int32_t)v->z < 0) { + code |= SCENERY_OUTCODE_RIGHT; + } + if ((int32_t)v->z - (int32_t)v->x < 0) { + code |= SCENERY_OUTCODE_LEFT; + } + if ((int32_t)v->y + (int32_t)v->z < 0) { + code |= SCENERY_OUTCODE_BOTTOM; + } + if ((int32_t)v->z - (int32_t)v->y < 0) { + code |= SCENERY_OUTCODE_TOP; + } + return code; +} + + +// ProjectV2ToScreen (chunk5 line 3759). Performs the perspective +// divide for X then Y, biasing into the 280x96 viewport. The original +// returns column in A and row in Y; we collapse to the screen X/Y +// pair the renderer expects. +// +// chunk5's PerspectiveDivide returns a signed 7-bit-fraction value; +// we scale to native pixel coordinates by mapping $7F to the viewport +// half-width. +bool sceneryProjectVertexToScreen(const SceneryVertexT *v, int16_t *outX, int16_t *outY) { + if (v->z <= 0) { + return false; + } + // chunk5 ProjectVertex: table-based persp + biases. + // screen X (color cols 0..139) = $46 + qx_byte + // screen Y (rows 0..98) = $31 - qy_byte + // Multiply X by 2 to convert chunk5 color cols -> port native px. + int16_t qx = perspectiveDivideX(v->x, v->z); + int16_t qy = perspectiveDivide(v->y, v->z); + int32_t sxColor = 0x46 + (int32_t)(int8_t)qx; + int32_t sx = sxColor * 2; + int32_t sy = 0x31 - (int32_t)(int8_t)qy; + + if (sx < INT16_MIN) sx = INT16_MIN; + if (sx > INT16_MAX) sx = INT16_MAX; + if (sy < INT16_MIN) sy = INT16_MIN; + if (sy > INT16_MAX) sy = INT16_MAX; + + *outX = (int16_t)sx; + *outY = (int16_t)sy; + return true; +} + + +// EmitPrimaryVertex (chunk5 L6919). Append `slot` to the pool, AND +// its outcode into the polygon accumulator $D3. Caps at 60 entries +// to match chunk5's `cpy #$3C / bcs L6843` guard. +void sceneryEmitPrimary(SceneryPipelineT *pipe, const SceneryVertexT *slot) { + if (pipe->cur.poolCount >= SCENERY_VERTEX_POOL_CAP) { + return; + } + pipe->pool[pipe->cur.poolCount++] = *slot; + pipe->cur.polygonOutcode &= slot->outcode; +} + + +// ============================================================ +// 4-pass Sutherland-Hodgman 3D frustum clipper. +// +// Mirrors chunk5's PolygonScanFillSetup -> PolygonClipTopPass -> +// PolygonClipRightPass -> PolygonClipBottomPass cascade at +// src/chunk5.s:2884+. Operates on camera-space (post-TransformVertex, +// pre-PerspectiveDivide) XYZ vertices. +// +// Per-plane test: a vertex is "inside" the plane if the half-space +// equation is non-negative. The four planes are: +// Left: Z - X >= 0 +// Top: Z - Y >= 0 +// Right: X + Z >= 0 +// Bottom: Y + Z >= 0 +// +// Intersection of edge V0 -> V1 with a plane uses similar triangles: +// solve for fraction `t` along the edge where the plane equation +// crosses zero, then linearly interpolate all components. +// +// Chunk5's `ClipVertex2ToLeft/Top/Right/Bottom` does this with +// integer math + overflow recovery (HalveBothVertices on V-flag). +// We use int32 intermediates here -- chunk5's halving was a 6502 +// space-saving for the multiply; on modern CPUs we have the bits. + +typedef int (*PlaneFn)(const SceneryVertexT *v); + +static int planeLeft (const SceneryVertexT *v) { return v->z - v->x; } +static int planeTop (const SceneryVertexT *v) { return v->z - v->y; } +static int planeRight (const SceneryVertexT *v) { return v->z + v->x; } +static int planeBottom (const SceneryVertexT *v) { return v->z + v->y; } + + +// Compute the intersection vertex along edge V0 -> V1 where the +// half-space equation transitions sign. `pe0` and `pe1` are the +// plane-equation evaluations at V0 and V1 (one positive, one +// negative or zero). The interpolation fraction is pe0/(pe0-pe1). +static SceneryVertexT clipIntersect(const SceneryVertexT *v0, + const SceneryVertexT *v1, + int pe0, int pe1) { + SceneryVertexT out; + // t = pe0 / (pe0 - pe1). Scale by 4096 for fixed-point divide + // to avoid floats while keeping enough precision for 280-pixel- + // wide projection. pe0 - pe1 is non-zero because the signs + // differ (or one is zero) -- handled by the caller (we won't + // get here unless they straddle the plane). + int denom = pe0 - pe1; + if (denom == 0) denom = 1; // defensive; shouldn't happen + // Scaled t in Q12. Clamp to [0, 4096] for safety in case of + // accumulated arithmetic error. + int t = (int)(((int64_t)pe0 * 4096) / denom); + if (t < 0) t = 0; + if (t > 4096) t = 4096; + out.x = (int16_t)(v0->x + (((int32_t)(v1->x - v0->x) * t) >> 12)); + out.y = (int16_t)(v0->y + (((int32_t)(v1->y - v0->y) * t) >> 12)); + out.z = (int16_t)(v0->z + (((int32_t)(v1->z - v0->z) * t) >> 12)); + // Post-clip outcode is 0 (= on the plane, inside) on the + // dimensions that mattered for this pass. The caller's next + // pass will re-classify if needed. + out.outcode = 0; + return out; +} + + +// One Sutherland-Hodgman pass: read `n` vertices from `in`, emit +// clipped vertices into `out`, return new vertex count. +static int clipPass(SceneryVertexT *out, const SceneryVertexT *in, int n, int cap, PlaneFn planeFn) { + if (n == 0) return 0; + int outN = 0; + const SceneryVertexT *prev = &in[n - 1]; + int prevPE = planeFn(prev); + for (int i = 0; i < n; i++) { + const SceneryVertexT *cur = &in[i]; + int curPE = planeFn(cur); + bool prevIn = (prevPE >= 0); + bool curIn = (curPE >= 0); + if (curIn) { + if (!prevIn && outN < cap) { + out[outN++] = clipIntersect(prev, cur, prevPE, curPE); + } + if (outN < cap) { + out[outN++] = *cur; + } + } else { + if (prevIn && outN < cap) { + out[outN++] = clipIntersect(prev, cur, prevPE, curPE); + } + } + prev = cur; + prevPE = curPE; + } + return outN; +} + + +int sceneryClipPolygon3D(SceneryVertexT *in, SceneryVertexT *out, int inCount, int cap, bool *outIsIn) { + if (inCount < 3) { + if (outIsIn) *outIsIn = true; + return inCount; + } + int n = clipPass(out, in, inCount, cap, planeLeft); + if (n < 3) { if (outIsIn) *outIsIn = false; return 0; } + n = clipPass(in, out, n, cap, planeTop); + if (n < 3) { if (outIsIn) *outIsIn = true; return 0; } + n = clipPass(out, in, n, cap, planeRight); + if (n < 3) { if (outIsIn) *outIsIn = false; return 0; } + n = clipPass(in, out, n, cap, planeBottom); + if (outIsIn) *outIsIn = true; + return n; +} diff --git a/port/src/sceneryVm.c b/port/src/sceneryVm.c new file mode 100644 index 0000000..1e9fbdc --- /dev/null +++ b/port/src/sceneryVm.c @@ -0,0 +1,2837 @@ +// Scenery interpreter VM. +// +// Each scenery record begins with an opcode byte. Opcodes 0x46+ or +// any opcode with bit 7 set terminate the stream (matches the +// original FS2 dispatcher's `bmi/bcc` test). Recognised opcodes +// dispatch into the helpers below. +// +// The advance counts come from the chunk5 SceneryOpcodeTable +// dispatcher (line 961 of src/chunk5.s) and were validated by the +// extractstations.c walker. Opcodes mapped to `SceneryOpInvalid` in +// chunk5 ($08, $0C, $0F, $10, $15-17, $1F, $26-27, $2A, $2C-2E, $30, +// $34, $36-3F, $43-45) terminate the stream rather than guess an +// advance, since chunk5's dispatcher resets state and bails on those. + +#include +#include +#include +#include +#include "camera.h" +#include "chunk5Setup.h" +#include "chunk5Transform.h" +#include "cpu6502.h" +#include "hires.h" +#include "renderer.h" +#include "sceneryProjection.h" +#include "sceneryVm.h" +#include "palette.h" + + +// Temporary diagnostic counters surfaced via SCENERY_STATS env var. +static int sceneryOpsTotal; +static int sceneryVertexEmits; +static int sceneryProjectedDraws; +static int sceneryVtxBehind; +static int sceneryVtxOutcodeAnd; +static int sceneryStatsEnabled = -1; + + +static int sceneryStatsOn(void) { + if (sceneryStatsEnabled < 0) { + sceneryStatsEnabled = (getenv("SCENERY_STATS") != NULL) ? 1 : 0; + } + return sceneryStatsEnabled; +} + + +static int sceneryOpTraceEnabled = -1; +static int sceneryOpTraceOn(void) { + if (sceneryOpTraceEnabled < 0) { + sceneryOpTraceEnabled = (getenv("SCENERY_OP_TRACE") != NULL) ? 1 : 0; + } + return sceneryOpTraceEnabled; +} + + +// FS2 scenery opcodes. Names match the chunk5 disassembly comments. +typedef enum SceneryOpE { + SCENERY_OP_VERTEX_DRAW = 0x00, // L69C8 emit-vertex-and-draw (line via L7963) + SCENERY_OP_VERTEX_LO = 0x01, // L6888 emit-line-segment (no draw) + SCENERY_OP_VERTEX_LO2 = 0x02, // L689F emit-vertex-2 (draws via L6AE3) + SCENERY_OP_CALL_64K_2 = 0x03, // SceneryOpCall64K_2 (advance 6) + SCENERY_OP_L6DAF = 0x04, // L6DAF + SCENERY_OP_ADF_STATION = 0x05, // 9-byte ADF station record + SCENERY_OP_DRAW_LINE = 0x06, + SCENERY_OP_DRAW_LINE3D = 0x07, // L6BA0 (14 bytes) + SCENERY_OP_SKIP3_A = 0x09, // 3-byte filler + SCENERY_OP_SKIP3_B = 0x0A, + SCENERY_OP_REL_JUMP = 0x0B, + SCENERY_OP_HEADER = 0x0D, // 6-byte scenery header + SCENERY_OP_CALL_64K = 0x0E, // 64K: 3-byte jump-absolute via chunk3 callback; 48K: 1-byte rts (pops recursion) + SCENERY_OP_SKIP_1 = 0x11, + SCENERY_OP_SET_COLOR = 0x12, + SCENERY_OP_L6DED = 0x13, // L6E17-driven (10 bytes) + SCENERY_OP_L6DF3 = 0x14, // L6E17-driven (10 bytes) + SCENERY_OP_SUB_INVOKE = 0x18, + SCENERY_OP_RETURN = 0x19, + SCENERY_OP_L6D66 = 0x1A, // 5-byte memory copy + SCENERY_OP_MODE_WHITE = 0x1B, // patch line kernel for white draw + SCENERY_OP_DAY_ONLY = 0x1C, // skip if night + SCENERY_OP_NAV_STATION = 0x1D, // 11-byte NAV station record + SCENERY_OP_COM_RECORD = 0x1E, // variable-length COM/airport + SCENERY_OP_CULL_1 = 0x20, // 9-byte 1-axis cull + SCENERY_OP_CULL_2 = 0x21, // 15-byte 2-axis cull + SCENERY_OP_CULL_3 = 0x22, // 21-byte 3-axis cull + SCENERY_OP_JUMP_BITS = 0x23, // SceneryOpJumpIfBitsClear (7 bytes) + SCENERY_OP_PUSH_ORIGIN = 0x24, // SceneryOpPushOriginWithStash (8 bytes) + SCENERY_OP_L6D8E = 0x25, // L6D8E (5 bytes) + SCENERY_OP_L6F1D = 0x28, // L6F1D (8 bytes, comparison) + SCENERY_OP_COPY_TO_D2 = 0x29, // saved-state copy + SCENERY_OP_L69E6 = 0x2B, // L69E6 (5 bytes) + SCENERY_OP_RESET_STATE = 0x2F, + SCENERY_OP_REFRESH_LO = 0x31, // SceneryOpRefreshCachedXform80C5 (6 bytes) + SCENERY_OP_VERTEX_PRIM = 0x32, // L68BA emit-primary-vertex + SCENERY_OP_VERTEX_END = 0x33, // L68E9 emit-vertex-and-close + SCENERY_OP_VERTEX_2D = 0x35, // L69A7 2D-vertex + SCENERY_OP_VERTEX_HI = 0x40, // L688F (FS2 polygon vertex variant) + SCENERY_OP_VERTEX_HI2 = 0x41, // L68A6 + SCENERY_OP_VERTEX_HI3 = 0x42, // L694E + // Port-only extension. Real chunk5 maps $46+ to "stream end" + // via the `bmi/bcs` boundary check; the demo fixture uses $46 + // as an explicit end sentinel. + SCENERY_OP_END = 0x46 +} SceneryOpE; + + +// Hook context for interpreter-routed ops. Defined here so doCopyToD2 +// (which fires the polygon-fill chain via the 6502 interpreter) can +// reference the hook functions and ctx struct below. +typedef struct EmitCurveHookCtxT EmitCurveHookCtxT; +struct EmitCurveHookCtxT { + SceneryStateT *state; + int drawsEmitted; +}; +static bool emitCurveDrawColorLineHook(Cpu6502T *cpu, void *userData); +static bool drawColorSpanHook(Cpu6502T *cpu, void *userData); + +static void advance(SceneryStateT *state, uint16_t bytes); +static void doAdfRecord(SceneryStateT *state); +static void doCall64K(SceneryStateT *state); +static void doCall64KRotated(SceneryStateT *state); +static void doComRecord(SceneryStateT *state); +static void doCopyToD2(SceneryStateT *state); +static void doCullByOutcodeList(SceneryStateT *state); +static void doCullN(SceneryStateT *state, int axes); +static void doDayOnly(SceneryStateT *state); +static void doDrawLine(SceneryStateT *state); +static void doDrawProjectedSegment(SceneryStateT *state, const SceneryVertexT *a, const SceneryVertexT *b); +typedef enum EmitV1ModeE EmitV1ModeE; +static void doEmitV1(SceneryStateT *state, EmitV1ModeE mode, bool xformA); +static void doEmitV2(SceneryStateT *state, bool drawSegment, bool xformA); +static void doEnterLocalFrame(SceneryStateT *state); +static void doHeader(SceneryStateT *state); +static void doJumpIfBeyondXY(SceneryStateT *state, bool checkZ); +static void doJumpIfBitsClear(SceneryStateT *state); +static void doJumpIfWordCompare(SceneryStateT *state); +static void doModeWhite(SceneryStateT *state); +static void doNavRecord(SceneryStateT *state); +static void doPushOriginWithStash(SceneryStateT *state); +static void doRefreshCachedXform(SceneryStateT *state, bool xformA); +static void doRelJump(SceneryStateT *state); +static void doResetState(SceneryStateT *state); +static void doSetColor(SceneryStateT *state); +static void doSkip1(SceneryStateT *state); +static void doStoreImmWord(SceneryStateT *state); +static void doSubInvoke(SceneryStateT *state); +static void doVertexCachedV1(SceneryStateT *state); +static void doVertexCachedV2(SceneryStateT *state); +static void doVertexCachedDraw(SceneryStateT *state); +static void doWriteWord(SceneryStateT *state); +static void sceneryComputeBaseL631D(SceneryStateT *state); +static int16_t readWordSigned(const uint8_t *p); +static int32_t signed24(uint8_t lo, uint8_t mid, uint8_t hi); +static bool streamExhausted(const SceneryStateT *state); + + +static void advance(SceneryStateT *state, uint16_t bytes) { + state->cursor += bytes; + if (state->cursor > state->streamEnd) { + state->cursor = state->streamEnd; + } +} + + +static int32_t signed24(uint8_t lo, uint8_t mid, uint8_t hi) { + uint32_t u = (uint32_t)lo | ((uint32_t)mid << 8) | ((uint32_t)hi << 16); + if (u & 0x800000) { + u |= 0xFF000000; + } + return (int32_t)u; +} + + +// Opcode $05: ADF station record. Layout: +// $05, freqLo, freqHi, x*3, y*3 (9 bytes total). +// freqLo/freqHi are the BCD pair + high digit per chunk3 +// LookupADFStation. Position is signed-24 little-endian. +static void doAdfRecord(SceneryStateT *state) { + if (state->cursor + 9 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->stationCb != NULL) { + SceneryStationT s; + s.type = SCENERY_STATION_ADF; + s.freq = (uint16_t)(state->cursor[1] | ((uint16_t)state->cursor[2] << 8)); + s.x = signed24(state->cursor[3], state->cursor[4], state->cursor[5]); + s.y = signed24(state->cursor[6], state->cursor[7], state->cursor[8]); + s.z = 0; + s.name = NULL; + state->stationCb(state, &s); + } + advance(state, 9); +} + + +// Opcode $1D: NAV (VOR) station record. Layout: +// $1D, freqLo, freqHi, x*3, y*3, z*2 (11 bytes total). +// freq is BCD-packed per DecodeBCDFreqString. Position is x/y in +// signed-24 LE; the trailing 2 bytes are a signed-16 z (altitude). +static void doNavRecord(SceneryStateT *state) { + if (state->cursor + 11 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->stationCb != NULL) { + SceneryStationT s; + s.type = SCENERY_STATION_NAV; + s.freq = (uint16_t)(state->cursor[1] | ((uint16_t)state->cursor[2] << 8)); + s.x = signed24(state->cursor[3], state->cursor[4], state->cursor[5]); + s.y = signed24(state->cursor[6], state->cursor[7], state->cursor[8]); + s.z = (int32_t)(int16_t)((uint16_t)state->cursor[9] + | ((uint16_t)state->cursor[10] << 8)); + s.name = NULL; + state->stationCb(state, &s); + } + advance(state, 11); +} + + +// Opcode $1E: COM/airport record. Layout: +// $1E, recordLen, comFreqLo, comFreqHi, x*3, y*3, z*3, name string... +// Total advance is `recordLen + 2` (opcode + length byte). The name +// runs from offset 13 to recordLen+2. recordLen <= 11 means no name. +static void doComRecord(SceneryStateT *state) { + if (state->cursor + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + uint8_t recordLen = state->cursor[1]; + if (state->cursor + recordLen + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->stationCb != NULL) { + static char nameBuf[16]; + SceneryStationT s; + s.type = SCENERY_STATION_COM; + s.freq = (uint16_t)(state->cursor[2] | ((uint16_t)state->cursor[3] << 8)); + s.x = signed24(state->cursor[4], state->cursor[5], state->cursor[6]); + s.y = signed24(state->cursor[7], state->cursor[8], state->cursor[9]); + s.z = 0; + int nameLen = recordLen - 11; + if (nameLen > 0 && nameLen < (int)sizeof(nameBuf) - 1) { + for (int i = 0; i < nameLen; i++) { + uint8_t c = state->cursor[13 + i]; + nameBuf[i] = (c >= 32 && c < 127) ? (char)c : '?'; + } + nameBuf[nameLen] = '\0'; + s.name = nameBuf; + } else { + s.name = NULL; + } + state->stationCb(state, &s); + } + // chunk5 SceneryOpCOMRecord advances by `recordLen` (NOT + // recordLen + 2): cursor[1] is the total byte count starting + // from the opcode, so $8B += recordLen lands directly on the + // next opcode. See chunk5 line 147-150 (`pla; clc; adc $8B; + // sta $8B`). + advance(state, recordLen); +} + + +static void doDrawLine(SceneryStateT *state) { + // record: opcode, x1, y1, x2, y2 (signed bytes, viewport- + // centred "colour pixel" offsets). + if (state->cursor + 5 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + int8_t sx1 = (int8_t)state->cursor[1]; + int8_t sy1 = (int8_t)state->cursor[2]; + int8_t sx2 = (int8_t)state->cursor[3]; + int8_t sy2 = (int8_t)state->cursor[4]; + + // Signed offsets are biased into the viewport. X is doubled + // because the original treats column 0..139 as one "colour + // pixel" each, mapped to the 280-pixel hires line. + const int16_t cx = NATIVE_WIDTH / 2; + const int16_t cy = VIEWPORT_BOTTOM / 2; + int16_t x1 = (int16_t)(cx + sx1 * 2); + int16_t y1 = (int16_t)(cy + sy1); + int16_t x2 = (int16_t)(cx + sx2 * 2); + int16_t y2 = (int16_t)(cy + sy2); + + rendererDrawLine(state->renderer, x1, y1, x2, y2); + advance(state, 5); +} + + +// $0D HEADER: 6-byte record (opcode + 5 data bytes 1..5). Mirrors +// chunk5 SceneryOpHeader + LA63A: copies bytes 1..5 to a virtual +// $08E5..$08E9 header slot, relocates $08E7/$08E8 by the cursor unless +// byte 5 bit 7 is set, then triggers a demand-load that copies +// count*256 bytes from the .SD scenery file (sector $08E5, count +// $08E6) to the relocated dest at $08E7/$08E8. +// +// $08E9 has a per-section cache index (low 2 bits): chunk5's LA63A +// only re-fires the load when the cached section ID at $08EA[idx] +// differs from $08E5. We mirror that to avoid per-frame redundant +// copies. +static void doHeader(SceneryStateT *state) { + if (state->cursor + 6 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 6); + return; + } + ptrdiff_t cursorOff = state->cursor - state->stream; + if (cursorOff < 0 || cursorOff + 6 > (ptrdiff_t)(state->streamEnd - state->stream)) { + advance(state, 6); + return; + } + + uint8_t *ram = state->writableRam; + // Copy header bytes 1..5 into $08E5..$08E9. + ram[0x08E5] = state->cursor[1]; + ram[0x08E6] = state->cursor[2]; + ram[0x08E7] = state->cursor[3]; + ram[0x08E8] = state->cursor[4]; + ram[0x08E9] = state->cursor[5]; + // Relocate $08E7/$08E8 by the cursor unless byte 5 bit 7 set. + if ((state->cursor[5] & 0x80) == 0) { + uint16_t off = (uint16_t)state->cursor[3] + | ((uint16_t)state->cursor[4] << 8); + uint16_t base = (uint16_t)cursorOff; + uint16_t reloc = (uint16_t)(off + base); + ram[0x08E7] = (uint8_t)(reloc & 0xFF); + ram[0x08E8] = (uint8_t)(reloc >> 8); + } + // chunk5 masks low 2 bits back into $08E9 before the cache. + ram[0x08E9] &= 0x03; + + uint8_t cacheIdx = ram[0x08E9] & 0x03; + uint8_t sectionId = ram[0x08E5]; + if (ram[0x08EA + cacheIdx] == sectionId) { + // Same section already cached; skip the load. + advance(state, 6); + return; + } + ram[0x08EA + cacheIdx] = sectionId; + // chunk5 LA64C clears higher cache slots after a miss. + for (int i = cacheIdx + 1; i < 4; i++) { + ram[0x08EA + i] = 0; + } + + // SCENERY_DEMAND_LOAD env var gates the .SD demand-load. Set + // to "0" to load from byte 0 of the section (the natural + // chunk4 ASM behaviour); set to a digit (e.g. "14") to apply a + // source-byte SKIP, which is what MAME's captured RAM appears + // to have done (RAM[$A887] matches sid $44 byte 14, not byte 0). + // The skip lets us experimentally find which entry point makes + // the chunk5 VM walk into the water-color SetColor at $B781. + // Default behaviour: don't demand-load (keep captured-RAM + // residue, which is the only known-working path so far). + const char *envDemandLoad = getenv("SCENERY_DEMAND_LOAD"); + if (state->sceneryFile != NULL && state->sceneryFileSize > 0 + && envDemandLoad != NULL) { + int srcSkip = atoi(envDemandLoad); + if (srcSkip < 0 || srcSkip > 64) srcSkip = 0; + uint8_t count = ram[0x08E6]; + uint16_t dest = (uint16_t)ram[0x08E7] + | ((uint16_t)ram[0x08E8] << 8); + // ASM-faithful sectionId -> file offset: + // chunk4.s::ComputeBlockFromSector sets + // L1E0B = ((sid >> 2) + 1) * 2 (ProDOS block idx) + // L1E0C = sid & 3 (sub-block sector) + // FetchSectorFromDisk shifts further to produce the + // disk block# passed to ReadBlocks; offline this + // collapses to: + // file_offset(sid) = ((sid >> 2) + 1) * 4096 + // + (sid & 3) * 256 + // Verified: sid $2C -> 49152 matches captured dispatcher. + uint32_t srcOff = ((((uint32_t)sectionId) >> 2) + 1) * 4096u + + ((uint32_t)sectionId & 3u) * 256u + + (uint32_t)srcSkip; + uint32_t length = (uint32_t)count * 256u; + if (srcOff < state->sceneryFileSize) { + if (srcOff + length > state->sceneryFileSize) { + length = state->sceneryFileSize - srcOff; + } + // Skip the copy if the source range is entirely + // zero. The .SD is the concatenation of file + // blocks 0..N; .blocks[k]==0 marks file block k + // as unused, and those bytes in .SD are zero. + // Without this guard, doHeader would clobber the + // destination ($A84E+ in the SD3 dispatcher) + // with all-zero bytes that the interpreter then + // treats as $00 vertex_emit ops with zero-delta + // streams -- producing the all-vertices-collapse + // symptom for sectionIds that don't have real + // data on disk. + bool allZero = true; + for (uint32_t i = 0; i < length; i++) { + if (state->sceneryFile[srcOff + i] != 0) { + allZero = false; + break; + } + } + if (!allZero) { + for (uint32_t i = 0; i < length; i++) { + uint16_t addr = (uint16_t)(dest + i); + if (addr >= 0xC000) { + break; + } + ram[addr] = state->sceneryFile[srcOff + i]; + } + } + } + } + + advance(state, 6); +} + + +// chunk5 L631D: per-section base computation. Reads three 16-bit +// signed values from $7E/$7F, $80/$81, $82/$83 (row 2 of the rotation +// matrix written by sceneryAttachCamera) and a 16-bit scale factor +// from $68/$69, then stores 24-bit results. +// +// CRITICAL byte layout: chunk5's L631D code stores the 24-bit result +// out of order. After `jsr L6363` the 24-bit product is at +// $C7..$C9 (LO, MID, HI in that order), and L631D copies: +// +// sta $4C ; $4C = $C7 = LO byte +// LDAX $C8 ; A=$C8 (MID), X=$C9 (HI) +// STAX $4A ; $4A = MID, $4B = HI +// +// So the resulting layout is: $4A = MID, $4B = HI, $4C = LO. +// Downstream reads of $4A/$4B as a 16-bit LE value therefore get the +// UPPER 16 bits of the 24-bit signed product (i.e. product >> 8). +// +// The factor of 2 comes from chunk5 L6363's `asl $C7 / rol $C8 / rol +// $C9` after MultiplyAXByC2. +static void sceneryComputeBaseL631D(SceneryStateT *state) { + if (state->writableRam == NULL) { + return; + } + uint8_t *ram = state->writableRam; + int32_t scale = (int32_t)((int16_t)((uint16_t)ram[0x68] | ((uint16_t)ram[0x69] << 8))); + // MAME-patched L631D at $6352 negates the scale first, then + // signed-multiplies via $1569 (Q1.15 signed multiplier), and + // stores the 16-bit result. Effective formula: + // base[i] = -((matrix[i] * scale) / 32768) signed + // + // The result is stored DIRECTLY into the section-base accumulator + // at $2A..$2F (the 16-bit pairs $2A/$2B, $2C/$2D, $2E/$2F). + // + // An earlier port version also wrote $4A..$52 from this formula, + // but that's a different semantic: $4A..$52 is the per-vertex + // transform accumulator that the transform body at $7F60+ + // writes during each vertex multiply. MAME's L631D leaves + // $4A..$52 alone -- writing to it from L631D creates ghost + // values that intermediate handlers (e.g. the next $40 emit's + // LDA $4A path) read as if they were valid base data, producing + // wrong V1/V2 camera-space coords. The transform's own + // prologue at $7F03 copies $2A..$2F into $18..$1D where the + // per-vertex multiply uses them. So writing only to $2A..$2F + // matches MAME byte-for-byte and avoids cross-contamination. + struct { + uint16_t srcAddr; + uint16_t loAddr; + uint16_t hiAddr; + } slots[3] = { + { 0x007E, 0x002A, 0x002B }, // row 2 X -> base X at $2A/$2B + { 0x0080, 0x002C, 0x002D }, // row 2 Y -> base Y at $2C/$2D + { 0x0082, 0x002E, 0x002F }, // row 2 Z -> base Z at $2E/$2F + }; + int16_t base16[3]; + for (int i = 0; i < 3; i++) { + int16_t v = (int16_t)((uint16_t)ram[slots[i].srcAddr] + | ((uint16_t)ram[slots[i].srcAddr + 1] << 8)); + int32_t product32 = (int32_t)v * (int32_t)scale; + int16_t b16 = (int16_t)(-(product32 >> 15)); + base16[i] = b16; + uint16_t u = (uint16_t)b16; + ram[slots[i].loAddr] = (uint8_t)( u & 0xFF); + ram[slots[i].hiAddr] = (uint8_t)((u >> 8) & 0xFF); + } + // Mirror the new base into the int8 projection pipeline so + // sceneryProjectXZ's accumulator starts from the section base. + sceneryPipelineSetBase(&state->pipeline, base16[0], base16[1], base16[2]); + if (getenv("CHUNK5_TRACE") != NULL) { + int16_t row7E = (int16_t)((uint16_t)ram[0x7E] | ((uint16_t)ram[0x7F] << 8)); + int16_t row80 = (int16_t)((uint16_t)ram[0x80] | ((uint16_t)ram[0x81] << 8)); + int16_t row82 = (int16_t)((uint16_t)ram[0x82] | ((uint16_t)ram[0x83] << 8)); + fprintf(stderr, + " L631D: scale=%d ($68/$69) row2=(%d,%d,%d) -> base=(%d,%d,%d)\n", + (int)scale, row7E, row80, row82, + base16[0], base16[1], base16[2]); + } +} + + +// $07 SceneryOpEnterLocalFrame: 14-byte record. Mirrors chunk5 +// SceneryOpEnterLocalFrame -> L6BB0: +// 1. Subtract 6 stream-encoded 16-bit anchor coords from the +// aircraft scenery position ($5A/$5C/$5E/$60/$62/$64). +// 2. Variant byte at cursor[1] picks an axis-permutation: +// 0 -> L6CCE: ROL-shift hi-bytes into $66/$68/$6A (chunk2) +// 2 -> L6C6E: swap lo/hi bytes between scratch ($18/$1B/$1E) +// and result ($66/$68/$6A) +// 4 -> L6C89: ASL each scratch hi byte 4 times into $66/$68/$6A +// 5 (default fall-through) -> L6C53: copy scratch verbatim +// 3. L631D recomputes the section base ($4A/$4D/$50) from the new +// $7E/$80/$82 row-2 coords scaled by $68/$69. +// +// 6502 SBC: A - M - (1 - C). Sets carry on no-borrow. +static uint8_t scenerySbc8(uint8_t a, uint8_t m, uint8_t *carry) { + uint16_t r = (uint16_t)a + (uint16_t)((uint8_t)~m) + (uint16_t)(*carry & 1); + *carry = (r > 0xFFu) ? 1 : 0; + return (uint8_t)(r & 0xFFu); +} + + +// Run the 4x (asl A; rol lo; rol hi) cascade chunk5 uses in L6CCE +// and L6C89. The accumulator value `aIn` (= chunk5's A on entry) is +// shifted left while its MSB feeds into the low-byte's bit 0; the +// low-byte's MSB then propagates into the high-byte. Net effect on +// the 16-bit value: shifted left by 4, with the source byte's high +// nibble entering the low nibble of the new 16-bit value. +static void sceneryShiftCascade4(uint8_t *lo, uint8_t *hi, uint8_t aIn) { + uint8_t a = aIn; + for (int i = 0; i < 4; i++) { + uint8_t aslCarry = (uint8_t)((a & 0x80u) ? 1u : 0u); + a = (uint8_t)((uint16_t)a << 1); + uint8_t loCarry = (uint8_t)((*lo & 0x80u) ? 1u : 0u); + *lo = (uint8_t)(((uint16_t)*lo << 1) | aslCarry); + *hi = (uint8_t)(((uint16_t)*hi << 1) | loCarry); + } +} + + +// chunk5 L6BB0+L6CCE/L6C6E/L6C89/L6C53/L6D28: $07 and $24 share this +// frame setup. Reads anchors from the stream, subtracts them from +// aircraft zero-page slots ($5A/$5C/$5E/$60/$62/$64), writes scratch +// into $18/$19, $66/$67, $1B/$1C, $68/$69, $1E/$1F, $6A/$6B, then +// dispatches on the variant byte to compute the final $66-$6B +// values. Stash mode (= $24, AD set) skips the $5A/$5E/$62 reads so +// only 6 stream bytes get consumed; non-stash (= $07) reads all 12. +static void doFrameSetup(SceneryStateT *state, bool stashed, uint16_t recLen) { + if (state->cursor + recLen > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, recLen); + return; + } + uint8_t *ram = state->writableRam; + uint8_t variantByte = state->cursor[1]; + // chunk5: tax (= X = variant). Then dex; dex on entry to the + // dispatch. We treat variantByte as unsigned 8-bit and apply + // the same dec/dec to get the dispatch index. + int16_t vidx = (int16_t)variantByte - 2; // dex; dex + // SBC chain over the stream; chunk5 SECs at the very top of + // L6BB0 then again before the L6BFE/L6C1E reads, but the carry + // is already preserved through the no-skip cases. + uint8_t carry = 1; // SEC at L6BB0 + uint16_t y = 1; // Y after the variant byte read. + + if (!stashed) { + // MAME L6BB0 ($6BBF): lda $5A; iny; sbc ($8B),y; sta $18. + y++; ram[0x18] = scenerySbc8(ram[0x5A], state->cursor[y], &carry); + y++; ram[0x19] = scenerySbc8(ram[0x5B], state->cursor[y], &carry); + } + // lda $5C; iny; sbc ($8B),y; sta $66. + y++; ram[0x66] = scenerySbc8(ram[0x5C], state->cursor[y], &carry); + y++; ram[0x67] = scenerySbc8(ram[0x5D], state->cursor[y], &carry); + + carry = 1; // sec + // MAME-patched L6BB0 writes Y scratch to $1A/$1B (NOT $1B/$1C like + // source). The radar branch at $0836 may pull from $0AB2/$0AB3 + // instead of $5E/$5F. Non-radar non-stash path uses $5E/$5F. + bool radarMode = (ram[0x0836] != 0); + if (!stashed) { + if (radarMode) { + y++; ram[0x1A] = scenerySbc8(ram[0x0AB2], state->cursor[y], &carry); + y++; ram[0x1B] = scenerySbc8(ram[0x0AB3], state->cursor[y], &carry); + } else { + y++; ram[0x1A] = scenerySbc8(ram[0x5E], state->cursor[y], &carry); + y++; ram[0x1B] = scenerySbc8(ram[0x5F], state->cursor[y], &carry); + } + } + // MAME L6BB0 cam Y delta source: $0AB4/$0AB5 (radar) or $60/$61 + // (non-radar). lda; iny; sbc; sta $68/$69. + if (radarMode) { + y++; ram[0x68] = scenerySbc8(ram[0x0AB4], state->cursor[y], &carry); + y++; ram[0x69] = scenerySbc8(ram[0x0AB5], state->cursor[y], &carry); + } else { + y++; ram[0x68] = scenerySbc8(ram[0x60], state->cursor[y], &carry); + y++; ram[0x69] = scenerySbc8(ram[0x61], state->cursor[y], &carry); + } + + carry = 1; // sec + // MAME's Z scratch slot is $1C/$1D (NOT $1E/$1F like source). + if (!stashed) { + y++; ram[0x1C] = scenerySbc8(ram[0x62], state->cursor[y], &carry); + y++; ram[0x1D] = scenerySbc8(ram[0x63], state->cursor[y], &carry); + } + // MAME cam Z delta: lda $64; iny; sbc; sta $6A. + y++; ram[0x6A] = scenerySbc8(ram[0x64], state->cursor[y], &carry); + y++; ram[0x6B] = scenerySbc8(ram[0x65], state->cursor[y], &carry); + + // MAME L6BB0 variant dispatch ($6C50+): dex; dex; bmi (vidx<0); + // bne (vidx!=0); jmp L6CCE (vidx==0). MAME's slot layout for + // Y/Z scratch is $1A/$1B (Y) and $1C/$1D (Z) — NOT source's + // $1B/$1C and $1E/$1F. Cascades read $19/$1B/$1D for X/Y/Z. + if (vidx < 0) { + // L6C53 ($6C62): copy scratch into result. + ram[0x66] = ram[0x18]; ram[0x67] = ram[0x19]; + ram[0x68] = ram[0x1A]; ram[0x69] = ram[0x1B]; + ram[0x6A] = ram[0x1C]; ram[0x6B] = ram[0x1D]; + } else if (vidx == 0) { + // L6CCE ($6CDD): variant byte = 2. + // For each axis: hi = old lo; lo = scratch_hi ($19/$1B/$1D); + // then 4x (asl scratch_lo; rol lo; rol hi). + ram[0x67] = ram[0x66]; ram[0x66] = ram[0x19]; + sceneryShiftCascade4(&ram[0x66], &ram[0x67], ram[0x18]); + ram[0x69] = ram[0x68]; ram[0x68] = ram[0x1B]; + sceneryShiftCascade4(&ram[0x68], &ram[0x69], ram[0x1A]); + ram[0x6B] = ram[0x6A]; ram[0x6A] = ram[0x1D]; + sceneryShiftCascade4(&ram[0x6A], &ram[0x6B], ram[0x1C]); + } else if (vidx == 2) { + // L6C6E ($6C7D): variant byte = 4. + uint8_t tmpX = ram[0x66]; + ram[0x66] = ram[0x19]; ram[0x67] = tmpX; + uint8_t tmpY = ram[0x68]; + ram[0x68] = ram[0x1B]; ram[0x69] = tmpY; + uint8_t tmpZ = ram[0x6A]; + ram[0x6A] = ram[0x1D]; ram[0x6B] = tmpZ; + } else if (vidx == 4) { + // L6C89 ($6C98): variant byte = 6. 4x asl/rol cascade + // with A from scratch hi byte ($19/$1B/$1D). + sceneryShiftCascade4(&ram[0x66], &ram[0x67], ram[0x19]); + sceneryShiftCascade4(&ram[0x68], &ram[0x69], ram[0x1B]); + sceneryShiftCascade4(&ram[0x6A], &ram[0x6B], ram[0x1D]); + } + // else L6D28 fall-through (other variants): leave the freshly + // computed L6BB0 values in place. + + // MAME-patched L6D28 epilogue ($6D37): if variant byte >= 6, + // ADD 4 to $68/$69 (with carry into $69). This replaces source + // chunk5.s's L6D28 zero-fallback "inc $68 if both zero". MAME + // applies the +4 unconditionally for high variants. + if (variantByte >= 0x06) { + uint16_t scale16 = (uint16_t)ram[0x68] + (uint16_t)((uint16_t)ram[0x69] << 8); + scale16 = (uint16_t)(scale16 + 4); + ram[0x68] = (uint8_t)(scale16 & 0xFF); + ram[0x69] = (uint8_t)((scale16 >> 8) & 0xFF); + } + // MAME L631D cache check at $38/$39 (NOT $35/$36 like source). + // Fire L631D only when scale ($68/$69) changed from the prior + // cached value; otherwise the base accumulator at $2A..$2F is + // still valid. + bool scaleChanged = (ram[0x68] != ram[0x38]) || (ram[0x69] != ram[0x39]); + ram[0x38] = ram[0x68]; + if (scaleChanged) { + sceneryComputeBaseL631D(state); + } + ram[0x39] = ram[0x69]; + if (getenv("PORT_FRAME_DUMP") != NULL) { + fprintf(stderr, + " frame-setup: op=%s variant=$%02X cam=(%d,%d,%d) base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] L631D=%s\n", + stashed ? "$24" : "$07", variantByte, + (int16_t)((uint16_t)ram[0x66] | ((uint16_t)ram[0x67] << 8)), + (int16_t)((uint16_t)ram[0x68] | ((uint16_t)ram[0x69] << 8)), + (int16_t)((uint16_t)ram[0x6A] | ((uint16_t)ram[0x6B] << 8)), + ram[0x4A], ram[0x4B], ram[0x4C], + ram[0x4D], ram[0x4E], ram[0x4F], + ram[0x50], ram[0x51], ram[0x52], + scaleChanged ? "fired" : "cached"); + } + advance(state, recLen); +} + + +static void doEnterLocalFrame(SceneryStateT *state) { + doFrameSetup(state, false, 14); +} + + +static void doPushOriginWithStash(SceneryStateT *state) { + // MAME-patched chunk5 SceneryOpPushOriginWithStash (decoded from + // sceneryRam_FS2.1.bin at $6B73) pre-populates scratch ZP + // $18/$19, $1A/$1B (or $1B/$1C if radar flag $0836 != 0), + // $1C/$1D from aircraft position ($5A/$5B, $5E/$5F, $62/$63) + // BEFORE the L6BB0 SBC chain. The SLOT LAYOUT differs from + // source chunk5.s which uses $18/$19, $1B/$1C, $1E/$1F. MAME's + // patches compact the scratch slots by 1 each axis. The + // L6C89/L6CCE/L6C6E variant cascades read these via + // $19/$1B/$1D instead of source's $19/$1C/$1F. + if (state->writableRam != NULL) { + uint8_t *ram = state->writableRam; + ram[0x18] = ram[0x5A]; // X scratch + ram[0x19] = ram[0x5B]; + // MAME's PushOriginWithStash always writes $1C/$1D first + // ($1C = $62, $1D = $63 = Z scratch) THEN checks the + // radar flag at $0836 and writes the Y scratch to either + // $1A/$1B (non-radar) or $1A/$1B (radar from $0AB2/$0AB3, + // but it overwrites $1A/$1B in both branches). + ram[0x1C] = ram[0x62]; // Z scratch + ram[0x1D] = ram[0x63]; + if (ram[0x0836] == 0) { + ram[0x1A] = ram[0x5E]; // Y scratch (non-radar) + ram[0x1B] = ram[0x5F]; + } else { + ram[0x1A] = ram[0x0AB2]; + ram[0x1B] = ram[0x0AB3]; + } + } + doFrameSetup(state, true, 8); +} + + +// $23 SceneryOpJumpIfBitsClear: jumps when both mask bytes have no +// set bits in common with the bytes at the supplied pointer. Layout: +// $23, jumpLo, jumpHi, ptrLo, ptrHi, mask1, mask2 (7 bytes total). +// chunk5 logic: jump iff (mask2 & *(ptr+1) == 0) AND +// (mask1 & *(ptr+0) == 0). Otherwise advance 7. +static void doJumpIfBitsClear(SceneryStateT *state) { + if (state->cursor + 7 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 7); + return; + } + uint16_t ptr = (uint16_t)state->cursor[3] | ((uint16_t)state->cursor[4] << 8); + uint8_t mask1 = state->cursor[5]; + uint8_t mask2 = state->cursor[6]; + uint8_t v0 = state->writableRam[ptr]; + uint8_t v1 = state->writableRam[(uint16_t)(ptr + 1)]; + if ((mask2 & v1) == 0 && (mask1 & v0) == 0) { + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + state->cursor += jumpOff; + if (state->cursor < state->stream || state->cursor >= state->streamEnd) { + state->cursor = state->streamEnd; + } + return; + } + advance(state, 7); +} + + +// $28 SceneryOpJumpIfWordCompare. 8-byte record: +// $28, mode, jumpOffLo, jumpOffHi, ptr1Lo, ptr1Hi, ptr2Lo, ptr2Hi +// Mode selects the comparison test on the two 16-bit values *ptr1 vs +// *ptr2 (chunk5.s line 2247): +// 0 = equal +// 1 = signed less-than (ptr1 < ptr2) +// 2 = signed less-than (alternate path, ptr1 > ptr2 in the source's +// reversed form) +// On match, cursor += signed jumpOff; otherwise advance past the 8-byte +// record. +static void doJumpIfWordCompare(SceneryStateT *state) { + if (state->cursor + 8 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 8); + return; + } + uint8_t mode = state->cursor[1]; + int16_t jumpOff = readWordSigned(state->cursor + 2); + uint16_t ptr1 = (uint16_t)state->cursor[4] | ((uint16_t)state->cursor[5] << 8); + uint16_t ptr2 = (uint16_t)state->cursor[6] | ((uint16_t)state->cursor[7] << 8); + int16_t v1 = (int16_t)((uint16_t)state->writableRam[ptr1] + | ((uint16_t)state->writableRam[(uint16_t)(ptr1 + 1)] << 8)); + int16_t v2 = (int16_t)((uint16_t)state->writableRam[ptr2] + | ((uint16_t)state->writableRam[(uint16_t)(ptr2 + 1)] << 8)); + bool match = false; + if (mode == 0) { + match = (v1 == v2); + } else if (mode == 1) { + match = (v1 < v2); + } else if (mode == 2) { + match = (v1 > v2); + } + if (match) { + state->cursor += jumpOff; + if (state->cursor < state->stream || state->cursor >= state->streamEnd) { + state->cursor = state->streamEnd; + } + return; + } + advance(state, 8); +} + + +// $1A WriteWord: copy a 16-bit word from `*src` to `*dst`. Layout: +// $1A, dstLo, dstHi, srcLo, srcHi (5 bytes total). +static void doWriteWord(SceneryStateT *state) { + if (state->cursor + 5 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam != NULL) { + uint16_t dst = (uint16_t)state->cursor[1] | ((uint16_t)state->cursor[2] << 8); + uint16_t src = (uint16_t)state->cursor[3] | ((uint16_t)state->cursor[4] << 8); + state->writableRam[dst] = state->writableRam[src]; + state->writableRam[(uint16_t)(dst + 1)] = state->writableRam[(uint16_t)(src + 1)]; + } + advance(state, 5); +} + + +// $25 StoreImmWord: write a 16-bit immediate to `*dst`. Layout: +// $25, dstLo, dstHi, immLo, immHi (5 bytes total). Used heavily by +// the section-init bytecode to patch zero-page state ($0846/$0848 etc). +static void doStoreImmWord(SceneryStateT *state) { + if (state->cursor + 5 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam != NULL) { + uint16_t dst = (uint16_t)state->cursor[1] | ((uint16_t)state->cursor[2] << 8); + state->writableRam[dst] = state->cursor[3]; + state->writableRam[(uint16_t)(dst + 1)] = state->cursor[4]; + } + advance(state, 5); +} + + +// $1B SceneryOpModeWhite: clear day-only skip and set draw color to +// hires-white. Mirrors chunk5 (which restores the line-kernel paint +// opcodes; for our SDL renderer the moral equivalent is "draw normally +// in white"). +static void doModeWhite(SceneryStateT *state) { + state->dayOnlySkip = false; + if (state->renderer != NULL) { + rendererSetDrawColor(state->renderer, COLOR_WHITE); + } + advance(state, 1); +} + + +// $1C SceneryOpDayOnly: at night, mark subsequent draws as suppressed +// (chunk5 patches the hires line kernel with BPL skips so ground objects +// vanish in dark scenes). At day, no-op. ModeWhite ($1B) clears the +// flag. +static void doDayOnly(SceneryStateT *state) { + // Walk-all mode: never suppress draws based on time of day, so + // every reachable polygon is emitted regardless of dispatcher + // night/day state. + if (state->walkAllPaths) { + state->dayOnlySkip = false; + advance(state, 1); + return; + } + if (state->isNight) { + state->dayOnlySkip = true; + } + advance(state, 1); +} + + +// $04 SceneryOpCullByOutcodeList: walk a list of cached-vertex indices +// terminated by a high-bit byte. For each listed vertex, look up its +// 8-byte cache slot at $0140 + idx*8: byte 7 carries a "behind camera" +// flag (high bit) and byte 6 carries the 6-bit outcode against the +// frustum half-spaces. chunk5 L6DAF: $DC starts at $F8; if any vertex +// has byte 7 high-bit clear (= in front of camera), $DC resets to 0 +// and the polygon is rendered. Otherwise byte 6 is ANDed into $DC; if +// the AND stays nonzero, every listed vertex shares at least one +// off-screen half-plane and we jump to the cull target. Layout: +// $04, jumpLo, jumpHi, [vIdx]+ , +// Opcode $29 (SceneryOpCopyToD2). chunk5 source: copies 9 bytes of +// saved state (= the polygon's $07.. shadow into $D2..) and either +// emits a clipped line (if $2C clear) OR triggers polygon FILL via +// L6F98 -> L7826 (if $2C set). For the boot-Meigs water polygon the +// $2C-set path fires: with $0876=$22 (=> ToHiresColorTable[2]= +// HIRES_VIOLET), the polygon edge rasterizer paints VIOLET pixels +// at the polygon's row range, producing the Lake Michigan water +// visible at boot. +// +// Our port doesn't yet maintain the $D3 polygon-outcode accumulator +// or the $2C saved-state flag. We approximate by always firing fill +// when polyCount >= 3. The polygon vertex buffer is populated by +// each $40/$41 vertex emit's projected screen coordinates. +static void doCopyToD2(SceneryStateT *state) { + // chunk5 polygon fill (L7826) reads color from $0876 via + // MapColorAndPrepRowRoutine (chunk5.s:3895): code = $0876 & $0F, + // hires = ToHiresColorTable[code]. For boot Meigs, $0876 = $22 + // -> code = 2 -> HIRES_VIOLET (= "water color (day)" per source + // comment at chunk5.s:3823). The boot value is preset by the + // chunk1 loader; we seed it in main.c's runScreenshot. + uint8_t fillHires = state->renderer != NULL ? state->renderer->hiresColor : 0; + if (state->writableRam != NULL) { + uint8_t code = state->writableRam[0x0876] & 0x0F; + fillHires = kSceneryToHires[code]; + } + // chunk5 source SceneryOpCopyToD2 (chunk5.s:1407+): branches on + // the polygon-mode flag at $2C. When set, runs the 4-pass + // clipper + scan-fill on the accumulated PrimVerts (= polygon + // mode). When clear, copies 9 bytes from V1-shadow ($07..) to + // V2 ($D4..) and emits a single line via EmitClippedLine + // (= wireframe mode). Our port skips the line-emit path + // (because $40/$41 already drew the lines as wireframes during + // emit) and just clears state. + // + // Polygon-mode flag is $29 (MAME-patched chunk5 location). + // Source chunk5 used $2C, but $2C is reused in the MAME- + // patched binary as the MID byte of the section base Y + // accumulator (see sceneryComputeBaseL631D), so reading it + // here would alias polygon-mode to a non-zero baseY byte + // and falsely trigger the fill path. + bool polygonMode = (state->writableRam != NULL) && (state->writableRam[0x29] != 0); + if (sceneryOpTraceOn()) { + fprintf(stderr, " $29 CopyToD2: polyCount=%d $0876=$%02X fillHires=%d polyMode=%d\n", + state->polyCount, + state->writableRam != NULL ? state->writableRam[0x0876] : 0, + fillHires, polygonMode); + } + if (state->renderer != NULL && polygonMode) { + // Byte-faithful path: populate MAME-patched chunk5's + // PrimVert* arrays from polyV3D and run the actual + // $29 handler at $67DD through the 6502 interpreter. + // The handler runs PolygonScanFillSetup -> 4-pass + // clipper -> ProjectVertices -> BuildEdgeList -> per- + // row DrawColorSpan calls; we hook DrawColorSpan + // ($78E0) and forward each scanline to port's + // rendererDrawColorSpan, then simulate RTS so MAME's + // hires bytes (which port doesn't use) aren't + // actually written. + int interpEmitted = 0; + if (state->writableRam != NULL && state->polyV3DCount >= 3) { + uint8_t *ram = state->writableRam; + int count = state->polyV3DCount; + if (count > 60) count = 60; // chunk5 caps at $3C + // chunk5 $D3 = running AND of each vertex's Z + // HIGH byte (not the 6-plane outcode). The + // polygon-fill kernel BMIs on $D3: bit 7 set + // means every vertex's Z hi is negative + // (= polygon entirely behind cam after auto- + // scale shift), cull. + uint8_t zHiAnd = 0xFF; + for (int i = 0; i < count; i++) { + int16_t x = state->polyV3D[i].x; + int16_t y = state->polyV3D[i].y; + int16_t z = state->polyV3D[i].z; + ram[0x0AF9 + i] = (uint8_t)( x & 0xFF); + ram[0x0B39 + i] = (uint8_t)((x >> 8) & 0xFF); + ram[0x0B79 + i] = (uint8_t)( y & 0xFF); + ram[0x0BB9 + i] = (uint8_t)((y >> 8) & 0xFF); + ram[0x0BF9 + i] = (uint8_t)( z & 0xFF); + uint8_t zHi = (uint8_t)((z >> 8) & 0xFF); + ram[0x0C39 + i] = zHi; + zHiAnd &= zHi; + } + ram[0xB5] = (uint8_t)count; // pool count + ram[0xD3] = zHiAnd; // ZHi AND for cull + // Zero PrimVert slots beyond our count so the + // 4-pass clipper's edge-build pass doesn't + // see stale RAM-dump bytes as legitimate + // vertex data. + for (int i = count; i < 64; i++) { + ram[0x0AF9 + i] = 0; + ram[0x0B39 + i] = 0; + ram[0x0B79 + i] = 0; + ram[0x0BB9 + i] = 0; + ram[0x0BF9 + i] = 0; + ram[0x0C39 + i] = 0; + } + ram[0x25] = (uint8_t)(count - 1); // max edge index + uint8_t outcodeAnd = zHiAnd; // for diagnostic print + ram[0x29] = 0xFF; // polygon mode armed + // Snapshot+restore $8B/$8C (= dispatcher cursor) + // so the interpreter doesn't disturb port's + // stream-walk position. + uint8_t save8B = ram[0x8B]; + uint8_t save8C = ram[0x8C]; + + Cpu6502T cpu; + cpu6502Init(&cpu, ram); + cpu.s = 0xFD; + EmitCurveHookCtxT ctx = { state, 0 }; + Cpu6502HookT hooks[] = { + { 0x795A, emitCurveDrawColorLineHook, &ctx }, + { 0x78E0, drawColorSpanHook, &ctx }, + }; + if (getenv("PORT_FILL_TRACE") != NULL) { + static int traceCount; + extern void fillTraceFn(Cpu6502T *cpu, void *userData); + cpu6502SetTrace(&cpu, fillTraceFn, &traceCount); + traceCount = 0; + } + // $67FF is the $29 (CopyToD2 / fill) handler entry: + // $67FF: LDA $D3; BMI $680A; DEC $B5; BMI $680A + // $6807: JSR $6F67 (PolygonScanFillSetup) + // $680A: LDA #$00; STA $29; LDA #$01; JMP $6812 + // ($67DD is the $2F = SceneryOpResetState entry which + // ARMS polygon mode but doesn't run the fill.) + bool ok = cpu6502RunWithHooks(&cpu, 0x67FF, 0x680A, + hooks, 2, 5000000); + interpEmitted = ctx.drawsEmitted; + if (sceneryOpTraceOn()) { + fprintf(stderr, " $29 fill via interpreter: ok=%d spans=%d count=%d $D3=$%02X\n", + ok ? 1 : 0, ctx.drawsEmitted, count, outcodeAnd); + } + ram[0x8B] = save8B; + ram[0x8C] = save8C; + } + // Always run C clipper alongside the interpreter for + // now. The interpreter's 18-span output for boot Meigs + // covers only the polygon's perimeter (= row 53 + edges + // 54..61 + row 99 horizontals); the C clipper fills the + // interior rows the interpreter misses. Once the MAME + // span logger lands (task #16) we'll have a reference + // to see whether the interpreter is meant to be the + // primary path or whether the chunk5 fill is also a + // perimeter-only operation in MAME. + (void)interpEmitted; + // MAME's $78E0 logger (tmp/mame_spans_frozen.lua) reports + // ZERO DrawColorSpan hits across 2000 boot Meigs frames. + // FS2 at boot Meigs renders entirely via DrawColorLine + // (= wireframes) and does not call the polygon fill + // kernel for the water/ground polygons. So PORT_NO_POLY_FILL + // matches MAME's actual behaviour; the C clipper path is + // kept as a fallback for the world.c fixture but the + // default for the live game is to leave fills off. + bool useClipper = (state->polyV3DCount >= 3) + && (getenv("PORT_NO_POLY_FILL") == NULL) + && (getenv("PORT_LEGACY_POLY_FILL") == NULL); + int16_t cxs[64]; + int16_t cys[64]; + int filledCount = 0; + if (useClipper) { + bool resultInIn = false; + int clipped = sceneryClipPolygon3D(state->polyV3D, + state->polyV3DOut, + state->polyV3DCount, + 64, + &resultInIn); + const SceneryVertexT *clippedVerts = resultInIn + ? state->polyV3D : state->polyV3DOut; + for (int i = 0; i < clipped && filledCount < 64; i++) { + int16_t sx, sy; + if (!sceneryProjectVertexToScreen(&clippedVerts[i], &sx, &sy)) { + // Behind camera after clip -- + // shouldn't normally happen but + // guard against rounding. + continue; + } + cxs[filledCount] = (int16_t)(sx / 2); + cys[filledCount] = sy; + filledCount++; + } + } else if (state->polyCount >= 3) { + // Legacy 2D-only path -- still useful for the + // world.c fixture and when PORT_LEGACY_POLY_FILL + // is forced. + for (int i = 0; i < state->polyCount; i++) { + cxs[i] = (int16_t)(state->polyXs[i] / 2); + cys[i] = state->polyYs[i]; + } + filledCount = state->polyCount; + } + if (getenv("PORT_POLY_DUMP") != NULL) { + fprintf(stderr, " $29 fill verts (clipped=%d, src3D=%d, src2D=%d):", + filledCount, state->polyV3DCount, state->polyCount); + for (int i = 0; i < filledCount; i++) { + fprintf(stderr, " (%d,%d)", cxs[i] * 2, cys[i]); + } + fprintf(stderr, "\n $29 src3D vertices:"); + for (int i = 0; i < state->polyV3DCount; i++) { + fprintf(stderr, " (%d,%d,%d)", + state->polyV3D[i].x, + state->polyV3D[i].y, + state->polyV3D[i].z); + } + fprintf(stderr, "\n"); + } + if (filledCount >= 3) { + uint8_t savedColor = state->renderer->hiresColor; + state->renderer->hiresColor = fillHires; + rendererFillPolygon(state->renderer, + cxs, + cys, + filledCount); + state->renderer->hiresColor = savedColor; + } + } + state->polyCount = 0; + state->polyV3DCount = 0; + // chunk5 source `Op29PolygonFillBranch_ClearAndAdvance`: + // lda #$00; sta $2C + // Clears polygon-mode flag at end of $29 so subsequent $40/$41 + // emits resume drawing lines (= wireframe mode). The source + // uses $2C; a previous version of this comment claimed the + // 64K patch table re-targeted it to $29, but Apply64KPatchTable + // doesn't touch this site. Whatever $29 mirroring the port does + // is to match scenery-driven self-modification observed in the + // captured MAME RAM, not a one-time boot patch. + if (state->writableRam != NULL) { + state->writableRam[0x29] = 0x00; + // $2C is the section-base Y MID byte in MAME-patched + // chunk5 (sceneryComputeBaseL631D writes it); do NOT + // clear it here or the next vertex transform loses the + // baseY component. + } + advance(state, 1); +} + + +static void doCullByOutcodeList(SceneryStateT *state) { + if (state->cursor + 4 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + uint16_t off = 3; // skip opcode + 2-byte jumpOff + uint8_t acc = 0xF8; // chunk5 initial mask (bits 7..3) + bool anyOnScreen = false; + // Walk vertex-index bytes until terminator (bit 7 set). + while (state->cursor + off < state->streamEnd) { + uint8_t b = state->cursor[off]; + off++; + if (b & 0x80) { + break; + } + if (state->writableRam == NULL || b >= SCENERY_CACHED_POOL_CAP) { + // Without cache data, can't cull -- treat as + // on-screen so we fall through and render. + anyOnScreen = true; + continue; + } + uint16_t slot = SCENERY_CACHED_POOL_BASE + ((uint16_t)b * 8); + const uint8_t *cv = state->writableRam + slot; + if ((cv[7] & 0x80) == 0) { + // Vertex in front of camera -> resets $DC. + anyOnScreen = true; + acc = 0; + } else { + acc &= cv[6]; + } + } + // Walk-all mode: take BOTH branches. + if (state->walkAllPaths) { + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + const uint8_t *fallthroughCursor = state->cursor + off; + const uint8_t *jumpCursor = state->cursor + jumpOff; + state->cursor = fallthroughCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = jumpCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = state->streamEnd; + return; + } + // Cull when accumulator stays nonzero AND no vertex was + // on-screen. Otherwise fall through to draw the polygon. + if (!anyOnScreen && acc != 0) { + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + const uint8_t *jumpCursor = state->cursor + jumpOff; + if (jumpCursor >= state->stream && jumpCursor < state->streamEnd) { + state->cursor = jumpCursor; + return; + } + } + advance(state, off); +} + + +// $13 SceneryOpJumpIfBeyondXY / $14 JumpIfBeyondXYZ: jump past a record +// if the camera is too far from a reference XY (or XYZ) point. Layout: +// $13/$14, jumpLo, jumpHi, refXLo, refXHi, dxLo, dxHi, dyLo, dyHi +// ($14 also reads dzLo, dzHi but advance is the same -- chunk5 +// consumes 9 bytes for both, see L6E17 in chunk5.s). +// +// The check: |cam.x - refX| > dx OR |cam.y - refY| > dy => jump. For +// $14, additionally |cam.z - refZ| > dz. We approximate by reading the +// camera scenery position from $5C/$5D and $64/$65 (already mirrored +// in writableRam by sceneryAttachCamera). +static void doJumpIfBeyondXY(SceneryStateT *state, bool checkZ) { + (void)checkZ; + if (state->cursor + 9 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + // Walk-all mode: take BOTH branches via recursion. The + // visited[] array stops infinite loops; callers terminate when + // we leave state->cursor at streamEnd. + if (state->walkAllPaths) { + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + const uint8_t *fallthroughCursor = state->cursor + 9; + const uint8_t *jumpCursor = state->cursor + jumpOff; + state->cursor = fallthroughCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = jumpCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 9); + return; + } + uint8_t *ram = state->writableRam; + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + // chunk5 L6E17 reads bound at cursor+3..+4 ($98/$99), then for + // each axis reads (refLo, refHi) at +5..+6, +7..+8 and tests + // | refX - cam_X | > bound. The order is Y first ($64/$65), + // then X ($5C/$5D). + int16_t bound = (int16_t)((uint16_t)state->cursor[3] + | ((uint16_t)state->cursor[4] << 8)); + int16_t camY = (int16_t)((uint16_t)ram[0x64] | ((uint16_t)ram[0x65] << 8)); + int16_t camX = (int16_t)((uint16_t)ram[0x5C] | ((uint16_t)ram[0x5D] << 8)); + int16_t refY = (int16_t)((uint16_t)state->cursor[5] + | ((uint16_t)state->cursor[6] << 8)); + int16_t refX = (int16_t)((uint16_t)state->cursor[7] + | ((uint16_t)state->cursor[8] << 8)); + int32_t dY = (int32_t)refY - camY; + if (dY < 0) dY = -dY; + int32_t dX = (int32_t)refX - camX; + if (dX < 0) dX = -dX; + bool beyond = (dY > bound) || (dX > bound); + if (beyond) { + state->cursor += jumpOff; + if (state->cursor < state->stream || state->cursor >= state->streamEnd) { + state->cursor = state->streamEnd; + } + } else { + advance(state, 9); + } +} + + +// $31/$42 SceneryOpRefreshCachedXform80C5/7EBC: transform a stream +// vertex packet via the matching transform into V2 ($D4..$D9), +// classify against the frustum (outcode at byte 6), and write 8 +// bytes to the cache slot at $0140 + idx*8 so $04 can later AND its +// outcode bits. chunk5 snapshots $D2..$DA around the call so vertex +// 2 state isn't perturbed; the cache write copies $D4..$DB. +// chunk5 sets $DB := $FF before the transform so the high bit is +// always set, marking "behind-camera classification valid" for $04's +// bmi check. +// +// Stream layout differs by opcode: +// $42 (xform-B, TransformVertex7EBC): opcode + idx + 4 vertex +// bytes (X lo/hi, Z lo/hi) = 6 bytes total. +// $31 (xform-A, TransformVertex80C5): opcode + idx + 6 vertex +// bytes (X lo/hi, Y lo/hi, Z lo/hi) = 8 bytes total. +static void doRefreshCachedXform(SceneryStateT *state, bool xformA) { + uint16_t recLen = (uint16_t)(xformA ? 8 : 6); + if (state->cursor + recLen > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, recLen); + return; + } + uint8_t *ram = state->writableRam; + uint8_t idx = state->cursor[1]; + if (idx >= SCENERY_CACHED_POOL_CAP) { + advance(state, recLen); + return; + } + // Snapshot $D3..$DB (vertex 2 state) so we can restore it. + uint8_t snap[9]; + for (int i = 0; i < 9; i++) { + snap[i] = ram[0xD3 + i]; + } + ram[0xDB] = 0xFF; + // The transform reads its vertex packet starting at stream + 1 + // (i.e. cursor+2 once we pass cursor+1 here). The idx byte sits + // at cursor+1; passing cursor+1 puts the packet at the + // expected offset for both transforms. + if (xformA) { + chunk5TransformVertex80C5(ram, state->cursor + 1, 0xD4); + } else { + chunk5TransformVertex7EBC(ram, state->cursor + 1, 0xD4); + } + // Classify V2 to compute outcode bits. + SceneryVertexT v2; + v2.x = (int16_t)((uint16_t)ram[0xD4] | ((uint16_t)ram[0xD5] << 8)); + v2.y = (int16_t)((uint16_t)ram[0xD6] | ((uint16_t)ram[0xD7] << 8)); + v2.z = (int16_t)((uint16_t)ram[0xD8] | ((uint16_t)ram[0xD9] << 8)); + uint8_t outcode = sceneryClassifyVertex(&v2); + ram[0xDA] = outcode; + // Cache write: 8 bytes from $D4..$DB into slot. + uint16_t addr = SCENERY_CACHED_POOL_BASE + ((uint16_t)idx * 8); + for (int i = 0; i < 8; i++) { + ram[addr + i] = ram[0xD4 + i]; + } + // Restore $D3..$DB. + for (int i = 0; i < 9; i++) { + ram[0xD3 + i] = snap[i]; + } + advance(state, recLen); +} + + +// Read a 1-byte vertex index from the stream and load the 8-byte +// cached vertex from the pool at $0140 + index*8. Returns the byte +// pointer or NULL if the cache slot is out of range. +static const uint8_t *sceneryFetchCachedVertex(SceneryStateT *state, uint8_t idx) { + if (state->writableRam == NULL) { + return NULL; + } + if (idx >= SCENERY_CACHED_POOL_CAP) { + return NULL; + } + uint16_t addr = SCENERY_CACHED_POOL_BASE + ((uint16_t)idx * 8); + return state->writableRam + addr; +} + + +// $32 SceneryOpVertexCachedV1: fetch cached vertex by index and load +// into v1 ($CB..$D2 in FS2 zero-page; we mirror into pipeline.cur.v1). +// Layout: $32, idx (2 bytes total). +static void doVertexCachedV1(SceneryStateT *state) { + if (state->cursor + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + const uint8_t *cv = sceneryFetchCachedVertex(state, state->cursor[1]); + if (cv != NULL) { + state->pipeline.cur.v1.x = (int16_t)((uint16_t)cv[0] | ((uint16_t)cv[1] << 8)); + state->pipeline.cur.v1.y = (int16_t)((uint16_t)cv[2] | ((uint16_t)cv[3] << 8)); + state->pipeline.cur.v1.z = (int16_t)((uint16_t)cv[4] | ((uint16_t)cv[5] << 8)); + // chunk5 L68C7: outcode comes from cv[6] only when + // cv[7]'s high bit is set (= "behind camera / + // off-screen flag valid"); otherwise the vertex is + // on-screen and its outcode is 0. + state->pipeline.cur.v1.outcode = (cv[7] & 0x80) ? cv[6] : 0; + sceneryProjectVertexToScreen(&state->pipeline.cur.v1, + &state->pipeline.cur.v1ScreenX, + &state->pipeline.cur.v1ScreenY); + } + advance(state, 2); +} + + +// $33 SceneryOpVertexCachedV2: same as $32 but for v2 and tail-jumps to +// the polygon emit path (we just draw v1->v2 segment when both are in +// front of the camera). +static void doVertexCachedV2(SceneryStateT *state) { + if (state->cursor + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + const uint8_t *cv = sceneryFetchCachedVertex(state, state->cursor[1]); + if (cv != NULL) { + state->pipeline.cur.v2.x = (int16_t)((uint16_t)cv[0] | ((uint16_t)cv[1] << 8)); + state->pipeline.cur.v2.y = (int16_t)((uint16_t)cv[2] | ((uint16_t)cv[3] << 8)); + state->pipeline.cur.v2.z = (int16_t)((uint16_t)cv[4] | ((uint16_t)cv[5] << 8)); + // Same outcode logic as $32 (see doVertexCachedV1). + state->pipeline.cur.v2.outcode = (cv[7] & 0x80) ? cv[6] : 0; + sceneryProjectVertexToScreen(&state->pipeline.cur.v2, + &state->pipeline.cur.v2ScreenX, + &state->pipeline.cur.v2ScreenY); + // chunk5 SceneryOpVertexCachedV2 (L68FB) tail-jumps into + // EmitClippedLine which draws V1 -> V2 (using whatever + // V1 currently holds -- typically set by a preceding $32). + // V1 is at $CB..$D2; V2 at $D4..$DB. + const SceneryVertexT *v1 = &state->pipeline.cur.v1; + const SceneryVertexT *v2 = &state->pipeline.cur.v2; + if (!state->dayOnlySkip + && state->renderer != NULL + && v1->z > 0 + && v2->z > 0 + && (v1->outcode & v2->outcode) == 0) { + doDrawProjectedSegment(state, v1, v2); + } + // chunk5 EmitClippedLine cleanup at L6B2F: copy V2 back + // over V1 so the next emit chains. Do that here too. + state->pipeline.cur.v1 = state->pipeline.cur.v2; + } + advance(state, 2); +} + + +// $35 SceneryOpVertexCachedDraw: load cached vertex into v1 and plot a +// single point if it's on-screen. Layout: $35, idx (2 bytes). +static void doVertexCachedDraw(SceneryStateT *state) { + if (state->cursor + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + const uint8_t *cv = sceneryFetchCachedVertex(state, state->cursor[1]); + if (cv != NULL) { + state->pipeline.cur.v1.x = (int16_t)((uint16_t)cv[0] | ((uint16_t)cv[1] << 8)); + state->pipeline.cur.v1.y = (int16_t)((uint16_t)cv[2] | ((uint16_t)cv[3] << 8)); + state->pipeline.cur.v1.z = (int16_t)((uint16_t)cv[4] | ((uint16_t)cv[5] << 8)); + state->pipeline.cur.v1.outcode = (cv[7] & 0x80) ? cv[6] : 0; + int16_t sx; + int16_t sy; + if (!state->dayOnlySkip + && state->pipeline.cur.v1.outcode == 0 + && sceneryProjectVertexToScreen(&state->pipeline.cur.v1, &sx, &sy) + && state->renderer != NULL) { + // No single-pixel API; emit a 1-pixel line. + rendererDrawLine(state->renderer, sx, sy, sx, sy); + } + } + advance(state, 2); +} + + +// $20 / $21 / $22 — CULL_1 / CULL_2 / CULL_3. Each record: +// opcode, jumpLo, jumpHi, [ptrLo, ptrHi, lowLo, lowHi, highLo, highHi] x N +// Reads N axis ranges (1, 2 or 3). For each axis: looks up a 16-bit +// signed value at zero-page (ptr lives in writableRam) and tests +// `low <= value <= high`. If any axis is OUT of range, jump to +// (cursor + jumpOff). If all in range, advance past the record and +// continue with the next opcode. +// +// Mirrors chunk5 SceneryOpCullIfOutside1/2/3 + TestSceneryRange. The +// dispatcher at $A800 uses CULL_2 to route the camera to one of three +// section bodies (or the fallback at $A848). +static void doCullN(SceneryStateT *state, int axes) { + uint16_t recLen = (uint16_t)(3 + axes * 6); // 9, 15, 21 + if (state->cursor + recLen > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + // Walk-all mode: take BOTH branches. + if (state->walkAllPaths) { + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + const uint8_t *fallthroughCursor = state->cursor + recLen; + const uint8_t *jumpCursor = state->cursor + jumpOff; + state->cursor = fallthroughCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = jumpCursor; + if (state->cursor >= state->stream && state->cursor < state->streamEnd) { + sceneryRun(state); + } + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + // No RAM to read aircraft state from -- fall back to + // the always-pass behaviour. + advance(state, recLen); + return; + } + int16_t jumpOff = (int16_t)((uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8)); + + bool inside = true; + for (int axis = 0; axis < axes; axis++) { + const uint8_t *p = state->cursor + 3 + axis * 6; + uint16_t ptrAddr = (uint16_t)p[0] | ((uint16_t)p[1] << 8); + int16_t low = (int16_t)((uint16_t)p[2] | ((uint16_t)p[3] << 8)); + int16_t high = (int16_t)((uint16_t)p[4] | ((uint16_t)p[5] << 8)); + int16_t value = (int16_t)((uint16_t)state->writableRam[ptrAddr] + | ((uint16_t)state->writableRam[(uint16_t)(ptrAddr + 1)] << 8)); + if (value < low || value > high) { + inside = false; + break; + } + } + if (inside) { + advance(state, recLen); + } else { + state->cursor += jumpOff; + if (state->cursor < state->stream || state->cursor >= state->streamEnd) { + state->cursor = state->streamEnd; + } + } +} + + +static void doRelJump(SceneryStateT *state) { + if (state->cursor + 3 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + int16_t offset = readWordSigned(state->cursor + 1); + state->cursor += offset; + if (state->cursor < state->stream || state->cursor >= state->streamEnd) { + state->cursor = state->streamEnd; + } +} + + +// $0E SceneryOpCall64K is mode-dependent (verified vs. chunk5.s:1341): +// +// 48K mode: `lda Has64K / beq SceneryOpReturn` -- $0E falls into a +// bare `rts` which pops one level of dispatcher recursion. +// The opcode is 1 byte in the stream; the cursor is NOT +// advanced past it. The rts returns to whoever called +// `ProcessScenery` (top-level loop) or whichever +// `$18 SubInvoke` recursed into us. +// +// 64K mode: `jmp SceneryOp64KCallback` (chunk3.s:3129) -- the chunk3 +// callback reads a 2-byte target from cursor[1..2] into +// ($A5/$A6) and tail-jumps to `SceneryJumpToFetched` +// (chunk5.s:2448) which sets `$8B/$8C` = target and re-enters +// the dispatcher. So in 64K mode $0E is effectively a +// 3-byte "jump absolute". +// +// Reference RAM image (port/sceneryRam_FS2.1.bin) comes from 64K-mode +// MAME so we implement the 64K semantics: read the 2-byte target, set +// the cursor if reachable, else fall back to the 48K "return from +// dispatcher" behaviour. +static void doCall64K(SceneryStateT *state) { + if (state->cursor + 3 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam != NULL) { + uint16_t target = (uint16_t)state->cursor[1] + | ((uint16_t)state->cursor[2] << 8); + ptrdiff_t targetOff = (ptrdiff_t)target; + if (targetOff >= 0 && targetOff < (ptrdiff_t)(state->streamEnd - state->stream)) { + state->cursor = state->stream + targetOff; + return; + } + } + // 48K-fallback path: rts pops dispatcher recursion. Match by + // signalling exitDispatch so the surrounding sceneryRun loop + // returns to the caller without advancing past the opcode -- + // the cursor stays on $0E so a re-entry behaves the same. + state->exitDispatch = true; +} + + +// $03 SceneryOpCall64K_2: 64K-mode SceneryRotatedTransform (chunk3.s:2662). +// Reads: +// cursor[1] = rotation angle byte +// cursor[2..3] = X displacement (signed 16-bit LE) +// cursor[4..5] = Y displacement (signed 16-bit LE) +// Builds a 2D rotation matrix at chunk3 RAM LF244..LF25F (= the +// placeholder bytes inside the $F240 template), then runs the scenery +// interpreter on $F240 to emit a quad + 3 sides using the matrix. +// chunk3 must be loaded into writableRam at $D300-$F3FF for this to work. +static void doCall64KRotated(SceneryStateT *state) { + if (state->cursor + 6 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 6); + return; + } + uint8_t *ram = state->writableRam; + // Sanity: chunk3 template marker at $F240 should be $2F (ResetState). + // If chunk3 isn't loaded the byte will be garbage; bail to keep the + // stream walk aligned. + if (ram[0xF240] != 0x2F) { + advance(state, 6); + return; + } + + // 1. angle byte = -(stream_angle + ram[$0849] + ram[$0847]) + $41 + // chunk3.s:2663-2672 + uint8_t streamAngle = state->cursor[1]; + uint8_t angle = (uint8_t)(streamAngle + ram[0x0849] + ram[0x0847]); + angle = (uint8_t)(~angle); // eor #$FF + angle = (uint8_t)(angle + 0x41); // adc #$41 (carry was clear) + // 2. sin = chunk5L1763(angle, 0); -cos = chunk5L1768(angle, 0) + // chunk5L1763 returns Q1.15 sin; chunk5L1768 returns Q1.15 of "sin shifted -64" + // = sin(angle - 90 deg) = -cos(angle). + int16_t sinV = chunk5L177B((uint8_t)(angle), 0); // L1763 == cos via L177B from byte + int16_t cosV = chunk5L1778((uint8_t)(angle), 0); // L1768 == sin (shifted) -> -cos + // chunk3 stores sinV in $A9/$AA, cosV in $BE/$BF and $C4/$C5. + + // 3. Read X/Y disp from stream + int16_t xDisp = (int16_t)((uint16_t)state->cursor[2] | ((uint16_t)state->cursor[3] << 8)); + int16_t yDisp = (int16_t)((uint16_t)state->cursor[4] | ((uint16_t)state->cursor[5] << 8)); + + // 4. Compute the matrix entries via ScaleC2ByC4 (chunk5ScaleC2ByC4 is + // the port equivalent). The chunk3 code does six multiplies: + // $18 = X * cos ($3E/$3F * $C4/$C5 = xDisp * cosV) + // $1B = Y * sin (after Y is loaded, scaled by $A9/$AA = sinV) + // $1E = Y * cos ($9A/$9B * $BE/$BF = yDisp * cosV) + // $A9 = X * sin (re-uses $A9 slot; intermediate) + // LF244 = $18 + $A9 carry-extended (= xDisp*cos + xDisp*sin? + // actually combinations build the full transform). + // + // Direct port: + int16_t v18 = chunk5ScaleC2ByC4(xDisp, cosV); // chunk3.s:2690 ScaleC2ByC4 X*cos + int16_t v1E = chunk5ScaleC2ByC4(yDisp, cosV); // chunk3.s:2738 Y*cos + int16_t v1B = chunk5ScaleC2ByC4(xDisp, sinV); // chunk3.s:2712 X*sin (= replaces $1B) + int16_t a9b = chunk5ScaleC2ByC4(yDisp, sinV); // chunk3.s:2749 Y*sin (-> $A9/$AA) + + // chunk3 then computes: + // LF244 = $A9 + $18 (Y*sin + X*cos) -- adc carry chain + // LF24E = -LF244 (16-bit negation via eor+adc) + // LF247 = $1B + $1E (X*sin + Y*cos) + // LF251 = -LF247 + // LF242 = $1B - $1E (X*sin - Y*cos) + // LF24C = -LF242 + // LF249 = $18 - $A9 (X*cos - Y*sin) + // LF253 = -LF249 + // LF258 = $1B ;LF259 = $1B hi + // LF25A = $18 ;LF25B = $18 hi + // LF25C = -$1B ;LF25D = -$1B hi + // LF25E = -$18 ;LF25F = -$18 hi + int32_t s244 = (int32_t)(int16_t)(a9b + v18); // (Y*sin) + (X*cos) + int32_t s247 = (int32_t)v1B + (int32_t)v1E; // (X*sin) + (Y*cos) + int32_t s242 = (int32_t)v1B - (int32_t)v1E; // (X*sin) - (Y*cos) + int32_t s249 = (int32_t)v18 - (int32_t)a9b; // (X*cos) - (Y*sin) + int16_t r244 = (int16_t)s244; + int16_t r247 = (int16_t)s247; + int16_t r242 = (int16_t)s242; + int16_t r249 = (int16_t)s249; + + // Write the placeholders in the chunk3 template (which IS at $F242- + // $F254 inside the $F240 template). Each EmitV1/EmitV2 op has 4 byte + // placeholders: xLo, xHi, yLo, yHi. LF242/LF244/LF247/LF249 are the + // same memory as bytes 2..5 of the $40 + $41 ops at $F241/$F246/$F24B. + ram[0xF242] = (uint8_t)( r242 & 0xFF); + ram[0xF243] = (uint8_t)((r242 >> 8) & 0xFF); + ram[0xF244] = (uint8_t)( r244 & 0xFF); + ram[0xF245] = (uint8_t)((r244 >> 8) & 0xFF); + ram[0xF247] = (uint8_t)( r247 & 0xFF); + ram[0xF248] = (uint8_t)((r247 >> 8) & 0xFF); + ram[0xF249] = (uint8_t)( r249 & 0xFF); + ram[0xF24A] = (uint8_t)((r249 >> 8) & 0xFF); + // chunk3.s:2758-2796 also stores negated copies; we inline-negate. + ram[0xF24C] = (uint8_t)((-(int32_t)r242) & 0xFF); + ram[0xF24D] = (uint8_t)(((-(int32_t)r242) >> 8) & 0xFF); + ram[0xF24E] = (uint8_t)((-(int32_t)r244) & 0xFF); + ram[0xF24F] = (uint8_t)(((-(int32_t)r244) >> 8) & 0xFF); + ram[0xF251] = (uint8_t)((-(int32_t)r247) & 0xFF); + ram[0xF252] = (uint8_t)(((-(int32_t)r247) >> 8) & 0xFF); + ram[0xF253] = (uint8_t)((-(int32_t)r249) & 0xFF); + ram[0xF254] = (uint8_t)(((-(int32_t)r249) >> 8) & 0xFF); + ram[0xF258] = (uint8_t)( v1B & 0xFF); + ram[0xF259] = (uint8_t)((v1B >> 8) & 0xFF); + ram[0xF25A] = (uint8_t)( v18 & 0xFF); + ram[0xF25B] = (uint8_t)((v18 >> 8) & 0xFF); + ram[0xF25C] = (uint8_t)((-(int32_t)v1B) & 0xFF); + ram[0xF25D] = (uint8_t)(((-(int32_t)v1B) >> 8) & 0xFF); + ram[0xF25E] = (uint8_t)((-(int32_t)v18) & 0xFF); + ram[0xF25F] = (uint8_t)(((-(int32_t)v18) >> 8) & 0xFF); + + // 5. Save cursor; set cursor to ram + $F240; recurse interpreter. + // chunk3.s:2797-2809 also writes 0 to $0876 / $0877 to clear the + // in-progress flag; we mirror that. + ram[0x0876] = 0; + ram[0x0877] = 0; + const uint8_t *savedCursor = state->cursor; + const uint8_t *savedStream = state->stream; + const uint8_t *savedStreamEnd = state->streamEnd; + state->stream = ram; + state->streamEnd = ram + 0x10000; + state->cursor = ram + 0xF240; + sceneryRun(state); + state->cursor = savedCursor; + state->stream = savedStream; + state->streamEnd = savedStreamEnd; + advance(state, 6); +} + + +static void doSetColor(SceneryStateT *state) { + // record: opcode, colorCode (2 bytes total) + // + // chunk5 SceneryOpSetColor (chunk5.s:3834): reads the next byte, + // indexes ToHiresColorTable[16] (= chunk5.s:3825), and configures + // SetPixelDrawMode with one of HIRES_BLACK1/VIOLET/GREEN/WHITE1. + // We forward both: the modern-palette index for the legacy + // panel-area drawer, and the chunk5 hires code for the + // bit-faithful hires bitplane plot. + if (state->cursor + 2 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + uint8_t code = state->cursor[1]; + if (state->renderer != NULL) { + rendererSetDrawColor(state->renderer, paletteFromSceneryCode(code)); + rendererSetHiresColor(state->renderer, kSceneryToHires[code & 0x0F]); + } + advance(state, 2); +} + + +static void doSkip1(SceneryStateT *state) { + advance(state, 1); +} + + +static void doSubInvoke(SceneryStateT *state) { + // record: opcode, lo, hi (relative offset to subrecord stream) + if (state->cursor + 3 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + int16_t offset = readWordSigned(state->cursor + 1); + const uint8_t *resumePoint = state->cursor + 3; + const uint8_t *subEntry = state->cursor + offset; + + if (subEntry < state->stream || subEntry >= state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + + state->cursor = subEntry; + state->subDepth++; + sceneryRun(state); + state->subDepth--; + // Clear exitDispatch so the parent sceneryRun keeps iterating. + // The flag is set by $0E SceneryOpCall64K when running its 48K + // fallback path (= rts pops dispatcher recursion -- see + // chunk5.s:1341+). After we return to the parent, the parent's + // iteration should continue from resumePoint normally. + state->exitDispatch = false; + state->cursor = resumePoint; +} + + +static int16_t readWordSigned(const uint8_t *p) { + return (int16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); +} + + +// Vertex-1-emit modes: +// EMIT_V1_PLOT = $00 chunk5 SceneryOpEmitV1XformAndPlot. After +// projecting V1, plot a single pixel via L7963 +// (PlotColorPixel) when V1 is in-frustum. +// EMIT_V1_LINE = $02 chunk5 SceneryOpEmitV2Xform80C5 -- not used +// via this path (V2 emits do that). +// EMIT_V1_SILENT = $01 / $40 -- emit only, no draw. +typedef enum EmitV1ModeE { + EMIT_V1_SILENT = 0, + EMIT_V1_PLOT = 1, +} EmitV1ModeE; + + +static void doEmitV1(SceneryStateT *state, EmitV1ModeE mode, bool xformA) { + uint16_t recLen = (uint16_t)(xformA ? 7 : 5); + if (state->cursor + recLen > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + // chunk5 chooses transform by opcode group: $00/$01/$02 use + // TransformVertex80C5 (X/Y/Z stream, 16-bit ZPScale multiply, + // 7-byte record); $40/$41/$42 use TransformVertex7EBC (X/Z + // stream, 8-bit op_l1818 multiply, 5-byte record). The + // xform-A path is what gets buildings rendered with explicit + // per-vertex Y -- without it every vertex would inherit the + // section base Y and project onto the horizon. + if (state->writableRam != NULL) { + if (xformA) { + chunk5TransformVertex80C5(state->writableRam, state->cursor, 0xCB); + } else { + chunk5TransformVertex7EBC(state->writableRam, state->cursor, 0xCB); + } + state->pipeline.cur.v1.x = + (int16_t)((uint16_t)state->writableRam[0xCB] + | ((uint16_t)state->writableRam[0xCC] << 8)); + state->pipeline.cur.v1.y = + (int16_t)((uint16_t)state->writableRam[0xCD] + | ((uint16_t)state->writableRam[0xCE] << 8)); + state->pipeline.cur.v1.z = + (int16_t)((uint16_t)state->writableRam[0xCF] + | ((uint16_t)state->writableRam[0xD0] << 8)); + state->pipeline.cur.v1.outcode = sceneryClassifyVertex(&state->pipeline.cur.v1); + } else { + sceneryProjectStreamVertex(&state->pipeline, state->cursor + 1, &state->pipeline.cur.v1); + } + // PORT_VTX_DUMP: log V1 after transform so we can diff per-vertex + // against fs2trace --draws output (= the authentic FS2 6502 + // transform). Cursor offset is logged so identical fetch sites + // can be paired up. + if (getenv("PORT_VTX_DUMP") != NULL) { + fprintf(stderr, " POST-$%02X V1=(%6d,%6d,%6d) cur=$%04tX", + xformA ? 0x00 : 0x40, + state->pipeline.cur.v1.x, + state->pipeline.cur.v1.y, + state->pipeline.cur.v1.z, + state->cursor - state->stream); + if (state->writableRam != NULL) { + int16_t cX = (int16_t)((uint16_t)state->writableRam[0x66] + | ((uint16_t)state->writableRam[0x67] << 8)); + int16_t cY = (int16_t)((uint16_t)state->writableRam[0x68] + | ((uint16_t)state->writableRam[0x69] << 8)); + int16_t cZ = (int16_t)((uint16_t)state->writableRam[0x6A] + | ((uint16_t)state->writableRam[0x6B] << 8)); + int16_t b2A = (int16_t)((uint16_t)state->writableRam[0x2A] + | ((uint16_t)state->writableRam[0x2B] << 8)); + int16_t b2C = (int16_t)((uint16_t)state->writableRam[0x2C] + | ((uint16_t)state->writableRam[0x2D] << 8)); + int16_t b2E = (int16_t)((uint16_t)state->writableRam[0x2E] + | ((uint16_t)state->writableRam[0x2F] << 8)); + fprintf(stderr, " cam=(%d,%d,%d) base2A=(%d,%d,%d)", + cX, cY, cZ, b2A, b2C, b2E); + } + fprintf(stderr, "\n"); + } + sceneryProjectVertexToScreen(&state->pipeline.cur.v1, + &state->pipeline.cur.v1ScreenX, + &state->pipeline.cur.v1ScreenY); + if (mode == EMIT_V1_PLOT && !state->dayOnlySkip) { + // chunk5 $00 path: if in-frustum (`$CA == 0`), plot a + // single pixel at the projected screen X/Y. We use a + // 1x1 line as the renderer doesn't expose a separate + // pixel-plot. + if (state->pipeline.cur.v1.outcode == 0 + && state->pipeline.cur.v1.z > 0 + && state->renderer != NULL) { + sceneryProjectedDraws++; + rendererDrawLine(state->renderer, + state->pipeline.cur.v1ScreenX, + state->pipeline.cur.v1ScreenY, + state->pipeline.cur.v1ScreenX, + state->pipeline.cur.v1ScreenY); + } + if (sceneryOpTraceOn() && sceneryProjectedDraws < 8) { + fprintf(stderr, " plot v1: world(%d, %d) -> screen(%d, %d), z=%d outcode=$%02X\n", + state->pipeline.cur.v1.x, state->pipeline.cur.v1.z, + state->pipeline.cur.v1ScreenX, + state->pipeline.cur.v1ScreenY, + state->pipeline.cur.v1.z, + state->pipeline.cur.v1.outcode); + } + } + // Polygon-fill accumulator: $40/$41 chains of vertex emits + // build a polygon. $00/$32 ops also start a polygon (= xform-A + // path). Reset and seed with v1 so subsequent $41 ops grow the + // edge list. The $29 (CopyToD2) op consumes and fills. + state->polyCount = 0; + if (state->polyCount < 64 + && state->pipeline.cur.v1.outcode == 0 + && state->pipeline.cur.v1.z > 0) { + state->polyXs[state->polyCount] = state->pipeline.cur.v1ScreenX; + state->polyYs[state->polyCount] = state->pipeline.cur.v1ScreenY; + state->polyCount++; + } + // 3D-vertex accumulator for the Sutherland-Hodgman clipper. + // Unlike the 2D buffer above, this captures ALL vertices + // regardless of frustum status -- the clipper at $29 needs + // both inside AND outside vertices to compute edge + // intersections that expand the projected screen-Y range. + state->polyV3DCount = 0; + if (state->polyV3DCount < 64) { + state->polyV3D[state->polyV3DCount++] = state->pipeline.cur.v1; + } + advance(state, recLen); +} + + +// $02 (SceneryOpEmitV2Xform80C5) and $41 (SceneryOpEmitV2Xform7EBC) +// emit vertex 2 and -- when $2C is clear -- draw a line from the +// CURRENT v1 to the new v2 (chunk5 EmitClippedLine path). The port's +// approximation: draw v1 -> new_v2 whenever drawSegment is set. +// Opcode $2B: SceneryOpEmitCurve. chunk5.s line 1516+ reads two +// 4-byte vertex packets, transforms both via TransformVertex7EBC +// (= xform-B), and draws an 8-segment cubic-ish curve via linear +// midpoint subdivision. Total record = 1 op + 4 V1 bytes + 4 V2 +// bytes = 9 bytes. We treat it minimally as "process V1 + V2, +// draw V1 -> V2 as a single straight segment" -- losing the curve +// detail but at least drawing the right line and advancing past +// the record so subsequent opcodes execute. +static void doEmitCurve(SceneryStateT *state); + +static void doEmitV2(SceneryStateT *state, bool drawSegment, bool xformA) { + uint16_t recLen = (uint16_t)(xformA ? 7 : 5); + if (state->cursor + recLen > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + // chunk5 vertex 2 lives at $D4..$D9. xform-A reads 6-byte + // X/Y/Z stream; xform-B reads 4-byte X/Z (Y comes from base). + if (state->writableRam != NULL) { + if (xformA) { + chunk5TransformVertex80C5(state->writableRam, state->cursor, 0xD4); + } else { + chunk5TransformVertex7EBC(state->writableRam, state->cursor, 0xD4); + } + state->pipeline.cur.v2.x = + (int16_t)((uint16_t)state->writableRam[0xD4] + | ((uint16_t)state->writableRam[0xD5] << 8)); + state->pipeline.cur.v2.y = + (int16_t)((uint16_t)state->writableRam[0xD6] + | ((uint16_t)state->writableRam[0xD7] << 8)); + state->pipeline.cur.v2.z = + (int16_t)((uint16_t)state->writableRam[0xD8] + | ((uint16_t)state->writableRam[0xD9] << 8)); + state->pipeline.cur.v2.outcode = sceneryClassifyVertex(&state->pipeline.cur.v2); + } else { + sceneryProjectStreamVertex(&state->pipeline, state->cursor + 1, &state->pipeline.cur.v2); + } + if (getenv("PORT_VTX_DUMP") != NULL) { + fprintf(stderr, " POST-$%02X V2=(%6d,%6d,%6d) cur=$%04tX\n", + xformA ? 0x02 : 0x41, + state->pipeline.cur.v2.x, + state->pipeline.cur.v2.y, + state->pipeline.cur.v2.z, + state->cursor - state->stream); + } + sceneryProjectVertexToScreen(&state->pipeline.cur.v2, + &state->pipeline.cur.v2ScreenX, + &state->pipeline.cur.v2ScreenY); + // chunk5 source SceneryOpEmitV2Xform7EBC ($41, line 1314-1317): + // jsr L6803 ; do transform + // lda $2C ; check saved-state flag + // bne L689C ; if non-zero, SKIP line draw (polygon mode) + // jmp EmitClippedLine ; if zero, draw the v1->v2 line + // MAME-patched chunk5 uses $29 instead of $2C for this flag. + // $2F (SceneryOpResetState) sets the flag to $FF (polygon mode); + // $29 (SceneryOpCopyToD2) clears it to 0 at the end of polygon + // fill. Wireframe segments (= Hancock antenna, etc.) fire $40/$41 + // outside polygon mode, so the line gets drawn. + bool polygonMode = (state->writableRam != NULL) && (state->writableRam[0x29] != 0); + if (drawSegment && !state->dayOnlySkip && !polygonMode) { + const SceneryVertexT *v1 = &state->pipeline.cur.v1; + const SceneryVertexT *v2 = &state->pipeline.cur.v2; + bool bothBehind = (v1->z <= 0 && v2->z <= 0); + if (bothBehind) { + sceneryVtxBehind++; + } else if ((v1->outcode & v2->outcode) != 0) { + sceneryVtxOutcodeAnd++; + } else if (state->renderer != NULL) { + doDrawProjectedSegment(state, v1, v2); + } + } + // chunk5 EmitClippedLine cleanup at L6B2F: copy unclipped V2 + // shadow ($DB..$E2) back over V1 ($C9..$D2) so the next + // segment chains off the just-drawn endpoint without a + // re-transform. Mirror that here by copying V2 onto V1 after + // every emit -- so subsequent $41/$02 calls produce a polyline. + // Same chunk5 layout: V1 is at $CB..$D0 (the port slot we wrote + // in doEmitV1), V2 at $D4..$D9. Copy 6 bytes. + if (state->writableRam != NULL) { + state->writableRam[0xCB] = state->writableRam[0xD4]; + state->writableRam[0xCC] = state->writableRam[0xD5]; + state->writableRam[0xCD] = state->writableRam[0xD6]; + state->writableRam[0xCE] = state->writableRam[0xD7]; + state->writableRam[0xCF] = state->writableRam[0xD8]; + state->writableRam[0xD0] = state->writableRam[0xD9]; + } + state->pipeline.cur.v1 = state->pipeline.cur.v2; + state->pipeline.cur.v1ScreenX = state->pipeline.cur.v2ScreenX; + state->pipeline.cur.v1ScreenY = state->pipeline.cur.v2ScreenY; + // Append v2 to polygon accumulator. Each $41 in a $40,$41,$41,...,$29 + // chain extends the polygon edge list. Skip behind-camera vertices. + if (state->polyCount < 64 + && state->pipeline.cur.v2.outcode == 0 + && state->pipeline.cur.v2.z > 0) { + state->polyXs[state->polyCount] = state->pipeline.cur.v2ScreenX; + state->polyYs[state->polyCount] = state->pipeline.cur.v2ScreenY; + state->polyCount++; + } + // 3D-vertex accumulator parallel to the 2D one -- captures + // every vertex (in-frustum or not) so the 4-pass clipper at + // $29 can find true edge-plane intersections. + if (state->polyV3DCount < 64) { + state->polyV3D[state->polyV3DCount++] = state->pipeline.cur.v2; + } + advance(state, recLen); +} + + +// Near-plane clip. Mirrors chunk5 ClipBothVerticesToFrustum / +// ClipVertex2ToFrustum. Walks each axis pair: if one endpoint is in +// front (z>0) and the other is behind (z<=0), interpolate to the near +// plane (z=1) and replace the behind endpoint with the clipped vertex. +// Returns false when both endpoints are behind (line is invisible). +// Compute the half-space value for one frustum plane, signed so that +// positive = inside the frustum. Mirrors chunk5 +// ClipBothVerticesToFrustum which clips V1->V2 to all five planes in +// sequence (near, left, right, top, bottom). Each plane's equation is +// a linear function of x/y/z; the crossing fraction along the edge is +// pe0/(pe0-pe1) where pe0/pe1 are the equations evaluated at the two +// endpoints. +static int32_t planeEqLine(const SceneryVertexT *v, int plane) { + switch (plane) { + case 0: return (int32_t)v->z; // NEAR: z >= 0 + case 1: return (int32_t)v->z - (int32_t)v->x; // LEFT: z - x >= 0 + case 2: return (int32_t)v->z + (int32_t)v->x; // RIGHT: z + x >= 0 + case 3: return (int32_t)v->z - (int32_t)v->y; // TOP: z - y >= 0 + default: return (int32_t)v->z + (int32_t)v->y; // BOTTOM:z + y >= 0 + } +} + + +// 5-plane frustum clipper for a line segment V1 -> V2, matching +// chunk5's ClipBothVerticesToFrustum byte-for-byte. +// +// chunk5 ClipVertex2ToLeft/Right/Top/Bottom (chunk5.s ~$7576+) does +// the clip in TWO stages per plane: +// 1. Compute Q (Q1.15) = pe_out / (pe_out - pe_in), via signed +// shift-subtract divide (= int32 (pe_out << 15) / (pe_out - pe_in) +// in C, which produces the same result MAME's L16A2 does because +// Q is small enough that the 15-iteration loop and naive C divide +// converge). +// 2. For each component (x, y, z), compute displacement = +// ScaleC2ByC4(Q, V_in.axis - V_out.axis) where ScaleC2ByC4 is +// chunk4's Q1.15 signed multiply (chunk5ScaleC2ByC4). Then +// V_out.axis += displacement. +// 3. After the displacement update, SNAP the axes that define the +// plane (e.g. LEFT plane snaps new.x = new.z exactly; RIGHT +// snaps new.x = -new.z, etc.). This is what makes MAME's clipped +// V1 land EXACTLY on the plane (z == x for LEFT) rather than +// one unit inside it. +// +// The earlier parametric Q12 clip produced V1 a fraction of a unit +// inside the plane, which projected to off-by-1 screen rows after +// the perspective divide. With the snap, port matches MAME's $BB1D +// V1 = (934, -248, 934) bit-for-bit. +static bool clipLineFrustum(SceneryVertexT *a, SceneryVertexT *b) { + for (int plane = 0; plane < 5; plane++) { + int32_t pa = planeEqLine(a, plane); + int32_t pb = planeEqLine(b, plane); + bool aIn = (pa >= 0); + bool bIn = (pb >= 0); + if (!aIn && !bIn) { + return false; + } + if (aIn && bIn) { + continue; + } + SceneryVertexT *out = aIn ? b : a; + SceneryVertexT *in = aIn ? a : b; + int32_t pOut = aIn ? pb : pa; + int32_t pIn = aIn ? pa : pb; + // Q1.15 quotient = pe_out / (pe_out - pe_in). Since + // pe_out < 0 and pe_in >= 0, the denominator is + // negative and the numerator is negative => positive + // ratio in [0, 1]. Clamp the final to Q1.15 range. + int32_t denom = pOut - pIn; // negative + if (denom == 0) { + return false; + } + int32_t numer = pOut; // negative + int32_t qQ15 = (int32_t)(((int64_t)numer << 15) / denom); + if (qQ15 < 0) qQ15 = 0; + if (qQ15 > 32767) qQ15 = 32767; + int16_t Q = (int16_t)qQ15; + int16_t dx16 = (int16_t)((int32_t)in->x - out->x); + int16_t dy16 = (int16_t)((int32_t)in->y - out->y); + int16_t dz16 = (int16_t)((int32_t)in->z - out->z); + int16_t adj_x = chunk5ScaleC2ByC4(Q, dx16); + int16_t adj_y = chunk5ScaleC2ByC4(Q, dy16); + int16_t adj_z = chunk5ScaleC2ByC4(Q, dz16); + int32_t nx = (int32_t)out->x + adj_x; + int32_t ny = (int32_t)out->y + adj_y; + int32_t nz = (int32_t)out->z + adj_z; + if (nx < -32768) nx = -32768; + if (nx > 32767) nx = 32767; + if (ny < -32768) ny = -32768; + if (ny > 32767) ny = 32767; + if (nz < -32768) nz = -32768; + if (nz > 32767) nz = 32767; + // Snap to the plane exactly. chunk5's ClipVertex2ToX + // stores the new z/y into both the plane axes (e.g. + // STA $D4 / STA $D8 for LEFT) so x and z share bytes + // -- equivalent to forcing new.x = new.z after the + // update. + switch (plane) { + case 0: nz = 0; break; // NEAR: z = 0 + case 1: nx = nz; break; // LEFT: x = z + case 2: nx = -nz; break; // RIGHT: x = -z + case 3: ny = nz; break; // TOP: y = z + case 4: ny = -nz; break; // BOTTOM: y = -z + } + out->x = (int16_t)nx; + out->y = (int16_t)ny; + out->z = (int16_t)nz; + out->outcode = sceneryClassifyVertex(out); + } + return true; +} + + +static bool clipNearPlane(SceneryVertexT *a, SceneryVertexT *b) { + return clipLineFrustum(a, b); +} + + +static void doDrawProjectedSegment(SceneryStateT *state, const SceneryVertexT *a, const SceneryVertexT *b) { + // Make working copies so we can clip without mutating the + // pipeline-stored vertices. + SceneryVertexT aa = *a; + SceneryVertexT bb = *b; + if (!clipNearPlane(&aa, &bb)) { + return; + } + if (getenv("PORT_CLIP_DUMP") != NULL) { + ptrdiff_t curOff = state->cursor - state->stream; + fprintf(stderr, + " clip @ $%04tX: V1raw=(%d,%d,%d) V2raw=(%d,%d,%d) -> V1c=(%d,%d,%d) V2c=(%d,%d,%d)\n", + curOff, + a->x, a->y, a->z, + b->x, b->y, b->z, + aa.x, aa.y, aa.z, + bb.x, bb.y, bb.z); + } + // After near-plane clip, re-classify the endpoints against + // the four side planes. chunk5 EmitClippedLine ($6AE3) calls + // ClipBothVerticesToFrustum after the initial $CA & $D3 cull; + // when even after clipping the endpoints still share a + // half-plane the line is culled (chunk5 line 1973 path: + // `bne L6B2F`). Mirror that by re-OR'ing outcodes here, then + // dropping the segment when both endpoints fall outside the + // same side plane. + aa.outcode = sceneryClassifyVertex(&aa); + bb.outcode = sceneryClassifyVertex(&bb); + if ((aa.outcode & bb.outcode) != 0) { + return; + } + int16_t ax; + int16_t ay; + int16_t bx; + int16_t by; + if (!sceneryProjectVertexToScreen(&aa, &ax, &ay)) { + return; + } + if (!sceneryProjectVertexToScreen(&bb, &bx, &by)) { + return; + } + sceneryProjectedDraws++; + // PORT_DRAW_DUMP emits each line in the same shape as MAME's + // tmp/mame_drawlist_long.txt (color-pixel coords X/2, Y, then + // V1/V2 camera-space x/y/z) so the two lists can be diffed + // directly. The MAME logger captures DrawColorLine call args, + // which are (x1,y1,x2,y2) in 0..139/0..191 color-pixel units. + if (getenv("PORT_DRAW_DUMP") != NULL) { + int cx1 = (int)ax / 2; + int cx2 = (int)bx / 2; + if (cx1 < 0) cx1 = 0; + if (cx2 < 0) cx2 = 0; + if (cx1 > 139) cx1 = 139; + if (cx2 > 139) cx2 = 139; + ptrdiff_t curOff = state->cursor - state->stream; + fprintf(stderr, + "draw %4d: cur=$%04X (%3d,%3d)-(%3d,%3d) V1=(%6d,%6d,%6d) V2=(%6d,%6d,%6d)\n", + sceneryProjectedDraws, + (unsigned int)(curOff & 0xFFFF), + cx1, (int)ay, cx2, (int)by, + (int)a->x, (int)a->y, (int)a->z, + (int)b->x, (int)b->y, (int)b->z); + } + rendererDrawLine(state->renderer, ax, ay, bx, by); +} + + +// Shared hook context used by interpreter-routed ops. Captures +// DrawColorLine ($795A) and DrawColorSpan ($78E0) calls from +// MAME-patched chunk5 and forwards each one to the port renderer. +// Struct already forward-declared near the top of this file so +// doCopyToD2 can use it. + + +// Simulate RTS: pop the JSR return address and resume at retAddr+1. +static void cpu6502SimulateRts(Cpu6502T *cpu) { + uint8_t lo = cpu->mem[0x0100 + (uint8_t)(cpu->s + 1)]; + uint8_t hi = cpu->mem[0x0100 + (uint8_t)(cpu->s + 2)]; + cpu->s = (uint8_t)(cpu->s + 2); + cpu->pc = (uint16_t)((uint16_t)lo | ((uint16_t)hi << 8)) + 1; +} + + +// DrawColorSpan hook for the interpreter. MAME-patched DrawColorSpan +// at $78E0 expects: +// A = span width in colour pixels (0..139) +// $27 = right-edge colour-pixel column +// $B1 = current hires row (= scan row) +// We convert to port's renderer convention (X in 280-pixel native +// coords, native row) and call rendererDrawColorSpan, then simulate +// RTS so MAME's hires bytes aren't actually written. +static bool drawColorSpanHook(Cpu6502T *cpu, void *userData) { + EmitCurveHookCtxT *ctx = (EmitCurveHookCtxT *)userData; + SceneryStateT *state = ctx->state; + uint8_t *ram = cpu->mem; + // chunk5 DrawColorSpan ($78E0) entry contract: + // A = span width in colour pixels (loop count) + // ZP $24 = right-edge colour-pixel column (0..139) + // ZP $B1 = current hires row + int width = cpu->a; + int rightX = ram[0x24]; + int row = ram[0x00B1]; + if (state->renderer != NULL && row >= 0 && row < 192) { + if (rightX > 139) { + rightX = 139; + } + if (width > rightX + 1) { + width = rightX + 1; + } + if (width > 0) { + rendererDrawColorSpan(state->renderer, + (int16_t)rightX, + (int16_t)width, + (int16_t)row); + if (getenv("PORT_SPAN_DUMP") != NULL) { + fprintf(stderr, " span: row=%d rightX=%d width=%d (cols %d..%d)\n", + row, rightX, width, + rightX - width + 1, rightX); + } + ctx->drawsEmitted++; + } + } + cpu6502SimulateRts(cpu); + return true; +} + + +void fillTraceFn(Cpu6502T *cpu, void *userData) { + int *cnt = (int *)userData; + static int hits67DD; + static int hits6F67; + static int hits77FB; // before row loop (after edge build) + static int hits7820; // row loop entry + static int hits7842; // horiz JSR $78E0 + static int hits7847; // post-horiz JMP $786A + static int hits784A; // non-horiz interpolate + static int hits786D; // after edge walk + static int hits789D; // emit-spans loop top + static int hits78A6; // emit-spans JSR $78E0 + static int hits78AF; // row done (INC $B1) + switch (cpu->pc) { + case 0x67DD: hits67DD++; break; + case 0x6F67: hits6F67++; break; + case 0x77FB: hits77FB++; break; + case 0x7820: hits7820++; + if (hits7820 <= 8) { + fprintf(stderr, " row entry #%d: $B1=$%02X $22=$%02X $25=$%02X\n", + hits7820, cpu->mem[0xB1], cpu->mem[0x22], cpu->mem[0x25]); + } + break; + case 0x7842: hits7842++; + if (hits7842 <= 16) { + fprintf(stderr, " [%d] horiz JSR: X=%d edge_xLo=$%02X xHi=$%02X yHi=$%02X zHi=$%02X $B1=$%02X\n", + hits7842, cpu->x, + cpu->mem[0x0AF9 + cpu->x], + cpu->mem[0x0B39 + cpu->x], + cpu->mem[0x0BB9 + cpu->x], + cpu->mem[0x0C39 + cpu->x], + cpu->mem[0xB1]); + } + break; + case 0x7847: hits7847++; break; + case 0x784A: hits784A++; + if (hits784A == 1) { + int n = cpu->mem[0x25] + 1; + if (n > 64) n = 64; + fprintf(stderr, "FIRST $784A: $22=$%02X $25=$%02X $EF=$%02X $F0=$%02X $B1=$%02X\n", + cpu->mem[0x22], cpu->mem[0x25], cpu->mem[0xEF], cpu->mem[0xF0], cpu->mem[0xB1]); + fprintf(stderr, " XLo (top_row) [0..%d]: ", n-1); + for (int i = 0; i < n; i++) fprintf(stderr, "%02X ", cpu->mem[0x0AF9 + i]); + fprintf(stderr, "\n XHi (row_cnt-1): "); + for (int i = 0; i < n; i++) fprintf(stderr, "%02X ", cpu->mem[0x0B39 + i]); + fprintf(stderr, "\n YHi (top_col): "); + for (int i = 0; i < n; i++) fprintf(stderr, "%02X ", cpu->mem[0x0BB9 + i]); + fprintf(stderr, "\n ZHi (slope.hi): "); + for (int i = 0; i < n; i++) fprintf(stderr, "%02X ", cpu->mem[0x0C39 + i]); + fprintf(stderr, "\n"); + } + break; + case 0x786D: hits786D++; break; + case 0x789D: hits789D++; break; + case 0x78A6: hits78A6++; + if (hits78A6 <= 8) { + fprintf(stderr, " [%d] emit-pair JSR: X=%d A=%d $24=$%02X $B1=$%02X\n", + hits78A6, cpu->x, cpu->a, + cpu->mem[0x24], cpu->mem[0xB1]); + } + break; + case 0x78AF: hits78AF++; break; + case 0x78E0: + // Hook intercepts; if we get here in trace it means + // hook missed. Should NEVER print. + fprintf(stderr, "[%05d] $78E0 (trace, hook missed!): A=%d $27=%d $B1=%d\n", + *cnt, cpu->a, cpu->mem[0x27], cpu->mem[0xB1]); + break; + case 0x6751: + fprintf(stderr, "SUMMARY: 67DD=%d 6F67=%d 77FB=%d 7820=%d 7842=%d 784A=%d 786D=%d 789D=%d 78A6=%d 78AF=%d\n", + hits67DD, hits6F67, hits77FB, hits7820, hits7842, + hits784A, hits786D, hits789D, hits78A6, hits78AF); + hits67DD = hits6F67 = hits77FB = hits7820 = 0; + hits7842 = hits7847 = hits784A = hits786D = 0; + hits789D = hits78A6 = hits78AF = 0; + break; + } + (*cnt)++; +} + + +static bool emitCurveDrawColorLineHook(Cpu6502T *cpu, void *userData) { + EmitCurveHookCtxT *ctx = (EmitCurveHookCtxT *)userData; + SceneryStateT *state = ctx->state; + uint8_t *ram = cpu->mem; + int16_t x1c = (int8_t)ram[0xE9]; + int16_t y1 = (int8_t)ram[0xEA]; + int16_t x2c = (int8_t)ram[0xEB]; + int16_t y2 = (int8_t)ram[0xEC]; + if (ram[0xE9] != 0) x1c = ram[0xE9]; + if (ram[0xEB] != 0) x2c = ram[0xEB]; + if (ram[0xEA] != 0) y1 = ram[0xEA]; + if (ram[0xEC] != 0) y2 = ram[0xEC]; + int16_t sx1 = (int16_t)(x1c * 2); + int16_t sx2 = (int16_t)(x2c * 2); + if (state->renderer != NULL) { + SceneryVertexT a, b; + a.x = (int16_t)((uint16_t)ram[0xCB] | ((uint16_t)ram[0xCC] << 8)); + a.y = (int16_t)((uint16_t)ram[0xCD] | ((uint16_t)ram[0xCE] << 8)); + a.z = (int16_t)((uint16_t)ram[0xCF] | ((uint16_t)ram[0xD0] << 8)); + a.outcode = 0; + b.x = (int16_t)((uint16_t)ram[0xD4] | ((uint16_t)ram[0xD5] << 8)); + b.y = (int16_t)((uint16_t)ram[0xD6] | ((uint16_t)ram[0xD7] << 8)); + b.z = (int16_t)((uint16_t)ram[0xD8] | ((uint16_t)ram[0xD9] << 8)); + b.outcode = 0; + sceneryProjectedDraws++; + if (getenv("PORT_DRAW_DUMP") != NULL) { + fprintf(stderr, + "draw %4d: cur=$%04X (%3d,%3d)-(%3d,%3d) V1=(%6d,%6d,%6d) V2=(%6d,%6d,%6d)\n", + sceneryProjectedDraws, + (unsigned)(state->cursor - state->stream) & 0xFFFFU, + ram[0xE9], ram[0xEA], ram[0xEB], ram[0xEC], + a.x, a.y, a.z, b.x, b.y, b.z); + } + rendererDrawLine(state->renderer, sx1, y1, sx2, y2); + } + ctx->drawsEmitted++; + // Simulate RTS: pop return addr + 1, set PC. + uint8_t lo = cpu->mem[0x0100 + (uint8_t)(cpu->s + 1)]; + uint8_t hi = cpu->mem[0x0100 + (uint8_t)(cpu->s + 2)]; + cpu->s = (uint8_t)(cpu->s + 2); + cpu->pc = (uint16_t)((uint16_t)lo | ((uint16_t)hi << 8)) + 1; + return true; +} + + +// $2B EmitCurve handler. Defers to the MAME-patched bytecode at +// $69FB via the 6502 interpreter so the 8-segment curve subdivision +// (transform, exponent alignment, step computation, per-segment +// DrawColorLine) is byte-faithful. The DrawColorLine target ($795A +// in the patched binary) is hooked to call back into the port's +// rendererDrawLine so each segment lands in our framebuffer. +static void doEmitCurve(SceneryStateT *state) { + if (state->cursor + 9 > state->streamEnd) { + state->cursor = state->streamEnd; + return; + } + if (state->writableRam == NULL) { + advance(state, 9); + return; + } + // Set up $8B/$8C so the handler's `lda ($8B),Y` reads from + // our stream cursor. Save/restore around the interpreter run. + ptrdiff_t curOff = state->cursor - state->stream; + uint8_t save8B = state->writableRam[0x8B]; + uint8_t save8C = state->writableRam[0x8C]; + state->writableRam[0x8B] = (uint8_t)(curOff & 0xFF); + state->writableRam[0x8C] = (uint8_t)((curOff >> 8) & 0xFF); + + Cpu6502T cpu; + cpu6502Init(&cpu, state->writableRam); + cpu.s = 0xFD; + // Sentinel return: when the $2B handler runs `JMP $6751` + // (dispatcher continuation) the interpreter halts. We use + // $6751 as the stop PC directly rather than relying on RTS. + EmitCurveHookCtxT ctx = { state, 0 }; + // MAME-patched $2B handler entry is at $69FB (NOT $68A4 which + // is a $00/$01 V1-only path). $69FB patches both transform + // call sites ($681B/$681C and $685C/$685D) to TransformVertex + // 7EBC ($7E8E), JSRs V1 transform at $6859, decs $8B, JSRs V2 + // transform at $6818, aligns exponents (compare $32 to $08E2), + // then enters the 8-segment subdivision loop. Returns via JMP + // $6751 (dispatcher continuation). + bool ok = cpu6502RunWithHook(&cpu, 0x69FB, 0x6751, + 0x795A, emitCurveDrawColorLineHook, &ctx, + 1000000); + (void)ok; + + state->writableRam[0x8B] = save8B; + state->writableRam[0x8C] = save8C; + advance(state, 9); +} + + +static void doResetState(SceneryStateT *state) { + // chunk5 SceneryOpResetState (chunk5.s:1341): + // lda #$FF; sta $2C + // lda #$00; sta $B5 + // Despite the misleading "Reset" name, this ARMS polygon-fill + // mode -- $2C = $FF tells subsequent $00/$01/$02/$40/$41 + // vertex emits to APPEND to the polygon buffer instead of + // drawing lines. The closing $29 (CopyToD2) consumes the + // accumulated polygon (running the 4-pass clipper + + // PolygonScanFillSetup) and clears $2C back to 0. + // + // (An older comment claimed MAME-patched layout writes to + // $29 instead of $2C, attributed to Apply64KPatchTable. That + // attribution was wrong; the divergence between source $2C + // and the RAM-snapshot $29 came from scenery $1A/$25 opcodes + // mutating the chunk5 code area mid-flight. We still mirror + // $29 below to match the captured-RAM convention used + // elsewhere in the port; the source-correct location is $2C.) + state->pipeline.cur.poolCount = 0; + state->pipeline.cur.polygonOutcode = 0xFF; + if (state->writableRam != NULL) { + state->writableRam[0x29] = 0xFF; + // $2C is the section-base Y MID byte (see L631D); do + // not stash polygon-mode there in this binary. + } + advance(state, 1); +} + + +void sceneryAttachCamera(SceneryStateT *state, const struct CameraT *cam) { + state->camera = cam; + if (cam == NULL) { + return; + } + // Load the chunk5 polygon pipeline state from the camera. The + // FS2 stream encodes vertex coords in scenery units + // (= metres * AC_SCENERY_UNITS_PER_METRE), so the camera must + // be in scenery units too -- otherwise the (vertex - camera) + // subtraction in TransformVertex7EBC mixes units. + // Convert in Q16.16 first then shift, so non-integer metres + // (e.g. 95.67m -> 287 scenery units) round to the nearest + // chunk5-side value instead of dropping fractional metres. + int32_t wxUnits = (int32_t)((int64_t)cam->worldX * 3 >> CAM_POS_FRACT_BITS); + int32_t wzUnits = (int32_t)((int64_t)cam->worldZ * 3 >> CAM_POS_FRACT_BITS); + int32_t wxMetres = cam->worldX >> CAM_POS_FRACT_BITS; + int32_t wzMetres = cam->worldZ >> CAM_POS_FRACT_BITS; + (void)wxMetres; (void)wzMetres; + if (wxUnits > 32767) wxUnits = 32767; + if (wxUnits < -32768) wxUnits = -32768; + if (wzUnits > 32767) wzUnits = 32767; + if (wzUnits < -32768) wzUnits = -32768; + sceneryPipelineSetCamera(&state->pipeline, (int16_t)wxUnits, (int16_t)wzUnits); + + // Aircraft altitude in scenery units. chunk5's $68/$69 is the + // camera-vs-section Y cell delta from L6BB0; from per-section + // L631D it's used as a 16-bit scale multiplied against the + // camera Y-axis row. In chunk5's coord convention $60/$61 + // (aircraft Y cell) holds altitude in cell units, NOT in the + // same upper-16 units used for X/Z. Treat it as raw metres + // for now until a $07/$24 implementation gives us the proper + // section-relative delta. + int32_t wyMetres = cam->worldY >> CAM_POS_FRACT_BITS; + int32_t wyUnits = wyMetres; + if (wyUnits > 32767) wyUnits = 32767; + if (wyUnits < -32768) wyUnits = -32768; + + // Zero-page mirror for the chunk5 polygon pipeline: + // + // $5C/$5D = camera X upper-16 (cull-test slot) + // $64/$65 = camera Z upper-16 (cull-test slot) + // + // $66/$67 = camera-vs-section X delta (TransformVertex7EBC + // SBC slot for vertex X). Assumes the section sits + // at world origin -> delta = camera X. + // $68/$69 = camera-vs-section Y delta -> camera_Y_alt. + // L631D multiplies this by matrix row 2 to get the + // section base contribution. + // $6A/$6B = camera-vs-section Z delta = camera Z. + // + // Chunk5 normally writes $66..$6B from the $07/$24 opcode + // handlers via L6BB0; absent that, the boot's stale values were + // left here. Forcing them per-frame keeps the polygon math + // honest until $07/$24 are fully implemented. + if (state->writableRam != NULL) { + // wxUnits/wzUnits already scaled to scenery units above. + state->writableRam[0x005C] = (uint8_t)((uint16_t)wxUnits & 0xFF); + state->writableRam[0x005D] = (uint8_t)(((uint16_t)wxUnits >> 8) & 0xFF); + state->writableRam[0x0064] = (uint8_t)((uint16_t)wzUnits & 0xFF); + state->writableRam[0x0065] = (uint8_t)(((uint16_t)wzUnits >> 8) & 0xFF); + // Aircraft altitude is a 16-bit LE value at $5F:$60 (NOT + // $60:$61 as I first guessed). Confirmed by chunk5.s + // IntegrateClimbRate comment ("Integrate climb rate + // ($0843, signed 16-bit) into altitude ($5F/$60)") and + // chunk2.s ComputeWindComponents ("Pick a wind layer + // record based on altitude ($5F/$60)"). The boot RAM + // dump has $5F=$03 $60=$00 = altitude 3 = the ground + // clamp ("Clamps altitude to a minimum of $0003"). + // + // Layout note: this 16-bit altitude STRADDLES the 6-pair + // ZP boundary chunk5's L6BB0 SBC chain reads ($5A/$5B, + // $5C/$5D, $5E/$5F, $60/$61, $62/$63, $64/$65 are six + // 16-bit pairs feeding camera-vs-section deltas). So + // altitude shares its low byte with the high byte of + // the $5E/$5F pair and its high byte with the low byte + // of the $60/$61 pair -- weird, but it's chunk5's + // convention. Keep $5F/$60 in sync with cam->worldY. + uint16_t altUnits = (uint16_t)wyUnits; + if (altUnits < 3) { + altUnits = 3; // chunk5 ground clamp + } + state->writableRam[0x005F] = (uint8_t)( altUnits & 0xFF); + state->writableRam[0x0060] = (uint8_t)((altUnits >> 8) & 0xFF); + // chunk5 sets $66/$67/$68/$69/$6A/$6B = camera - section + // (per-section delta) via $07/$24's L6BB0 path. Until + // that fires we seed with (0, altitude, 0) so vertex + // transforms read sane camera state on the first ops. + state->writableRam[0x0066] = 0; + state->writableRam[0x0067] = 0; + state->writableRam[0x0068] = (uint8_t)((uint16_t)wyUnits & 0xFF); + state->writableRam[0x0069] = (uint8_t)(((uint16_t)wyUnits >> 8) & 0xFF); + state->writableRam[0x006A] = 0; + state->writableRam[0x006B] = 0; + // PORT_FORCE_MAME_CAMSECT=1: seed $66/$67/$6A/$6B with + // the frozen MAME state ($66/$67=$0718=1816, $6A/$6B= + // $FCC3=-829) instead of zero. Tests whether port's + // $07/$24 handlers are wrong (= these slots stay at the + // forced value because the walk's $07/$24 ops will + // overwrite normally; but if port's $07/$24 is also + // wrong, MAME's values let downstream transforms use + // approximately correct data). + if (getenv("PORT_FORCE_MAME_CAMSECT") != NULL) { + state->writableRam[0x0066] = 0x18; + state->writableRam[0x0067] = 0x07; + state->writableRam[0x006A] = 0xC3; + state->writableRam[0x006B] = 0xFC; + } + } + + // Build the rotation matrix per-frame from the camera (mirrors + // chunk5's SetupViewProjection). Earlier the port reused the + // stale matrix from the FS2TRACE_BOOT-generated RAM dump, but + // that's frozen at the camera state fs2trace had at dump time + // -- it doesn't follow our port's pitch/bank/yaw. + // + // cameraGet2x3Matrix derives row 1 (camera X) and row 3 (camera + // Z) from the cam->rot Q1.15 matrix, dropping to int8 with $7F + // == 1.0. That's the same shape chunk5's ROL-cascade produces + // at $79/$7B/$7D + $85/$87/$89 (high bytes of int16 components). + // Row 2 (camera Y / altitude) is handled separately by the + // section-base mechanism and isn't part of this 2x3 transform. + int8_t rowX[3]; + int8_t rowZ[3]; + int16_t row[3][3]; + cameraGet2x3Matrix(cam, rowX, rowZ); + // chunk5 lays $78..$89 out as R (camera-to-world), so use + // cam->rotChunk5 (R) for the writableRam mirror, not cam->rot + // (R^T). cameraGet2x3Matrix above still pulls from cam->rot + // because the port's int8 projection pipeline keeps its own + // R^T-shaped matRow1/matRow2 convention. + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + row[i][j] = cam->rotChunk5[i][j]; + } + } + + // chunk5 L6301 col shifts (col 0 >>= 1, col 2 >>= 2) are + // applied INSIDE chunk5SetupViewProjection, so cam->rotChunk5 + // already carries them. Don't re-apply here. + // + // The int8 projection pipeline matrix (matRow1/matRow2) is + // derived from cam->rot via cameraGet2x3Matrix above, which + // does NOT apply the shifts -- so we still need to shift the + // int8 rows for the projection pipeline. + rowX[0] >>= 1; + rowX[2] >>= 2; + rowZ[0] >>= 1; + rowZ[2] >>= 2; + sceneryPipelineSetMatrix(&state->pipeline, rowX, rowZ); + + // PORT_FORCE_MAME_MATRIX=1 overrides the computed matrix with + // MAME's captured $78..$89 from boot Meigs (frozen frame). Used + // to A/B test whether the projection bias is in the matrix + // build or downstream. The frozen RAM holds (16382, 0, 0; + // 0, 32760, 100; 0, -401, 8190). + if (getenv("PORT_FORCE_MAME_MATRIX") != NULL) { + row[0][0] = 16382; row[0][1] = 0; row[0][2] = 0; + row[1][0] = 0; row[1][1] = 32760; row[1][2] = 100; + row[2][0] = 0; row[2][1] = -401; row[2][2] = 8190; + } + + // Mirror the full 3x3 matrix (post-L6301) into chunk5's + // $78..$89 slot layout so downstream consumers (including + // L631D-style section base computations triggered by + // SceneryOpEnterLocalFrame) see the live values. Each row + // holds three int16 components in LE (lo at $78/$7A/$7C/..., + // hi at $79/$7B/$7D/...). The high-byte view at $79/$7B/$7D + + // $85/$87/$89 matches what sceneryPipelineSetMatrix received. + if (state->writableRam != NULL) { + // Row 1 (camera X axis components in world frame). + state->writableRam[0x78] = (uint8_t)( row[0][0] & 0xFF); + state->writableRam[0x79] = (uint8_t)((row[0][0] >> 8) & 0xFF); + state->writableRam[0x7A] = (uint8_t)( row[0][1] & 0xFF); + state->writableRam[0x7B] = (uint8_t)((row[0][1] >> 8) & 0xFF); + state->writableRam[0x7C] = (uint8_t)( row[0][2] & 0xFF); + state->writableRam[0x7D] = (uint8_t)((row[0][2] >> 8) & 0xFF); + // Row 2 (camera Y / altitude axis). chunk5's + // SceneryOpEnterLocalFrame -> L631D reads these as + // 16-bit, multiplied by the section's scale factor at + // $68/$69, to compute the section base $4A/$4D/$50. + state->writableRam[0x7E] = (uint8_t)( row[1][0] & 0xFF); + state->writableRam[0x7F] = (uint8_t)((row[1][0] >> 8) & 0xFF); + state->writableRam[0x80] = (uint8_t)( row[1][1] & 0xFF); + state->writableRam[0x81] = (uint8_t)((row[1][1] >> 8) & 0xFF); + state->writableRam[0x82] = (uint8_t)( row[1][2] & 0xFF); + state->writableRam[0x83] = (uint8_t)((row[1][2] >> 8) & 0xFF); + // Row 3 (camera Z axis components in world frame). + state->writableRam[0x84] = (uint8_t)( row[2][0] & 0xFF); + state->writableRam[0x85] = (uint8_t)((row[2][0] >> 8) & 0xFF); + state->writableRam[0x86] = (uint8_t)( row[2][1] & 0xFF); + state->writableRam[0x87] = (uint8_t)((row[2][1] >> 8) & 0xFF); + state->writableRam[0x88] = (uint8_t)( row[2][2] & 0xFF); + state->writableRam[0x89] = (uint8_t)((row[2][2] >> 8) & 0xFF); + } + + // L631D fires from $07/$24 ops as the dispatcher walks the + // scenery stream -- not eagerly at camera attach time. The + // boot RAM dump already has $4A..$52 = MAME's post-boot + // section base for Meigs; preserving it lets vertex + // transforms produce coords matching the reference until the + // first $07/$24 fires and recomputes the new base from + // section anchor data. +} + + +void sceneryAttachStationCb(SceneryStateT *state, SceneryStationCbF cb, void *userData) { + state->stationCb = cb; + state->userData = userData; +} + + +void sceneryInit(SceneryStateT *state, const uint8_t *stream, uint32_t length, RenderStateT *renderer) { + state->stream = stream; + state->cursor = stream; + state->streamEnd = stream + length; + state->writableRam = NULL; + state->sceneryFile = NULL; + state->sceneryFileSize = 0; + state->renderer = renderer; + state->camera = NULL; + state->subDepth = 0; + state->stationCb = NULL; + state->userData = NULL; + state->dayOnlySkip = false; + state->isNight = false; + state->walkAllPaths = false; + state->polyCount = 0; + sceneryPipelineReset(&state->pipeline); + memset(state->visited, 0, sizeof(state->visited)); +} + + +void sceneryRun(SceneryStateT *state) { + // Counters are global static so they accumulate across the + // top-level call AND any recursive sub-invokes ($18) that + // occur inside opcode handlers. Reset only on the first + // (non-recursive) entry; recursive calls have subDepth > 0. + if (state->subDepth == 0) { + sceneryOpsTotal = 0; + sceneryVertexEmits = 0; + sceneryProjectedDraws = 0; + sceneryVtxBehind = 0; + sceneryVtxOutcodeAnd = 0; + } + state->exitDispatch = false; + if (sceneryStatsOn()) { + fprintf(stderr, "scenery: enter run, cursor offset=$%04tX, first byte=$%02X\n", + state->cursor - state->stream, + (state->cursor < state->streamEnd) ? *state->cursor : 0); + } + while (!streamExhausted(state)) { + uint8_t op = *state->cursor; + sceneryOpsTotal++; + if (op == 0x00 || op == 0x01 || op == 0x02 || op == 0x40 || op == 0x41 || op == 0x42) { + sceneryVertexEmits++; + } + // Real chunk5 dispatcher: opcodes with bit 7 set or >= + // $46 fall off the SceneryOpcodeTable end and trigger + // SceneryStreamEnd. Same here. + if ((op & 0x80) || op >= SCENERY_OP_END) { + if (sceneryStatsOn()) { + fprintf(stderr, "scenery: terminator op=$%02X at offset $%04tX -- ops=%d vertex=%d behind=%d outcodeAnd=%d draws=%d\n", + op, state->cursor - state->stream, + sceneryOpsTotal, sceneryVertexEmits, sceneryVtxBehind, sceneryVtxOutcodeAnd, sceneryProjectedDraws); + } + return; + } + // Cycle guard so an offline walker can be invoked from + // many entries without blowing through 100k+ ops on + // every replay. Live rendering doesn't need this and + // can re-enter the same record fine, so we only mark + // when the visited array has been allocated. + ptrdiff_t off = state->cursor - state->stream; + if (off >= 0 && off < (ptrdiff_t)sizeof(state->visited)) { + if (state->visited[off]) { + return; + } + state->visited[off] = 1; + } + if (sceneryOpTraceOn()) { + if (state->writableRam != NULL && getenv("PORT_BASE_DUMP") != NULL) { + uint8_t *r = state->writableRam; + fprintf(stderr, + " op @ $%04tX: $%02X (sub=%d) base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] cam=[%02X%02X,%02X%02X,%02X%02X]\n", + state->cursor - state->stream, op, state->subDepth, + r[0x4A], r[0x4B], r[0x4C], + r[0x4D], r[0x4E], r[0x4F], + r[0x50], r[0x51], r[0x52], + r[0x67], r[0x66], + r[0x69], r[0x68], + r[0x6B], r[0x6A]); + } else { + fprintf(stderr, " op @ $%04tX: $%02X (sub=%d)\n", + state->cursor - state->stream, op, state->subDepth); + } + } + switch (op) { + // Vertex-emit family. The XZ-stream variants + // ($00-$02, $40-$42) project a stream-embedded + // vertex into v1 or v2; $00/$02/$40/$42 draw + // the segment from the previous endpoint + // (via chunk5's L7963 / L6AE3 paths), $01/$41 + // emit silently for later use by polygon + // closure ops. + case SCENERY_OP_VERTEX_DRAW: doEmitV1(state, EMIT_V1_PLOT, true); break; // $00 SceneryOpEmitV1XformAndPlot (xform-A) + case SCENERY_OP_VERTEX_LO: doEmitV1(state, EMIT_V1_SILENT, true); break; // $01 SceneryOpEmitV1Xform80C5 (xform-A) + case SCENERY_OP_VERTEX_LO2: doEmitV2(state, true, true); break; // $02 SceneryOpEmitV2Xform80C5 (xform-A) + case SCENERY_OP_CALL_64K_2: doCall64KRotated(state); break; // $03 chunk3 SceneryRotatedTransform + case SCENERY_OP_L6DAF: doCullByOutcodeList(state); break; // $04 + case SCENERY_OP_ADF_STATION: doAdfRecord(state); break; + case SCENERY_OP_DRAW_LINE: doDrawLine(state); break; + case SCENERY_OP_DRAW_LINE3D: doEnterLocalFrame(state); break; // $07 = SceneryOpEnterLocalFrame, fires L631D + case SCENERY_OP_SKIP3_A: advance(state, 3); break; + case SCENERY_OP_SKIP3_B: advance(state, 3); break; + case SCENERY_OP_REL_JUMP: doRelJump(state); break; + case SCENERY_OP_HEADER: doHeader(state); break; + case SCENERY_OP_CALL_64K: doCall64K(state); break; // $0E chunk3 SceneryOp64KCallback + case SCENERY_OP_SKIP_1: doSkip1(state); break; + case SCENERY_OP_SET_COLOR: doSetColor(state); break; + case SCENERY_OP_L6DED: doJumpIfBeyondXY(state, false); break; // $13 + case SCENERY_OP_L6DF3: doJumpIfBeyondXY(state, true); break; // $14 + case SCENERY_OP_SUB_INVOKE: doSubInvoke(state); break; + case SCENERY_OP_RETURN: return; + case SCENERY_OP_L6D66: doWriteWord(state); break; + case SCENERY_OP_MODE_WHITE: doModeWhite(state); break; // $1B + case SCENERY_OP_DAY_ONLY: doDayOnly(state); break; // $1C + case SCENERY_OP_NAV_STATION: doNavRecord(state); break; + case SCENERY_OP_COM_RECORD: doComRecord(state); break; + case SCENERY_OP_CULL_1: doCullN(state, 1); break; + case SCENERY_OP_CULL_2: doCullN(state, 2); break; + case SCENERY_OP_CULL_3: doCullN(state, 3); break; + case SCENERY_OP_JUMP_BITS: doJumpIfBitsClear(state); break; // $23 + case SCENERY_OP_PUSH_ORIGIN: doPushOriginWithStash(state); break; // $24 + case SCENERY_OP_L6D8E: doStoreImmWord(state); break; // $25 store imm16 + case SCENERY_OP_L6F1D: doJumpIfWordCompare(state); break; // $28 + case SCENERY_OP_COPY_TO_D2: doCopyToD2(state); break; + case SCENERY_OP_L69E6: doEmitCurve(state); break; + case SCENERY_OP_RESET_STATE: doResetState(state); break; + // L6987 family: 1 opcode + 1 vertex-index byte + // (chunk5 L6987 reads ($8B),Y then jsr AddTo8B + // with A=1). Indexed vertices come from a + // pre-loaded 0x0140 vertex table; we don't + // load that yet, so just advance. + case SCENERY_OP_REFRESH_LO: doRefreshCachedXform(state, true); break; // $31 SceneryOpRefreshCachedXform80C5 (xform-A, 8 bytes) + case SCENERY_OP_VERTEX_PRIM: doVertexCachedV1(state); break; // $32 + case SCENERY_OP_VERTEX_END: doVertexCachedV2(state); break; // $33 + case SCENERY_OP_VERTEX_2D: doVertexCachedDraw(state); break; // $35 + // $40/$41/$42 are L7EBC-tail variants of + // $01/$02/$31 (silent V1 emit / V2-emit-and-line / + // cache refresh). See chunk5 SceneryOpcodeTable + // line ~1030: $40 = SceneryOpEmitV1Xform7EBC + // (silent), $41 = SceneryOpEmitV2Xform7EBC + // (DRAWS line v1->v2), $42 = + // SceneryOpRefreshCachedXform7EBC (cache + // refresh, no draw). + case SCENERY_OP_VERTEX_HI: doEmitV1(state, EMIT_V1_SILENT, false); break; // $40 SceneryOpEmitV1Xform7EBC (xform-B) + case SCENERY_OP_VERTEX_HI2: doEmitV2(state, true, false); break; // $41 SceneryOpEmitV2Xform7EBC (xform-B) + // $42 SceneryOpRefreshCachedXform7EBC: 6-byte + // record = opcode + 1-byte vertex-pool idx + 4 + // vertex bytes. chunk5 L6987 advances $8B by 1 + // (past idx) and TransformVertex7EBC's tail at + // L8091 advances $8B by 5 more (past op + 4 + // vertex bytes), totaling 6. + case SCENERY_OP_VERTEX_HI3: doRefreshCachedXform(state, false); break; // $42 SceneryOpRefreshCachedXform7EBC (xform-B, 6 bytes) + default: + // Opcodes that map to SceneryOpInvalid + // in chunk5 ($08, $0C, $0F, $10, $15-17, + // $1F, $26-27, $2A, $2C-2E, $30, $34, + // $36-3F, $43-45) -- the real dispatcher + // resets state and bails, so terminate + // the stream here too. + if (sceneryStatsOn()) { + fprintf(stderr, "scenery: invalid $%02X at offset $%04tX -- ops=%d vertex=%d draws=%d\n", + op, state->cursor - state->stream, + sceneryOpsTotal, sceneryVertexEmits, sceneryProjectedDraws); + } + return; + } + if (state->exitDispatch) { + // MAME-patched RTS-after-line semantics: a $41 + // line emit terminates the current dispatcher + // iteration. Return to caller (= doSubInvoke + // for sub-streams, top-level for main loop). + return; + } + } + if (sceneryStatsOn()) { + fprintf(stderr, "scenery: ops=%d vertex=%d draws=%d (stream end)\n", + sceneryOpsTotal, sceneryVertexEmits, sceneryProjectedDraws); + } +} + + +// Walk the bytecode starting at `entryOffset`, then again from every +// byte position the visited cursor still hasn't covered. The cycle +// guard inside sceneryRun bounds the total work to O(stream length). +// Used by the offline station extractor. +void sceneryWalkFrom(SceneryStateT *state, uint32_t entryOffset) { + if (entryOffset >= (uint32_t)(state->streamEnd - state->stream)) { + return; + } + state->cursor = state->stream + entryOffset; + sceneryRun(state); +} + + +static bool streamExhausted(const SceneryStateT *state) { + if (state->cursor >= state->streamEnd) { + return true; + } + // chunk5's SceneryReadUntilC0 stops loading sector data when + // the dest page enters $C000-$CFFF (Apple II I/O range), so + // any jump that lands the cursor in that window is reading + // from unloaded RAM. Treat it as a terminator instead of + // walking into zeros (which `dispatch as $00 single-pixel + // plot` ad infinitum). $D000+ is fine -- the language card + // RAM region IS loaded for some sections. + ptrdiff_t off = state->cursor - state->stream; + if (off >= 0xC000 && off < 0xD000) { + return true; + } + return false; +} diff --git a/port/src/timeOfDay.c b/port/src/timeOfDay.c new file mode 100644 index 0000000..a9f1d53 --- /dev/null +++ b/port/src/timeOfDay.c @@ -0,0 +1,103 @@ +// Time-of-day implementation. See timeOfDay.h. + +#include "timeOfDay.h" + + +// FS2 chunk3 `DayPhaseTable` (verified against out/3_d300-f3ff at +// $DE52). 4 seasonal rows of 8 bytes each; each row holds four +// (minutes, hour) pairs: dawn-start, sunrise, sunset, dusk-end. +static const uint8_t dayPhaseTable[4][8] = { + // Winter + { 0x00, 0x06, 0x1E, 0x06, 0x00, 0x13, 0x1E, 0x13 }, + // Spring + { 0x00, 0x07, 0x1E, 0x07, 0x00, 0x11, 0x1E, 0x11 }, + // Summer (matches winter -- table quirk preserved from ROM) + { 0x00, 0x06, 0x1E, 0x06, 0x00, 0x13, 0x1E, 0x13 }, + // Fall + { 0x00, 0x05, 0x1E, 0x05, 0x00, 0x15, 0x1E, 0x15 } +}; + +// One in-game minute per N frames. At 60 fps that gives roughly +// 1 sim-hour every 4 real seconds -- aggressive but visible. The FS2 +// runtime had its own counter that ticked off Hours/Minutes from the +// frame loop; we keep an analogous accumulator. +#define TIME_FRAMES_PER_MINUTE 4 + + +static int compareHM(uint8_t hours, uint8_t minutes, uint8_t targetMin, uint8_t targetHour); + + +// Subtract target from (hours, minutes). Returns negative iff +// (hours:minutes) < (targetHour:targetMin). Mirrors FS2's +// `sbc DayPhaseTable+...` chain. +static int compareHM(uint8_t hours, uint8_t minutes, uint8_t targetMin, uint8_t targetHour) { + int now = (int)hours * 60 + (int)minutes; + int target = (int)targetHour * 60 + (int)targetMin; + return now - target; +} + + +const char *timeOfDayPhaseName(DayPhaseE phase) { + switch (phase) { + case DAY_PHASE_DAY: return "DAY"; + case DAY_PHASE_TWILIGHT: return "TWILIGHT"; + case DAY_PHASE_NIGHT: return "NIGHT"; + default: return "?"; + } +} + + +void timeOfDayInit(TimeOfDayT *t) { + t->hours = 12; + t->minutes = 0; + t->frameSubMinute = 0; + t->season = SEASON_SUMMER; + timeOfDayRecomputePhase(t); +} + + +void timeOfDayRecomputePhase(TimeOfDayT *t) { + const uint8_t *row = dayPhaseTable[(int)t->season & 0x03]; + // Before dawn-start -> Night. + if (compareHM(t->hours, t->minutes, row[0], row[1]) < 0) { + t->phase = DAY_PHASE_NIGHT; + return; + } + // Before sunrise -> Twilight (dawn). + if (compareHM(t->hours, t->minutes, row[2], row[3]) < 0) { + t->phase = DAY_PHASE_TWILIGHT; + return; + } + // Before sunset -> Day. + if (compareHM(t->hours, t->minutes, row[4], row[5]) < 0) { + t->phase = DAY_PHASE_DAY; + return; + } + // Before dusk-end -> Twilight (dusk). + if (compareHM(t->hours, t->minutes, row[6], row[7]) < 0) { + t->phase = DAY_PHASE_TWILIGHT; + return; + } + t->phase = DAY_PHASE_NIGHT; +} + + +void timeOfDaySet(TimeOfDayT *t, uint8_t hours, uint8_t minutes) { + t->hours = (uint8_t)(hours % 24); + t->minutes = (uint8_t)(minutes % 60); + timeOfDayRecomputePhase(t); +} + + +void timeOfDayStep(TimeOfDayT *t) { + t->frameSubMinute++; + if (t->frameSubMinute >= TIME_FRAMES_PER_MINUTE) { + t->frameSubMinute = 0; + t->minutes++; + if (t->minutes >= 60) { + t->minutes = 0; + t->hours = (uint8_t)((t->hours + 1) % 24); + } + timeOfDayRecomputePhase(t); + } +} diff --git a/port/src/title.c b/port/src/title.c new file mode 100644 index 0000000..c65d068 --- /dev/null +++ b/port/src/title.c @@ -0,0 +1,86 @@ +// Pre-flight title / config screen. See title.h. + +#include "font.h" +#include "framebuffer.h" +#include "title.h" +#include "types.h" + + +typedef struct MenuItemT { + TitleSelectionE id; + const char *label; +} MenuItemT; + + +static const MenuItemT menuItems[] = { + { TITLE_FREE_FLIGHT, "FREE FLIGHT" }, + { TITLE_DEMO, "DEMO MODE" }, + { TITLE_SLEW, "SLEW MODE" }, + { TITLE_WW1_ACE, "WW1 ACE" }, + { TITLE_QUIT, "QUIT" } +}; + +#define MENU_COUNT (int)(sizeof(menuItems) / sizeof(menuItems[0])) + + +bool titleHandleKey(TitleStateT *t, const SDL_Event *ev) { + if (ev->type != SDL_KEYDOWN) { + return t->done; + } + switch (ev->key.keysym.sym) { + case SDLK_UP: + case SDLK_w: + if ((int)t->cursor > 0) { + t->cursor = (TitleSelectionE)((int)t->cursor - 1); + } else { + t->cursor = (TitleSelectionE)(MENU_COUNT - 1); + } + break; + case SDLK_DOWN: + case SDLK_s: + if ((int)t->cursor + 1 < MENU_COUNT) { + t->cursor = (TitleSelectionE)((int)t->cursor + 1); + } else { + t->cursor = (TitleSelectionE)0; + } + break; + case SDLK_RETURN: + case SDLK_KP_ENTER: + case SDLK_SPACE: + t->done = true; + break; + case SDLK_ESCAPE: + t->cursor = TITLE_QUIT; + t->done = true; + break; + case SDLK_1: t->cursor = TITLE_FREE_FLIGHT; t->done = true; break; + case SDLK_2: t->cursor = TITLE_DEMO; t->done = true; break; + case SDLK_3: t->cursor = TITLE_SLEW; t->done = true; break; + case SDLK_4: t->cursor = TITLE_WW1_ACE; t->done = true; break; + default: break; + } + return t->done; +} + + +void titleDraw(const TitleStateT *t, FramebufferT *fb) { + framebufferClear(fb, COLOR_BLACK); + fontDrawString(fb, 70, 20, "FLIGHT SIMULATOR II", COLOR_WHITE); + fontDrawString(fb, 90, 32, "(C PORT - 2026)", COLOR_WHITE); + + for (int i = 0; i < MENU_COUNT; i++) { + int16_t y = (int16_t)(70 + i * 14); + bool selected = ((int)t->cursor == i); + fontDrawString(fb, 100, y, selected ? ">" : " ", COLOR_WHITE); + fontDrawString(fb, 116, y, menuItems[i].label, COLOR_WHITE); + } + + fontDrawString(fb, 30, 160, "ARROW KEYS / WASD TO MOVE", COLOR_WHITE); + fontDrawString(fb, 30, 172, "ENTER OR 1-4 TO START", COLOR_WHITE); +} + + +void titleInit(TitleStateT *t) { + t->cursor = TITLE_FREE_FLIGHT; + t->done = false; +} diff --git a/port/src/wind.c b/port/src/wind.c new file mode 100644 index 0000000..30acdb0 --- /dev/null +++ b/port/src/wind.c @@ -0,0 +1,155 @@ +// FS2 wind system port. See wind.h for the data flow. + +#include "fs2math.h" +#include "math6502.h" +#include "wind.h" + + +// `ScaleC2ByAX` shorthand. The FS2 routine takes a signed 16-bit +// `value` and `scale`, and returns `(value * scale) / 32768`. +#define SCALE_C2_BY_AX(value, scale) fs2ScaleByAX((int16_t)(value), (int16_t)(scale)) + + +static const WindLayerT *pickLayer(const WindStateT *w, uint16_t altitude16, uint8_t *isSurface); +static int16_t resolveComponent(uint8_t magnitudeByte, int16_t sinValue, uint8_t scaleHi); + + +// Pick the wind-layer record matching altitude. The four bands map +// directly onto FS2's `useSurface` / `useLayer1/2/3` branches. +static const WindLayerT *pickLayer(const WindStateT *w, uint16_t altitude16, uint8_t *isSurface) { + *isSurface = 0; + if (altitude16 < w->altThreshold1) { + *isSurface = 1; + return &w->surface; + } + if (altitude16 < w->altThreshold2) { + return &w->layer1; + } + if (altitude16 < w->altThreshold3) { + return &w->layer2; + } + return &w->layer3; +} + + +// Mirrors the `MultiplyXY` -> `MultiplyXYAndHalve` chain that +// `ComputeWindComponents` runs to convert (magnitude, sin/cos) into +// the 16-bit signed component. FS2's `L1818` is a signed 7-bit × +// 7-bit -> 14-bit multiply that takes `Y` (here magnitude byte) and +// `X` (here `sinValue >> 8`, the high byte of the 16-bit sin/cos). +// The result is then halved by `L180C` and finally scaled by +// `$09DE/$09DF` via `ScaleC2ByAX`. We collapse the chain into a +// single signed-multiply / halve / scale step. +static int16_t resolveComponent(uint8_t magnitudeByte, int16_t sinValue, uint8_t scaleHi) { + int8_t magS = (int8_t)magnitudeByte; + int8_t sinS = (int8_t)((sinValue >> 8) & 0xFF); + int16_t product = math6502SignedMul(magS, sinS); // L1818 + int16_t halved = (int16_t)(product >> 1); // L180C halve + // FS2's L180C is followed by a `ScaleC2ByAX` against + // `$09DE/$09DF`. With `$09DF` being the high byte (and the + // low byte zero by default) the scale becomes `(halved * + // scaleHi*256) / 32768 = halved * scaleHi / 128`. + int16_t scale16 = (int16_t)((uint16_t)scaleHi << 8); + return SCALE_C2_BY_AX(halved, scale16); +} + + +void windApply(WindStateT *w, bool onGround, int32_t *deltaX_q1616, int32_t *deltaZ_q1616) { + if (onGround) { + *deltaX_q1616 = 0; + *deltaZ_q1616 = 0; + return; + } + + // FS2 subtracts the 16-bit wind component from the low half + // of the 32-bit position word; the high half carries through + // sign-extended. With Q16.16 worldX/Z that means the delta is + // `-component` placed straight into the low 16 fractional bits. + *deltaX_q1616 = -(int32_t)w->componentX; + *deltaZ_q1616 = -(int32_t)w->componentZ; + + // Bank-accumulator update: scale the Z component by the + // cached layer byte and add into $09AF/$09B0. FS2 reads the + // scaled value via `ScaleC2ByAX` of `($00:$62) * WindLayerByte1`. + // The high byte of the position low-word was originally `$62`; + // here we feed the high byte of `componentZ` as a stand-in + // for the FS2 cell, since both come from the same wind branch. + int16_t hiZ = (int16_t)((w->componentZ >> 8) & 0xFF); + int16_t scaledZ = SCALE_C2_BY_AX(hiZ << 8, (int16_t)((uint16_t)w->layerByte1 << 8)); + int32_t accum = (int32_t)w->bankAccum + (int32_t)scaledZ; + if (accum > 32767) { + accum = 32767; + } + if (accum < -32768) { + accum = -32768; + } + w->bankAccum = (int16_t)accum; + + // Turbulence kick: only when WindLayerByte1 bit 0 is set. The + // sign cycles `+1, 0, -1, 0` as `UpdateCounter` rolls past + // every $40 ticks (FS2 tests bits 7,6 of UpdateCounter). + int16_t kickRaw = 0; + if (w->layerByte1 & 0x01) { + uint8_t mask = (uint8_t)(w->updateCounter & 0xC0); + if (mask == 0x00) { + kickRaw = (int16_t)0xFF00; // turbNeg + } else if (mask == 0x80) { + kickRaw = (int16_t)0x0100; // turbPos + } + // 0x40 / 0xC0 -> kick stays zero + } + int16_t scale = (int16_t)((uint16_t)w->scaleByteHi << 8 | w->scaleByteLo); + w->turbKick = SCALE_C2_BY_AX(kickRaw, scale); +} + + +void windCompute(WindStateT *w, uint16_t altitude16) { + uint8_t isSurface; + const WindLayerT *layer = pickLayer(w, altitude16, &isSurface); + w->surfaceFlag = isSurface; + w->layerByte1 = layer->turbByte; + + // Resolve the direction angle: layer.direction + yokeOffset1 + // (always), plus yokeOffset2 if the surface band was picked. + uint8_t direction = (uint8_t)(layer->direction + w->yokeOffset1); + if (isSurface) { + direction = (uint8_t)(direction + w->yokeOffset2); + } + + // FS2 takes both `sin(direction)` and `sin(direction - $40)` + // -- the second is `-cos(direction)`. The X component uses + // the shifted sin; Z uses the un-shifted. + int16_t sinShifted = math6502Sin((uint8_t)(direction - 0x40)); + int16_t sinDir = math6502Sin(direction); + + uint8_t magnitudeByte = (uint8_t)layer->magnitude; + w->componentX = resolveComponent(magnitudeByte, sinShifted, w->scaleByteHi); + w->componentZ = resolveComponent(magnitudeByte, sinDir, w->scaleByteHi); +} + + +void windInit(WindStateT *w) { + w->altThreshold1 = 0; + w->altThreshold2 = 0; + w->altThreshold3 = 0; + + const WindLayerT empty = { 0, 0, 0, 0 }; + w->surface = empty; + w->layer1 = empty; + w->layer2 = empty; + w->layer3 = empty; + + w->yokeOffset1 = 0; + w->yokeOffset2 = 0; + w->scaleByteLo = 0; + w->scaleByteHi = 0; + w->updateCounter = 0; + + w->componentX = 0; + w->componentZ = 0; + w->layerByte1 = 0; + w->surfaceFlag = 0; + + w->bankAccum = 0; + w->turbKick = 0; +} diff --git a/port/src/world.c b/port/src/world.c new file mode 100644 index 0000000..003f49b --- /dev/null +++ b/port/src/world.c @@ -0,0 +1,241 @@ +// 3D world fixture: a runway, a tower, a control building, a strip +// of water, and a row of mountains. World units are roughly metres. +// +// `worldRender` drives the chunk5-faithful scenery pipeline +// (sceneryProjection.c), so the demo data exercises the same math +// real FS2 scenery would. Each line projects via the L7EBC port and +// the L7C39 PerspectiveDivide port; output goes to the renderer's +// line primitive. + +#include +#include "camera.h" +#include "math6502.h" +#include "projection.h" +#include "sceneryProjection.h" +#include "types.h" +#include "world.h" + + +// Scratch pipeline shared across worldRender invocations. Each frame +// the world driver reseats the camera + matrix + per-altitude base +// before projecting vertices. +static SceneryPipelineT worldPipe; +static int worldPipeInited; + + +static int radarLineToScreen(const CameraT *cam, int16_t wx, int16_t wz, + int16_t metresPerPixel_q88, + int16_t cx, int16_t cy, + int16_t *outX, int16_t *outY); + + +// Each row defines one coloured line segment in world space. +// Coordinates: +X east, +Y up, +Z north (forward). +static const WorldLineT worldLines[] = { + // Runway outline at the origin, oriented along +Z. + { -10, 0, 0, 10, 0, 0, COLOR_RUNWAY }, + { 10, 0, 0, 10, 0, 200, COLOR_RUNWAY }, + { 10, 0, 200, -10, 0, 200, COLOR_RUNWAY }, + { -10, 0, 0, -10, 0, 200, COLOR_RUNWAY }, + // Centreline dashes + { 0, 0, 20, 0, 0, 30, COLOR_WHITE }, + { 0, 0, 50, 0, 0, 60, COLOR_WHITE }, + { 0, 0, 80, 0, 0, 90, COLOR_WHITE }, + { 0, 0, 110, 0, 0, 120, COLOR_WHITE }, + { 0, 0, 140, 0, 0, 150, COLOR_WHITE }, + { 0, 0, 170, 0, 0, 180, COLOR_WHITE }, + + // Control tower (a tall thin box) east of the runway. + { 35, 0, 30, 35, 25, 30, COLOR_BUILDING }, + { 45, 0, 30, 45, 25, 30, COLOR_BUILDING }, + { 35, 0, 40, 35, 25, 40, COLOR_BUILDING }, + { 45, 0, 40, 45, 25, 40, COLOR_BUILDING }, + { 35, 25, 30, 45, 25, 30, COLOR_BUILDING }, + { 35, 25, 40, 45, 25, 40, COLOR_BUILDING }, + { 35, 25, 30, 35, 25, 40, COLOR_BUILDING }, + { 45, 25, 30, 45, 25, 40, COLOR_BUILDING }, + + // A flat hangar west of the runway. + { -50, 0, 25, -50, 12, 25, COLOR_BUILDING }, + { -30, 0, 25, -30, 12, 25, COLOR_BUILDING }, + { -50, 0, 55, -50, 12, 55, COLOR_BUILDING }, + { -30, 0, 55, -30, 12, 55, COLOR_BUILDING }, + { -50, 12, 25, -30, 12, 25, COLOR_BUILDING }, + { -50, 12, 55, -30, 12, 55, COLOR_BUILDING }, + { -50, 12, 25, -50, 12, 55, COLOR_BUILDING }, + { -30, 12, 25, -30, 12, 55, COLOR_BUILDING }, + + // Water strip beyond the runway. + { -150, 0, 250, 150, 0, 250, COLOR_WATER }, + { -150, 0, 270, 150, 0, 270, COLOR_WATER }, + { -150, 0, 290, 150, 0, 290, COLOR_WATER }, + + // Mountain ridge much further away. + { -300, 0, 600, -200, 60, 580, COLOR_MOUNTAIN }, + { -200, 60, 580, -100, 30, 600, COLOR_MOUNTAIN }, + { -100, 30, 600, 0, 80, 620, COLOR_MOUNTAIN }, + { 0, 80, 620, 100, 25, 600, COLOR_MOUNTAIN }, + { 100, 25, 600, 220, 70, 580, COLOR_MOUNTAIN }, + { 220, 70, 580, 300, 0, 600, COLOR_MOUNTAIN }, + + // Ground reference grid (so motion is visible). + // North-south lines every 40 metres. + { -200, 0, -40, -200, 0, 600, COLOR_DIRT }, + { -120, 0, -40, -120, 0, 600, COLOR_DIRT }, + { -40, 0, -40, -40, 0, 0, COLOR_DIRT }, + { -40, 0, 200, -40, 0, 600, COLOR_DIRT }, + { 40, 0, -40, 40, 0, 0, COLOR_DIRT }, + { 40, 0, 200, 40, 0, 600, COLOR_DIRT }, + { 120, 0, -40, 120, 0, 600, COLOR_DIRT }, + { 200, 0, -40, 200, 0, 600, COLOR_DIRT }, + // East-west lines every 80 metres. + { -200, 0, 0, 200, 0, 0, COLOR_DIRT }, + { -200, 0, 80, 200, 0, 80, COLOR_DIRT }, + { -200, 0, 160, 200, 0, 160, COLOR_DIRT }, + { -200, 0, 320, 200, 0, 320, COLOR_DIRT }, + { -200, 0, 400, 200, 0, 400, COLOR_DIRT }, + { -200, 0, 480, 200, 0, 480, COLOR_DIRT }, + { -200, 0, 560, 200, 0, 560, COLOR_DIRT }, +}; + +#define WORLD_LINE_COUNT (sizeof(worldLines) / sizeof(worldLines[0])) + + +// Project a world point (X, Z) onto the radar viewport, accounting +// for camera yaw so North-up rotates with the player heading. +// `wx`/`wz` are world-unit metres (int16); `metresPerPixel_q88` is +// the inverse zoom in Q8.8 metres / pixel. +static int radarLineToScreen(const CameraT *cam, int16_t wx, int16_t wz, + int16_t metresPerPixel_q88, + int16_t cx, int16_t cy, + int16_t *outX, int16_t *outY) { + // Translate to camera-relative metres (drop the Q16.16 fraction). + int32_t dx = wx - (cam->worldX >> CAM_POS_FRACT_BITS); + int32_t dz = wz - (cam->worldZ >> CAM_POS_FRACT_BITS); + // Rotate by -yaw. sin/cos are Q1.15. + int32_t yawSin = math6502Sin(cam->yaw); + int32_t yawCos = math6502Cos(cam->yaw); + int32_t rx_metres = (dx * yawCos - dz * yawSin) >> 15; + int32_t rz_metres = (dx * yawSin + dz * yawCos) >> 15; + // Pixels = metres * 256 / metresPerPixel_q88. + if (metresPerPixel_q88 <= 0) { + return 0; + } + int32_t sx = (int32_t)cx + (rx_metres << 8) / metresPerPixel_q88; + int32_t sy = (int32_t)cy - (rz_metres << 8) / metresPerPixel_q88; + if (sx < -1024 || sx > 1024 || sy < -1024 || sy > 1024) { + return 0; + } + *outX = (int16_t)sx; + *outY = (int16_t)sy; + return 1; +} + + +void worldRenderRadar(const CameraT *cam, RenderStateT *renderer, int16_t metresPerPixel_q88) { + const int16_t cx = NATIVE_WIDTH / 2; + const int16_t cy = VIEWPORT_BOTTOM / 2; + for (size_t i = 0; i < WORLD_LINE_COUNT; i++) { + const WorldLineT *L = &worldLines[i]; + int16_t x1, y1, x2, y2; + if (!radarLineToScreen(cam, L->x1, L->z1, metresPerPixel_q88, cx, cy, &x1, &y1)) { + continue; + } + if (!radarLineToScreen(cam, L->x2, L->z2, metresPerPixel_q88, cx, cy, &x2, &y2)) { + continue; + } + rendererSetDrawColor(renderer, L->color); + rendererDrawLine(renderer, x1, y1, x2, y2); + } + // Player aircraft as a "+" at viewport centre. + rendererSetDrawColor(renderer, COLOR_WHITE); + rendererDrawLine(renderer, (int16_t)(cx - 4), cy, (int16_t)(cx + 4), cy); + rendererDrawLine(renderer, cx, (int16_t)(cy - 6), cx, (int16_t)(cy + 2)); +} + + +void worldRender(const CameraT *cam, RenderStateT *renderer) { + if (!worldPipeInited) { + sceneryPipelineReset(&worldPipe); + worldPipeInited = 1; + } + + // Push the camera state into the pipeline once per frame. + // Camera world coords are Q16.16; chunk5's $66/$67/$6A/$6B are + // int16 world units. Drop the fraction and clamp so flying + // past 32 km doesn't wrap negative. + int32_t wxUnits = cam->worldX >> CAM_POS_FRACT_BITS; + int32_t wzUnits = cam->worldZ >> CAM_POS_FRACT_BITS; + if (wxUnits > 32767) wxUnits = 32767; + if (wxUnits < -32768) wxUnits = -32768; + if (wzUnits > 32767) wzUnits = 32767; + if (wzUnits < -32768) wzUnits = -32768; + sceneryPipelineSetCamera(&worldPipe, (int16_t)wxUnits, (int16_t)wzUnits); + + int8_t rowX[3]; + int8_t rowZ[3]; + cameraGet2x3Matrix(cam, rowX, rowZ); + sceneryPipelineSetMatrix(&worldPipe, rowX, rowZ); + + for (size_t i = 0; i < WORLD_LINE_COUNT; i++) { + const WorldLineT *L = &worldLines[i]; + + // Per-line altitude is supplied via the section base + // ($4A/$4D/$50 in chunk5). The base contributes + // directly to camY in L7EBC's running accumulator, so + // an altitude of `y` becomes a baseY of + // `(int16_t)(y - cam->worldY)` scaled to match the + // camera-space rotation we already applied to the XZ + // plane. + // + // chunk5 expresses base as the *post-rotation* camera- + // space contribution -- the section center is fed into + // $18/$1B/$1E pre-rotation only when it's already in + // camera coords. Since worldLines[] uses world Y + // directly, we project the (0, y - camY, 0) vector + // through the camera rotation, then take that as the + // base. + // dy is in metres (drop Q16.16 fraction). rot is Q1.15; + // (rot * metres) >> 15 yields metres again. + int32_t dy = (int32_t)L->y1 - (cam->worldY >> CAM_POS_FRACT_BITS); + int16_t baseX = (int16_t)((cam->rot[0][1] * dy) >> CAM_ROT_FRACT_BITS); + int16_t baseY = (int16_t)((cam->rot[1][1] * dy) >> CAM_ROT_FRACT_BITS); + int16_t baseZ = (int16_t)((cam->rot[2][1] * dy) >> CAM_ROT_FRACT_BITS); + sceneryPipelineSetBase(&worldPipe, baseX, baseY, baseZ); + + SceneryVertexT a; + SceneryVertexT b; + sceneryProjectXZ(&worldPipe, (int16_t)L->x1, (int16_t)L->z1, &a); + // Re-set base for endpoint b in case y2 != y1 (the + // mountain ridge does this). Real scenery wouldn't, + // since both endpoints share the section base. + if (L->y2 != L->y1) { + int32_t dy2 = (int32_t)L->y2 - (cam->worldY >> CAM_POS_FRACT_BITS); + sceneryPipelineSetBase(&worldPipe, + (int16_t)((cam->rot[0][1] * dy2) >> CAM_ROT_FRACT_BITS), + (int16_t)((cam->rot[1][1] * dy2) >> CAM_ROT_FRACT_BITS), + (int16_t)((cam->rot[2][1] * dy2) >> CAM_ROT_FRACT_BITS)); + } + sceneryProjectXZ(&worldPipe, (int16_t)L->x2, (int16_t)L->z2, &b); + + // Trivial reject: both endpoints share an off-screen + // half-space (chunk5 $D3 "polygon outcode" AND test). + if ((a.outcode & b.outcode) != 0) { + continue; + } + + int16_t x1; + int16_t y1; + int16_t x2; + int16_t y2; + if (!sceneryProjectVertexToScreen(&a, &x1, &y1)) { + continue; + } + if (!sceneryProjectVertexToScreen(&b, &x2, &y2)) { + continue; + } + + rendererSetDrawColor(renderer, L->color); + rendererDrawLine(renderer, x1, y1, x2, y2); + } +} diff --git a/port/src/ww1ace.c b/port/src/ww1ace.c new file mode 100644 index 0000000..bbcaf91 --- /dev/null +++ b/port/src/ww1ace.c @@ -0,0 +1,712 @@ +// FS2 World War 1 Ace combat mode. See ww1ace.h for the data flow. + +#include +#include "camera.h" +#include "font.h" +#include "framebuffer.h" +#include "math6502.h" +#include "projection.h" +#include "renderer.h" +#include "types.h" +#include "ww1ace.h" + + +// Spawn ring around the player: enemies start `SPAWN_RADIUS` metres +// away and `SPAWN_ALTITUDE` metres above the ground. All other +// magnitudes are in world-units (metres) packed Q16.16. +#define SPAWN_RADIUS_M 400 +#define SPAWN_ALTITUDE_M 60 +#define HIT_RANGE_M 80 // gun-fire kill radius +#define HIT_RANGE_M2 (HIT_RANGE_M * HIT_RANGE_M) +#define ATTACK_DRIFT_Q88 77 // ~0.30 metres / frame +#define ATTACK_DRIFT_VERT_K 102 // 0.4 in Q8.8 (vertical scale on attack) +#define RETURN_DRIFT_Q88 154 // ~0.60 metres / frame +#define DAMAGE_PROXIMITY_M 60 +// AIM_CONE_DOT is 0.85 (cos of half-angle); we test the squared form +// `dotNum^2 >= AIM_CONE_DOT^2 * d^2` to avoid the sqrt. Q16 fixed: +// 0.85^2 ~= 0.7225 -> 47349 in Q16. We rescale below. +#define AIM_CONE_DOT2_Q16 47349 + +// Enemy fire-back parameters. +// ENEMY_FIRE_RANGE_M : max distance an enemy will open up +// ENEMY_FIRE_CONE_DOT2 : Q16 squared cone (~0.92 cos = ~23 deg half) +// ENEMY_FIRE_COOLDOWN_F : frames between an enemy's bursts +// BULLET_SPEED_Q88 : 110 metres / frame (~0.43 m/frame * 256) +// BULLET_LIFE_FRAMES : self-expire after ~1.5 s at 30 fps +// PLAYER_FIRE_COOLDOWN : throttles auto-repeat on the gun +// PLAYER_HIT_DAMAGE : damage units per enemy bullet impact +// PLAYER_LETHAL_DAMAGE : aircraft fails / crashes at this threshold +#define ENEMY_FIRE_RANGE_M 220 +#define ENEMY_FIRE_CONE_DOT2 56950 // 0.87^2 in Q16 +#define ENEMY_FIRE_COOLDOWN_F 24 +#define PLAYER_FIRE_COOLDOWN 8 +#define BULLET_SPEED_Q88 (110 * 256 / 5) // ~5.5 m/frame +#define BULLET_LIFE_FRAMES 45 +#define PLAYER_HIT_DAMAGE 4 +#define PLAYER_LETHAL_DAMAGE 200 + +// Bomb physics. Bombs inherit player velocity at drop time, then fall +// under gravity, accelerating downward. Impact at ground (Y <= 0) +// counts as a hit if within BOMB_BLAST_M metres of an enemy. +#define BOMB_GRAVITY_Q88 24 // 0.09 m/frame^2 +#define BOMB_BLAST_M 60 +#define BOMB_BLAST_M2 (BOMB_BLAST_M * BOMB_BLAST_M) + +// Maneuvering AI: enemies bank/jink via a sinusoidal phase advance. +// MANEUVER_RATE controls how fast the phase rolls (= turn rate). +#define MANEUVER_RATE 3 +#define MANEUVER_AMPL_Q88 200 // ~0.78 m/frame lateral jink + + +static uint16_t rngNext(WW1AceStateT *s); +static void drawEnemyMarker(const WW1EnemyT *e, const CameraT *cam, RenderStateT *renderer); +static void drawWarReportEnemyRow(FramebufferT *fb, int slot, WW1EnemyStatusE status); +static void respawnEnemy(WW1AceStateT *s, WW1EnemyT *e, int32_t playerX, int32_t playerZ); +static int spawnBullet(WW1AceStateT *s, bool fromEnemy, + int32_t x, int32_t y, int32_t z, + int16_t vx, int16_t vy, int16_t vz); +static bool updateBullets(WW1AceStateT *s, + int32_t playerX, int32_t playerY, int32_t playerZ); +static void updateBombs(WW1AceStateT *s); +static void enemyFireAtPlayer(WW1AceStateT *s, WW1EnemyT *e, + int32_t playerX, int32_t playerY, int32_t playerZ); +static void enemyManeuver(WW1AceStateT *s, WW1EnemyT *e, + int32_t playerX, int32_t playerY, int32_t playerZ); + +// Distance helpers operate on metres (Q16.16 worldUnit >> 16). Squaring +// would overflow int32 in Q16.16, so we drop to integer metres before +// squaring. +static int32_t metresFromQ1616(int32_t v_q1616); + + +// Tiny LCG (matches Mike Brennan's classic glibc parameters in spirit; +// good enough for AI jitter). +static uint16_t rngNext(WW1AceStateT *s) { + s->rngState = (uint16_t)(s->rngState * 25173u + 13849u); + return s->rngState; +} + + +// Render one enemy aircraft as a small horizontal line in 3D space. +// We project the centre of the enemy plus a wing-span pair, clip in +// camera space, and emit a `rendererDrawLine`. +static void drawEnemyMarker(const WW1EnemyT *e, const CameraT *cam, RenderStateT *renderer) { + const int32_t wingHalf_q1616 = 8 * CAM_POS_FRACT_ONE; + + ProjectedT a; + ProjectedT b; + cameraTransform(cam, e->worldX - wingHalf_q1616, e->worldY, e->worldZ, &a.cx, &a.cy, &a.cz); + cameraTransform(cam, e->worldX + wingHalf_q1616, e->worldY, e->worldZ, &b.cx, &b.cy, &b.cz); + a.outcode = projectionOutcode(a.cx, a.cy, a.cz); + b.outcode = projectionOutcode(b.cx, b.cy, b.cz); + if (!projectionClipLine(&a, &b)) { + return; + } + int16_t x1; + int16_t y1; + int16_t x2; + int16_t y2; + if (!projectionToScreen(a.cx, a.cy, a.cz, &x1, &y1)) { + return; + } + if (!projectionToScreen(b.cx, b.cy, b.cz, &x2, &y2)) { + return; + } + rendererSetDrawColor(renderer, COLOR_WHITE); + rendererDrawLine(renderer, x1, y1, x2, y2); + + // Vertical fin: short stem rising from the wing centre. + int16_t midX = (int16_t)((x1 + x2) / 2); + int16_t finY = (int16_t)((y1 + y2) / 2 - 3); + rendererDrawLine(renderer, midX, (int16_t)((y1 + y2) / 2), midX, finY); +} + + +static void drawWarReportEnemyRow(FramebufferT *fb, int slot, WW1EnemyStatusE status) { + char label[16]; + char value[2]; + int16_t x = (slot & 1) ? 132 : 16; + int16_t y = 96 + (slot / 2) * 12; + snprintf(label, sizeof(label), "ENEMY %d =", slot + 1); + snprintf(value, sizeof(value), "%d", (int)status); + fontDrawString(fb, x, y, label, COLOR_WHITE); + fontDrawString(fb, (int16_t)(x + 60), y, value, COLOR_WHITE); +} + + +static void respawnEnemy(WW1AceStateT *s, WW1EnemyT *e, int32_t playerX, int32_t playerZ) { + uint16_t r = rngNext(s); + uint8_t bearing = (uint8_t)(r & 0xFF); + // Project a SPAWN_RADIUS-metre ring around the player. + // sin/cos are Q1.15; world coords are Q16.16 metres. Multiply + // by the metre radius then shift to align fractional bits: + // (Q1.15 * metres) << 1 -> Q16.16 worldUnits (15 + 1 = 16). + int32_t cosB = math6502Cos(bearing); + int32_t sinB = math6502Sin(bearing); + e->status = WW1_ENEMY_ATTACKING; + e->worldX = playerX + ((cosB * SPAWN_RADIUS_M) << 1); + e->worldY = SPAWN_ALTITUDE_M * CAM_POS_FRACT_ONE; + e->worldZ = playerZ + ((sinB * SPAWN_RADIUS_M) << 1); + e->velX = 0; + e->velY = 0; + e->velZ = 0; + e->heading = (uint8_t)(r >> 8); + e->maneuverPhase = (uint8_t)(r ^ 0x5A); // desync per-enemy jink + e->fireCooldown = (uint8_t)(ENEMY_FIRE_COOLDOWN_F + (r & 0x1F)); +} + + +static int32_t metresFromQ1616(int32_t v_q1616) { + return v_q1616 >> CAM_POS_FRACT_BITS; +} + + +void ww1aceDrawWarReport(const WW1AceStateT *s, FramebufferT *fb) { + framebufferFillRect(fb, 0, 0, NATIVE_WIDTH, NATIVE_HEIGHT, COLOR_BLACK); + fontDrawString(fb, 60, 8, "***** WAR REPORT *****", COLOR_WHITE); + + char buf[40]; + snprintf(buf, sizeof(buf), "ENEMY PLANES SHOT DOWN = %3u", s->score); + fontDrawString(fb, 16, 32, buf, COLOR_WHITE); + snprintf(buf, sizeof(buf), "BOMB HITS = %3u", s->bombHits); + fontDrawString(fb, 16, 48, buf, COLOR_WHITE); + snprintf(buf, sizeof(buf), "AIRCRAFT DAMAGE BY ENEMY = %3u", s->damageByEnemy); + fontDrawString(fb, 16, 64, buf, COLOR_WHITE); + + fontDrawString(fb, 16, 80, "ENEMY STATUS: 0=SHOT DOWN", COLOR_WHITE); + fontDrawString(fb, 88, 88, "1=RETURNING OR HOME", COLOR_WHITE); + fontDrawString(fb, 88, 96, "2=ATTACKING", COLOR_WHITE); + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + drawWarReportEnemyRow(fb, i, s->enemies[i].status); + } + fontDrawString(fb, 16, 168, "PRESS ANY KEY TO RESUME BATTLE", COLOR_WHITE); +} + + +void ww1aceDropBomb(WW1AceStateT *s) { + if (!s->enabled) { + return; + } + if (s->bombs == 0) { + return; + } + s->bombs--; + // Drop a bomb from the player's reported position with a small + // downward velocity. Hit detection happens in updateBombs when + // the bomb impacts ground (Y <= 0). Caller passes player coords + // via ww1aceUpdate; we store nothing here since the player's + // world position is read from outside in ww1aceUpdate. For the + // drop, we use position (0, alt, 0) relative -- the next + // update tick we'll seed real coords. To keep the drop accurate + // we expose a second helper ww1aceDropBombAt below; this + // legacy entry just bumps the counter. + if (s->bombHits < 0xFFFF) { + // Pessimistic: don't auto-count as a hit anymore. + // Hits are awarded in updateBombs() on actual impact. + } +} + + +void ww1aceDropBombAt(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, + int16_t playerVelX_q88, int16_t playerVelZ_q88) { + if (!s->enabled || s->bombs == 0) { + return; + } + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + WW1BombT *b = &s->bombsInFlight[i]; + if (b->active) { + continue; + } + b->active = true; + b->worldX = playerX; + b->worldY = playerY; + b->worldZ = playerZ; + b->velX = playerVelX_q88; + b->velY = 0; // starts at zero, gravity accelerates + b->velZ = playerVelZ_q88; + s->bombs--; + return; + } +} + + +void ww1aceFireGun(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, uint8_t playerYaw) { + if (!s->enabled || s->playerFireCooldown != 0) { + return; + } + // Aim direction (forward unit vector at given yaw). Q1.15. + int32_t yawSin = math6502Sin(playerYaw); + int32_t yawCos = math6502Cos(playerYaw); + // Spawn a bullet flying along yaw direction. Y velocity is 0 + // (= flat shot); enemies above/below need to be in our forward + // cone for the bullet's hit-radius to catch them. + int16_t bvx = (int16_t)((yawSin * BULLET_SPEED_Q88) >> 15); + int16_t bvz = (int16_t)((yawCos * BULLET_SPEED_Q88) >> 15); + spawnBullet(s, false, playerX, playerY, playerZ, bvx, 0, bvz); + s->playerFireCooldown = PLAYER_FIRE_COOLDOWN; +} + + +// Bombsight crosshair drawn at the centre of the viewport when WW1 is +// active. Mirrors FS2 chunk3 BombSightOverlayPixels: concentric ring +// with crosshair through the centre (a stand-in for the original +// pre-rendered bitmap). +static void ww1aceDrawBombsight(FramebufferT *fb) { + const int16_t cx = NATIVE_WIDTH / 2; + const int16_t cy = VIEWPORT_BOTTOM / 2; + + // Outer ring: 8 short tick marks at radius 14. 360/45 = 8 + // around the circle == byte-angle step of 32 (256 / 8). + const int16_t r = 14; + for (uint16_t step = 0; step < 8; step++) { + uint8_t ang = (uint8_t)(step * 32); + int32_t cosA = math6502Cos(ang); // Q1.15 + int32_t sinA = math6502Sin(ang); + int16_t ax = (int16_t)(cx + ((cosA * r ) >> 15)); + int16_t ay = (int16_t)(cy + ((sinA * r ) >> 15)); + int16_t bx = (int16_t)(cx + ((cosA * (r + 4)) >> 15)); + int16_t by = (int16_t)(cy + ((sinA * (r + 4)) >> 15)); + if (ax < 0 || ax >= NATIVE_WIDTH || bx < 0 || bx >= NATIVE_WIDTH) { + continue; + } + if (ay < 0 || ay >= NATIVE_HEIGHT || by < 0 || by >= NATIVE_HEIGHT) { + continue; + } + fb->pixels[ay * NATIVE_WIDTH + ax] = (uint8_t)COLOR_WHITE; + fb->pixels[by * NATIVE_WIDTH + bx] = (uint8_t)COLOR_WHITE; + } + // Crosshair through the centre. + for (int16_t d = -8; d <= 8; d++) { + int16_t x = (int16_t)(cx + d); + int16_t y = (int16_t)(cy + d); + if (x >= 0 && x < NATIVE_WIDTH) { + fb->pixels[cy * NATIVE_WIDTH + x] = (uint8_t)COLOR_WHITE; + } + if (y >= 0 && y < NATIVE_HEIGHT) { + fb->pixels[y * NATIVE_WIDTH + cx] = (uint8_t)COLOR_WHITE; + } + } +} + + +void ww1aceHudDraw(const WW1AceStateT *s, FramebufferT *fb) { + if (!s->enabled) { + return; + } + char buf[32]; + snprintf(buf, sizeof(buf), "ACE %3u BOMBS %02u", s->score, s->bombs); + fontDrawString(fb, 90, 4, buf, COLOR_WHITE); + ww1aceDrawBombsight(fb); +} + + +void ww1aceInit(WW1AceStateT *s) { + s->enabled = false; + s->showWarReport = false; + s->score = 0; + s->bombs = 8; + s->damageByEnemy = 0; + s->bombHits = 0; + s->playerFireCooldown = 0; + s->rngState = 0xACE1; + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + s->enemies[i].status = WW1_ENEMY_SHOT_DOWN; + s->enemies[i].worldX = 0; + s->enemies[i].worldY = 0; + s->enemies[i].worldZ = 0; + s->enemies[i].velX = 0; + s->enemies[i].velY = 0; + s->enemies[i].velZ = 0; + s->enemies[i].heading = 0; + s->enemies[i].maneuverPhase = 0; + s->enemies[i].fireCooldown = 0; + } + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + s->bullets[i].active = false; + } + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + s->bombsInFlight[i].active = false; + } +} + + +void ww1aceRender(const WW1AceStateT *s, const CameraT *cam, RenderStateT *renderer) { + if (!s->enabled) { + return; + } + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + if (s->enemies[i].status == WW1_ENEMY_SHOT_DOWN) { + continue; + } + drawEnemyMarker(&s->enemies[i], cam, renderer); + } +} + + +void ww1aceToggle(WW1AceStateT *s, int32_t playerX, int32_t playerZ) { + s->enabled = !s->enabled; + if (s->enabled) { + s->score = 0; + s->bombs = 8; + s->damageByEnemy = 0; + s->bombHits = 0; + s->playerFireCooldown = 0; + s->showWarReport = false; + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + respawnEnemy(s, &s->enemies[i], playerX, playerZ); + } + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + s->bullets[i].active = false; + } + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + s->bombsInFlight[i].active = false; + } + } else { + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + s->enemies[i].status = WW1_ENEMY_SHOT_DOWN; + } + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + s->bullets[i].active = false; + } + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + s->bombsInFlight[i].active = false; + } + } +} + + +// Spawn a bullet into the free pool. Returns the slot index or -1 if +// the pool is full. +static int spawnBullet(WW1AceStateT *s, bool fromEnemy, + int32_t x, int32_t y, int32_t z, + int16_t vx, int16_t vy, int16_t vz) { + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + if (s->bullets[i].active) { + continue; + } + s->bullets[i].active = true; + s->bullets[i].fromEnemy = fromEnemy; + s->bullets[i].worldX = x; + s->bullets[i].worldY = y; + s->bullets[i].worldZ = z; + s->bullets[i].velX = vx; + s->bullets[i].velY = vy; + s->bullets[i].velZ = vz; + s->bullets[i].framesLeft = BULLET_LIFE_FRAMES; + return i; + } + return -1; +} + + +// Advance bullets, expire stale ones, check hits. +// Player bullets hit any ATTACKING enemy within HIT_RANGE_M / 2. +// Enemy bullets hit player within DAMAGE_PROXIMITY_M. +// Returns true if a lethal player hit occurred this frame. +static bool updateBullets(WW1AceStateT *s, + int32_t playerX, int32_t playerY, int32_t playerZ) { + bool lethal = false; + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + WW1BulletT *b = &s->bullets[i]; + if (!b->active) { + continue; + } + // Q8.8 velocity contributes the upper byte directly to + // Q16.16 position; the lower 8 bits accumulate too via + // the shifted multiply. Net: position += vel << 8. + b->worldX += (int32_t)b->velX << 8; + b->worldY += (int32_t)b->velY << 8; + b->worldZ += (int32_t)b->velZ << 8; + if (b->framesLeft == 0) { + b->active = false; + continue; + } + b->framesLeft--; + + if (b->fromEnemy) { + int32_t dx = metresFromQ1616(b->worldX - playerX); + int32_t dy = metresFromQ1616(b->worldY - playerY); + int32_t dz = metresFromQ1616(b->worldZ - playerZ); + int32_t d2 = dx * dx + dy * dy + dz * dz; + if (d2 <= DAMAGE_PROXIMITY_M * DAMAGE_PROXIMITY_M) { + if (s->damageByEnemy < 0xFFFF) { + s->damageByEnemy = (uint16_t) + (s->damageByEnemy + PLAYER_HIT_DAMAGE); + } + if (s->damageByEnemy >= PLAYER_LETHAL_DAMAGE) { + lethal = true; + } + b->active = false; + } + } else { + // Player bullet -- check each attacking enemy. + int hitRange = HIT_RANGE_M / 2; + int hitRange2 = hitRange * hitRange; + for (int k = 0; k < WW1_ENEMY_COUNT; k++) { + WW1EnemyT *e = &s->enemies[k]; + if (e->status != WW1_ENEMY_ATTACKING) { + continue; + } + int32_t dx = metresFromQ1616(b->worldX - e->worldX); + int32_t dy = metresFromQ1616(b->worldY - e->worldY); + int32_t dz = metresFromQ1616(b->worldZ - e->worldZ); + int32_t d2 = dx * dx + dy * dy + dz * dz; + if (d2 <= hitRange2) { + e->status = WW1_ENEMY_SHOT_DOWN; + if (s->score < 0xFFFF) { + s->score++; + } + b->active = false; + break; + } + } + } + } + return lethal; +} + + +// Advance bombs in flight under gravity. On Y <= 0 impact, check +// nearby SHOT_DOWN / RETURNING ground positions; any enemy in the +// blast radius counts as a hit and gets respawned via WW1_ENEMY_SHOT_DOWN. +static void updateBombs(WW1AceStateT *s) { + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + WW1BombT *b = &s->bombsInFlight[i]; + if (!b->active) { + continue; + } + b->velY -= BOMB_GRAVITY_Q88; // accumulating fall + b->worldX += (int32_t)b->velX << 8; + b->worldY += (int32_t)b->velY << 8; + b->worldZ += (int32_t)b->velZ << 8; + if (b->worldY > 0) { + continue; + } + // Impact. Look for an enemy within blast radius. + bool hit = false; + for (int k = 0; k < WW1_ENEMY_COUNT; k++) { + WW1EnemyT *e = &s->enemies[k]; + if (e->status == WW1_ENEMY_SHOT_DOWN) { + continue; + } + int32_t dx = metresFromQ1616(b->worldX - e->worldX); + int32_t dz = metresFromQ1616(b->worldZ - e->worldZ); + if (dx * dx + dz * dz <= BOMB_BLAST_M2) { + e->status = WW1_ENEMY_SHOT_DOWN; + if (s->score < 0xFFFF) { + s->score++; + } + hit = true; + break; + } + } + if (hit && s->bombHits < 0xFFFF) { + s->bombHits++; + } + b->active = false; + } +} + + +// Enemy AI: maneuver toward player with sinusoidal lateral jink so the +// approach isn't a straight line. Also climb to player altitude when +// significantly below or above. +static void enemyManeuver(WW1AceStateT *s, WW1EnemyT *e, + int32_t playerX, int32_t playerY, int32_t playerZ) { + (void)s; + int32_t dx = metresFromQ1616(playerX - e->worldX); + int32_t dy = metresFromQ1616(playerY - e->worldY); + int32_t dz = metresFromQ1616(playerZ - e->worldZ); + uint32_t d2 = (uint32_t)(dx * dx + dz * dz + 1); + uint16_t d = math6502Sqrt(d2); + if (d == 0) { + d = 1; + } + // Forward step toward player (Q8.8 metres / frame). + int32_t fwdX = (dx * ATTACK_DRIFT_Q88) / (int32_t)d; + int32_t fwdZ = (dz * ATTACK_DRIFT_Q88) / (int32_t)d; + // Vertical step: scaled down so vertical changes are gentler. + int32_t vY = (dy * ATTACK_DRIFT_Q88 * ATTACK_DRIFT_VERT_K) + / ((int32_t)d * 256); + // Lateral jink: rotate the forward direction by ±90 deg and + // modulate by sin(maneuverPhase) so the enemy weaves. + int32_t jinkAmount = (int32_t)math6502Sin(e->maneuverPhase); // Q1.15 + int32_t latX = (-fwdZ * jinkAmount) >> 15; + int32_t latZ = ( fwdX * jinkAmount) >> 15; + latX = (latX * MANEUVER_AMPL_Q88) >> 8; + latZ = (latZ * MANEUVER_AMPL_Q88) >> 8; + + e->velX = (int16_t)(fwdX + latX); + e->velY = (int16_t)vY; + e->velZ = (int16_t)(fwdZ + latZ); + e->worldX += (int32_t)e->velX << 8; + e->worldY += (int32_t)e->velY << 8; + e->worldZ += (int32_t)e->velZ << 8; + e->maneuverPhase = (uint8_t)(e->maneuverPhase + MANEUVER_RATE); + + // Update heading toward velocity vector (rough; byte angle). + // atan2(velX, velZ) -> byte angle. Cheap approximation via the + // dominant component. + if (e->velX != 0 || e->velZ != 0) { + // Build a byte angle by interpolating between quadrant + // boundaries based on |velX| vs |velZ|. + int16_t ax = e->velX < 0 ? (int16_t)-e->velX : e->velX; + int16_t az = e->velZ < 0 ? (int16_t)-e->velZ : e->velZ; + uint16_t denom = (uint16_t)(ax + az + 1); + uint8_t octant = (uint8_t)(((uint32_t)ax * 32) / denom); + if (e->velZ >= 0) { + e->heading = (e->velX >= 0) ? (uint8_t)octant + : (uint8_t)(0 - octant); + } else { + e->heading = (e->velX >= 0) ? (uint8_t)(128 - octant) + : (uint8_t)(128 + octant); + } + } +} + + +// Enemy fires a bullet at the player when in range AND aimed forward +// (= dot of (enemy->player) with enemy heading vector is positive and +// well above the cone threshold). Cooldown throttles bursts. +static void enemyFireAtPlayer(WW1AceStateT *s, WW1EnemyT *e, + int32_t playerX, int32_t playerY, int32_t playerZ) { + if (e->fireCooldown > 0) { + e->fireCooldown--; + return; + } + int32_t dx = metresFromQ1616(playerX - e->worldX); + int32_t dy = metresFromQ1616(playerY - e->worldY); + int32_t dz = metresFromQ1616(playerZ - e->worldZ); + int32_t d2 = dx * dx + dy * dy + dz * dz; + if (d2 > ENEMY_FIRE_RANGE_M * ENEMY_FIRE_RANGE_M) { + return; + } + if (d2 < HIT_RANGE_M2 / 4) { + // Too close to bother firing; we're already in damage + // proximity. + return; + } + // Heading vector (Q1.15) from enemy's byte angle. + int32_t hX = math6502Sin(e->heading); + int32_t hZ = math6502Cos(e->heading); + // Cone test, same shape as the player's gun. + int64_t dotNum = (int64_t)dx * hX + (int64_t)dz * hZ; + if (dotNum <= 0) { + return; + } + int64_t lhs = (dotNum >> 15) * (dotNum >> 15); + int64_t rhs = ((int64_t)d2 * ENEMY_FIRE_CONE_DOT2) >> 16; + if (lhs < rhs) { + return; + } + // Fire: bullet starts at enemy position, moves toward player. + uint16_t d = math6502Sqrt((uint32_t)d2 + 1); + if (d == 0) { + d = 1; + } + int16_t bvx = (int16_t)((dx * BULLET_SPEED_Q88) / (int32_t)d); + int16_t bvy = (int16_t)((dy * BULLET_SPEED_Q88) / (int32_t)d); + int16_t bvz = (int16_t)((dz * BULLET_SPEED_Q88) / (int32_t)d); + spawnBullet(s, true, e->worldX, e->worldY, e->worldZ, bvx, bvy, bvz); + e->fireCooldown = ENEMY_FIRE_COOLDOWN_F; +} + + +bool ww1aceUpdate(WW1AceStateT *s, int32_t playerX, int32_t playerY, int32_t playerZ, + uint8_t playerYaw) { + (void)playerYaw; + if (!s->enabled) { + return false; + } + if (s->playerFireCooldown > 0) { + s->playerFireCooldown--; + } + for (int i = 0; i < WW1_ENEMY_COUNT; i++) { + WW1EnemyT *e = &s->enemies[i]; + switch (e->status) { + case WW1_ENEMY_ATTACKING: { + enemyManeuver(s, e, playerX, playerY, playerZ); + enemyFireAtPlayer(s, e, playerX, playerY, playerZ); + // Proximity ramming damage (= mid-air + // collision risk). Same shape as before + // but using the post-maneuver position. + int32_t dx = metresFromQ1616(playerX - e->worldX); + int32_t dy = metresFromQ1616(playerY - e->worldY); + int32_t dz = metresFromQ1616(playerZ - e->worldZ); + uint16_t d = math6502Sqrt(dx * dx + dy * dy + dz * dz); + if (d < DAMAGE_PROXIMITY_M && (rngNext(s) & 0xFF) < 4) { + if (s->damageByEnemy < 0xFFFF) { + s->damageByEnemy++; + } + } + break; + } + case WW1_ENEMY_RETURNING: { + int32_t dx = metresFromQ1616(e->worldX - playerX); + int32_t dz = metresFromQ1616(e->worldZ - playerZ); + uint16_t d = math6502Sqrt(dx * dx + dz * dz); + if (d > 0) { + int32_t stepX = (dx * RETURN_DRIFT_Q88) / (int32_t)d; + int32_t stepZ = (dz * RETURN_DRIFT_Q88) / (int32_t)d; + e->worldX += stepX << 8; + e->worldZ += stepZ << 8; + } + if ((int32_t)d > SPAWN_RADIUS_M + (SPAWN_RADIUS_M >> 1)) { + respawnEnemy(s, e, playerX, playerZ); + } + break; + } + case WW1_ENEMY_SHOT_DOWN: + default: + break; + } + } + bool lethal = updateBullets(s, playerX, playerY, playerZ); + updateBombs(s); + return lethal; +} + + +// Render bullets + bombs as dots. +void ww1aceRenderProjectiles(const WW1AceStateT *s, const CameraT *cam, RenderStateT *renderer) { + if (!s->enabled) { + return; + } + for (int i = 0; i < WW1_BULLET_COUNT; i++) { + const WW1BulletT *b = &s->bullets[i]; + if (!b->active) { + continue; + } + int32_t cx, cy, cz; + cameraTransform(cam, b->worldX, b->worldY, b->worldZ, &cx, &cy, &cz); + if (cz <= 0) { + continue; + } + int16_t sx, sy; + if (!projectionToScreen(cx, cy, cz, &sx, &sy)) { + continue; + } + rendererSetDrawColor(renderer, + b->fromEnemy ? COLOR_ORANGE : COLOR_WHITE); + // 2x1 dash so a single round is visible. + rendererDrawLine(renderer, sx, sy, (int16_t)(sx + 1), sy); + } + for (int i = 0; i < WW1_BOMB_COUNT; i++) { + const WW1BombT *b = &s->bombsInFlight[i]; + if (!b->active) { + continue; + } + int32_t cx, cy, cz; + cameraTransform(cam, b->worldX, b->worldY, b->worldZ, &cx, &cy, &cz); + if (cz <= 0) { + continue; + } + int16_t sx, sy; + if (!projectionToScreen(cx, cy, cz, &sx, &sy)) { + continue; + } + rendererSetDrawColor(renderer, COLOR_WHITE); + // Vertical 1x2 dash so a falling bomb reads as such. + rendererDrawLine(renderer, sx, sy, sx, (int16_t)(sy + 1)); + } +} diff --git a/port/tools/chunk5SetupTest.c b/port/tools/chunk5SetupTest.c new file mode 100644 index 0000000..998e0d7 --- /dev/null +++ b/port/tools/chunk5SetupTest.c @@ -0,0 +1,209 @@ +// Validates the C transliteration in chunk5Setup.c against the +// fs2trace oracle (which runs the actual chunk5/chunk4 binaries on +// a 6502 emulator). + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include "chunk5Setup.h" + + +static int runOracle(const char *cmd, int *out) { + FILE *p = popen(cmd, "r"); + if (p == NULL) { + fprintf(stderr, "popen failed: %s\n", cmd); + return -1; + } + char line[256]; + long val = 0; + int found = 0; + while (fgets(line, sizeof(line), p) != NULL) { + char *eq = strrchr(line, '='); + if (eq != NULL) { + val = strtol(eq + 1, NULL, 10); + found = 1; + } + } + pclose(p); + if (!found) { + return -1; + } + *out = (int)val; + return 0; +} + + +static int testScale(int16_t a, int16_t b) { + int16_t mine = chunk5ScaleC2ByC4(a, b); + char cmd[128]; + snprintf(cmd, sizeof(cmd), + "/home/scott/claude/flight/port/bin/fs2trace --zpscale %d %d 2>/dev/null", + (int)a, (int)b); + int oracle; + if (runOracle(cmd, &oracle) != 0) { + fprintf(stderr, "oracle failed for (%d, %d)\n", (int)a, (int)b); + return -1; + } + if (mine != oracle) { + printf(" MISMATCH: ScaleC2ByC4(%6d, %6d) = %6d oracle=%6d delta=%+d\n", + (int)a, (int)b, (int)mine, oracle, (int)mine - oracle); + return 1; + } + return 0; +} + + +static int testL177B(uint8_t a, uint8_t x) { + int16_t mine = chunk5L177B(a, x); + char cmd[128]; + snprintf(cmd, sizeof(cmd), + "/home/scott/claude/flight/port/bin/fs2trace --l177b %d %d 2>/dev/null", + (int)a, (int)x); + int oracle; + if (runOracle(cmd, &oracle) != 0) { + fprintf(stderr, "oracle failed for L177B(%d, %d)\n", (int)a, (int)x); + return -1; + } + if (mine != oracle) { + printf(" MISMATCH: L177B(%3d, %3d) = %6d oracle=%6d delta=%+d\n", + (int)a, (int)x, (int)mine, oracle, (int)mine - oracle); + return 1; + } + return 0; +} + + +int main(void) { + // Print L177B cos lookup samples for direct inspection. + printf("L177B cos lookups (sub=0):\n"); + for (int a = 0; a <= 256; a += 32) { + int16_t v = chunk5L177B((uint8_t)(a & 0xFF), 0); + printf(" L177B(byte=%3d, x=0) = %6d\n", a & 0xFF, v); + } + + int rc = chunk5SetupSelfTest(); + printf("\nL177B self-test: %s (case %d)\n", rc == 0 ? "PASS" : "FAIL", -rc); + + printf("\nScaleC2ByC4 vs fs2trace --zpscale oracle:\n"); + struct { int16_t a, b; } cases[] = { + { 0, 16384 }, { 16384, 0 }, + { 1, 1 }, { -1, -1 }, + { 100, 100 }, { -100, 100 }, + { 256, 256 }, { 1024, 1024 }, + { 4096, 4096 }, { 8192, 8192 }, + { 16383, 16383 }, { 16384, 16384 }, + { 16383, 32767 }, { 16384, 32767 }, + { 32767, 32767 }, { -32768, 32767 }, + { 32767, -32768 }, { 1, 32767 }, + { 2, 32767 }, { 3, 32767 }, + { 10000, 10000 }, { 20000, 30000 }, + { -109, 32767 }, { -1234, 5678 }, + { 16383, 16384 }, { 16384, 16383 }, + }; + int fails = 0; + for (size_t i = 0; i < sizeof(cases)/sizeof(cases[0]); i++) { + if (testScale(cases[i].a, cases[i].b) > 0) { + fails++; + } + } + if (fails == 0) { + printf(" all %zu cases PASS\n", sizeof(cases)/sizeof(cases[0])); + } else { + printf(" %d/%zu cases failed\n", fails, + sizeof(cases)/sizeof(cases[0])); + } + + // Sweep L177B over byte angles (every 8) and sub-byte values + // (every 32). ~256 calls; each is one fs2trace invocation, + // total ~5 sec. + printf("\nL177B sweep vs fs2trace --l177b oracle:\n"); + int l177bFails = 0; + int l177bTotal = 0; + for (int a = 0; a < 256; a += 8) { + for (int x = 0; x < 256; x += 32) { + l177bTotal++; + if (testL177B((uint8_t)a, (uint8_t)x) > 0) { + l177bFails++; + } + } + } + if (l177bFails == 0) { + printf(" all %d L177B cases PASS\n", l177bTotal); + } else { + printf(" %d/%d L177B cases failed\n", l177bFails, l177bTotal); + } + + // Sweep the full SetupViewProjection cascade against + // fs2trace --matrix. + printf("\nSetupViewProjection sweep vs fs2trace --matrix oracle:\n"); + struct { int16_t y, p, b; uint8_t vd; } svpCases[] = { + { 0, 0, 0, 0 }, + { -109, 0, 0, 0 }, + { -109, 0, 0, 15 }, + { 16384, 0, 0, 0 }, + { 0, 16384, 0, 0 }, + { 0, 0, 16384, 0 }, + { 8000, 4000, 0, 0 }, + { 8000, 4000, 0, 3 }, + {-12345, 500, 1000, 7 }, + }; + int svpFails = 0; + for (size_t i = 0; i < sizeof(svpCases)/sizeof(svpCases[0]); i++) { + int16_t mine[3][3]; + chunk5SetupViewProjection(svpCases[i].y, svpCases[i].p, + svpCases[i].b, svpCases[i].vd, 0, + mine); + char cmd[256]; + snprintf(cmd, sizeof(cmd), + "FS2TRACE_USE_ORIG=1 " + "/home/scott/claude/flight/port/bin/fs2trace --matrix " + "%d %d %d %d 2>/dev/null", + (int)svpCases[i].y, (int)svpCases[i].p, + (int)svpCases[i].b, (int)svpCases[i].vd); + FILE *p = popen(cmd, "r"); + int16_t oracle[3][3] = {{0}}; + if (p != NULL) { + char buf[256]; + int row = 0; + while (fgets(buf, sizeof(buf), p) != NULL) { + int v0, v1, v2; + if (sscanf(buf, " row %*d: %d %d %d", &v0, &v1, &v2) == 3 + && row < 3) { + oracle[row][0] = (int16_t)v0; + oracle[row][1] = (int16_t)v1; + oracle[row][2] = (int16_t)v2; + row++; + } + } + pclose(p); + } + bool match = true; + for (int r = 0; r < 3; r++) { + for (int c = 0; c < 3; c++) { + if (mine[r][c] != oracle[r][c]) match = false; + } + } + if (!match) { + printf(" MISMATCH (yaw=%d pitch=%d bank=%d vd=%d):\n", + svpCases[i].y, svpCases[i].p, svpCases[i].b, svpCases[i].vd); + for (int r = 0; r < 3; r++) { + printf(" mine row %d: %6d %6d %6d | oracle: %6d %6d %6d\n", + r, mine[r][0], mine[r][1], mine[r][2], + oracle[r][0], oracle[r][1], oracle[r][2]); + } + svpFails++; + } + } + if (svpFails == 0) { + printf(" all %zu SetupViewProjection cases PASS\n", + sizeof(svpCases)/sizeof(svpCases[0])); + } else { + printf(" %d/%zu SetupViewProjection cases failed\n", + svpFails, sizeof(svpCases)/sizeof(svpCases[0])); + } + return (rc != 0 || fails > 0 || l177bFails > 0 || svpFails > 0) ? 1 : 0; +} diff --git a/port/tools/dumpStations.c b/port/tools/dumpStations.c new file mode 100644 index 0000000..38d6698 --- /dev/null +++ b/port/tools/dumpStations.c @@ -0,0 +1,277 @@ +// Offline scenery station dumper. +// +// Loads an A2.SD* scenery file and runs the port's actual scenery +// VM (port/sceneryVm.c) over it -- same dispatcher, same advance +// counts, same SceneryOpInvalid termination -- to collect every ADF +// ($05), NAV ($1D), and COM ($1E) station record reachable from +// every entry point in the FS2 content region. +// +// Differs from extractstations.c (the original hand-rolled walker) +// in two ways: +// +// 1. Uses the same code path the running game would, so any future +// changes to the dispatcher are picked up here automatically. +// 2. Records are dispatched through a callback instead of a global +// array, so the tool can dedupe / filter inline. +// +// Stubs out renderer/palette since we don't draw anything on a station +// scan -- those calls only fire on $06 (DrawLine) and $12 (SetColor) +// records and have no bearing on station extraction. + +#include +#include +#include +#include + +#include "sceneryVm.h" +#include "palette.h" + + +typedef struct CollectedT { + SceneryStationT station; + char nameBuf[16]; +} CollectedT; + + +#define MAX_COLLECTED 8192 + + +static CollectedT collected[MAX_COLLECTED]; +static int collectedCount; + + +// BCD validation -- same rules as extractstations.c, lifted from +// chunk5 DecodeBCDFreqString and chunk3 LookupADFStation. +static int adfFreqValid(uint16_t freq); +static int bcdNibblesValid(uint8_t b); +static int comFreqValid(uint16_t freq); +static int navFreqValid(uint16_t freq); +static void onStation(SceneryStateT *state, const SceneryStationT *s); +static int processFile(const char *path); + + +static int bcdNibblesValid(uint8_t b) { + return ((b & 0x0F) <= 0x09) && ((b >> 4) <= 0x09); +} + + +static int adfFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo)) { + return 0; + } + if ((hi & 0xF0) != 0x00 || (hi & 0x0F) > 0x09) { + return 0; + } + int khz = (hi & 0x0F) * 100 + ((lo >> 4) * 10) + (lo & 0x0F); + return (khz >= 200 && khz <= 999); +} + + +static int navFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { + return 0; + } + if (hi == 0x10) { + return lo >= 0x80; + } + if (hi == 0x11) { + return lo <= 0x79; + } + return 0; +} + + +static int comFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { + return 0; + } + if (hi == 0x11) { + return lo >= 0x80; + } + if (hi == 0x12) { + return 1; + } + if (hi == 0x13) { + return lo <= 0x69; + } + return 0; +} + + +static void onStation(SceneryStateT *state, const SceneryStationT *s) { + (void)state; + if (collectedCount >= MAX_COLLECTED) { + return; + } + switch (s->type) { + case SCENERY_STATION_ADF: + if (!adfFreqValid(s->freq)) return; + break; + case SCENERY_STATION_NAV: + if (!navFreqValid(s->freq)) return; + break; + case SCENERY_STATION_COM: + if (!comFreqValid(s->freq)) return; + break; + } + CollectedT *c = &collected[collectedCount++]; + c->station = *s; + c->nameBuf[0] = '\0'; + if (s->name != NULL) { + size_t n = strlen(s->name); + if (n >= sizeof(c->nameBuf)) { + n = sizeof(c->nameBuf) - 1; + } + memcpy(c->nameBuf, s->name, n); + c->nameBuf[n] = '\0'; + c->station.name = c->nameBuf; + } +} + + +static int processFile(const char *path) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", path); + return 0; + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc((size_t)sz); + if (buf == NULL || fread(buf, 1, (size_t)sz, f) != (size_t)sz) { + fclose(f); + free(buf); + return 0; + } + fclose(f); + + // Use a heap-allocated state to keep the 200KB visited array + // off the stack. + SceneryStateT *state = malloc(sizeof(SceneryStateT)); + if (state == NULL) { + free(buf); + return 0; + } + sceneryInit(state, buf, (uint32_t)sz, NULL); + sceneryAttachStationCb(state, onStation, NULL); + + int before = collectedCount; + // Walk every byte in the FS2 content region. The cycle guard + // inside sceneryRun makes this O(file size). + for (uint32_t entry = 0x2000; entry < (uint32_t)sz; entry++) { + sceneryWalkFrom(state, entry); + } + + int adf = 0; + int nav = 0; + int com = 0; + for (int i = before; i < collectedCount; i++) { + switch (collected[i].station.type) { + case SCENERY_STATION_ADF: adf++; break; + case SCENERY_STATION_NAV: nav++; break; + case SCENERY_STATION_COM: com++; break; + } + } + printf("%-40s ADF=%d NAV=%d COM=%d\n", path, adf, nav, com); + + free(state); + free(buf); + return 1; +} + + +int main(int argc, char **argv) { + if (argc < 2) { + fprintf(stderr, "usage: %s file.SD [file.SD ...]\n", argv[0]); + return 1; + } + + int adf = 0; + int nav = 0; + int com = 0; + for (int i = 1; i < argc; i++) { + if (!processFile(argv[i])) { + continue; + } + } + for (int i = 0; i < collectedCount; i++) { + switch (collected[i].station.type) { + case SCENERY_STATION_ADF: adf++; break; + case SCENERY_STATION_NAV: nav++; break; + case SCENERY_STATION_COM: com++; break; + } + } + printf("\ntotal: %d ADF, %d NAV, %d COM\n", adf, nav, com); + for (int i = 0; i < collectedCount; i++) { + const SceneryStationT *s = &collected[i].station; + printf(" %c freq=$%04X x=%d y=%d z=%d %s\n", + (char)s->type, s->freq, s->x, s->y, s->z, + s->name != NULL ? s->name : ""); + } + return 0; +} + + +// --- Stubs so we don't have to link the SDL-driven renderer --- + +void rendererSetDrawColor(struct RenderStateT *state, ColorE color) { + (void)state; + (void)color; +} + + +void rendererSetHiresColor(struct RenderStateT *state, uint8_t hiresCode) { + (void)state; + (void)hiresCode; +} + + +void rendererDrawLine(struct RenderStateT *state, int16_t x1, int16_t y1, int16_t x2, int16_t y2) { + (void)state; + (void)x1; + (void)y1; + (void)x2; + (void)y2; +} + + +void rendererDrawColorSpan(struct RenderStateT *state, int16_t xRight, int16_t length, int16_t y) { + (void)state; + (void)xRight; + (void)length; + (void)y; +} + + +void rendererFillPolygon(struct RenderStateT *state, const int16_t *xs, const int16_t *ys, int count) { + (void)state; + (void)xs; + (void)ys; + (void)count; +} + + +ColorE paletteFromSceneryCode(uint8_t code) { + (void)code; + return COLOR_BLACK; +} + + +// sceneryAttachCamera (in sceneryVm.c) calls into camera.c -- stub +// since station scans don't need a real camera matrix. +void cameraGet2x3Matrix(const struct CameraT *cam, int8_t outRowX[3], int8_t outRowZ[3]) { + (void)cam; + outRowX[0] = 0; + outRowX[1] = 0; + outRowX[2] = 0; + outRowZ[0] = 0; + outRowZ[1] = 0; + outRowZ[2] = 0; +} diff --git a/port/tools/extractCityPolygons.c b/port/tools/extractCityPolygons.c new file mode 100644 index 0000000..043e912 --- /dev/null +++ b/port/tools/extractCityPolygons.c @@ -0,0 +1,68 @@ +// Build a port-compatible RAM dump that has the FS2.1 polygon +// region (file offset $D000+) placed at the dispatcher-expected dest +// address ($BD28 in chicago city RAM). +// +// Usage: extractCityPolygons +// +// The chunk5 standard scenery loader (FetchSectorFromDisk → +// ReadBlocks) can't reach FS2.1 blocks past index ~$5F, so the +// polygon region at file offset $D000+ never makes it into RAM via +// fs2trace's normal boot. We manually copy that region into the dump +// at $BD28 (overlaying the database-only section that chunk5 loaded). + +#include +#include +#include +#include + + +int main(int argc, char **argv) { + if (argc != 4) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + + FILE *fr = fopen(argv[1], "rb"); + if (fr == NULL) { perror(argv[1]); return 1; } + uint8_t ram[65536]; + if (fread(ram, 1, 65536, fr) != 65536) { + fprintf(stderr, "%s: not 64K\n", argv[1]); + fclose(fr); + return 1; + } + fclose(fr); + + FILE *fp = fopen(argv[2], "rb"); + if (fp == NULL) { perror(argv[2]); return 1; } + fseek(fp, 0, SEEK_END); + long pSize = ftell(fp); + fseek(fp, 0, SEEK_SET); + uint8_t *poly = malloc(pSize); + fread(poly, 1, pSize, fp); + fclose(fp); + + // Polygon region in FS2.1: file offset $D000 onwards (~block + // 104+). Copy it on top of the city dump's $BD28 so the + // dispatcher's HEADER target now points to actual polygons + // instead of the COM/NAV database. + uint16_t dest = 0xBD28; + long polyOff = 0xD000; + long maxLen = 0xC000 - dest; // don't cross into $C000 I/O region + long avail = pSize - polyOff; + long copyLen = (avail < maxLen) ? avail : maxLen; + if (copyLen <= 0) { + fprintf(stderr, "polygon region empty\n"); + free(poly); + return 1; + } + memcpy(&ram[dest], &poly[polyOff], copyLen); + + FILE *fo = fopen(argv[3], "wb"); + if (fo == NULL) { perror(argv[3]); free(poly); return 1; } + fwrite(ram, 1, 65536, fo); + fclose(fo); + free(poly); + fprintf(stderr, "Copied %ld bytes from %s @ $%lX to %s @ $%04X\n", + copyLen, argv[2], polyOff, argv[3], dest); + return 0; +} diff --git a/port/tools/extractFS2Sections.c b/port/tools/extractFS2Sections.c new file mode 100644 index 0000000..36c2392 --- /dev/null +++ b/port/tools/extractFS2Sections.c @@ -0,0 +1,207 @@ +// Walk the FS2.1 ProDOS scenery file and emit each section as a +// separate clean bytecode blob. No 6502 emulation, no 64K dump +// scaffolding. Just file -> per-section files. +// +// A "section" in chunk5 terms is a sequence of scenery opcodes loaded +// by a $0D HEADER. We don't have an explicit section table, so we +// scan for plausible section starts ($13 cull or $21 cull at sector +// boundaries) and walk forward until we hit a stream terminator +// ($79+, $80+, or an invalid opcode). For each one we emit: +// +// out_section_.bin -- raw bytes from start to terminator +// +// This is a best-effort static dump. The output isn't guaranteed to +// be a complete chunk5 section (some have $0B back-jumps that +// reference yet other regions of the file), but it gives us clean +// chunks to inspect, diff, and load into the port without the boot +// machinery overhead. + +#include +#include +#include +#include + + +static int isTerminator(uint8_t op) { + // chunk5 dispatcher: opcodes with bit 7 set or >= $46 fall off + // the SceneryOpcodeTable and reset the stream. + if (op & 0x80) return 1; + if (op >= 0x46) return 1; + // Specific $08/$0C/$0F/$10/$15-17/$1F/$26-27/$2A/$2C-2E/$30/ + // $34/$36-3F/$43-45 are SceneryOpInvalid and also terminate. + switch (op) { + case 0x08: case 0x0C: case 0x0F: case 0x10: + case 0x15: case 0x16: case 0x17: + case 0x1F: + case 0x26: case 0x27: case 0x2A: + case 0x2C: case 0x2D: case 0x2E: + case 0x30: + case 0x34: + case 0x36: case 0x37: case 0x38: case 0x39: + case 0x3B: case 0x3C: case 0x3D: case 0x3E: case 0x3F: + case 0x43: case 0x44: case 0x45: + return 1; + } + return 0; +} + + +// Static-walk advance for each opcode. Returns 0 if we should stop +// (terminator or unknown that needs more state). Best-effort -- no +// full state machine. +static int advanceLength(uint8_t op, const uint8_t *p, size_t avail) { + switch (op) { + case 0x00: case 0x01: case 0x02: + case 0x40: case 0x41: + return 5; // emit-vertex 5-byte + case 0x42: + return 6; // refresh-cache: op + idx + 4 vertex bytes + case 0x03: + return 6; // SceneryOpCall64K_2 + case 0x04: + // CullByOutcodeList: walks vertex idx bytes + // until terminator with bit 7 set. + { + size_t off = 3; + while (off < avail) { + if (p[off++] & 0x80) break; + } + return (int)off; + } + case 0x05: // ADF station + return 9; + case 0x06: // DrawLine + return 5; + case 0x07: // EnterLocalFrame + return 14; + case 0x09: case 0x0A: // SKIP3 + return 3; + case 0x0B: // REL_JUMP -- can't follow statically; stop + return 0; + case 0x0D: // HEADER + return 6; + case 0x0E: + return 1; + case 0x11: // SKIP_1 + return 1; + case 0x12: // SetColor + return 2; + case 0x13: case 0x14: // JumpIfBeyondXY/XYZ -- consume but don't follow + return 9; + case 0x18: // SUB_INVOKE -- can't follow statically; stop + return 0; + case 0x19: // RETURN + return 0; + case 0x1A: // WriteWord + return 5; + case 0x1B: case 0x1C: // ModeWhite / DayOnly + return 1; + case 0x1D: // NAV station + return 11; + case 0x1E: // COM record (variable length, byte 1 = total) + if (avail < 2) return 0; + return p[1]; + case 0x20: // CullN_1 + return 9; + case 0x21: // CullN_2 + return 15; + case 0x22: // CullN_3 + return 21; + case 0x23: // JumpIfBitsClear + return 7; + case 0x24: // PushOriginWithStash + return 8; + case 0x25: // StoreImmWord + return 5; + case 0x28: // JumpIfWordCompare + return 8; + case 0x29: // CopyToD2 + return 1; + case 0x2B: // EmitCurve (best-effort) + return 12; + case 0x2F: // ResetState + return 1; + case 0x31: case 0x32: case 0x33: + case 0x35: + return 2; + } + return 0; // unknown / can't advance +} + + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + FILE *f = fopen(argv[1], "rb"); + if (f == NULL) { perror(argv[1]); return 1; } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc(size); + fread(buf, 1, size, f); + fclose(f); + + printf("FS2.1: %ld bytes\n", size); + + int sectionsFound = 0; + int reachedSection = 0; + long off = 0; + while (off < size) { + // Plausible section start: $13 ($14) cull or $21 + // ($20/$22) cull at a 256-byte boundary. + uint8_t op = buf[off]; + int sectorBoundary = (off & 0xFF) == 0; + int looksLikeSection = sectorBoundary && ( + op == 0x13 || op == 0x14 || + op == 0x20 || op == 0x21 || op == 0x22); + if (!looksLikeSection) { + off++; + continue; + } + + // Walk forward, counting bytes until terminator. + long start = off; + long polygonOps = 0; + long opCount = 0; + while (off < size) { + uint8_t cur = buf[off]; + if (isTerminator(cur)) break; + int len = advanceLength(cur, &buf[off], (size_t)(size - off)); + if (len <= 0) { + // $0B / $18 / $19 -- can't follow. + break; + } + opCount++; + if (cur == 0x00 || cur == 0x40 || cur == 0x41) { + polygonOps++; + } + if (off + len > size) break; + off += len; + } + + long sectionLen = off - start; + if (sectionLen >= 16 && polygonOps > 0) { + char name[256]; + snprintf(name, sizeof(name), + "%s/section_%05lX_polyops%ld_oplen%ld.bin", + argv[2], start, polygonOps, opCount); + FILE *out = fopen(name, "wb"); + if (out != NULL) { + fwrite(&buf[start], 1, sectionLen, out); + fclose(out); + printf(" $%05lX: %ld bytes, %ld ops, %ld polygon emits -> %s\n", + start, sectionLen, opCount, polygonOps, name); + if (polygonOps >= 5) reachedSection++; + } + sectionsFound++; + } + if (off == start) off++; // avoid infinite loop on stuck cursor + } + + printf("\n%d candidate sections found, %d with >=5 polygon emits\n", + sectionsFound, reachedSection); + free(buf); + return 0; +} diff --git a/port/tools/extractscenery.c b/port/tools/extractscenery.c new file mode 100644 index 0000000..1617ac8 --- /dev/null +++ b/port/tools/extractscenery.c @@ -0,0 +1,103 @@ +// FS2 scenery section extractor. +// +// Each FS2 scenery disk is loaded by chunk5 via six descriptors at +// LA60F..LA619 in chunk5 (verified in src/chunk5.s): +// +// LA60F $0322 LoadSceneryFile0 +// LA611 $0625 LoadSceneryFile1 +// LA613 $032D LoadSceneryFile2 +// LA615 $012B LoadSceneryFile3 +// LA617 $0142 LoadSceneryFile4 +// LA619 $012C bootstrap (Common) +// +// The descriptor encodes (high = sector count, low = starting sector). +// Each sector is 256 bytes; sector N lives at file offset N*256. The +// san-inc pack zeroes tracks 0-1 of every scenery disk (boot loader +// stripped) so sectors 0..31 are zero-filled and the real content +// begins at sector $20 (file offset 0x2000). +// +// `LoadSceneryFileCommon` also re-runs the loader for the bootstrap +// descriptor on every call, but with a destination address read from +// the first word of the just-loaded file. So the file's first two +// bytes are a "load me here" pointer. +// +// This tool dumps each named section to a flat .bin so we can inspect +// the byte layout and feed the chunks into the C port. + +#include +#include +#include +#include + + +typedef struct DescriptorT { + const char *name; + uint16_t descriptor; // raw FS2 16-bit value +} DescriptorT; + + +static const DescriptorT descriptors[] = { + { "File0", 0x0322 }, + { "File1", 0x0625 }, + { "File2", 0x032D }, + { "File3", 0x012B }, + { "File4", 0x0142 }, + { "bootstrap", 0x012C }, + { NULL, 0 } +}; + + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s in.dsk out_dir\n", argv[0]); + return 1; + } + FILE *f = fopen(argv[1], "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", argv[1]); + return 1; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc((size_t)size); + if (buf == NULL || fread(buf, 1, (size_t)size, f) != (size_t)size) { + fprintf(stderr, "could not read %s\n", argv[1]); + fclose(f); + free(buf); + return 1; + } + fclose(f); + + for (int i = 0; descriptors[i].name != NULL; i++) { + uint16_t d = descriptors[i].descriptor; + uint8_t startSector = (uint8_t)(d & 0xFF); + uint8_t count = (uint8_t)((d >> 8) & 0xFF); + uint32_t offset = (uint32_t)startSector * 256; + uint32_t length = (uint32_t)count * 256; + if (offset + length > (uint32_t)size) { + fprintf(stderr, "%s: out of range (offset 0x%X, length %u, file %ld)\n", + descriptors[i].name, offset, length, size); + continue; + } + + char outPath[1024]; + snprintf(outPath, sizeof(outPath), "%s/%s.bin", argv[2], descriptors[i].name); + FILE *out = fopen(outPath, "wb"); + if (out == NULL) { + fprintf(stderr, "cannot open %s\n", outPath); + continue; + } + fwrite(buf + offset, 1, length, out); + fclose(out); + + // Quick header preview: file's first word is the + // suggested load address. + uint16_t loadAddr = (uint16_t)(buf[offset] | (buf[offset + 1] << 8)); + printf("%-10s sector=$%02X count=%u file_offset=0x%04X length=%u hdr_word=$%04X -> %s\n", + descriptors[i].name, startSector, count, offset, length, loadAddr, outPath); + } + + free(buf); + return 0; +} diff --git a/port/tools/extractstations.c b/port/tools/extractstations.c new file mode 100644 index 0000000..e5b9e8d --- /dev/null +++ b/port/tools/extractstations.c @@ -0,0 +1,519 @@ +// FS2 station extractor. +// +// Walks the scenery bytecode in each extracted A2.SD* file and pulls +// out NAV ($1D), ADF ($05), and COM ($1E) station records. With the +// loader correctly identifying the bytecode entry as file offset +// 0x9000 (verified via fs2trace), this gives us the per-disk station +// database that drives the VOR1/VOR2 + ADF + COM panel readouts. +// +// Each disk's bytecode ends in a RETURN ($19) or terminator (>= $46 +// or bit 7 set). We scan until that point, plus an optional second +// pass starting from the next plausible bytecode block (file offsets +// 0xA000, 0xC000) to catch records reachable via JMP/sub-invoke that +// our static walker can't follow. + +#include +#include +#include +#include + + +#define BYTECODE_ENTRY 0x9000 +#define MAX_STATIONS 4096 + + +typedef struct StationT { + uint16_t freq; // 16-bit frequency identifier (encoding varies by record type) + int32_t x; // 24-bit signed position (X / east) + int32_t y; // 24-bit signed position (Y / north) + int32_t z; // optional 24-bit Z (NAV only) + char name[16]; + char type; // 'A'=ADF, 'N'=NAV, 'C'=COM +} StationT; + + +static int adv[256]; +static int stationCount; +static StationT stations[MAX_STATIONS]; + + +static int bcdNibblesValid(uint8_t b); +static int comFreqValid(uint16_t freq); +static int adfFreqValid(uint16_t freq); +static int navFreqValid(uint16_t freq); +static int32_t signed24(uint8_t lo, uint8_t mid, uint8_t hi); +static int scanBytecode(const uint8_t *bytes, int length, int entry, int regionIdx); + + +// FS2 packs station frequencies in BCD (chunk5 DecodeBCDFreqString, +// chunk3 LookupADFStation). Reject any record whose freq bytes have +// non-BCD nibbles or fall outside the legal radio band -- those are +// almost always our brute-force scan misreading vertex / polygon +// data as a station record. + +static int bcdNibblesValid(uint8_t b) { + return ((b & 0x0F) <= 0x09) && ((b >> 4) <= 0x09); +} + + +// ADF: byte 1 = ADFFreqLowPacked (BCD pair, mid+low digits), byte 2 = +// ADFFreqHighDigit (single digit -- high nibble must be 0). chunk3 +// packs as e.g. $34, $02 -> displayed as "234" kHz, so the 3-digit +// range here is 200..999 kHz (NDB beacons; FS2 doesn't represent the +// upper 1605..1750 kHz portion since it lacks a 4th digit). +static int adfFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo)) { + return 0; + } + if ((hi & 0xF0) != 0x00 || (hi & 0x0F) > 0x09) { + return 0; + } + int khz = (hi & 0x0F) * 100 + ((lo >> 4) * 10) + (lo & 0x0F); + return (khz >= 200 && khz <= 999); +} + + +// NAV (VOR): byte 1 / byte 2 are the BCD pairs from +// DecodeBCDFreqString -- byte1 = lower 2 digits, byte2 = upper 2. +// Legal civil VOR band is 108.00..117.95 MHz: hi=$10 requires lo>=$80 +// (108.0..109.95) and hi=$11 requires lo<=$79 (110.0..117.95). +static int navFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { + return 0; + } + if (hi == 0x10) { + return lo >= 0x80; + } + if (hi == 0x11) { + return lo <= 0x79; + } + return 0; +} + + +// COM (airport tower / ATIS): same BCD packing as NAV. Apple II FS2 +// uses the civil aviation 118..137 MHz band; high-byte BCD is $11 +// ($1180+), $12, or $13 ($1300..$1369). +static int comFreqValid(uint16_t freq) { + uint8_t lo = (uint8_t)(freq & 0xFF); + uint8_t hi = (uint8_t)(freq >> 8); + if (!bcdNibblesValid(lo) || !bcdNibblesValid(hi)) { + return 0; + } + if (hi == 0x11) { + return lo >= 0x80; + } + if (hi == 0x12) { + return 1; + } + if (hi == 0x13) { + return lo <= 0x69; + } + return 0; +} + + +static int32_t signed24(uint8_t lo, uint8_t mid, uint8_t hi) { + uint32_t u = (uint32_t)lo | ((uint32_t)mid << 8) | ((uint32_t)hi << 16); + // Sign extend if bit 23 set. + if (u & 0x800000) { + u |= 0xFF000000; + } + return (int32_t)u; +} + + +// `visited` tracks positions we've already walked from in this file +// pass to break cycles introduced by JMP / SUB_INVOKE chains. Sized +// to a typical scenery file (143 KB). +static uint8_t visited[200000]; + + +static int scanBytecode(const uint8_t *bytes, int length, int entry, int regionIdx) { + (void)regionIdx; + int pos = entry; + int found = 0; + int ops = 0; + if (pos < 0 || pos >= length || pos >= (int)sizeof(visited)) { + return 0; + } + while (pos < length && pos < (int)sizeof(visited)) { + if (visited[pos]) { + break; + } + visited[pos] = 1; + uint8_t op = bytes[pos]; + if ((op & 0x80) || op >= 0x46) { + break; + } + int a = adv[op]; + + // Follow $0B (JMP relative). Operand is a signed 16-bit + // offset from the current position. Recurse so the + // primary walk continues past this branch. + if (op == 0x0B && pos + 3 <= length) { + int16_t off = (int16_t)(bytes[pos + 1] | (bytes[pos + 2] << 8)); + scanBytecode(bytes, length, pos + off, regionIdx); + // Don't fall through past JMP -- it's + // unconditional, so we're done with this branch. + break; + } + // Follow $18 (sub-invoke). Operand is a signed 16-bit + // relative offset to the subroutine; on return ($19) + // we continue past the 3-byte sub-invoke record. + if (op == 0x18 && pos + 3 <= length) { + int16_t off = (int16_t)(bytes[pos + 1] | (bytes[pos + 2] << 8)); + scanBytecode(bytes, length, pos + off, regionIdx); + // After sub-invoke, fall through to advance 3. + } + // Follow cull-on-outside jump targets ($20/$21/$22). + // Each record's first 2 bytes after the opcode are a + // signed 16-bit relative target; the FS2 dispatcher + // takes that target when the cull range test fails. + // We try BOTH branches statically since we don't know + // the camera state at extract time. + if ((op == 0x20 || op == 0x21 || op == 0x22) && pos + 3 <= length) { + int16_t off = (int16_t)(bytes[pos + 1] | (bytes[pos + 2] << 8)); + scanBytecode(bytes, length, pos + off, regionIdx); + // Fall through to advance past the inside-path + // record (9 / 15 / 21 bytes). + } + + // Pull station data before advancing. + if (op == 0x05 && pos + 9 <= length && stationCount < MAX_STATIONS) { + uint16_t freq = (uint16_t)(bytes[pos + 1] | (bytes[pos + 2] << 8)); + if (adfFreqValid(freq)) { + StationT *s = &stations[stationCount++]; + s->type = 'A'; + s->freq = freq; + s->x = signed24(bytes[pos + 3], bytes[pos + 4], bytes[pos + 5]); + s->y = signed24(bytes[pos + 6], bytes[pos + 7], bytes[pos + 8]); + s->z = 0; + s->name[0] = '\0'; + found++; + } + } + if (op == 0x1D && pos + 11 <= length && stationCount < MAX_STATIONS) { + uint16_t freq = (uint16_t)(bytes[pos + 1] | (bytes[pos + 2] << 8)); + if (navFreqValid(freq)) { + StationT *s = &stations[stationCount++]; + s->type = 'N'; + s->freq = freq; + s->x = signed24(bytes[pos + 3], bytes[pos + 4], bytes[pos + 5]); + s->y = signed24(bytes[pos + 6], bytes[pos + 7], bytes[pos + 8]); + s->z = signed24(bytes[pos + 8], bytes[pos + 9], bytes[pos + 10]); + s->name[0] = '\0'; + found++; + } + } + if (op == 0x1E) { + if (pos + 1 >= length) { + break; + } + int recLen = bytes[pos + 1]; + if (pos + recLen + 2 <= length && stationCount < MAX_STATIONS) { + uint16_t freq = (uint16_t)(bytes[pos + 2] | (bytes[pos + 3] << 8)); + if (comFreqValid(freq)) { + StationT *s = &stations[stationCount++]; + s->type = 'C'; + s->freq = freq; + s->x = signed24(bytes[pos + 4], bytes[pos + 5], bytes[pos + 6]); + s->y = signed24(bytes[pos + 7], bytes[pos + 8], bytes[pos + 9]); + s->z = 0; + int nameStart = pos + 13; + int nameLen = recLen - 11; + if (nameLen > 0 && nameLen < (int)sizeof(s->name) - 1) { + // Apple II text storage uses bit 7 to mark + // "normal" characters (set high bit). Mask + // it off before validating; bytes that are + // still non-printable after masking get '?'. + for (int i = 0; i < nameLen; i++) { + uint8_t c = bytes[nameStart + i] & 0x7F; + s->name[i] = (c >= 32 && c < 127) ? (char)c : '?'; + } + s->name[nameLen] = '\0'; + } else { + s->name[0] = '\0'; + } + found++; + } + } + a = recLen + 2; + } + + if (a < 0) { + // SceneryOpInvalid in chunk5 -- chunk dispatcher + // would reset state and bail. Treat as a stream + // terminator rather than guessing. + break; + } + if (a == 0) { + // Opcode not in our table at all (not an + // explicit Invalid either). Advance 1 byte and + // try again -- cycle detection bounds the damage + // and we may still hit stations downstream. + a = 1; + } + pos += a; + ops++; + if (ops > 100000) { + break; + } + } + return ops; +} + + +// Region tag attached to each station so the C-emit output records +// where a station was first seen (useful when debugging). +static char stationRegion[MAX_STATIONS]; + + +// Dedupe stations by (type, freq, x, y). z is excluded because the +// extractor occasionally reads z=0 vs z=junk depending on whether the +// trailing bytes of an 11-byte NAV record line up with surrounding +// scenery; type/freq/x/y are reliable. +static int stationKeyEq(const StationT *a, const StationT *b) { + return a->type == b->type + && a->freq == b->freq + && a->x == b->x + && a->y == b->y; +} + + +// Stable sort key: type then freq then x then y. Lets the embedded +// array be scanned linearly with cache-friendly behaviour. +static int stationCmp(const void *pa, const void *pb) { + const StationT *a = pa; + const StationT *b = pb; + if (a->type != b->type) return (a->type < b->type) ? -1 : 1; + if (a->freq != b->freq) return (a->freq < b->freq) ? -1 : 1; + if (a->x != b->x ) return (a->x < b->x ) ? -1 : 1; + if (a->y != b->y ) return (a->y < b->y ) ? -1 : 1; + return 0; +} + + +int main(int argc, char **argv) { + const char *cOutput = NULL; + int argi = 1; + while (argi < argc && argv[argi][0] == '-') { + if (strcmp(argv[argi], "--c-output") == 0 && argi + 1 < argc) { + cOutput = argv[++argi]; + argi++; + continue; + } + fprintf(stderr, "unknown option %s\n", argv[argi]); + return 1; + } + if (argi >= argc) { + fprintf(stderr, "usage: %s [--c-output FILE] file.dsk [file.dsk ...]\n", argv[0]); + return 1; + } + + // Advance counts pulled from chunk5 dispatcher (scanning every + // `lda #$N; jmp SceneryOpAdvanceAndContinue` / `jmp AddTo8B` + // exit). Most opcodes fall through their main path; a few + // (vertex-emit family) take different advances depending on + // L7EBC's inner branch -- I use the most common 5-byte path. + int t[][2] = { + { 0x00, 5 }, { 0x01, 5 }, { 0x02, 5 }, // L69C8/L6888/L689F: L7EBC reads 4 stream bytes then `lda #$05; jmp AddTo8B` (chunk5 line 4356) + { 0x03, 6 }, { 0x04, 2 }, { 0x05, 9 }, // Call64K_2 + skip + ADF station + { 0x06, 5 }, { 0x07, 14 }, // DrawLine + L6BA0 + { 0x09, 3 }, { 0x0A, 3 }, { 0x0B, 3 }, // Skip3 + JMP relative + { 0x0D, 6 }, { 0x0E, 1 }, // Header + Call64K (no-op fall through) + { 0x11, 1 }, { 0x12, 2 }, // Skip1 + SetColor + { 0x13, 10 }, { 0x14, 10 }, // L6E17-driven (advance 9 + 1 from caller) + { 0x18, 3 }, { 0x19, 1 }, // SubInvoke + Return + { 0x1A, 5 }, { 0x1B, 1 }, { 0x1C, 1 }, // L6D66 + ModeWhite + DayOnly + { 0x1D, 11 }, // NAV station + { 0x20, 9 }, { 0x21, 15 }, { 0x22, 21 }, // 1/2/3-axis cull + { 0x23, 7 }, { 0x24, 8 }, { 0x25, 5 }, // L6EF1 + L6B64 + L6D8E + { 0x28, 8 }, { 0x29, 1 }, { 0x2B, 5 }, // L6F1D + CopyToD2 + L69E6 (guess) + { 0x2F, 1 }, + { 0x31, 2 }, { 0x32, 2 }, { 0x33, 2 }, { 0x35, 2 }, // L6987-family: 1 opcode + 1 vertex-index byte + { 0x40, 5 }, { 0x41, 5 }, { 0x42, 5 } // L688F/L68A6/L694E: same L7EBC tail as $00-$02 + }; + for (size_t i = 0; i < sizeof(t) / sizeof(t[0]); i++) { + adv[t[i][0]] = t[i][1]; + } + + // Opcodes that dispatch to SceneryOpInvalid in chunk5 -- treat + // them as stream terminators so the walker doesn't desync by + // guessing a 1-byte advance over what's almost certainly data + // misaligned as bytecode. Marked with -1 (sentinel for "halt"). + int invalid[] = { + 0x08, 0x0C, 0x0F, 0x10, + 0x15, 0x16, 0x17, 0x1F, + 0x26, 0x27, 0x2A, 0x2C, + 0x2D, 0x2E, 0x30, 0x34, + 0x36, 0x37, 0x38, 0x39, + 0x3A, 0x3B, 0x3C, 0x3D, + 0x3E, 0x3F, 0x43, 0x44, 0x45 + }; + for (size_t i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { + adv[invalid[i]] = -1; + } + + int totalAdf = 0; + int totalNav = 0; + int totalCom = 0; + + for (; argi < argc; argi++) { + FILE *f = fopen(argv[argi], "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", argv[argi]); + continue; + } + fseek(f, 0, SEEK_END); + long sz = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc((size_t)sz); + if (buf == NULL || fread(buf, 1, (size_t)sz, f) != (size_t)sz) { + fclose(f); + free(buf); + continue; + } + fclose(f); + + int beforeAdf = 0, beforeNav = 0, beforeCom = 0; + for (int i = 0; i < stationCount; i++) { + if (stations[i].type == 'A') beforeAdf++; + if (stations[i].type == 'N') beforeNav++; + if (stations[i].type == 'C') beforeCom++; + } + int countBefore = stationCount; + + // Reset visited tracker for this file. + memset(visited, 0, sizeof(visited)); + + // Walk from every byte in the FS2 content region. + // Real scenery records are nested inside outer opcodes + // (e.g. a NAV record at file offset $01452 sits in the + // payload of the $13 record at $01449), so 256-byte + // sector starts miss most of them. The `visited` guard + // makes the walk O(file_size) overall: once a position + // is visited any later entry that reaches it bails + // immediately. SceneryOpInvalid + freq validation prune + // the false-record paths. + for (int entry = 0x2000; entry < (int)sz; entry++) { + scanBytecode(buf, (int)sz, entry, argi); + } + + int gotAdf = 0, gotNav = 0, gotCom = 0; + for (int i = countBefore; i < stationCount; i++) { + if (stations[i].type == 'A') gotAdf++; + if (stations[i].type == 'N') gotNav++; + if (stations[i].type == 'C') gotCom++; + // Tag with region label (last path component + // after the last '/' or '\\') for the C-emit + // comment. + const char *base = argv[argi]; + for (const char *p = argv[argi]; *p != '\0'; p++) { + if (*p == '/' || *p == '\\') { + base = p + 1; + } + } + // Pack first letter of region tail (e.g. "A2.SD3" -> '3'). + char tag = '?'; + for (const char *p = base; *p != '\0'; p++) { + if (*p >= '0' && *p <= '9') { tag = *p; break; } + } + stationRegion[i] = tag; + } + fprintf(stderr, "%-40s ADF=%d NAV=%d COM=%d\n", argv[argi], gotAdf, gotNav, gotCom); + (void)beforeAdf; (void)beforeNav; (void)beforeCom; + totalAdf += gotAdf; + totalNav += gotNav; + totalCom += gotCom; + + free(buf); + } + + // Dedupe (cross-region): same physical station appears in + // every overlapping disk, plus the brute-force walker hits + // each in-disk record from multiple aliasing entry points. + qsort(stations, (size_t)stationCount, sizeof(stations[0]), stationCmp); + int unique = 0; + for (int i = 0; i < stationCount; i++) { + if (unique > 0 && stationKeyEq(&stations[i], &stations[unique - 1])) { + // Prefer the entry that has a name attached + // (COM records carry airport names). + if (stations[unique - 1].name[0] == '\0' && stations[i].name[0] != '\0') { + stations[unique - 1] = stations[i]; + stationRegion[unique - 1] = stationRegion[i]; + } + continue; + } + stations[unique] = stations[i]; + stationRegion[unique] = stationRegion[i]; + unique++; + } + int beforeUnique = stationCount; + stationCount = unique; + + int uniqueAdf = 0, uniqueNav = 0, uniqueCom = 0; + for (int i = 0; i < stationCount; i++) { + if (stations[i].type == 'A') uniqueAdf++; + if (stations[i].type == 'N') uniqueNav++; + if (stations[i].type == 'C') uniqueCom++; + } + + fprintf(stderr, "\nraw: %d ADF, %d NAV, %d COM (%d total)\n", totalAdf, totalNav, totalCom, beforeUnique); + fprintf(stderr, "unique: %d ADF, %d NAV, %d COM (%d total)\n", uniqueAdf, uniqueNav, uniqueCom, stationCount); + + if (cOutput != NULL) { + FILE *out = fopen(cOutput, "w"); + if (out == NULL) { + fprintf(stderr, "cannot write %s\n", cOutput); + return 1; + } + fprintf(out, "// Generated by port/tools/extractstations --c-output. Do not edit.\n"); + fprintf(out, "// Source: A2.SD* scenery files. %d unique stations.\n\n", stationCount); + fprintf(out, "#ifndef SCENERY_STATIONS_DATA_H\n"); + fprintf(out, "#define SCENERY_STATIONS_DATA_H\n\n"); + fprintf(out, "#include \n\n"); + fprintf(out, "typedef struct StationDataT {\n"); + fprintf(out, " char type; // 'A'=ADF, 'N'=NAV, 'C'=COM\n"); + fprintf(out, " uint16_t freq; // BCD-packed (NAV/COM) or BCD pair + high digit (ADF)\n"); + fprintf(out, " int32_t x; // FS2 scenery units, +X = east\n"); + fprintf(out, " int32_t y; // +Y = north\n"); + fprintf(out, " int32_t z; // NAV only (altitude); 0 for ADF/COM\n"); + fprintf(out, " char name[16]; // COM airport name; \"\" otherwise\n"); + fprintf(out, " char region; // ASCII digit of source A2.SD* file\n"); + fprintf(out, "} StationDataT;\n\n"); + fprintf(out, "static const StationDataT kSceneryStations[] = {\n"); + for (int i = 0; i < stationCount; i++) { + StationT *s = &stations[i]; + fprintf(out, " { '%c', 0x%04X, %9d, %9d, %9d, \"", + s->type, s->freq, s->x, s->y, s->z); + for (int j = 0; j < (int)sizeof(s->name) && s->name[j] != '\0'; j++) { + char c = s->name[j]; + if (c == '"' || c == '\\') { + fputc('\\', out); + } + fputc(c, out); + } + fprintf(out, "\", '%c' },\n", stationRegion[i]); + } + fprintf(out, "};\n\n"); + fprintf(out, "#define SCENERY_STATIONS_COUNT ((int)(sizeof(kSceneryStations) / sizeof(kSceneryStations[0])))\n\n"); + fprintf(out, "#endif\n"); + fclose(out); + fprintf(stderr, "wrote %s (%d entries)\n", cOutput, stationCount); + return 0; + } + + printf("\ntotal raw: %d ADF, %d NAV, %d COM\n", totalAdf, totalNav, totalCom); + printf("unique: %d ADF, %d NAV, %d COM\n", uniqueAdf, uniqueNav, uniqueCom); + for (int i = 0; i < stationCount; i++) { + StationT *s = &stations[i]; + printf(" %c freq=$%04X x=%d y=%d z=%d %s\n", + s->type, s->freq, s->x, s->y, s->z, s->name); + } + return 0; +} diff --git a/port/tools/findPolySections.c b/port/tools/findPolySections.c new file mode 100644 index 0000000..8353b89 --- /dev/null +++ b/port/tools/findPolySections.c @@ -0,0 +1,97 @@ +// Scan an FS2 scenery file for clusters of vertex-emit opcodes. +// chunk5 vertex opcodes are $00, $01, $02, $40, $41, $42 (5 bytes each +// in 80C5/7EBC variants), and the cached-vertex ops $32/$33/$35 (2-3 +// bytes). Polygon scenery sections concentrate these; pure +// COM/NAV-database sections have almost none. +// +// We slide a 64-byte window over the file and report windows where +// at least 6 opcode bytes look like vertex emits. Output: file +// offsets where polygon clusters start, plus the dominant opcode. + +#include +#include +#include +#include + + +static int isVertexOp(uint8_t b) { + switch (b) { + case 0x00: + case 0x01: + case 0x02: + case 0x40: + case 0x41: + case 0x42: + return 5; // 5-byte stream-vertex record + case 0x32: + case 0x33: + case 0x35: + return 2; // 2-byte cached-vertex record + default: + return 0; + } +} + + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 1; + } + FILE *f = fopen(argv[1], "rb"); + if (f == NULL) { + perror(argv[1]); + return 1; + } + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc(size); + if (buf == NULL) { + fclose(f); + return 1; + } + fread(buf, 1, size, f); + fclose(f); + + const int windowSize = 64; + const int threshold = 6; // min vertex-op count per window + int lastReportedOff = -1; + printf("# File %s: %ld bytes (%ld blocks of 512)\n", + argv[1], size, size / 512); + printf("# Windows of %d bytes with >=%d vertex-op bytes:\n", + windowSize, threshold); + printf("# offset block sector vertex-ops first-bytes (first 16 hex)\n"); + + // Look for windows where we see the $41-pattern (V2 emit + line + // draw) repeated regularly. Real chunk5 polygon sections look + // like `41 xx xx xx xx 41 xx xx xx xx ...`. Count $41 ops at + // 5-byte stride. + for (long off = 0; off + windowSize <= size; off++) { + int regularCount = 0; + for (int i = 0; i + 5 < windowSize; i += 5) { + if (buf[off + i] == 0x41 || buf[off + i] == 0x42) { + regularCount++; + } + } + int totalNonZero = 0; + for (int i = 0; i < windowSize; i++) { + if (buf[off + i] != 0) totalNonZero++; + } + if (regularCount >= 5 && totalNonZero > windowSize / 2 + && off > lastReportedOff + windowSize) { + long block = off / 512; + long sector = off / 256; + printf("# $%06lX block=%4ld sec=%4ld $41-stride=%2d nonzero=%d hex: ", + off, block, sector, regularCount, totalNonZero); + for (int i = 0; i < 16; i++) { + printf("%02x ", buf[off + i]); + } + printf("\n"); + lastReportedOff = off; + } + } + + free(buf); + return 0; +} diff --git a/port/tools/fs2trace.c b/port/tools/fs2trace.c new file mode 100644 index 0000000..c32cadf --- /dev/null +++ b/port/tools/fs2trace.c @@ -0,0 +1,2234 @@ +// FS2 sector-read tracer. +// +// Boots a synthetic Apple //e environment with the FS2 chunks loaded +// into RAM, hooks the SmartPort entry point at $C70D, then trampolines +// into the LoadSceneryFile* entry points and watches every block read. +// +// This is NOT a full Apple //e emulator. It only implements enough of +// the 6502 instruction set + zero-page / RAM model to run the FS2 +// loader code path. The Disk II / SmartPort layer is replaced with a +// single hook that: +// - logs the block number (read from RBBlockNumber at chunk4) +// - copies 512 bytes from the .dsk image at file_offset = block * 512 +// - returns "success" via CLC/RTS +// +// Usage: +// fs2trace [entry] +// entry is one of File0..File4 (default File0). Output is one +// "BLOCK $XXXX" line per read. + +#include +#include +#include +#include +#include + +#define MEM_SIZE 65536 +#define BLOCK_SIZE 512 +#define SECTOR_SIZE 256 +#define MAX_TRACE_BLKS 4096 + + +static uint8_t mem[MEM_SIZE]; +static uint8_t pc_low; +static uint16_t pc; +static uint8_t reg_a, reg_x, reg_y, reg_s; +static uint8_t flag_n, flag_v, flag_d, flag_i, flag_z, flag_c; +static int cycles; +static int stop; +// Set by FS2TRACE_DUMP_AT_BLOCK once the requested block count is +// reached. The main step loop checks this and exits cleanly without +// the noisy "last 16 PCs" trail that `stop` triggers. +static int dumpRequested; + +// Disk image: scenery disk for the trace. +static uint8_t *diskImage; +static size_t diskSize; + + +// Port of chunk3 `SceneryNibbleDecode` ($D52D). Despite the name in +// the disassembly, this is really an ENCODER -- it takes raw input +// bytes and produces an Apple-disk-style nibble format where every +// output byte has bits 7,5,3,1 forced to 1 (the `$AA` pattern). Each +// input byte expands to two output bytes: +// +// out[0] = (in >> 1) | $AA +// out[1] = in | $AA +// +// A running EOR checksum is fed by the input stream, then emitted as +// two trailing bytes (`(cksum>>1)|$AA` then `cksum|$AA`). The original +// fills RAM from $3B60 forward and stops once the destination high +// byte hits $3E -- so the loop emits exactly `(0x3E00 - 0x3B60) / 2` +// = 720 input bytes in. +// +// Returns the number of input bytes consumed (always 720). +static int nibbleDecode(const uint8_t *src, uint8_t *dst, uint8_t *checksumOut) { + uint8_t cksum = 0; + int written = 0; + int target = 0x3E00 - 0x3B60; // bytes + int srcPos = 0; + while (written + 2 <= target) { + uint8_t b = src[srcPos]; + cksum ^= b; + dst[written++] = (uint8_t)((b >> 1) | 0xAA); + dst[written++] = (uint8_t)( b | 0xAA); + srcPos++; + } + if (checksumOut != NULL) { + checksumOut[0] = (uint8_t)((cksum >> 1) | 0xAA); + checksumOut[1] = (uint8_t)( cksum | 0xAA); + } + return srcPos; +} + +// Trace buffer. +static int tracedBlocks[MAX_TRACE_BLKS]; +static int traceCount; + + +// Forward declarations. +static uint8_t rd(uint16_t addr); +static void wr(uint16_t addr, uint8_t v); +static void push(uint8_t v); +static uint8_t pop(void); +static void push16(uint16_t v); +static uint16_t pop16(void); +static uint8_t fetch(void); +static uint16_t fetch16(void); +static void setNZ(uint8_t v); +static uint8_t getP(void); +static void setP(uint8_t v); +static void step(void); +static void hookSmartPort(void); +static int loadChunk(const char *path, uint16_t addr, size_t maxLen); + + +static uint8_t rd(uint16_t addr) { + return mem[addr]; +} + + +// Apple II LCBANK split: $D000-$FFFF can be either ROM, LCBANK1, or +// LCBANK2 depending on softswitches at $C080-$C08F. fs2trace treats +// the entire $0000-$FFFF as plain RAM, but FS2 boot relies on hires +// page writes NOT corrupting the chunk binaries that live at +// $D300-$F3FF (chunk3) and $F600-$FBFF (chunk2). Ignore writes to +// $D000-$FFFF so chunk binaries stay intact. This breaks Apply64K- +// PatchTable if it ever targets a high address (it doesn't -- all +// patch table addresses are in $6000-$AE00, chunk5 main). +// +// Without this guard the hires drawing loop at $1C07 (sta +// (hires_ptr1),y) corrupts $FA67 and $FA71-3 in chunk2 (the wind +// code), causing fs2trace to halt at $FA73 on undocumented opcode +// $1B before more loader iterations can run. +static void wr(uint16_t addr, uint8_t v) { + const char *watchEnv = getenv("FS2TRACE_WATCH"); + if (watchEnv != NULL && mem[addr] != v) { + static int watchHits = 0; + static uint16_t watchLo = 0, watchHi = 0; + static int watchSetUp = 0; + if (!watchSetUp) { + unsigned long lo = strtoul(watchEnv, NULL, 0); + char *dash = strchr(watchEnv, '-'); + unsigned long hi = (dash != NULL) ? strtoul(dash + 1, NULL, 0) : lo; + watchLo = (uint16_t)lo; + watchHi = (uint16_t)hi; + watchSetUp = 1; + } + int watchCap = 50; + const char *capEnv = getenv("FS2TRACE_WATCH_CAP"); + if (capEnv != NULL) { + watchCap = (int)strtol(capEnv, NULL, 0); + } + if (addr >= watchLo && addr <= watchHi && watchHits < watchCap) { + fprintf(stderr, " watch: $%04X = $%02X (was $%02X) at PC $%04X cycles=%d\n", + addr, v, mem[addr], pc, cycles); + watchHits++; + } + } + // Protect chunk binaries from stray hires-page writes. If the + // emulator ever needs LCBANK semantics for real (e.g. a patch + // that targets $D000-$FFFF), this needs to grow into proper + // softswitch tracking. + if (addr >= 0xD000 && getenv("FS2TRACE_NO_LC_GUARD") == NULL) { + return; + } + mem[addr] = v; +} + + +// Zero-page-wrapping 16-bit read. For (zp),Y and (zp,X) addressing +// modes, the high byte of the pointer must come from `(zp + 1) & $FF` +// -- staying inside zero page even when zp == $FF. Without this wrap +// we'd read from $0100 (the stack) which corrupts the return address +// in subtle ways during scenery loader runs. +static uint16_t rd16zp(uint8_t zp) { + uint8_t lo = mem[zp]; + uint8_t hi = mem[(uint8_t)(zp + 1)]; + return (uint16_t)lo | ((uint16_t)hi << 8); +} + + +static void push(uint8_t v) { + mem[0x0100 + reg_s] = v; + reg_s--; +} + + +static uint8_t pop(void) { + reg_s++; + return mem[0x0100 + reg_s]; +} + + +static void push16(uint16_t v) { + push((uint8_t)(v >> 8)); + push((uint8_t)(v & 0xFF)); +} + + +static uint16_t pop16(void) { + uint8_t lo = pop(); + uint8_t hi = pop(); + return (uint16_t)lo | ((uint16_t)hi << 8); +} + + +static uint8_t fetch(void) { + return mem[pc++]; +} + + +static uint16_t fetch16(void) { + uint16_t lo = fetch(); + uint16_t hi = fetch(); + return lo | (hi << 8); +} + + +static void setNZ(uint8_t v) { + flag_n = (v & 0x80) ? 1 : 0; + flag_z = (v == 0) ? 1 : 0; +} + + +static uint8_t getP(void) { + return (uint8_t)((flag_n << 7) | (flag_v << 6) | 0x20 | (flag_d << 3) | + (flag_i << 2) | (flag_z << 1) | flag_c); +} + + +static void setP(uint8_t v) { + flag_n = (v & 0x80) ? 1 : 0; + flag_v = (v & 0x40) ? 1 : 0; + flag_d = (v & 0x08) ? 1 : 0; + flag_i = (v & 0x04) ? 1 : 0; + flag_z = (v & 0x02) ? 1 : 0; + flag_c = (v & 0x01) ? 1 : 0; +} + + +// Stub for chunk5 `PromptColorOrBW` ($AC3A). The real routine clears +// the viewport, draws the intro banner, and waits for the user to +// press 'A' (colour) or 'B' (black-and-white). Either choice copies +// 22 bytes from ColorModePatch ($AB65) or BWModePatch ($AB7B) into +// ColorOrBWModePatch ($0800), then returns. +// +// fs2trace can't render or read keys, so we simulate "user pressed +// A" inline: copy ColorModePatch -> $0800 and RTS to the caller. +// This unblocks the boot path so MainGameEntry can run through to +// the main loop where the patched scenery slots fire. +static void hookPromptColorOrBW(void) { + for (int i = 0; i < 22; i++) { + mem[0x0800 + i] = mem[0xAB65 + i]; + } + // Standard RTS: pop return address, add 1, set PC. + uint8_t lo = pop(); + uint8_t hi = pop(); + pc = (uint16_t)(lo | (hi << 8)) + 1; +} + + +// SmartPort hook: runs when PC reaches $C70D. Reads RBBlockNumber +// (3 bytes at chunk4-defined location, but we'll read it dynamically +// from the call params) and copies the requested block from the disk +// image into the SmartPort's data buffer. The caller's return is via +// the standard SmartPort calling convention: after `jsr $C70D` the +// command byte and parameter pointer are inline; we skip past them +// before returning. +static void hookSmartPort(void) { + // Apple SmartPort calling convention: + // jsr $C70D ; or whatever entry the firmware uses + // .byte command ; here, $01 = ReadBlock + // .word param_block_addr + // The return address pushed by JSR points to the inline + // command byte. We need to read the params, do the read, then + // bump the return address past the inline data. + uint16_t retLo = pop(); + uint16_t retHi = pop(); + uint16_t ret = retLo | (retHi << 8); // points one before inline cmd + // 6502 jsr pushes (retAddr - 1). + ret++; + + uint8_t command = mem[ret]; + uint16_t paramAddr = (uint16_t)(mem[ret + 1] | (mem[ret + 2] << 8)); + ret += 3; + + if (command == 0x01) { // ReadBlock + // ParamBlock layout (chunk4 RBParams): + // byte 0: parameter count ($03) + // byte 1: unit number + // bytes 2-3: data buffer addr + // bytes 4-6: block number (3 bytes, 24-bit) + uint16_t bufAddr = (uint16_t)(mem[paramAddr + 2] | (mem[paramAddr + 3] << 8)); + uint32_t blockNum = (uint32_t)mem[paramAddr + 4] + | ((uint32_t)mem[paramAddr + 5] << 8) + | ((uint32_t)mem[paramAddr + 6] << 16); + + if (traceCount < MAX_TRACE_BLKS) { + tracedBlocks[traceCount++] = (int)blockNum; + } + + // FS2TRACE_DUMP_AT_BLOCK: snapshot RAM right after the + // Nth block has been read and copied. The SD3 boot + // sequence loads 16 blocks ($0360-$036F) into $2600+ + // staging via SCRU0 -> $A7E0+ before the per-frame + // PatchSlot_FrameSync starts overwriting the dispatcher + // area. Stopping at block 16 captures the freshly-built + // dispatcher + per-section geometry at $A800-$AAFF + // before frames 1+ blow it away. + const char *stopAtEnv = getenv("FS2TRACE_DUMP_AT_BLOCK"); + if (stopAtEnv != NULL) { + int stopAt = (int)strtol(stopAtEnv, NULL, 0); + if (stopAt > 0 && traceCount == stopAt) { + dumpRequested = 1; + } + } + + size_t off = (size_t)blockNum * BLOCK_SIZE; + // Suppress reads that target our pre-loaded + // ReadBlockDataBuffer at $D575. FS2's boot reads block + // 0 (the .po boot block) into $D575, expecting it to + // contain the scenery block list -- but the san-inc + // pack .po has a standard ProDOS boot block there + // instead. Stomping $D575 with boot-block content + // breaks every subsequent block lookup. Our pre-fill + // already has the right .blocks data; preserve it. + bool skipWrite = (bufAddr >= 0xD575 && bufAddr < 0xD575 + 1024); + if (skipWrite) { + if (getenv("FS2TRACE_VERBOSE") != NULL) { + fprintf(stderr, " read block $%04X -> $%04X (skipped: protect block list)\n", + blockNum, bufAddr); + } + } else if (off + BLOCK_SIZE <= diskSize) { + memcpy(&mem[bufAddr], &diskImage[off], BLOCK_SIZE); + } else { + memset(&mem[bufAddr], 0, BLOCK_SIZE); + } + if (!skipWrite && getenv("FS2TRACE_VERBOSE") != NULL) { + fprintf(stderr, " read block $%04X -> $%04X (first byte: $%02X)\n", + blockNum, bufAddr, mem[bufAddr]); + } + flag_c = 0; // success + reg_a = 0; + } else { + fprintf(stderr, "unsupported SmartPort command $%02X\n", command); + flag_c = 1; + } + + // Return past the inline command + param pointer. + push((uint8_t)((ret - 1) >> 8)); + push((uint8_t)((ret - 1) & 0xFF)); + // Standard RTS path: pop and add 1. + retLo = pop(); + retHi = pop(); + pc = (uint16_t)((retHi << 8) | retLo) + 1; +} + + +// 6502 instruction step. Implements the documented opcodes used by +// FS2's loader path. Anything else trips the unknown-opcode path and +// stops the emulator with an error. +static void step(void) { + if (pc == 0xC70D || pc == 0xC700 || pc == 0xC709) { + hookSmartPort(); + return; + } + if (pc == 0xAC3A) { + hookPromptColorOrBW(); + return; + } + + uint8_t op = fetch(); + cycles++; + switch (op) { + // BRK -- treat as halt with status. Allow caller to + // disable the halt via FS2TRACE_NO_BRK_HALT (= treat + // BRK as RTS so we can chase past zero-padded chunk4 + // areas without aborting the dispatcher). + case 0x00: { + if (getenv("FS2TRACE_NO_BRK_HALT") != NULL) { + uint8_t rl = pop(); + uint8_t rh = pop(); + pc = (uint16_t)((rh << 8) | rl) + 1; + break; + } + fprintf(stderr, "BRK at $%04X cycles=%d\n", (uint16_t)(pc - 1), cycles); + fflush(stderr); + stop = 1; + return; + } + // NOP variants. + case 0xEA: break; + // CLC / SEC / CLD / SED / CLI / SEI / CLV + case 0x18: flag_c = 0; break; + case 0x38: flag_c = 1; break; + case 0xD8: flag_d = 0; break; + case 0xF8: flag_d = 1; break; + case 0x58: flag_i = 0; break; + case 0x78: flag_i = 1; break; + case 0xB8: flag_v = 0; break; + // Transfers. + case 0xAA: reg_x = reg_a; setNZ(reg_x); break; // TAX + case 0xA8: reg_y = reg_a; setNZ(reg_y); break; // TAY + case 0x8A: reg_a = reg_x; setNZ(reg_a); break; // TXA + case 0x98: reg_a = reg_y; setNZ(reg_a); break; // TYA + case 0xBA: reg_x = reg_s; setNZ(reg_x); break; // TSX + case 0x9A: reg_s = reg_x; break; // TXS + // Stack. + case 0x48: push(reg_a); break; // PHA + case 0x68: reg_a = pop(); setNZ(reg_a); break; // PLA + case 0x08: push((uint8_t)(getP() | 0x10)); break; // PHP + case 0x28: setP(pop()); break; // PLP + // INC/DEC X/Y + case 0xE8: reg_x++; setNZ(reg_x); break; // INX + case 0xC8: reg_y++; setNZ(reg_y); break; // INY + case 0xCA: reg_x--; setNZ(reg_x); break; // DEX + case 0x88: reg_y--; setNZ(reg_y); break; // DEY + + // LDA + case 0xA9: reg_a = fetch(); setNZ(reg_a); break; // LDA imm + case 0xA5: reg_a = rd(fetch()); setNZ(reg_a); break; // LDA zp + case 0xB5: reg_a = rd((uint8_t)(fetch() + reg_x)); setNZ(reg_a); break; // LDA zp,X + case 0xAD: reg_a = rd(fetch16()); setNZ(reg_a); break; // LDA abs + case 0xBD: { uint16_t a = fetch16(); reg_a = rd((uint16_t)(a + reg_x)); setNZ(reg_a); break; } // LDA abs,X + case 0xB9: { uint16_t a = fetch16(); reg_a = rd((uint16_t)(a + reg_y)); setNZ(reg_a); break; } // LDA abs,Y + case 0xA1: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); reg_a = rd(a); setNZ(reg_a); break; } // LDA (zp,X) + case 0xB1: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); reg_a = rd(a); setNZ(reg_a); break; } // LDA (zp),Y + + // LDX + case 0xA2: reg_x = fetch(); setNZ(reg_x); break; // LDX imm + case 0xA6: reg_x = rd(fetch()); setNZ(reg_x); break; // LDX zp + case 0xB6: reg_x = rd((uint8_t)(fetch() + reg_y)); setNZ(reg_x); break; // LDX zp,Y + case 0xAE: reg_x = rd(fetch16()); setNZ(reg_x); break; // LDX abs + case 0xBE: { uint16_t a = fetch16(); reg_x = rd((uint16_t)(a + reg_y)); setNZ(reg_x); break; } // LDX abs,Y + + // LDY + case 0xA0: reg_y = fetch(); setNZ(reg_y); break; // LDY imm + case 0xA4: reg_y = rd(fetch()); setNZ(reg_y); break; // LDY zp + case 0xB4: reg_y = rd((uint8_t)(fetch() + reg_x)); setNZ(reg_y); break; // LDY zp,X + case 0xAC: reg_y = rd(fetch16()); setNZ(reg_y); break; // LDY abs + case 0xBC: { uint16_t a = fetch16(); reg_y = rd((uint16_t)(a + reg_x)); setNZ(reg_y); break; } // LDY abs,X + + // STA + case 0x85: wr(fetch(), reg_a); break; // STA zp + case 0x95: wr((uint8_t)(fetch() + reg_x), reg_a); break; // STA zp,X + case 0x8D: wr(fetch16(), reg_a); break; // STA abs + case 0x9D: { uint16_t a = fetch16(); wr((uint16_t)(a + reg_x), reg_a); break; } // STA abs,X + case 0x99: { uint16_t a = fetch16(); wr((uint16_t)(a + reg_y), reg_a); break; } // STA abs,Y + case 0x81: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); wr(a, reg_a); break; } // STA (zp,X) + case 0x91: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); wr(a, reg_a); break; } // STA (zp),Y + + // STX + case 0x86: wr(fetch(), reg_x); break; // STX zp + case 0x96: wr((uint8_t)(fetch() + reg_y), reg_x); break; + case 0x8E: wr(fetch16(), reg_x); break; + // STY + case 0x84: wr(fetch(), reg_y); break; + case 0x94: wr((uint8_t)(fetch() + reg_x), reg_y); break; + case 0x8C: wr(fetch16(), reg_y); break; + + // INC zp / abs + case 0xE6: { uint8_t a = fetch(); uint8_t v = (uint8_t)(rd(a) + 1); wr(a, v); setNZ(v); break; } + case 0xF6: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = (uint8_t)(rd(a) + 1); wr(a, v); setNZ(v); break; } + case 0xEE: { uint16_t a = fetch16(); uint8_t v = (uint8_t)(rd(a) + 1); wr(a, v); setNZ(v); break; } + case 0xFE: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = (uint8_t)(rd(a) + 1); wr(a, v); setNZ(v); break; } + // DEC + case 0xC6: { uint8_t a = fetch(); uint8_t v = (uint8_t)(rd(a) - 1); wr(a, v); setNZ(v); break; } + case 0xD6: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = (uint8_t)(rd(a) - 1); wr(a, v); setNZ(v); break; } + case 0xCE: { uint16_t a = fetch16(); uint8_t v = (uint8_t)(rd(a) - 1); wr(a, v); setNZ(v); break; } + case 0xDE: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = (uint8_t)(rd(a) - 1); wr(a, v); setNZ(v); break; } + + // Logical / arithmetic helpers (define lambdas inline). + #define DO_ADC(v) do { uint16_t s = (uint16_t)reg_a + (uint16_t)(v) + (uint16_t)flag_c; \ + flag_c = (s > 0xFF) ? 1 : 0; \ + flag_v = ((reg_a ^ (v)) & 0x80) ? 0 : (((reg_a ^ s) & 0x80) ? 1 : 0); \ + reg_a = (uint8_t)s; setNZ(reg_a); } while (0) + #define DO_SBC(v) do { uint8_t vv = (uint8_t)~(v); \ + uint16_t s = (uint16_t)reg_a + (uint16_t)vv + (uint16_t)flag_c; \ + flag_c = (s > 0xFF) ? 1 : 0; \ + flag_v = ((reg_a ^ vv) & 0x80) ? 0 : (((reg_a ^ s) & 0x80) ? 1 : 0); \ + reg_a = (uint8_t)s; setNZ(reg_a); } while (0) + + case 0x69: { uint8_t v = fetch(); DO_ADC(v); break; } + case 0x65: { uint8_t v = rd(fetch()); DO_ADC(v); break; } + case 0x75: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_ADC(v); break; } + case 0x6D: { uint8_t v = rd(fetch16()); DO_ADC(v); break; } + case 0x7D: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_x)); DO_ADC(v); break; } + case 0x79: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_y)); DO_ADC(v); break; } + case 0x71: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); uint8_t v = rd(a); DO_ADC(v); break; } + case 0x61: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); uint8_t v = rd(a); DO_ADC(v); break; } + + case 0xE9: { uint8_t v = fetch(); DO_SBC(v); break; } + case 0xE5: { uint8_t v = rd(fetch()); DO_SBC(v); break; } + case 0xF5: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_SBC(v); break; } + case 0xED: { uint8_t v = rd(fetch16()); DO_SBC(v); break; } + case 0xFD: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_x)); DO_SBC(v); break; } + case 0xF9: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_y)); DO_SBC(v); break; } + case 0xF1: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); uint8_t v = rd(a); DO_SBC(v); break; } + case 0xE1: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); uint8_t v = rd(a); DO_SBC(v); break; } + + // CMP / CPX / CPY + #define DO_CMP(reg, v) do { uint16_t r = (uint16_t)(reg) + 0x100 - (uint16_t)(v); \ + flag_c = ((reg) >= (v)) ? 1 : 0; setNZ((uint8_t)(r & 0xFF)); } while (0) + case 0xC9: { uint8_t v = fetch(); DO_CMP(reg_a, v); break; } + case 0xC5: { uint8_t v = rd(fetch()); DO_CMP(reg_a, v); break; } + case 0xD5: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_CMP(reg_a, v); break; } + case 0xCD: { uint8_t v = rd(fetch16()); DO_CMP(reg_a, v); break; } + case 0xDD: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_x)); DO_CMP(reg_a, v); break; } + case 0xD9: { uint16_t a = fetch16(); uint8_t v = rd((uint16_t)(a + reg_y)); DO_CMP(reg_a, v); break; } + case 0xD1: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); uint8_t v = rd(a); DO_CMP(reg_a, v); break; } + case 0xC1: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); uint8_t v = rd(a); DO_CMP(reg_a, v); break; } + case 0xE0: { uint8_t v = fetch(); DO_CMP(reg_x, v); break; } + case 0xE4: { uint8_t v = rd(fetch()); DO_CMP(reg_x, v); break; } + case 0xEC: { uint8_t v = rd(fetch16()); DO_CMP(reg_x, v); break; } + case 0xC0: { uint8_t v = fetch(); DO_CMP(reg_y, v); break; } + case 0xC4: { uint8_t v = rd(fetch()); DO_CMP(reg_y, v); break; } + case 0xCC: { uint8_t v = rd(fetch16()); DO_CMP(reg_y, v); break; } + + // AND / ORA / EOR + #define DO_AND(v) do { reg_a &= (v); setNZ(reg_a); } while (0) + #define DO_ORA(v) do { reg_a |= (v); setNZ(reg_a); } while (0) + #define DO_EOR(v) do { reg_a ^= (v); setNZ(reg_a); } while (0) + case 0x29: { uint8_t v = fetch(); DO_AND(v); break; } + case 0x25: { uint8_t v = rd(fetch()); DO_AND(v); break; } + case 0x35: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_AND(v); break; } + case 0x2D: { uint8_t v = rd(fetch16()); DO_AND(v); break; } + case 0x3D: { uint16_t a = fetch16(); DO_AND(rd((uint16_t)(a + reg_x))); break; } + case 0x39: { uint16_t a = fetch16(); DO_AND(rd((uint16_t)(a + reg_y))); break; } + case 0x31: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); DO_AND(rd(a)); break; } + case 0x21: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); DO_AND(rd(a)); break; } + case 0x09: { uint8_t v = fetch(); DO_ORA(v); break; } + case 0x05: { uint8_t v = rd(fetch()); DO_ORA(v); break; } + case 0x15: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_ORA(v); break; } + case 0x0D: { uint8_t v = rd(fetch16()); DO_ORA(v); break; } + case 0x1D: { uint16_t a = fetch16(); DO_ORA(rd((uint16_t)(a + reg_x))); break; } + case 0x19: { uint16_t a = fetch16(); DO_ORA(rd((uint16_t)(a + reg_y))); break; } + case 0x11: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); DO_ORA(rd(a)); break; } + case 0x01: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); DO_ORA(rd(a)); break; } + case 0x49: { uint8_t v = fetch(); DO_EOR(v); break; } + case 0x45: { uint8_t v = rd(fetch()); DO_EOR(v); break; } + case 0x55: { uint8_t v = rd((uint8_t)(fetch() + reg_x)); DO_EOR(v); break; } + case 0x4D: { uint8_t v = rd(fetch16()); DO_EOR(v); break; } + case 0x5D: { uint16_t a = fetch16(); DO_EOR(rd((uint16_t)(a + reg_x))); break; } + case 0x59: { uint16_t a = fetch16(); DO_EOR(rd((uint16_t)(a + reg_y))); break; } + case 0x51: { uint8_t z = fetch(); uint16_t a = (uint16_t)(rd16zp(z) + reg_y); DO_EOR(rd(a)); break; } + case 0x41: { uint8_t z = (uint8_t)(fetch() + reg_x); uint16_t a = rd16zp(z); DO_EOR(rd(a)); break; } + + // BIT + case 0x24: { uint8_t v = rd(fetch()); flag_z = (reg_a & v) == 0 ? 1 : 0; flag_n = (v & 0x80) ? 1 : 0; flag_v = (v & 0x40) ? 1 : 0; break; } + case 0x2C: { uint8_t v = rd(fetch16()); flag_z = (reg_a & v) == 0 ? 1 : 0; flag_n = (v & 0x80) ? 1 : 0; flag_v = (v & 0x40) ? 1 : 0; break; } + + // ASL / LSR / ROL / ROR (accumulator + memory variants) + #define ASL(v) do { flag_c = ((v) & 0x80) ? 1 : 0; (v) = (uint8_t)((v) << 1); setNZ(v); } while (0) + #define LSR(v) do { flag_c = (v) & 1; (v) = (uint8_t)((v) >> 1); setNZ(v); } while (0) + #define ROL(v) do { uint8_t c = flag_c; flag_c = ((v) & 0x80) ? 1 : 0; (v) = (uint8_t)(((v) << 1) | c); setNZ(v); } while (0) + #define ROR(v) do { uint8_t c = flag_c; flag_c = (v) & 1; (v) = (uint8_t)(((v) >> 1) | (c << 7)); setNZ(v); } while (0) + case 0x0A: ASL(reg_a); break; + case 0x06: { uint8_t a = fetch(); uint8_t v = rd(a); ASL(v); wr(a, v); break; } + case 0x16: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = rd(a); ASL(v); wr(a, v); break; } + case 0x0E: { uint16_t a = fetch16(); uint8_t v = rd(a); ASL(v); wr(a, v); break; } + case 0x1E: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = rd(a); ASL(v); wr(a, v); break; } + case 0x4A: LSR(reg_a); break; + case 0x46: { uint8_t a = fetch(); uint8_t v = rd(a); LSR(v); wr(a, v); break; } + case 0x56: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = rd(a); LSR(v); wr(a, v); break; } + case 0x4E: { uint16_t a = fetch16(); uint8_t v = rd(a); LSR(v); wr(a, v); break; } + case 0x5E: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = rd(a); LSR(v); wr(a, v); break; } + case 0x2A: ROL(reg_a); break; + case 0x26: { uint8_t a = fetch(); uint8_t v = rd(a); ROL(v); wr(a, v); break; } + case 0x36: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = rd(a); ROL(v); wr(a, v); break; } + case 0x2E: { uint16_t a = fetch16(); uint8_t v = rd(a); ROL(v); wr(a, v); break; } + case 0x3E: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = rd(a); ROL(v); wr(a, v); break; } + case 0x6A: ROR(reg_a); break; + case 0x66: { uint8_t a = fetch(); uint8_t v = rd(a); ROR(v); wr(a, v); break; } + case 0x76: { uint8_t a = (uint8_t)(fetch() + reg_x); uint8_t v = rd(a); ROR(v); wr(a, v); break; } + case 0x6E: { uint16_t a = fetch16(); uint8_t v = rd(a); ROR(v); wr(a, v); break; } + case 0x7E: { uint16_t a = (uint16_t)(fetch16() + reg_x); uint8_t v = rd(a); ROR(v); wr(a, v); break; } + + // Branches. + #define BRANCH(cond) do { int8_t off = (int8_t)fetch(); if (cond) pc = (uint16_t)(pc + off); } while (0) + case 0x10: BRANCH(!flag_n); break; // BPL + case 0x30: BRANCH( flag_n); break; // BMI + case 0x50: BRANCH(!flag_v); break; // BVC + case 0x70: BRANCH( flag_v); break; // BVS + case 0x90: BRANCH(!flag_c); break; // BCC + case 0xB0: BRANCH( flag_c); break; // BCS + case 0xD0: BRANCH(!flag_z); break; // BNE + case 0xF0: BRANCH( flag_z); break; // BEQ + + // Jumps / subroutine. + case 0x4C: pc = fetch16(); break; // JMP abs + case 0x6C: { uint16_t a = fetch16(); // JMP (ind) + // 6502 page-boundary bug + uint16_t lo = mem[a]; + uint16_t hi = mem[(a & 0xFF00) | ((a + 1) & 0xFF)]; + pc = (uint16_t)(lo | (hi << 8)); + break; } + case 0x20: { // JSR abs + uint16_t target = fetch16(); + uint16_t retAddr = (uint16_t)(pc - 1); + push16(retAddr); + if (getenv("FS2TRACE_JSR") != NULL && (retAddr & 0xFF00) == 0x8000) { + fprintf(stderr, "JSR pushes $%04X (target $%04X)\n", retAddr, target); + } + if (target == 0x78E0 && getenv("FS2TRACE_JSR_78E0") != NULL) { + fprintf(stderr, " JSR $78E0 from PC $%04X A=$%02X $24=$%02X $B1=$%02X $0876=$%02X cycles=%d\n", + retAddr, reg_a, mem[0x24], mem[0xB1], mem[0x0876], cycles); + } + // FS2TRACE_PERSP=1: log every PerspectiveDivide + // call ($7BFD in MAME RAM). Inputs: A=num_hi, + // Y=num_lo, $C4/$C5=denominator. Output is in + // A on return. The self-modified table address + // lives at $7D47/$7D48 (= MAME's L7D76+1/+2, + // not source's $7D77/$7D78). + if (target == 0x7BFD && getenv("FS2TRACE_PERSP") != NULL) { + int16_t num = (int16_t)((uint16_t)reg_y | ((uint16_t)reg_a << 8)); + int16_t den = (int16_t)((uint16_t)mem[0xC4] | ((uint16_t)mem[0xC5] << 8)); + // MAME's L7D76 (= LDA abs,X) is at $7D48; the + // self-modified table address bytes are at + // $7D49 (lo) and $7D4A (hi). + fprintf(stderr, + " JSR PerspDiv from PC $%04X num=%6d den=%6d table=$%02X%02X\n", + retAddr, num, den, mem[0x7D4A], mem[0x7D49]); + } + pc = target; + break; } + case 0x60: { // RTS + // PC was advanced by fetch(); the RTS + // instruction itself was at pc-1. + uint16_t rtsAddr = (uint16_t)(pc - 1); + pc = (uint16_t)(pop16() + 1); + if (getenv("FS2TRACE_RTS") != NULL && rtsAddr == 0xD458) { + fprintf(stderr, "RTS@$D458 -> $%04X (S=$%02X)\n", pc, reg_s); + for (int s = 0; s < 8; s++) { + fprintf(stderr, " stack[$%02X] = $%02X\n", + (uint8_t)(reg_s - s), + mem[0x100 + (uint8_t)(reg_s - s)]); + } + } + // Capture PerspectiveDivide return value. + // MAME's PerspectiveDivide RTS is at $7D51 + // (= source's L7D7F equivalent). + if (rtsAddr == 0x7D51 && getenv("FS2TRACE_PERSP") != NULL) { + fprintf(stderr, " PerspDiv returns A=$%02X (signed=%d)\n", + reg_a, (int)(int8_t)reg_a); + } + break; } + case 0x40: setP(pop()); pc = pop16(); break; // RTI + + // 65C02 extensions used by chunk3. + case 0x14: { // TRB zp + uint8_t zp = fetch(); + uint8_t m = mem[zp]; + flag_z = ((reg_a & m) == 0); + mem[zp] = (uint8_t)(m & ~reg_a); + break; } + case 0x1C: { // TRB abs + uint16_t a = fetch16(); + uint8_t m = rd(a); + flag_z = ((reg_a & m) == 0); + wr(a, (uint8_t)(m & ~reg_a)); + break; } + case 0x04: { // TSB zp + uint8_t zp = fetch(); + uint8_t m = mem[zp]; + flag_z = ((reg_a & m) == 0); + mem[zp] = (uint8_t)(m | reg_a); + break; } + case 0x0C: { // TSB abs + uint16_t a = fetch16(); + uint8_t m = rd(a); + flag_z = ((reg_a & m) == 0); + wr(a, (uint8_t)(m | reg_a)); + break; } + case 0x80: { // BRA rel + int8_t off = (int8_t)fetch(); + pc = (uint16_t)(pc + off); + break; } + case 0x12: { // ORA (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + reg_a = (uint8_t)(reg_a | rd(a)); + setNZ(reg_a); + break; } + case 0x32: { // AND (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + reg_a = (uint8_t)(reg_a & rd(a)); + setNZ(reg_a); + break; } + case 0x52: { // EOR (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + reg_a = (uint8_t)(reg_a ^ rd(a)); + setNZ(reg_a); + break; } + case 0x72: { // ADC (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + uint8_t m = rd(a); + uint16_t r = (uint16_t)reg_a + (uint16_t)m + (uint16_t)flag_c; + flag_v = (((reg_a ^ m) & 0x80) == 0) + && (((reg_a ^ (uint8_t)r) & 0x80) != 0); + flag_c = r > 0xFF; + reg_a = (uint8_t)r; + setNZ(reg_a); + break; } + case 0x92: { // STA (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + wr(a, reg_a); + break; } + case 0xB2: { // LDA (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + reg_a = rd(a); + setNZ(reg_a); + break; } + case 0xD2: { // CMP (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + uint8_t m = rd(a); + flag_c = reg_a >= m; + setNZ((uint8_t)(reg_a - m)); + break; } + case 0xF2: { // SBC (zp) + uint8_t zp = fetch(); + uint16_t a = (uint16_t)mem[zp] | ((uint16_t)mem[(uint8_t)(zp+1)] << 8); + uint8_t m = rd(a); + uint16_t r = (uint16_t)reg_a + (uint16_t)((uint8_t)~m) + (uint16_t)flag_c; + flag_v = (((reg_a ^ m) & 0x80) != 0) + && (((reg_a ^ (uint8_t)r) & 0x80) != 0); + flag_c = r > 0xFF; + reg_a = (uint8_t)r; + setNZ(reg_a); + break; } + case 0x64: { // STZ zp + uint8_t zp = fetch(); + mem[zp] = 0; + break; } + case 0x74: { // STZ zp,X + uint8_t zp = fetch(); + mem[(uint8_t)(zp + reg_x)] = 0; + break; } + case 0x9C: { // STZ abs + uint16_t a = fetch16(); + wr(a, 0); + break; } + case 0x9E: { // STZ abs,X + uint16_t a = fetch16(); + wr((uint16_t)(a + reg_x), 0); + break; } + case 0x5A: push(reg_y); break; // PHY + case 0x7A: { reg_y = pop(); setNZ(reg_y); break; } // PLY + case 0xDA: push(reg_x); break; // PHX + case 0xFA: { reg_x = pop(); setNZ(reg_x); break; } // PLX + case 0x3A: reg_a = (uint8_t)(reg_a - 1); setNZ(reg_a); break; // DEC A + case 0x1A: reg_a = (uint8_t)(reg_a + 1); setNZ(reg_a); break; // INC A + + // 65C02 RMB/SMB ops: reset/set memory bit N of zp. + case 0x07: { uint8_t zp = fetch(); mem[zp] &= ~0x01; break; } // RMB0 + case 0x17: { uint8_t zp = fetch(); mem[zp] &= ~0x02; break; } // RMB1 + case 0x27: { uint8_t zp = fetch(); mem[zp] &= ~0x04; break; } // RMB2 + case 0x37: { uint8_t zp = fetch(); mem[zp] &= ~0x08; break; } // RMB3 + case 0x47: { uint8_t zp = fetch(); mem[zp] &= ~0x10; break; } // RMB4 + case 0x57: { uint8_t zp = fetch(); mem[zp] &= ~0x20; break; } // RMB5 + case 0x67: { uint8_t zp = fetch(); mem[zp] &= ~0x40; break; } // RMB6 + case 0x77: { uint8_t zp = fetch(); mem[zp] &= ~0x80; break; } // RMB7 + case 0x87: { uint8_t zp = fetch(); mem[zp] |= 0x01; break; } // SMB0 + case 0x97: { uint8_t zp = fetch(); mem[zp] |= 0x02; break; } // SMB1 + case 0xA7: { uint8_t zp = fetch(); mem[zp] |= 0x04; break; } // SMB2 + case 0xB7: { uint8_t zp = fetch(); mem[zp] |= 0x08; break; } // SMB3 + case 0xC7: { uint8_t zp = fetch(); mem[zp] |= 0x10; break; } // SMB4 + case 0xD7: { uint8_t zp = fetch(); mem[zp] |= 0x20; break; } // SMB5 + case 0xE7: { uint8_t zp = fetch(); mem[zp] |= 0x40; break; } // SMB6 + case 0xF7: { uint8_t zp = fetch(); mem[zp] |= 0x80; break; } // SMB7 + + // BBR/BBS rel: branch on bit reset/set in zp. + case 0x0F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x01)) pc = (uint16_t)(pc + off); break; } // BBR0 + case 0x1F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x02)) pc = (uint16_t)(pc + off); break; } // BBR1 + case 0x2F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x04)) pc = (uint16_t)(pc + off); break; } // BBR2 + case 0x3F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x08)) pc = (uint16_t)(pc + off); break; } // BBR3 + case 0x4F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x10)) pc = (uint16_t)(pc + off); break; } // BBR4 + case 0x5F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x20)) pc = (uint16_t)(pc + off); break; } // BBR5 + case 0x6F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x40)) pc = (uint16_t)(pc + off); break; } // BBR6 + case 0x7F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (!(mem[zp] & 0x80)) pc = (uint16_t)(pc + off); break; } // BBR7 + case 0x8F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x01) pc = (uint16_t)(pc + off); break; } // BBS0 + case 0x9F: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x02) pc = (uint16_t)(pc + off); break; } // BBS1 + case 0xAF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x04) pc = (uint16_t)(pc + off); break; } // BBS2 + case 0xBF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x08) pc = (uint16_t)(pc + off); break; } // BBS3 + case 0xCF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x10) pc = (uint16_t)(pc + off); break; } // BBS4 + case 0xDF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x20) pc = (uint16_t)(pc + off); break; } // BBS5 + case 0xEF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x40) pc = (uint16_t)(pc + off); break; } // BBS6 + case 0xFF: { uint8_t zp = fetch(); int8_t off = (int8_t)fetch(); if (mem[zp] & 0x80) pc = (uint16_t)(pc + off); break; } // BBS7 + + default: + fflush(stdout); + fprintf(stderr, "UNIMPL opcode $%02X at PC $%04X (cycles=%llu)\n", + op, (uint16_t)(pc - 1), (unsigned long long)cycles); + fflush(stderr); + stop = 1; + break; + } + (void)pc_low; +} + + +static int loadChunk(const char *path, uint16_t addr, size_t maxLen) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", path); + return 0; + } + size_t n = fread(&mem[addr], 1, maxLen, f); + fclose(f); + fprintf(stderr, "loaded %s -> $%04X..%04X (%zu bytes)\n", + path, addr, (uint16_t)(addr + n - 1), n); + return (int)n; +} + + +// --matrix mode: run chunk5 SetupViewProjection ($6000) with the +// supplied attitude / view inputs and dump the resulting $78..$89 +// matrix. Uses a captured MAME RAM image as the initial state so all +// ZP slots, chunk2-5 binaries, and demo wiring are already in place. +// +// Usage: +// fs2trace --matrix [ram.bin] +// +// Default ram.bin = tmp/capture_boot.bin (chunk5Oracle replacement). +static void loadOriginalChunks(void); // defined below + +static int matrixMode(int argc, char **argv) { + if (argc < 6 || argc > 7) { + fprintf(stderr, + "usage: %s --matrix [ram.bin]\n", + argv[0]); + return 2; + } + long yaw = strtol(argv[2], NULL, 0); + long pitch = strtol(argv[3], NULL, 0); + long bank = strtol(argv[4], NULL, 0); + long vd = strtol(argv[5], NULL, 0); + const char *ramPath = (argc > 6) + ? argv[6] + : "/home/scott/claude/flight/tmp/capture_boot.bin"; + + // FS2TRACE_USE_ORIG=1: load chunk4/chunk5 from out/ instead + // of from a captured RAM image. The captured chunk5 in the + // boot dump is HEAVILY patched by Apply64KPatchTable -- the + // SetupViewProjection control flow is rewritten there and no + // longer matches the source. Using the unpatched binaries + // gives source-faithful matrix output (matching the chunk5.s + // listing), which is what we want for validating the C + // transliteration in chunk5Setup.c. + if (getenv("FS2TRACE_USE_ORIG") != NULL) { + loadOriginalChunks(); + // ZP isn't initialised by the binaries; explicit zero + // is fine for SetupViewProjection (no read-before-write + // outside the inputs we poke below). + } else { + FILE *rf = fopen(ramPath, "rb"); + if (rf == NULL) { + fprintf(stderr, "cannot open RAM image %s\n", ramPath); + return 1; + } + size_t got = fread(mem, 1, MEM_SIZE, rf); + fclose(rf); + if (got != MEM_SIZE) { + fprintf(stderr, "RAM image %s short read (%zu bytes)\n", ramPath, got); + return 1; + } + } + + // Poke inputs over whatever the captured ZP held. + uint16_t y = (uint16_t)((yaw < 0) ? (yaw + 0x10000) : yaw); + uint16_t p = (uint16_t)((pitch < 0) ? (pitch + 0x10000) : pitch); + uint16_t b = (uint16_t)((bank < 0) ? (bank + 0x10000) : bank); + mem[0x6C] = (uint8_t)( y & 0xFF); + mem[0x6D] = (uint8_t)((y >> 8) & 0xFF); + mem[0x6E] = (uint8_t)( p & 0xFF); + mem[0x6F] = (uint8_t)((p >> 8) & 0xFF); + mem[0x70] = (uint8_t)( b & 0xFF); + mem[0x71] = (uint8_t)((b >> 8) & 0xFF); + mem[0x0A70] = (uint8_t)(vd & 0xFF); + + // Set up CPU and call $6000 = SetupViewProjection. Push a + // sentinel return so the routine's RTS lands at $FFFF (the + // SmartPort hook patches $C70D, but $FFFF isn't touched -- a + // BRK at $0000 halts the loop cleanly via stop=1 below). + reg_a = reg_x = reg_y = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + pc = 0x6000; + push16(0xFFFE); + + // FS2TRACE_MATRIX_BREAK=$XXXX prints state every time PC + // matches that address. Used to inspect intermediate state + // (e.g., set to $177A to see inputs to shifted L1778). + const char *brkEnv = getenv("FS2TRACE_MATRIX_BREAK"); + uint16_t brkPC = (brkEnv != NULL) + ? (uint16_t)strtol(brkEnv, NULL, 0) : 0xFFFE; + for (cycles = 0; cycles < 5000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + break; + } + if (pc == brkPC) { + fprintf(stderr, + "BRK pc=$%04X A=$%02X X=$%02X Y=$%02X " + "$72/$73=$%02X%02X $74/$75=$%02X%02X " + "$76/$77=$%02X%02X $CB=$%02X%02X\n", + pc, reg_a, reg_x, reg_y, + mem[0x73], mem[0x72], + mem[0x75], mem[0x74], + mem[0x77], mem[0x76], + mem[0xCC], mem[0xCB]); + } + step(); + } + + printf("inputs: yaw=%ld pitch=%ld bank=%ld VD=$%02X\n", + yaw, pitch, bank, (uint8_t)(vd & 0xFF)); + printf("matrix at $78..$89 (post-L6301 col shifts):\n"); + for (int row = 0; row < 3; row++) { + int rb = 0x78 + row * 6; + int v0 = (int16_t)(mem[rb] | (mem[rb + 1] << 8)); + int v1 = (int16_t)(mem[rb + 2] | (mem[rb + 3] << 8)); + int v2 = (int16_t)(mem[rb + 4] | (mem[rb + 5] << 8)); + printf(" row %d: %6d %6d %6d\n", row, v0, v1, v2); + } + // Cascade intermediates -- last values left after the routine + // returned. $72/$74/$76 are the rotated-angle inputs; + // $CB/$CD/$CF and $18/$D4/$D6 are the cos/sin lookups feeding + // the matrix construction. + if (getenv("FS2TRACE_MATRIX_DUMP") != NULL) { + #define R16(addr) ((int16_t)(mem[addr] | (mem[(addr)+1] << 8))) + printf("intermediates:\n"); + printf(" $72/$73 = %d ($%02X%02X)\n", R16(0x72), mem[0x73], mem[0x72]); + printf(" $74/$75 = %d ($%02X%02X)\n", R16(0x74), mem[0x75], mem[0x74]); + printf(" $76/$77 = %d ($%02X%02X)\n", R16(0x76), mem[0x77], mem[0x76]); + printf(" $CB/$CC = %d (sin of $72)\n", R16(0xCB)); + printf(" $CD/$CE = %d (sin of $74)\n", R16(0xCD)); + printf(" $CF/$D0 = %d (sin of $76)\n", R16(0xCF)); + printf(" $18/$19 = %d (cos of $72)\n", R16(0x18)); + printf(" $D4/$D5 = %d (cos of $74)\n", R16(0xD4)); + printf(" $D6/$D7 = %d (cos of $76)\n", R16(0xD6)); + printf(" $BA/$BB = %d (cos of VD<<4)\n", R16(0xBA)); + printf(" $BE/$BF = %d (sin of VD<<4)\n", R16(0xBE)); + #undef R16 + } + return 0; +} + + +// loadOriginalChunks: place chunk4 (at $0200) and chunk5 (at $6000) +// into mem[] from out/*-built. Used by the --zpscale and --l177b +// probes which target chunk4 routines in isolation -- the boot RAM +// dump shifts chunk4 code by 2 bytes due to Apply64KPatchTable, so +// L177B / ScaleC2ByC4 land at different addresses there. The .built +// binaries are unpatched. +static void loadOriginalChunks(void) { + memset(mem, 0, MEM_SIZE); + loadChunk("/home/scott/claude/flight/out/4_0200-25ff", 0x0200, 0x2400); + loadChunk("/home/scott/claude/flight/out/5_6000-b3df", 0x6000, 0x53E0); +} + + +// --scenery [ramfile]: runs the captured chunk5 ProcessScenery +// against a RAM image and counts how many DrawColorSpan calls fire. +// This tells us "how much MAME-equivalent scenery would draw if we +// ran the actual interpreter against this RAM state". Compare to +// the port's `SCENERY_STATS=1 draws=N` to see where the port +// diverges. Default RAM image is tmp/capture_boot.bin. +// +// The capture is patched by Apply64KPatchTable at runtime, so chunk5 +// addresses differ from the source-listing values. The jump table at +// $6000-$6020 provides indirection: $6006 jumps to ProcessScenery, +// $601B (DrawColorSpanRelay) jumps to DrawColorSpan. We use $6006 +// as the entry and watch for PC entering DrawColorSpan via the +// $601B relay's target. +static int sceneryMode(int argc, char **argv) { + const char *ramPath = (argc > 2) + ? argv[2] + : "/home/scott/claude/flight/tmp/capture_boot.bin"; + FILE *rf = fopen(ramPath, "rb"); + if (rf == NULL) { fprintf(stderr, "cannot open %s\n", ramPath); return 1; } + if (fread(mem, 1, MEM_SIZE, rf) != MEM_SIZE) { + fprintf(stderr, "RAM image short read\n"); + fclose(rf); + return 1; + } + fclose(rf); + + // Resolve DrawColorSpan via the $601B jump table slot + // (DrawColorSpanRelay). + if (mem[0x601B] != 0x4C) { + fprintf(stderr, "expected JMP at $601B, got $%02X\n", mem[0x601B]); + return 1; + } + uint16_t drawColorSpanPC = (uint16_t)(mem[0x601C] | (mem[0x601D] << 8)); + + // Reset the scenery cursor to the dispatcher entry (LA7E0 in + // the source = mem[$A7E0/$A7E1]). The captured cursor at + // $8B/$8C is the END-OF-FRAME position; without resetting + // we'd walk past the dispatcher into chunk2 territory. + uint16_t dispatcherEntry = (uint16_t)(mem[0xA7E0] | (mem[0xA7E1] << 8)); + mem[0x8B] = (uint8_t)( dispatcherEntry & 0xFF); + mem[0x8C] = (uint8_t)((dispatcherEntry >> 8) & 0xFF); + // Clear scenery in-progress flags so ProcessScenery starts + // fresh (chunk5.s lines 1053-1062). + mem[0x08F3] = 0; + mem[0x090A] = 0; + mem[0x08A9] = 0; + mem[0x08C4] = 0; + mem[0x008A] = 0; + // Invalidate HEADER section cache so demand-loads fire. + mem[0x08EA] = mem[0x08EB] = mem[0x08EC] = mem[0x08ED] = 0; + fprintf(stderr, "scenery: reset cursor to LA7E0 = $%04X, DrawColorSpan at $%04X\n", + dispatcherEntry, drawColorSpanPC); + // EmitClippedLine isn't in the jump table; locate via byte + // pattern would be ideal, but counting DrawColorSpan suffices + // -- every line eventually goes through it. + + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + long drawCount = 0; + long opCount = 0; + long emitOpCount = 0; // count cursor-driven scenery opcodes + + // Address of SceneryInterpreterStep / SceneryDispatch in capture + // (boot patches relocate it). Find by searching for the byte + // pattern after `lda ($8B),y; bmi; cmp #$46; bmi`. + // The fetch is `B1 8B 30 ?? C9 46 30 ??` (lda($8B),y; bmi ; cmp #$46; bmi ). + uint16_t fetchPC = 0; + for (int i = 0x6000; i < 0xB400; i++) { + if (mem[i] == 0xB1 && mem[i+1] == 0x8B && mem[i+2] == 0x30 + && mem[i+4] == 0xC9 && mem[i+5] == 0x46 && mem[i+6] == 0x30) { + fetchPC = (uint16_t)i; + break; + } + } + fprintf(stderr, " scenery fetch at $%04X (lda ($8B),y)\n", fetchPC); + + // Sequence in chunk5 MainLoop (chunk5.s line 5403+): + // SetupViewProjection ($6000) + // ShowSimpleCrashMessage / HandleCrashOrSplash (skipped here) + // FlipPagesFillViewportRelay ($6003) -- runs a SECOND scenery + // interpreter pass on the data at $8B/$8C derived from $77 + // (= the boot pre-render that draws horizon-line water/sky + // features in violet/blue, leaving STALE bytes in the hires + // page that the main scenery pass doesn't overwrite). + // ProcessScenery ($6006) + const uint16_t calls[3] = { 0x6000, 0x6003, 0x6006 }; + const char *callName[3] = { "SetupViewProjection", "FlipPagesFillViewport", "ProcessScenery" }; + for (int callIdx = 0; callIdx < 3; callIdx++) { + reg_a = reg_x = reg_y = 0; + push16(0xFFFE); + pc = calls[callIdx]; + fprintf(stderr, " -- %s ($%04X) --\n", callName[callIdx], pc); + long startOps = opCount; + long startDraws = drawCount; + long startFetches = emitOpCount; + uint16_t lastPC = 0; + long opsSinceFetch = 0; + long maxOpsSinceFetch = 0; + for (cycles = 0; cycles < 20000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + fprintf(stderr, " EXIT pc=$%04X after %ld ops, ops-since-last-fetch=%ld\n", + pc, opCount - startOps, opsSinceFetch); + break; + } + if (pc == drawColorSpanPC) { + drawCount++; + } + if (fetchPC != 0 && pc == fetchPC) { + emitOpCount++; + uint16_t cur = (uint16_t)(mem[0x8B] | (mem[0x8C] << 8)); + if (getenv("FS2TRACE_BASE") != NULL) { + // Print $4A..$52 BEFORE this op runs + // so we can pair "before $24" with + // "after $24" for diff. + fprintf(stderr, + " fetch #%ld cursor=$%04X opcode=$%02X base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] cam=[%02X%02X,%02X%02X,%02X%02X]\n", + emitOpCount - startFetches, + cur, mem[cur], + mem[0x4A], mem[0x4B], mem[0x4C], + mem[0x4D], mem[0x4E], mem[0x4F], + mem[0x50], mem[0x51], mem[0x52], + mem[0x67], mem[0x66], + mem[0x69], mem[0x68], + mem[0x6B], mem[0x6A]); + } else { + fprintf(stderr, + " fetch #%ld cursor=$%04X opcode=$%02X (S=$%02X)\n", + emitOpCount - startFetches, + cur, mem[cur], reg_s); + } + if (opsSinceFetch > maxOpsSinceFetch) { + maxOpsSinceFetch = opsSinceFetch; + } + opsSinceFetch = 0; + } + lastPC = pc; + opsSinceFetch++; + opCount++; + step(); + } + fprintf(stderr, + " last pc=$%04X reg_s=$%02X stop=%d max_ops_between_fetches=%ld\n", + lastPC, reg_s, stop, maxOpsSinceFetch); + fprintf(stderr, " %ld ops, %ld scenery-fetches, %ld draws\n", + opCount - startOps, emitOpCount - startFetches, + drawCount - startDraws); + if (callIdx == 0) { + // After SetupViewProjection, dump the matrix so + // we know the projection cascade succeeded. + fprintf(stderr, + " matrix $78..$89: " + "[%d %d %d / %d %d %d / %d %d %d]\n", + (int16_t)(mem[0x78] | (mem[0x79]<<8)), + (int16_t)(mem[0x7A] | (mem[0x7B]<<8)), + (int16_t)(mem[0x7C] | (mem[0x7D]<<8)), + (int16_t)(mem[0x7E] | (mem[0x7F]<<8)), + (int16_t)(mem[0x80] | (mem[0x81]<<8)), + (int16_t)(mem[0x82] | (mem[0x83]<<8)), + (int16_t)(mem[0x84] | (mem[0x85]<<8)), + (int16_t)(mem[0x86] | (mem[0x87]<<8)), + (int16_t)(mem[0x88] | (mem[0x89]<<8))); + } + } + fprintf(stderr, "Total: %ld ops, %ld scenery-fetches, %ld DrawColorSpan calls\n", + opCount, emitOpCount, drawCount); + return 0; +} + + +// --draws: run the FULL chunk5 dispatcher (SetupViewProjection + +// ProcessScenery) against an unpatched chunk5 binary, watching the +// DrawColorLine entry trampoline at $6009 and printing every line +// drawn. This produces a bit-exact reference list of polygons +// chunk5 would emit given the supplied input state -- the answer +// to "what should port draw to match the original FS2 binary." +// +// Usage: +// fs2trace --draws [ram.bin] +// FS2TRACE_PORT_STATE=1 sets up port-equivalent ZP for Meigs boot +// (camera $5C/$5D=287, $64/$65=804, $6C/$6D=-109, etc.) and +// overrides the matrix to MAME's runtime values exactly. +// +// Output: one line per draw with screen X1/Y1/X2/Y2 and the V1/V2 +// 3D coordinates from $CB..$D0 + $D4..$D9 at the moment of the call. +static int drawsMode(int argc, char **argv) { + const char *ramPath = (argc > 2) + ? argv[2] + : "/home/scott/claude/flight/port/sceneryRam_FS2.1.bin"; + FILE *rf = fopen(ramPath, "rb"); + if (rf == NULL) { fprintf(stderr, "cannot open %s\n", ramPath); return 1; } + if (fread(mem, 1, MEM_SIZE, rf) != MEM_SIZE) { + fprintf(stderr, "RAM image short read\n"); + fclose(rf); + return 1; + } + fclose(rf); + // The captured RAM is the MAME-patched chunk5 (Apply64KPatchTable + // installed at boot). Don't overlay the unpatched source -- + // that would clobber the runtime patches AND replace the + // dispatcher bytecode at $A800+ (which only exists in the + // patched RAM; source has dispatcher data at $B000). + // The patched chunk5 has dispatcher entry at $A800 = LA7E0. + // chunk3 ($D300-$F3FF) holds the 64K callbacks (LookupADFStation + // etc.) the patched chunk5 jumps into; load that too so those + // calls don't hit unmapped memory. chunk2 ($F600-$FBFF) holds + // a few helper routines. + // chunk3 ($D300-$F3FF) holds the 64K callbacks the patched + // chunk5 jumps into for $05/$1D/$1E station records, ADF + // input, magneto state, etc. The boot RAM dump captures the + // ROM bank at $D300+ (= zeros) instead of the LC-RAM-resident + // chunk3, so we have to overlay the source binary; routine + // addresses in source aren't quite the same as MAME's runtime + // layout but the in-record advance values are unaffected. + FILE *cf = fopen("/home/scott/claude/flight/out/3_d300-f3ff", "rb"); + if (cf != NULL) { + size_t n = fread(mem + 0xD300, 1, 0xF400 - 0xD300, cf); + fclose(cf); + fprintf(stderr, "loaded chunk3 (%zu bytes)\n", n); + } + cf = fopen("/home/scott/claude/flight/out/2_f600-fbff", "rb"); + if (cf != NULL) { (void)fread(mem + 0xF600, 1, 0xFC00 - 0xF600, cf); fclose(cf); } + + // Override the SceneryOpADFRecord / NAVRecord / COMRecord + // patches: source-binary chunk3 has these routines at + // different addresses than MAME's runtime, so the patched JMP + // targets in port_ram point at random source bytes. Restore + // the unpatched 48K behaviour (= just advance past the record) + // so the dispatcher doesn't crash entering chunk3. Station + // records don't draw anything anyway. + // SceneryOpADFRecord at $6021: source = "lda #$09; jmp $67FD" + // (= advance 9, continue). The patched JMP $DB3F would call + // chunk3 LookupADFStation but that maps to a different routine + // in source, so undo the patch. + // SceneryOpAdvanceAndContinue: in source chunk5 the entry + // trampoline at $6018 is `JMP SceneryOpAdvanceAndContinue`. The + // patched RAM preserves that trampoline, so we just read the + // target out of $6019/$601A. + uint16_t advanceAndContinue = 0; + if (mem[0x6018] == 0x4C) { + advanceAndContinue = (uint16_t)(mem[0x6019] | (mem[0x601A] << 8)); + } + fprintf(stderr, " SceneryOpAdvanceAndContinue at $%04X\n", advanceAndContinue); + if (mem[0x6021] == 0x4C && advanceAndContinue != 0) { // patched JMP -> chunk3 + mem[0x6021] = 0xA9; // lda + mem[0x6022] = 0x09; // #$09 + mem[0x6023] = 0x4C; // jmp + mem[0x6024] = (uint8_t)( advanceAndContinue & 0xFF); + mem[0x6025] = (uint8_t)((advanceAndContinue >> 8) & 0xFF); + fprintf(stderr, " unpatched SceneryOpADFRecord ($6021) -> $%04X\n", advanceAndContinue); + } + // Same for NAVRecord and COMRecord -- locate by SceneryOpcodeTable + // entries for $1D and $1E. + // Find SceneryOpcodeTable: `cmp #$46; bmi ` then the + // target is SceneryDispatch which loads from the table. Easier + // to search the table itself: the first entry should point + // at SceneryOpEmitV1XformAndPlot; the $1D entry is at + // table+$1D*2 and points at SceneryOpNAVRecord. We can find + // the table by looking for a known entry sequence. + // For now, scan the dispatcher area for any byte sequence that + // looks like `JMP ` (= $4C $xx $D[XYZ]) and patch + // back to advance-and-continue with appropriate length. + // chunk5.s says: $1D = NAVRecord (11-byte), $1E = COMRecord + // (variable-length). + // SceneryOpNAVRecord at... actually NAVRecord might or might + // not be patched the same way. Be defensive: scan the + // SceneryOpcodeTable for the $1D handler address and check + // for a JMP-pattern there. + + // DrawColorLine entry: in the patched binary, $6009 = JMP (relayed), + // and $601B = JMP DrawColorSpan. DrawColorLine itself is the + // function called from EmitClippedLine. Find its entry by + // scanning for the byte signature `lda $E9; sec; sbc $EB` + // (= chunk5.s line 3556). + uint16_t drawColorLinePC = 0; + for (int i = 0x6000; i < 0xB400; i++) { + if (mem[i] == 0xA5 && mem[i+1] == 0xE9 + && mem[i+2] == 0x38 + && mem[i+3] == 0xE5 && mem[i+4] == 0xEB) { + drawColorLinePC = (uint16_t)i; + break; + } + } + if (drawColorLinePC == 0) { + fprintf(stderr, "could not locate DrawColorLine\n"); + return 1; + } + fprintf(stderr, "DrawColorLine entry: $%04X\n", drawColorLinePC); + + // DrawColorSpan entry: chunk5 trampoline at $601B is `JMP DrawColorSpan`. + uint16_t drawColorSpanPC = 0; + if (mem[0x601B] == 0x4C) { + drawColorSpanPC = (uint16_t)(mem[0x601C] | (mem[0x601D] << 8)); + } + fprintf(stderr, "DrawColorSpan entry: $%04X\n", drawColorSpanPC); + + // SetEvenAndOddColorsAndPrepRowRoutine: scan for the byte signature + // `lda ColorTableEven,x; sta ColorByteEven` (= chunk5.s lines + // 3902-3903 = `BD ?? ?? 8D ?? ??`). The ColorTableEven address is + // at $7A00ish in MAME RAM, and ColorByteEven is some self-modified + // operand. Find the routine by looking for: BD ?? ?? 8D ?? ?? BD ?? ??. + // For now we just track its calls via the trampoline at $6024 if + // it's installed (= the older patched binary did this; check first). + uint16_t setEvenAndOddPC = 0; + // Try via the SceneryOpSetColor handler at the JMP that calls + // it (or via SetPixelDrawMode which we already have). + // Easier: scan for the ColorTableEven access pattern. + for (int i = 0x7000; i < 0xB400; i++) { + if (mem[i] == 0xBD // LDA abs,x + && mem[i+3] == 0x8D // STA abs + && mem[i+6] == 0xBD // LDA abs,x + && mem[i+9] == 0x8D // STA abs + && mem[i+12] == 0x8A) { // TXA + setEvenAndOddPC = (uint16_t)i; + break; + } + } + fprintf(stderr, "SetEvenAndOddColorsAndPrepRowRoutine entry: $%04X\n", setEvenAndOddPC); + + // Set up port-equivalent ZP state. The values mirror what + // port's runScreenshot + sceneryAttachCamera produce. + if (getenv("FS2TRACE_PORT_STATE") != NULL) { + // Camera position in scenery units ($5C=287, $64=804). + mem[0x5C] = 0x1F; mem[0x5D] = 0x01; + mem[0x64] = 0x24; mem[0x65] = 0x03; + // Altitude pair ($5E/$5F = 768 from boot; $60/$61 = 0). + mem[0x5E] = 0x00; mem[0x5F] = 0x03; + mem[0x60] = 0x00; mem[0x61] = 0x00; + // Rotation inputs ($6C/$6D = -109 yaw/X-axis; + // $6E/$6F = 0 pitch/Z-axis; $70/$71 = 0 bank/Y-axis). + mem[0x6C] = 0x93; mem[0x6D] = 0xFF; + mem[0x6E] = 0x00; mem[0x6F] = 0x00; + mem[0x70] = 0x00; mem[0x71] = 0x00; + mem[0x0A70] = 0x00; // ViewDirection + // Camera-section deltas. Port's sceneryAttachCamera + // sets $66/$67=0, $68/$69 = wyUnits (= cam.worldY=25 + // metres for Meigs boot, no scaling), $6A/$6B=0. Reset + // to match. + mem[0x66] = 0x00; mem[0x67] = 0x00; + mem[0x68] = 0x19; mem[0x69] = 0x00; // 25 + mem[0x6A] = 0x00; mem[0x6B] = 0x00; + fprintf(stderr, "FS2TRACE_PORT_STATE: ZP set for Meigs boot\n"); + } + + // Run SetupViewProjection ($6000) to compute the matrix at + // $78..$89 from the just-set $6C/$6E/$70 inputs. + reg_a = reg_x = reg_y = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + pc = 0x6000; + push16(0xFFFE); + for (cycles = 0; cycles < 5000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) break; + step(); + } + fprintf(stderr, + " matrix: row0=(%d,%d,%d) row1=(%d,%d,%d) row2=(%d,%d,%d)\n", + (int16_t)(mem[0x78] | (mem[0x79]<<8)), + (int16_t)(mem[0x7A] | (mem[0x7B]<<8)), + (int16_t)(mem[0x7C] | (mem[0x7D]<<8)), + (int16_t)(mem[0x7E] | (mem[0x7F]<<8)), + (int16_t)(mem[0x80] | (mem[0x81]<<8)), + (int16_t)(mem[0x82] | (mem[0x83]<<8)), + (int16_t)(mem[0x84] | (mem[0x85]<<8)), + (int16_t)(mem[0x86] | (mem[0x87]<<8)), + (int16_t)(mem[0x88] | (mem[0x89]<<8))); + + // Reset cursor to LA7E0 = $A800 (= clean dispatch start). + uint16_t dispatcherEntry = (uint16_t)(mem[0xA7E0] | (mem[0xA7E1] << 8)); + mem[0x8B] = (uint8_t)( dispatcherEntry & 0xFF); + mem[0x8C] = (uint8_t)((dispatcherEntry >> 8) & 0xFF); + // Clear in-progress flags so ProcessScenery starts fresh. + mem[0x08F3] = 0; + mem[0x090A] = 0; + mem[0x08A9] = 0; + mem[0x08C4] = 0; + mem[0x008A] = 0; + // Invalidate HEADER section cache so demand-loads fire (= match + // port's sceneryCacheInvalidated path). + mem[0x08EA] = mem[0x08EB] = mem[0x08EC] = mem[0x08ED] = 0; + // Reset $35/$36 (L631D base cache) so first $07/$24 forces + // a full base recompute. + mem[0x35] = 0; + mem[0x36] = 0; + fprintf(stderr, " cursor LA7E0 = $%04X\n", dispatcherEntry); + + // Find dispatcher fetch instruction (= the LDA ($8B),Y in + // SceneryInterpreterStep). Same heuristic as sceneryMode. + uint16_t fetchPC = 0; + for (int i = 0x6000; i < 0xB400; i++) { + if (mem[i] == 0xB1 && mem[i+1] == 0x8B && mem[i+2] == 0x30 + && mem[i+4] == 0xC9 && mem[i+5] == 0x46 && mem[i+6] == 0x30) { + fetchPC = (uint16_t)i; + break; + } + } + fprintf(stderr, " scenery fetch instruction at $%04X\n", fetchPC); + + // Run the chunk5 main-loop sequence: SetupViewProjection ($6000), + // FlipPagesFillViewport ($6003), ProcessScenery ($6006). The + // FlipPages pass is a SECOND scenery interpreter run on $0A78 + // data (= boot pre-render), which can draw water/horizon polygons + // in HIRES_VIOLET that ProcessScenery never touches. + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + long drawCount = 0; + long opCount = 0; + long fetchCount = 0; + int prevPCWasNotDraw = 1; + printf("# fs2trace --draws: chunk5 DrawColorLine sequence (3-pass)\n"); + const char *trace = getenv("FS2TRACE_DRAWS_TRACE"); + const uint16_t passes[3] = { 0x6000, 0x6003, 0x6006 }; + const char *passName[3] = { "SetupViewProjection", "FlipPagesFillViewport", "ProcessScenery" }; + for (int passIdx = 0; passIdx < 3; passIdx++) { + stop = 0; + reg_a = reg_x = reg_y = 0; + pc = passes[passIdx]; + push16(0xFFFE); + fprintf(stderr, " ===== %s ($%04X) =====\n", passName[passIdx], pc); + long passStartOps = opCount; + long passStartDraws = drawCount; + uint16_t lastPC = 0; + for (cycles = 0; cycles < 1000000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + fprintf(stderr, " hit sentinel pc=$%04X (last pc=$%04X) at op %ld\n", + pc, lastPC, opCount); + break; + } + if (fetchPC != 0 && pc == fetchPC) { + fetchCount++; + if (trace != NULL) { + uint16_t cur = mem[0x8B] | (mem[0x8C] << 8); + fprintf(stderr, + " fetch #%ld cur=$%04X op=$%02X $29=$%02X\n", + fetchCount, cur, mem[cur], mem[0x29]); + } + // FS2TRACE_VTX_DUMP: log V1/V2 (= mem[$CB..$D0] / + // mem[$D4..$D9]) on every fetch after a $40/$41 + // emit, so we can diff our port's transform output + // against the authentic FS2 trace per-vertex. + // FS2TRACE_FRAME_DUMP: log state at every $24/$07 + // op fetch + after it executes, so port-vs-MAME + // frame-setup divergence can be located precisely. + if (getenv("FS2TRACE_FRAME_DUMP") != NULL) { + static uint8_t prevFrameOp = 0; + static uint16_t prevFrameCur = 0; + if (prevFrameOp == 0x24 || prevFrameOp == 0x07) { + fprintf(stderr, + " POST-$%02X(var=$%02X) cam=(%d,%d,%d) base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] $19=$%02X $1C=$%02X $1F=$%02X $35=$%02X $36=$%02X (was cur=$%04X)\n", + prevFrameOp, + mem[prevFrameCur + 1], + (int16_t)(mem[0x66] | (mem[0x67] << 8)), + (int16_t)(mem[0x68] | (mem[0x69] << 8)), + (int16_t)(mem[0x6A] | (mem[0x6B] << 8)), + mem[0x4A], mem[0x4B], mem[0x4C], + mem[0x4D], mem[0x4E], mem[0x4F], + mem[0x50], mem[0x51], mem[0x52], + mem[0x19], mem[0x1C], mem[0x1F], + mem[0x35], mem[0x36], + prevFrameCur); + } + uint16_t cur = mem[0x8B] | (mem[0x8C] << 8); + if (mem[cur] == 0x24 || mem[cur] == 0x07) { + prevFrameOp = mem[cur]; + prevFrameCur = cur; + } else { + prevFrameOp = 0; + } + } + if (getenv("FS2TRACE_VTX_DUMP") != NULL) { + static uint8_t prevOp = 0; + if (prevOp == 0x40 || prevOp == 0x41 || prevOp == 0x42) { + int16_t v1x = (int16_t)(mem[0xCB] | (mem[0xCC] << 8)); + int16_t v1y = (int16_t)(mem[0xCD] | (mem[0xCE] << 8)); + int16_t v1z = (int16_t)(mem[0xCF] | (mem[0xD0] << 8)); + int16_t v2x = (int16_t)(mem[0xD4] | (mem[0xD5] << 8)); + int16_t v2y = (int16_t)(mem[0xD6] | (mem[0xD7] << 8)); + int16_t v2z = (int16_t)(mem[0xD8] | (mem[0xD9] << 8)); + fprintf(stderr, + " POST-$%02X V1=(%6d,%6d,%6d) V2=(%6d,%6d,%6d) cam=(%d,%d,%d) base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X]\n", + prevOp, v1x, v1y, v1z, v2x, v2y, v2z, + (int16_t)(mem[0x66] | (mem[0x67] << 8)), + (int16_t)(mem[0x68] | (mem[0x69] << 8)), + (int16_t)(mem[0x6A] | (mem[0x6B] << 8)), + mem[0x4A], mem[0x4B], mem[0x4C], + mem[0x4D], mem[0x4E], mem[0x4F], + mem[0x50], mem[0x51], mem[0x52]); + } + uint16_t cur = mem[0x8B] | (mem[0x8C] << 8); + prevOp = mem[cur]; + } + } + // Trace flow inside chunk3 (= 64K patched callbacks). + if (trace != NULL && pc >= 0xD300 && pc < 0xF400 && opCount > 2400) { + fprintf(stderr, " chunk3 pc=$%04X A=$%02X X=$%02X Y=$%02X SP=$%02X\n", + pc, reg_a, reg_x, reg_y, reg_s); + } + // Detect tight 6502 loops — same PC for many consecutive + // ops. If we sit at a single PC for >50k ops, log + bail. + static uint16_t stuckPC = 0; + static long stuckCount = 0; + if (pc == stuckPC) { + stuckCount++; + if (stuckCount == 50000) { + fprintf(stderr, + " STUCK at pc=$%04X for 50k ops at op=%ld (cur=$%04X)\n", + pc, opCount, mem[0x8B] | (mem[0x8C] << 8)); + break; + } + } else { + stuckPC = pc; + stuckCount = 1; + } + lastPC = pc; + // Track active hires color (= last A passed to SetPixelDrawMode). + // SetPixelDrawMode is at the JMP target stored at $6010-$6011 + // (= chunk5 trampoline `JMP SetPixelDrawMode` at $600F). + static uint16_t setPixelDrawModePC = 0xFFFF; + static uint8_t curHiresColor = 0xFF; + if (setPixelDrawModePC == 0xFFFF) { + // Resolve once: chunk5 has `4C lo hi` at $600F. + if (mem[0x600F] == 0x4C) { + setPixelDrawModePC = (uint16_t)(mem[0x6010] | (mem[0x6011] << 8)); + fprintf(stderr, " SetPixelDrawMode resolved at $%04X\n", setPixelDrawModePC); + } + } + if (setPixelDrawModePC != 0xFFFF && pc == setPixelDrawModePC) { + curHiresColor = reg_a; + if (getenv("FS2TRACE_LOG_COLORS") != NULL) { + fprintf(stderr, " SetPixelDrawMode A=$%02X (hires color %d) at op=%ld\n", + reg_a, reg_a & 0x07, opCount); + } + } + // Track SetEvenAndOddColorsAndPrepRowRoutine -- the SPAN + // fill color setter. On entry X = hires color code. + if (setEvenAndOddPC != 0 && pc == setEvenAndOddPC) { + curHiresColor = reg_x; + if (getenv("FS2TRACE_LOG_COLORS") != NULL) { + fprintf(stderr, " SetEvenAndOddColors X=$%02X (hires color %d) at op=%ld\n", + reg_x, reg_x & 0x07, opCount); + } + } + // Track DrawColorSpan calls (= horizontal span fill) so we can + // see polygon FILLS in addition to line draws. ZP $E9 = start col, + // A on entry = end col, $27 = right edge. + static int prevPCWasNotSpan = 1; + if (drawColorSpanPC != 0 && pc == drawColorSpanPC && prevPCWasNotSpan) { + uint8_t startCol = mem[0xE9]; + uint8_t endCol = reg_a; + uint8_t row = mem[0xB1]; + printf("span %4ld col=$%02X: row=%3d startCol=%3d endCol=%3d\n", + drawCount, curHiresColor, row, startCol, endCol); + prevPCWasNotSpan = 0; + } else if (pc != drawColorSpanPC) { + prevPCWasNotSpan = 1; + } + if (pc == drawColorLinePC && prevPCWasNotDraw) { + // First instruction of DrawColorLine. Capture + // the line endpoints + V1/V2. + uint8_t x1 = mem[0xE9]; + uint8_t y1 = mem[0xEA]; + uint8_t x2 = mem[0xEB]; + uint8_t y2 = mem[0xEC]; + int16_t v1x = (int16_t)(mem[0xCB] | (mem[0xCC] << 8)); + int16_t v1y = (int16_t)(mem[0xCD] | (mem[0xCE] << 8)); + int16_t v1z = (int16_t)(mem[0xCF] | (mem[0xD0] << 8)); + int16_t v2x = (int16_t)(mem[0xD4] | (mem[0xD5] << 8)); + int16_t v2y = (int16_t)(mem[0xD6] | (mem[0xD7] << 8)); + int16_t v2z = (int16_t)(mem[0xD8] | (mem[0xD9] << 8)); + drawCount++; + uint16_t curAt = mem[0x8B] | (mem[0x8C] << 8); + printf("draw %4ld cur=$%04X col=$%02X: (%3d,%3d)-(%3d,%3d) V1=(%6d,%6d,%6d) V2=(%6d,%6d,%6d) base=[%02X%02X%02X,%02X%02X%02X,%02X%02X%02X] cam=[%02X%02X,%02X%02X,%02X%02X]\n", + drawCount, curAt, curHiresColor, x1, y1, x2, y2, + v1x, v1y, v1z, v2x, v2y, v2z, + mem[0x4A], mem[0x4B], mem[0x4C], + mem[0x4D], mem[0x4E], mem[0x4F], + mem[0x50], mem[0x51], mem[0x52], + mem[0x67], mem[0x66], + mem[0x69], mem[0x68], + mem[0x6B], mem[0x6A]); + prevPCWasNotDraw = 0; + } else if (pc != drawColorLinePC) { + prevPCWasNotDraw = 1; + } + opCount++; + uint16_t prevPC = pc; + step(); + if (stop) { + fprintf(stderr, " step() set stop=1 prevPC=$%04X newPC=$%04X opCount=%ld\n", + prevPC, pc, opCount); + break; + } + } + fprintf(stderr, " pass-%s: %ld ops, %ld draws\n", + passName[passIdx], opCount - passStartOps, drawCount - passStartDraws); + } + fprintf(stderr, "Total: %ld 6502 ops, %ld dispatch fetches, %ld DrawColorLine calls (final pc=$%04X cur=$%04X)\n", + opCount, fetchCount, drawCount, pc, mem[0x8B] | (mem[0x8C] << 8)); + return 0; +} + + +// --xform: run chunk5 TransformVertex7EBC ($7EBC) on the original +// binary (= source-faithful, not the captured patched chunk5). Lets +// us validate the C transliteration in chunk5Transform.c against the +// asm for arbitrary inputs. +// +// Usage: +// fs2trace --xform [state_overrides...] +// +// Inputs encode the 4 vertex bytes that follow the opcode in the +// scenery stream. The routine reads them via ($8B),y and computes +// transformed XYZ at $D4..$D9 (or $CB..$D0). +static int xformMode(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, + "usage: %s --xform [ram.bin]\n", + argv[0]); + fprintf(stderr, + " Loads RAM image (default tmp/capture_boot.bin) for state\n" + " (matrix/base/camera), then overlays the ORIGINAL chunk5\n" + " binary at $6000-$B3DF (so $7EBC has source bytes).\n" + " Sets cursor $8B/$8C to stream_addr-1 (so opcode is at\n" + " ($8B),0 -- the typical layout when the dispatcher would\n" + " invoke a vertex-emit handler at that opcode), and calls\n" + " TransformVertex7EBC with Y=9 (V2 destination).\n" + " Prints V2 (=$D4..$D9).\n"); + return 2; + } + long streamAddr = strtol(argv[2], NULL, 0); + const char *ramPath = (argc > 3) + ? argv[3] + : "/home/scott/claude/flight/tmp/capture_boot.bin"; + + FILE *rf = fopen(ramPath, "rb"); + if (rf == NULL) { fprintf(stderr, "cannot open %s\n", ramPath); return 1; } + if (fread(mem, 1, MEM_SIZE, rf) != MEM_SIZE) { + fprintf(stderr, "RAM image short read\n"); + fclose(rf); + return 1; + } + fclose(rf); + // Overlay ONLY the chunk5 code regions that don't overlap + // loaded scenery sections. At boot Meigs, the loaded section + // sits at ~$B280+, so we overlay $6000-$B27F (= source-faithful + // chunk5 code) and keep $B280+ as the captured section data. + // Also overlay all of chunk4 ($0200-$25FF) which holds the + // ZPScale / cos table -- the captured RAM has it shifted + // and we need the source addresses. + FILE *cf = fopen("/home/scott/claude/flight/out/4_0200-25ff", "rb"); + if (cf != NULL) { + fread(mem + 0x0200, 1, 0x2400, cf); + fclose(cf); + } + cf = fopen("/home/scott/claude/flight/out/5_6000-b3df", "rb"); + if (cf != NULL) { + fread(mem + 0x6000, 1, 0xB280 - 0x6000, cf); + fclose(cf); + } + + // Cursor: opcode at streamAddr, vertex bytes at streamAddr+1. + // $8B/$8C = streamAddr (= the opcode address; ($8B),Y=0 reads + // the opcode, Y=1.. reads vertex bytes -- chunk5's normal layout). + mem[0x008B] = (uint8_t)( streamAddr & 0xFF); + mem[0x008C] = (uint8_t)((streamAddr >> 8) & 0xFF); + fprintf(stderr, + " state: $8B/$8C=$%04X matrix=[%d,%d,%d/%d,%d,%d/%d,%d,%d]\n", + (int)streamAddr, + (int)(int16_t)(mem[0x78] | (mem[0x79] << 8)), + (int)(int16_t)(mem[0x7A] | (mem[0x7B] << 8)), + (int)(int16_t)(mem[0x7C] | (mem[0x7D] << 8)), + (int)(int16_t)(mem[0x7E] | (mem[0x7F] << 8)), + (int)(int16_t)(mem[0x80] | (mem[0x81] << 8)), + (int)(int16_t)(mem[0x82] | (mem[0x83] << 8)), + (int)(int16_t)(mem[0x84] | (mem[0x85] << 8)), + (int)(int16_t)(mem[0x86] | (mem[0x87] << 8)), + (int)(int16_t)(mem[0x88] | (mem[0x89] << 8))); + fprintf(stderr, + " base: ($4A..$4C)=%02X%02X%02X ($4D..$4F)=%02X%02X%02X ($50..$52)=%02X%02X%02X\n", + mem[0x4A], mem[0x4B], mem[0x4C], + mem[0x4D], mem[0x4E], mem[0x4F], + mem[0x50], mem[0x51], mem[0x52]); + fprintf(stderr, + " cam ($66..$6B): %02X %02X %02X %02X %02X %02X\n", + mem[0x66], mem[0x67], mem[0x68], mem[0x69], mem[0x6A], mem[0x6B]); + fprintf(stderr, + " vertex bytes at $%04X: %02X %02X %02X %02X %02X\n", + (int)streamAddr, + mem[streamAddr+0], mem[streamAddr+1], mem[streamAddr+2], + mem[streamAddr+3], mem[streamAddr+4]); + + // The asm's TransformVertex7EBC reads destSlot from $E5 (Y + // on entry). ProcessVertex2 sets Y = $D4 before JSR + // (= absolute address of V2 slot), and the routine stores + // output via `sta $00,x` with X loaded from $E5. So Y on + // entry = $D4 (V2) or $CB (V1). + reg_y = 0xD4; + reg_a = reg_x = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + pc = 0x7EBC; + push16(0xFFFE); + const char *traceXform = getenv("FS2TRACE_XFORM_TRACE"); + for (cycles = 0; cycles < 5000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + break; + } + if (traceXform != NULL && pc >= 0x7EBC && pc <= 0x80AF) { + // Inside TransformVertex7EBC: print state. + fprintf(stderr, + "pc=$%04X A=$%02X X=$%02X Y=$%02X NVZC=%d%d%d%d " + "$18-$1A=%02X%02X%02X $1B-$1D=%02X%02X%02X $1E-$20=%02X%02X%02X " + "$9E$9F=%02X%02X $A2$A3=%02X%02X $2F=%02X\n", + pc, reg_a, reg_x, reg_y, + flag_n, flag_v, flag_z, flag_c, + mem[0x18], mem[0x19], mem[0x1A], + mem[0x1B], mem[0x1C], mem[0x1D], + mem[0x1E], mem[0x1F], mem[0x20], + mem[0x9E], mem[0x9F], mem[0xA2], mem[0xA3], + mem[0x2F]); + } + step(); + } + + int16_t v2x = (int16_t)((uint16_t)mem[0xD4] | ((uint16_t)mem[0xD5] << 8)); + int16_t v2y = (int16_t)((uint16_t)mem[0xD6] | ((uint16_t)mem[0xD7] << 8)); + int16_t v2z = (int16_t)((uint16_t)mem[0xD8] | ((uint16_t)mem[0xD9] << 8)); + printf("xform stream@$%04X -> V2=(%d,%d,%d)\n", + (int)streamAddr, (int)v2x, (int)v2y, (int)v2z); + return 0; +} + + +// --zpscale a b: runs ScaleC2ByC4 at $1569 in chunk4 with the +// supplied 16-bit signed inputs in $C2/$C3 and $C4/$C5; prints the +// result. Loads chunk4 freshly from out/4_0200-25ff so the address +// matches the binary (vs the captured RAM which is patched). +static int zpscaleMode(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "usage: %s --zpscale \n", argv[0]); + return 2; + } + long a = strtol(argv[2], NULL, 0); + long b = strtol(argv[3], NULL, 0); + loadOriginalChunks(); + + uint16_t ua = (uint16_t)((a < 0) ? (a + 0x10000) : a); + uint16_t ub = (uint16_t)((b < 0) ? (b + 0x10000) : b); + mem[0xC2] = (uint8_t)( ua & 0xFF); + mem[0xC3] = (uint8_t)((ua >> 8) & 0xFF); + mem[0xC4] = (uint8_t)( ub & 0xFF); + mem[0xC5] = (uint8_t)((ub >> 8) & 0xFF); + + // ScaleC2ByC4 lives at $1565 (chunk4.s line 1565); ZPScale's + // wrapper at $1544 handles the $C2/$C4 marshalling for an + // arbitrary output address. We've already populated $C2..$C5 + // directly so we call ScaleC2ByC4 ($1565) and pull the + // result out of A:X via the trampoline below. + reg_a = reg_x = reg_y = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + pc = 0x1569; + push16(0xFFFE); + + const char *traceEnv = getenv("FS2TRACE_ZPSCALE_TRACE"); + for (cycles = 0; cycles < 1000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + break; + } + if (traceEnv != NULL) { + fprintf(stderr, + "pc=$%04X A=$%02X X=$%02X Y=$%02X C=%d " + "C2=$%02X C3=$%02X C4=$%02X C5=$%02X " + "A7=$%02X A8=$%02X\n", + pc, reg_a, reg_x, reg_y, flag_c, + mem[0xC2], mem[0xC3], mem[0xC4], mem[0xC5], + mem[0xA7], mem[0xA8]); + } + step(); + } + // Result: A is low byte, X is high byte. + int16_t result = (int16_t)((uint16_t)reg_a | ((uint16_t)reg_x << 8)); + printf("ScaleC2ByC4(%ld, %ld) = %d\n", a, b, result); + return 0; +} + + +// --l177b A X [ramfile]: probe chunk4 L177B (cos lookup with sub-byte +// interpolation) in isolation. Returns the int16 result (A:X reg). +static int l177bMode(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "usage: %s --l177b [ram.bin]\n", argv[0]); + return 2; + } + long aIn = strtol(argv[2], NULL, 0); + long xIn = strtol(argv[3], NULL, 0); + if (argc > 4) { + FILE *rf = fopen(argv[4], "rb"); + if (rf == NULL) { fprintf(stderr, "cannot open %s\n", argv[4]); return 1; } + if (fread(mem, 1, MEM_SIZE, rf) != MEM_SIZE) { + fprintf(stderr, "RAM image short read\n"); + fclose(rf); + return 1; + } + fclose(rf); + } else { + loadOriginalChunks(); + } + reg_a = (uint8_t)(aIn & 0xFF); + reg_x = (uint8_t)(xIn & 0xFF); + reg_y = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + // L177B at $177B; L1778 (sin entry) is 3 bytes earlier. + // When running against a capture, FS2's boot patches shifted + // chunk4 code by 2 bytes, so the entry points are at $177D / + // $177A respectively. FS2TRACE_PC overrides explicitly. + const char *l1778Env = getenv("FS2TRACE_USE_L1778"); + const char *pcEnv = getenv("FS2TRACE_PC"); + if (pcEnv != NULL) { + pc = (uint16_t)strtol(pcEnv, NULL, 0); + } else { + pc = (l1778Env != NULL) ? 0x1778 : 0x177B; + } + push16(0xFFFE); + const char *traceEnv2 = getenv("FS2TRACE_L177B_TRACE"); + for (cycles = 0; cycles < 1000000 && !stop; ) { + if (pc == 0xFFFF || pc == 0x0000) { + break; + } + if (traceEnv2 != NULL) { + fprintf(stderr, + "pc=$%04X A=$%02X X=$%02X Y=$%02X N=%d Z=%d C=%d\n", + pc, reg_a, reg_x, reg_y, flag_n, flag_z, flag_c); + } + step(); + } + int16_t result = (int16_t)((uint16_t)reg_a | ((uint16_t)reg_x << 8)); + printf("L177B(%ld, %ld) = %d\n", aIn, xIn, result); + return 0; +} + + +int main(int argc, char **argv) { + // --matrix mode: chunk5 SetupViewProjection oracle. + if (argc >= 2 && strcmp(argv[1], "--matrix") == 0) { + return matrixMode(argc, argv); + } + // --zpscale: probe chunk4 ScaleC2ByC4 in isolation. + if (argc >= 2 && strcmp(argv[1], "--zpscale") == 0) { + return zpscaleMode(argc, argv); + } + // --l177b: probe chunk4 cos lookup. + if (argc >= 2 && strcmp(argv[1], "--l177b") == 0) { + return l177bMode(argc, argv); + } + // --scenery: count chunk5 ProcessScenery DrawColorSpan calls. + if (argc >= 2 && strcmp(argv[1], "--scenery") == 0) { + return sceneryMode(argc, argv); + } + // --xform: chunk5 TransformVertex7EBC oracle. + if (argc >= 2 && strcmp(argv[1], "--xform") == 0) { + return xformMode(argc, argv); + } + // --draws: full chunk5 DrawColorLine trace. + if (argc >= 2 && strcmp(argv[1], "--draws") == 0) { + return drawsMode(argc, argv); + } + + // --nibble in.bin out.bin: run the chunk3 nibble decoder on + // raw input bytes, emit encoded output. Used to verify the C + // port matches what the 6502 emulator produces. + if (argc == 4 && strcmp(argv[1], "--nibble") == 0) { + FILE *in = fopen(argv[2], "rb"); + FILE *out = fopen(argv[3], "wb"); + if (in == NULL || out == NULL) { + fprintf(stderr, "could not open files\n"); + return 1; + } + fseek(in, 0, SEEK_END); + size_t sz = (size_t)ftell(in); + fseek(in, 0, SEEK_SET); + uint8_t *buf = malloc(sz); + if (fread(buf, 1, sz, in) != sz) { + fprintf(stderr, "short read\n"); + return 1; + } + fclose(in); + uint8_t output[0x3E00 - 0x3B60]; + uint8_t cksum[2]; + int consumed = nibbleDecode(buf, output, cksum); + fwrite(output, 1, sizeof(output), out); + fwrite(cksum, 1, 2, out); + fclose(out); + fprintf(stderr, "consumed %d input bytes, wrote %zu output bytes\n", + consumed, sizeof(output) + 2); + free(buf); + return 0; + } + + const char *chunk4Path = "/home/scott/claude/flight/out/4_0200-25ff"; + const char *chunk5Path = "/home/scott/claude/flight/out/5_6000-b3df"; + const char *chunk3Path = "/home/scott/claude/flight/out/3_d300-f3ff"; + const char *chunk2Path = "/home/scott/claude/flight/out/2_f600-fbff"; + const char *diskPath = (argc > 1) ? argv[1] : "/home/scott/claude/flight/orig/flight simulator 2 with scenery PRODOS (san inc pack).po"; + const char *blocksPath = (argc > 2) ? argv[2] : "/home/scott/claude/flight/downloads/scenery/extracted/A2.SDS1.blocks"; + // Default entry: LoadSceneryFile1 (descriptor $0625, 6 sectors from + // sector $25). LoadSceneryFile0 ($A66B) reads sector $22 first and on + // four disks (SDS1, SD1, SD3, SD7A) the resulting LA7E0 word is a + // low-memory address (e.g. $0003) so the bootstrap copy stomps the + // stack page and the run halts. LoadSceneryFile1 sources LA7E0 from + // a different file region and lands every disk on a sane chunk5 + // address, so all 13 traces complete to the $FFFF sentinel. + uint16_t entryPC = (argc > 3) ? (uint16_t)strtol(argv[3], NULL, 0) : 0xA674; + + memset(mem, 0, MEM_SIZE); + + if (!loadChunk(chunk4Path, 0x0200, 0x2400)) return 1; + if (!loadChunk(chunk5Path, 0x6000, 0x53E0)) return 1; + if (!loadChunk(chunk3Path, 0xD300, 0x2100)) return 1; + if (!loadChunk(chunk2Path, 0xF600, 0x0600)) return 1; + + // Disk image. + FILE *df = fopen(diskPath, "rb"); + if (df == NULL) { fprintf(stderr, "cannot open %s\n", diskPath); return 1; } + fseek(df, 0, SEEK_END); + diskSize = (size_t)ftell(df); + fseek(df, 0, SEEK_SET); + diskImage = malloc(diskSize); + if (diskImage == NULL || fread(diskImage, 1, diskSize, df) != diskSize) { + fprintf(stderr, "cannot read %s\n", diskPath); + fclose(df); + return 1; + } + fclose(df); + fprintf(stderr, "disk: %s (%zu bytes)\n", diskPath, diskSize); + + // Patch the SmartPort entry: when the FS2 code calls $C70D it + // would normally land in the absent firmware; we trap the read + // by hooking pc==$C70D inside step(). + // Provide a sentinel BRK so any unintended fall-through halts. + mem[0xC70D] = 0x00; + + // 64K mode patch: chunk4 has six L1EAD..L1EC1 thunks that the + // 64K patch table rewrites to JMP into chunk3's + // SceneryLoaderEntry1..7. We replicate those JMP targets + // directly so the loader path actually reaches the chunk3 + // implementations (Apply64KPatchTable would otherwise need to + // be run too). + struct { uint16_t thunk; uint16_t entry; } patches[] = { + { 0x1EAD, 0xD3D0 }, // SceneryLoaderEntry1 + { 0x1EB0, 0xD3D3 }, // SceneryLoaderEntry2 + { 0x1EB3, 0xD3D6 }, // SceneryLoaderEntry3 + { 0x1EB6, 0xD3D9 }, // SceneryLoaderEntry4 + { 0x1EB9, 0xD3DC }, // SceneryLoaderEntry5 + { 0x1EBC, 0xD3DF }, // SceneryLoaderEntry6 + { 0x1EC1, 0xD3E2 }, // SceneryLoaderEntry7 + }; + for (size_t i = 0; i < sizeof(patches) / sizeof(patches[0]); i++) { + uint16_t t = patches[i].thunk; + uint16_t e = patches[i].entry; + mem[t] = 0x4C; // JMP abs + mem[t + 1] = (uint8_t)(e & 0xFF); + mem[t + 2] = (uint8_t)((e >> 8) & 0xFF); + } + // Also flag 64K mode (chunk4 L1E07). + mem[0x1E07] = 0x01; + + // Populate ReadBlockDataBuffer (chunk3 $D575) from the .blocks + // file produced by prodosextract. The list is a sequence of + // 16-bit little-endian ProDOS block numbers, one per logical + // file block. ReadBlockDataBuffer wants block-low bytes at + // offset 0..255 and block-high bytes at offset 256..511, so + // we split the entries on load. + // LD5C8 self-extension stub. The real FS2 boot decodes a + // chunk of code into LD5C8 = ReadBlockDataBuffer + 83 via the + // protected-disk loader path (SceneryReadDecoded -> nibble + // decode -> JSR into the decoded code). Without simulating + // that whole flow, JSR LD5C8 from chunk3 L416 / L514 lands on + // zero (BRK) and halts the emulator. We patch LD5C8 with a + // CLC; RTS so those calls are harmless no-ops -- enough to + // let the rest of the loader proceed end-to-end. + mem[0xD5C8] = 0x18; // CLC + mem[0xD5C9] = 0x60; // RTS + + // Block-list cap: chunk3's ReadBlockDataBuffer ($D575) holds + // 256 low bytes + 256 high bytes = 512 bytes total ($D575 + + // $D675). FS2 sector counters can index well past the boot's + // first 16 blocks (e.g. HEADER's section $76 -> entry 240), so + // we populate the FULL 256 entries. LD5C8 (= buf+83) is + // overwritten at runtime by the FS2 boot loader anyway. + FILE *bf = fopen(blocksPath, "rb"); + if (bf == NULL) { + fprintf(stderr, "warning: cannot open %s; falling back to identity map\n", blocksPath); + for (int i = 0; i < 256; i++) { + mem[0xD575 + i] = (uint8_t)(i & 0xFF); + } + } else { + uint8_t buf[2]; + int i = 0; + while (i < 256 && fread(buf, 1, 2, bf) == 2) { + mem[0xD575 + i] = buf[0]; + mem[0xD575 + 256 + i] = buf[1]; + i++; + } + fclose(bf); + fprintf(stderr, "loaded %d block-list entries from %s\n", i, blocksPath); + } + + // Run one or more entry points. Default is a single entry + // (back-compat with previous fs2trace usage). With --chain, + // run File1+File2+File3+File4 in sequence: each one's RAM + // changes accumulate, so LA7E0 ends up pointing at the full + // loaded scenery instead of just the partial File1 result. + // With --boot, start at MainGameEntry ($ABBA) so + // Apply64KPatchTable rewrites the PatchSlot_* dispatch slots + // before any scenery loader runs. PromptColorOrBW is stubbed + // (default = colour) so the trace doesn't block on a key. + bool chainMode = (getenv("FS2TRACE_CHAIN") != NULL); + bool bootMode = (getenv("FS2TRACE_BOOT") != NULL); + // FS2TRACE_CITY=N: load city scenery file N then run MainLoop + // so its dispatcher's $0D HEADER demand-loads fire. Cities are: + // N=0 -> LoadSceneryFile0 ($A66B) + // N=1 -> LoadSceneryFile1 ($A674) (Chicago) + // N=2 -> LoadSceneryFile2 ($A67D) (LA) + // N=3 -> LoadSceneryFile3 ($A686) (Seattle) + // N=4 -> LoadSceneryFile4 ($A68F) (NY) + const char *cityEnv = getenv("FS2TRACE_CITY"); + bool cityMode = (cityEnv != NULL); + uint16_t entries[8]; + int numEntries = 0; + if (cityMode) { + int n = (int)strtol(cityEnv, NULL, 0); + static const uint16_t cityEntry[5] = { + 0xA66B, 0xA674, 0xA67D, 0xA686, 0xA68F + }; + if (n >= 0 && n <= 4) { + // Sequence: full game init → load city's scenery + // → init dispatcher pointer → run interpreter + // once. We bypass MainLoop because its + // PatchSlot_FrameSync resets LA7E0 back to the + // WW1 dispatcher; instead we directly invoke + // LoadDispatcherPointer + L6006 (jmp + // ProcessScenery), so HEADER demand-loads fire + // against the city dispatcher we just loaded. + entries[numEntries++] = 0xABBA; // MainGameEntry init + entries[numEntries++] = cityEntry[n]; // Load city's dispatcher into LA7E0+ + entries[numEntries++] = 0xA61B; // LoadDispatcherPointer ($8B = LA7E0) + entries[numEntries++] = 0x6006; // jmp ProcessScenery + } else { + fprintf(stderr, "FS2TRACE_CITY: invalid index %d (must be 0-4)\n", n); + return 1; + } + } else if (bootMode) { + entries[numEntries++] = 0xABBA; // MainGameEntry + } else if (chainMode) { + entries[numEntries++] = 0xA674; // LoadSceneryFile1 + entries[numEntries++] = 0xA67D; // LoadSceneryFile2 + entries[numEntries++] = 0xA686; // LoadSceneryFile3 + entries[numEntries++] = 0xA68F; // LoadSceneryFile4 + } else { + entries[numEntries++] = entryPC; + } + + FILE *trace = (getenv("FS2TRACE_PCS") != NULL) ? fopen("tmp/fs2trace.pcs", "w") : NULL; + int totalCycles = 0; + + for (int e = 0; e < numEntries; e++) { + // Initialise machine state for this entry. Memory is + // preserved across entries (the whole point of chain + // mode) but registers and stack are reset. + reg_a = reg_x = reg_y = 0; + reg_s = 0xFF; + flag_n = flag_v = flag_d = flag_z = flag_c = 0; + flag_i = 1; + stop = 0; + pc = entries[e]; + + // Push a sentinel return address ($FFFF). When the + // entry RTSes, the next fetch lands on $0000 (BRK) + // which halts cleanly. + push16(0xFFFE); + + uint16_t lastPCs[16] = { 0 }; + int lastIdx = 0; + int sawSmartPort = 0; + int sawFetchSector = 0; + int blocksBefore = traceCount; + int entryStart = totalCycles; + + // Boot mode runs the full chunk5 main loop, which + // never returns -- we need a generous cycle budget so + // demand-driven scenery loads (triggered by HEADER + // opcodes during ProcessScenery) get a chance to fire. + // Override with FS2TRACE_CYCLES if needed. + const char *cycleEnv = getenv("FS2TRACE_CYCLES"); + int cycleLimit = bootMode ? 200000000 : 5000000; + if (cycleEnv != NULL) { + cycleLimit = (int)strtol(cycleEnv, NULL, 0); + } + // FS2TRACE_INIT_X / FS2TRACE_INIT_Z: pre-seed the + // aircraft scenery position (upper-16 of zero-page + // $5A-$65) before MainGameEntry runs. This forces the + // scenery dispatcher to fire HEADER demand-loads for the + // section containing those coords, so we can capture + // city-specific RAM dumps. Values are 16-bit unsigned + // (the upper word of the 24-bit scenery coordinate). + const char *initXEnv = getenv("FS2TRACE_INIT_X"); + const char *initZEnv = getenv("FS2TRACE_INIT_Z"); + if (initXEnv != NULL) { + uint16_t x = (uint16_t)strtol(initXEnv, NULL, 0); + mem[0x5C] = (uint8_t)(x & 0xFF); + mem[0x5D] = (uint8_t)((x >> 8) & 0xFF); + fprintf(stderr, "FS2TRACE_INIT_X: $5C/$5D = $%04X\n", x); + } + if (initZEnv != NULL) { + uint16_t z = (uint16_t)strtol(initZEnv, NULL, 0); + mem[0x64] = (uint8_t)(z & 0xFF); + mem[0x65] = (uint8_t)((z >> 8) & 0xFF); + fprintf(stderr, "FS2TRACE_INIT_Z: $64/$65 = $%04X\n", z); + } + // Re-apply the position patch every frame at the + // dispatcher entry point. Hook at $A61B + // (LoadDispatcherPointer) -- this runs AFTER chunk5's + // per-frame IntegratePhysicsStep ($87A2) but BEFORE the + // dispatcher reads $5C/$5D for the cull check. Hooking + // earlier (e.g. $877F MainLoop) doesn't work because + // IntegratePhysicsStep normalises position cells. + uint16_t patchX = (initXEnv != NULL) + ? (uint16_t)strtol(initXEnv, NULL, 0) : 0; + uint16_t patchZ = (initZEnv != NULL) + ? (uint16_t)strtol(initZEnv, NULL, 0) : 0; + for (cycles = 0; cycles < cycleLimit && !stop; ) { + if (pc == 0xC70D) { + sawSmartPort++; + } + if (pc == 0x1EC6) { + sawFetchSector++; + } + if (pc == 0xA61B) { + if (initXEnv != NULL) { + mem[0x5C] = (uint8_t)(patchX & 0xFF); + mem[0x5D] = (uint8_t)((patchX >> 8) & 0xFF); + } + if (initZEnv != NULL) { + mem[0x64] = (uint8_t)(patchZ & 0xFF); + mem[0x65] = (uint8_t)((patchZ >> 8) & 0xFF); + } + } + // FS2TRACE_FORCE_INBOUNDS: short-circuit the + // "beyond bounds" cull-redirect path at $6E6F so + // every $13/$14 and $20/$21/$22 cull falls + // through. Used to force every section's $0D + // HEADER to fire during boot, so the resulting + // RAM dump contains every reachable scenery + // section at its dispatcher-expected dest -- a + // single comprehensive dump per region without + // needing to fly the camera there. + if (pc == 0x6E6F && getenv("FS2TRACE_FORCE_INBOUNDS") != NULL) { + // TestSceneryRangeReject does pla*4 + + // jmp L00A5 to take the BEYOND path. + // Replace with TestSceneryRangeOk's RTS + // so the cull returns "in bounds" + // instead -- caller advances past the + // cull record. + pc = 0x6E6E; + } + if (pc == 0x6006) { + static int psHits = 0; + psHits++; + if (psHits <= 5) { + fprintf(stderr, "ProcessScenery (L6006) hit #%d: $5C/$5D=$%02X%02X $64/$65=$%02X%02X $8B/$8C=$%02X%02X\n", + psHits, mem[0x5D], mem[0x5C], mem[0x65], mem[0x64], + mem[0x8C], mem[0x8B]); + } + } + if (pc == 0xA63A) { + static int hdrHits = 0; + hdrHits++; + if (hdrHits <= 20) { + fprintf(stderr, "SceneryHeaderLoadIfMiss hit #%d: sectionId=$%02X count=$%02X dest=$%02X%02X cacheIdx=$%02X cache=$%02X%02X%02X%02X\n", + hdrHits, mem[0x08E5], mem[0x08E6], + mem[0x08E8], mem[0x08E7], mem[0x08E9], + mem[0x08EA], mem[0x08EB], mem[0x08EC], mem[0x08ED]); + } + } + if (pc == 0xA6CD) { + static int runHits = 0; + runHits++; + if (runHits <= 20) { + fprintf(stderr, "SceneryHeaderRunSection hit #%d: $9E=$%02X $9F=$%02X L1E01=$%02X L1E03=$%02X%02X L1E07=$%02X L1E09=$%02X\n", + runHits, mem[0x9E], mem[0x9F], mem[0x1E01], + mem[0x1E04], mem[0x1E03], mem[0x1E07], mem[0x1E09]); + } + } + if (pc == 0xA6DF) { + static int loopHits = 0; + loopHits++; + if (loopHits <= 5) { + fprintf(stderr, "SceneryHeaderRunSectionLoop (jsr L1EAD): $9F=$%02X L1E01=$%02X L1E07=$%02X L1E09=$%02X\n", + mem[0x9F], mem[0x1E01], mem[0x1E07], mem[0x1E09]); + } + } + if (pc == 0xA6F3) { + fprintf(stderr, "SceneryHeaderRunSectionFail: L1E01=$%02X carry-set\n", mem[0x1E01]); + } + lastPCs[lastIdx] = pc; + lastIdx = (lastIdx + 1) % 16; + if (trace != NULL) { + fprintf(trace, "$%04X\n", pc); + } + step(); + if (pc == 0xFFFF) { + break; + } + if (dumpRequested) { + fprintf(stderr, "FS2TRACE_DUMP_AT_BLOCK reached after block %d\n", + traceCount); + break; + } + } + totalCycles += cycles; + + fprintf(stderr, "entry $%04X: %d cycles, smartport=%d, fetch=%d, blocks=%d, final PC=$%04X stop=%d\n", + entries[e], cycles, sawSmartPort, sawFetchSector, + traceCount - blocksBefore, pc, stop); + if (stop) { + fprintf(stderr, " last 16 PCs: "); + for (int i = 0; i < 16; i++) { + int idx = (lastIdx + 15 - i) % 16; + fprintf(stderr, "$%04X ", lastPCs[idx]); + } + fprintf(stderr, "\n"); + break; + } + (void)entryStart; + } + + if (trace != NULL) { + fclose(trace); + } + + // Optional: dump key memory regions where the loader deposits + // data, so the caller can grep for the scenery bytecode entry. + if (getenv("FS2TRACE_DUMP") != NULL) { + FILE *dump = fopen("tmp/fs2trace.ram", "wb"); + if (dump != NULL) { + fwrite(mem, 1, MEM_SIZE, dump); + fclose(dump); + fprintf(stderr, "wrote tmp/fs2trace.ram (full 64K)\n"); + } + } + + // Show LA7E0 (the scenery entry pointer chunk5's + // LoadDispatcherPointer reads). + uint16_t la7e0 = (uint16_t)(mem[0xA7E0] | (mem[0xA7E1] << 8)); + fprintf(stderr, "LA7E0 = $%04X (scenery entry pointer)\n", la7e0); + + fprintf(stderr, "\n%d cycles total, %d block reads.\n", totalCycles, traceCount); + for (int i = 0; i < traceCount; i++) { + printf(" %d: BLOCK $%04X (%d)\n", i, tracedBlocks[i], tracedBlocks[i]); + } + + free(diskImage); + return 0; +} diff --git a/port/tools/imgDiagnose.sh b/port/tools/imgDiagnose.sh new file mode 100755 index 0000000..54a5da2 --- /dev/null +++ b/port/tools/imgDiagnose.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# imgDiagnose.sh - run imgStats / imgDiff on PNG or PPM inputs. +# PNGs are converted to PPM via ImageMagick into a sibling tmp file +# so the C tools never see the PNG and Claude never has to Read it. +# +# Usage: +# imgDiagnose.sh stats # single-image text summary +# imgDiagnose.sh diff [--ascii] # pairwise text diff +# +# Inputs may be .png, .ppm, or .pgm. PNG inputs are converted via +# `convert in.png ppm:tmpfile`. If two inputs differ in size, the +# larger one is resized to match the smaller (nearest neighbour) so +# differing PNG render scales (e.g. MAME 704x231 vs port 1120x768) +# can still be diffed without a manual pre-step. + +set -euo pipefail + +# Tools live in port/tools/.c and are built by port/Makefile into +# port/bin/. Resolve those paths from the script location. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PORT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +BIN_DIR="$PORT_DIR/bin" +TMP_DIR="$PORT_DIR/../tmp" +DIFF_BIN="$BIN_DIR/imgDiff" +STATS_BIN="$BIN_DIR/imgStats" + +if [[ ! -x "$DIFF_BIN" || ! -x "$STATS_BIN" ]]; then + (cd "$PORT_DIR" && make -s tools) >&2 +fi + +toPpm() { + local in="$1" + local out="$2" + local ext="${in##*.}" + if [[ "$ext" == "ppm" || "$ext" == "pgm" ]]; then + cp "$in" "$out" + else + convert "$in" -depth 8 "ppm:$out" + fi +} + +cmd="${1:-}" +shift || true + +case "$cmd" in + stats) + if [[ $# -ne 1 ]]; then + echo "usage: imgDiagnose.sh stats " >&2 + exit 2 + fi + tmp=$(mktemp --tmpdir="$TMP_DIR" imgDiag.XXXXXX.ppm) + trap 'rm -f "$tmp"' EXIT + toPpm "$1" "$tmp" + "$STATS_BIN" "$tmp" + ;; + diff) + if [[ $# -lt 2 ]]; then + echo "usage: imgDiagnose.sh diff [--ascii]" >&2 + exit 2 + fi + a="$1"; shift + b="$1"; shift + extra=("$@") + ta=$(mktemp --tmpdir="$TMP_DIR" imgDiagA.XXXXXX.ppm) + tb=$(mktemp --tmpdir="$TMP_DIR" imgDiagB.XXXXXX.ppm) + trap 'rm -f "$ta" "$tb"' EXIT + toPpm "$a" "$ta" + toPpm "$b" "$tb" + # Match sizes: resize the larger to the smaller (nearest neighbour). + wa=$(identify -format "%w" "$ta"); ha=$(identify -format "%h" "$ta") + wb=$(identify -format "%w" "$tb"); hb=$(identify -format "%h" "$tb") + if [[ "$wa" != "$wb" || "$ha" != "$hb" ]]; then + if (( wa * ha >= wb * hb )); then + convert "$ta" -filter point -resize "${wb}x${hb}!" -depth 8 "ppm:$ta" + else + convert "$tb" -filter point -resize "${wa}x${ha}!" -depth 8 "ppm:$tb" + fi + fi + "$DIFF_BIN" "$ta" "$tb" "${extra[@]}" + ;; + *) + cat >&2 < + imgDiagnose.sh diff [--ascii] +EOF + exit 2 + ;; +esac diff --git a/port/tools/imgDiff.c b/port/tools/imgDiff.c new file mode 100644 index 0000000..407befd --- /dev/null +++ b/port/tools/imgDiff.c @@ -0,0 +1,271 @@ +// imgDiff: textual diff between two PPM (P6) or PGM (P5) images. +// Same dimensions required. Outputs counts, mean/max abs error per channel, +// bbox of differing pixels, and a 16x12 ASCII density heatmap. +// +// Usage: imgDiff a.ppm b.ppm [--ascii] +// +// PNGs are not supported directly; convert via ImageMagick: +// convert in.png ppm:out.ppm + +#include +#include +#include +#include +#include +#include + + +typedef struct { + int width; + int height; + int channels; + uint8_t *pixels; +} ImageT; + + +static int loadImage(const char *path, ImageT *out); +static void freeImage(ImageT *img); +static int skipWhitespaceAndComments(FILE *f); +static int readToken(FILE *f, char *buf, size_t size); +static void reportDiff(const ImageT *a, const ImageT *b, bool ascii); + + +static void freeImage(ImageT *img) { + free(img->pixels); + img->pixels = NULL; +} + + +static int loadImage(const char *path, ImageT *out) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "imgDiff: cannot open %s\n", path); + return -1; + } + char magic[8]; + if (readToken(f, magic, sizeof(magic)) < 0) { + fclose(f); + return -1; + } + int channels = 0; + if (strcmp(magic, "P6") == 0) { + channels = 3; + } else if (strcmp(magic, "P5") == 0) { + channels = 1; + } else { + fprintf(stderr, "imgDiff: %s: unsupported magic '%s' (want P5/P6)\n", path, magic); + fclose(f); + return -1; + } + char tok[32]; + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int width = atoi(tok); + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int height = atoi(tok); + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int maxval = atoi(tok); + if (maxval != 255) { + fprintf(stderr, "imgDiff: %s: maxval %d not supported\n", path, maxval); + fclose(f); + return -1; + } + // readToken already consumed the single whitespace separator + // following maxval, so binary pixel data starts at the cursor. + size_t need = (size_t)width * (size_t)height * (size_t)channels; + uint8_t *pixels = malloc(need); + if (pixels == NULL) { + fclose(f); + fprintf(stderr, "imgDiff: out of memory\n"); + return -1; + } + if (fread(pixels, 1, need, f) != need) { + fprintf(stderr, "imgDiff: %s: short read\n", path); + free(pixels); + fclose(f); + return -1; + } + fclose(f); + out->width = width; + out->height = height; + out->channels = channels; + out->pixels = pixels; + return 0; +} + + +static int readToken(FILE *f, char *buf, size_t size) { + if (skipWhitespaceAndComments(f) < 0) { + return -1; + } + size_t i = 0; + int c; + while ((c = fgetc(f)) != EOF && !isspace(c)) { + if (i + 1 >= size) { + return -1; + } + buf[i++] = (char)c; + } + buf[i] = '\0'; + return (int)i; +} + + +static void reportDiff(const ImageT *a, const ImageT *b, bool ascii) { + int w = a->width; + int h = a->height; + int ch = a->channels; + size_t total = (size_t)w * (size_t)h; + size_t differ = 0; + uint64_t sumAbs = 0; + int maxAbs = 0; + int minX = w; + int minY = h; + int maxX = -1; + int maxY = -1; + uint64_t sumLumA = 0; + uint64_t sumLumB = 0; + const int gridX = 16; + const int gridY = 12; + int *grid = calloc((size_t)gridX * (size_t)gridY, sizeof(int)); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const uint8_t *pa = a->pixels + ((size_t)y * w + x) * ch; + const uint8_t *pb = b->pixels + ((size_t)y * w + x) * ch; + int d = 0; + for (int c = 0; c < ch; c++) { + int da = abs((int)pa[c] - (int)pb[c]); + if (da > d) { + d = da; + } + sumAbs += (uint64_t)da; + } + if (ch == 3) { + sumLumA += (uint64_t)((pa[0] + pa[1] + pa[2]) / 3); + sumLumB += (uint64_t)((pb[0] + pb[1] + pb[2]) / 3); + } else { + sumLumA += pa[0]; + sumLumB += pb[0]; + } + if (d != 0) { + differ++; + if (d > maxAbs) { + maxAbs = d; + } + if (x < minX) { minX = x; } + if (y < minY) { minY = y; } + if (x > maxX) { maxX = x; } + if (y > maxY) { maxY = y; } + int gx = (x * gridX) / w; + int gy = (y * gridY) / h; + grid[gy * gridX + gx]++; + } + } + } + double pct = total > 0 ? (100.0 * (double)differ / (double)total) : 0.0; + double mae = (double)sumAbs / ((double)total * (double)ch); + double lumA = (double)sumLumA / (double)total; + double lumB = (double)sumLumB / (double)total; + printf("dims: %dx%d (%zu px, %d ch)\n", w, h, total, ch); + printf("identical: %s\n", differ == 0 ? "true" : "false"); + printf("pixels_differ: %zu (%.2f%%)\n", differ, pct); + printf("mean_abs_err: %.2f\n", mae); + printf("max_abs_err: %d\n", maxAbs); + if (differ > 0) { + printf("bbox_diff: (%d,%d)-(%d,%d) size=%dx%d\n", + minX, minY, maxX, maxY, + maxX - minX + 1, maxY - minY + 1); + } else { + printf("bbox_diff: \n"); + } + printf("luminance_mean: a=%.1f b=%.1f delta=%+.1f\n", lumA, lumB, lumB - lumA); + if (ascii && differ > 0) { + int peak = 0; + for (int i = 0; i < gridX * gridY; i++) { + if (grid[i] > peak) { + peak = grid[i]; + } + } + printf("heatmap (%dx%d, '.'=0 '#'=peak=%d):\n", gridX, gridY, peak); + for (int gy = 0; gy < gridY; gy++) { + printf(" "); + for (int gx = 0; gx < gridX; gx++) { + int v = grid[gy * gridX + gx]; + char glyph; + if (v == 0) { + glyph = '.'; + } else { + int level = (v * 9 + peak - 1) / peak; + if (level < 1) { level = 1; } + if (level > 9) { level = 9; } + glyph = (char)('0' + level); + if (v == peak) { + glyph = '#'; + } + } + putchar(glyph); + } + putchar('\n'); + } + } + free(grid); +} + + +static int skipWhitespaceAndComments(FILE *f) { + int c; + while ((c = fgetc(f)) != EOF) { + if (c == '#') { + while ((c = fgetc(f)) != EOF && c != '\n') { + // discard comment + } + } else if (!isspace(c)) { + ungetc(c, f); + return 0; + } + } + return -1; +} + + +int main(int argc, char **argv) { + bool ascii = false; + const char *aPath = NULL; + const char *bPath = NULL; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--ascii") == 0) { + ascii = true; + } else if (aPath == NULL) { + aPath = argv[i]; + } else if (bPath == NULL) { + bPath = argv[i]; + } else { + fprintf(stderr, "imgDiff: unexpected arg '%s'\n", argv[i]); + return 2; + } + } + if (aPath == NULL || bPath == NULL) { + fprintf(stderr, "usage: %s a.ppm b.ppm [--ascii]\n", argv[0]); + return 2; + } + ImageT a = {0}; + ImageT b = {0}; + if (loadImage(aPath, &a) < 0) { + return 1; + } + if (loadImage(bPath, &b) < 0) { + freeImage(&a); + return 1; + } + if (a.width != b.width || a.height != b.height || a.channels != b.channels) { + fprintf(stderr, "imgDiff: dimensions differ: %dx%dx%d vs %dx%dx%d\n", + a.width, a.height, a.channels, + b.width, b.height, b.channels); + freeImage(&a); + freeImage(&b); + return 1; + } + reportDiff(&a, &b, ascii); + freeImage(&a); + freeImage(&b); + return 0; +} diff --git a/port/tools/imgStats.c b/port/tools/imgStats.c new file mode 100644 index 0000000..db1d85d --- /dev/null +++ b/port/tools/imgStats.c @@ -0,0 +1,261 @@ +// imgStats: text summary of a single PPM (P6) or PGM (P5). +// Reports dimensions, non-black coverage, luminance mean/histogram, +// and per-row/column ink density. Useful for asking "did anything draw?" +// and "where on the screen?" without ever Read'ing the PNG. +// +// Usage: imgStats img.ppm + +#include +#include +#include +#include +#include +#include + + +typedef struct { + int width; + int height; + int channels; + uint8_t *pixels; +} ImageT; + + +static int loadImage(const char *path, ImageT *out); +static int readToken(FILE *f, char *buf, size_t size); +static void reportStats(const ImageT *img); +static int skipWhitespaceAndComments(FILE *f); + + +static int loadImage(const char *path, ImageT *out) { + FILE *f = fopen(path, "rb"); + if (f == NULL) { + fprintf(stderr, "imgStats: cannot open %s\n", path); + return -1; + } + char magic[8]; + if (readToken(f, magic, sizeof(magic)) < 0) { + fclose(f); + return -1; + } + int channels = 0; + if (strcmp(magic, "P6") == 0) { + channels = 3; + } else if (strcmp(magic, "P5") == 0) { + channels = 1; + } else { + fprintf(stderr, "imgStats: %s: unsupported magic '%s'\n", path, magic); + fclose(f); + return -1; + } + char tok[32]; + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int width = atoi(tok); + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int height = atoi(tok); + if (readToken(f, tok, sizeof(tok)) < 0) { fclose(f); return -1; } + int maxval = atoi(tok); + if (maxval != 255) { + fprintf(stderr, "imgStats: %s: maxval %d not supported\n", path, maxval); + fclose(f); + return -1; + } + // readToken already consumed the single whitespace separator + // following maxval, so binary pixel data starts at the cursor. + size_t need = (size_t)width * (size_t)height * (size_t)channels; + uint8_t *pixels = malloc(need); + if (pixels == NULL) { + fclose(f); + return -1; + } + if (fread(pixels, 1, need, f) != need) { + fprintf(stderr, "imgStats: %s: short read\n", path); + free(pixels); + fclose(f); + return -1; + } + fclose(f); + out->width = width; + out->height = height; + out->channels = channels; + out->pixels = pixels; + return 0; +} + + +static int readToken(FILE *f, char *buf, size_t size) { + if (skipWhitespaceAndComments(f) < 0) { + return -1; + } + size_t i = 0; + int c; + while ((c = fgetc(f)) != EOF && !isspace(c)) { + if (i + 1 >= size) { + return -1; + } + buf[i++] = (char)c; + } + buf[i] = '\0'; + return (int)i; +} + + +static void reportStats(const ImageT *img) { + int w = img->width; + int h = img->height; + int ch = img->channels; + size_t total = (size_t)w * (size_t)h; + size_t nonBlack = 0; + uint64_t sumLum = 0; + // 8-bin luminance histogram + size_t lumHist[8] = {0}; + // Per-row and per-column ink (non-black) counts + int *rowInk = calloc((size_t)h, sizeof(int)); + int *colInk = calloc((size_t)w, sizeof(int)); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + const uint8_t *p = img->pixels + ((size_t)y * w + x) * ch; + int lum; + if (ch == 3) { + lum = (p[0] + p[1] + p[2]) / 3; + } else { + lum = p[0]; + } + sumLum += (uint64_t)lum; + lumHist[lum >> 5]++; + if (lum > 16) { + nonBlack++; + rowInk[y]++; + colInk[x]++; + } + } + } + double meanLum = (double)sumLum / (double)total; + double pct = (100.0 * (double)nonBlack) / (double)total; + printf("dims: %dx%d (%zu px, %d ch)\n", w, h, total, ch); + printf("non_black_px: %zu (%.2f%%)\n", nonBlack, pct); + printf("mean_lum: %.2f\n", meanLum); + printf("lum_hist (0-31, 32-63, ...): "); + for (int i = 0; i < 8; i++) { + printf("%zu%s", lumHist[i], i == 7 ? "\n" : " "); + } + // Row density: fold rows into 24 buckets + const int rowBuckets = 24; + int rowB[rowBuckets]; + memset(rowB, 0, sizeof(rowB)); + for (int y = 0; y < h; y++) { + rowB[(y * rowBuckets) / h] += rowInk[y]; + } + int rowPeak = 0; + for (int i = 0; i < rowBuckets; i++) { + if (rowB[i] > rowPeak) { + rowPeak = rowB[i]; + } + } + printf("row_ink (top->bottom, 24 buckets, '#'=peak=%d):\n ", rowPeak); + for (int i = 0; i < rowBuckets; i++) { + int v = rowB[i]; + char g; + if (v == 0) { + g = '.'; + } else if (rowPeak == 0) { + g = '.'; + } else if (v == rowPeak) { + g = '#'; + } else { + int level = (v * 9 + rowPeak - 1) / rowPeak; + if (level < 1) { level = 1; } + if (level > 9) { level = 9; } + g = (char)('0' + level); + } + putchar(g); + } + putchar('\n'); + // Column density: fold cols into 40 buckets + const int colBuckets = 40; + int colB[colBuckets]; + memset(colB, 0, sizeof(colB)); + for (int x = 0; x < w; x++) { + colB[(x * colBuckets) / w] += colInk[x]; + } + int colPeak = 0; + for (int i = 0; i < colBuckets; i++) { + if (colB[i] > colPeak) { + colPeak = colB[i]; + } + } + printf("col_ink (left->right, 40 buckets, '#'=peak=%d):\n ", colPeak); + for (int i = 0; i < colBuckets; i++) { + int v = colB[i]; + char g; + if (v == 0) { + g = '.'; + } else if (colPeak == 0) { + g = '.'; + } else if (v == colPeak) { + g = '#'; + } else { + int level = (v * 9 + colPeak - 1) / colPeak; + if (level < 1) { level = 1; } + if (level > 9) { level = 9; } + g = (char)('0' + level); + } + putchar(g); + } + putchar('\n'); + // Horizon detection: row with biggest jump in cumulative ink + // (heuristic for sky/ground transition) + int bestRow = -1; + long bestJump = 0; + long window = 8; + for (int y = (int)window; y < h - (int)window; y++) { + long up = 0; + long down = 0; + for (int k = 1; k <= (int)window; k++) { + up += rowInk[y - k]; + down += rowInk[y + k]; + } + long jump = labs(down - up); + if (jump > bestJump) { + bestJump = jump; + bestRow = y; + } + } + if (bestRow >= 0) { + printf("horizon_row_guess: %d (delta_ink=%ld over +-%ld rows)\n", + bestRow, bestJump, window); + } + free(rowInk); + free(colInk); +} + + +static int skipWhitespaceAndComments(FILE *f) { + int c; + while ((c = fgetc(f)) != EOF) { + if (c == '#') { + while ((c = fgetc(f)) != EOF && c != '\n') { + // discard + } + } else if (!isspace(c)) { + ungetc(c, f); + return 0; + } + } + return -1; +} + + +int main(int argc, char **argv) { + if (argc != 2) { + fprintf(stderr, "usage: %s img.ppm\n", argv[0]); + return 2; + } + ImageT img = {0}; + if (loadImage(argv[1], &img) < 0) { + return 1; + } + reportStats(&img); + free(img.pixels); + return 0; +} diff --git a/port/tools/matrixProbe.c b/port/tools/matrixProbe.c new file mode 100644 index 0000000..b1222dd --- /dev/null +++ b/port/tools/matrixProbe.c @@ -0,0 +1,148 @@ +// matrixProbe: drives the port's matrix construction (sceneryAttachCamera) +// with controlled inputs and dumps the resulting $78..$89 matrix. +// +// Usage: matrixProbe [worldX worldY worldZ] +// yaw/pitch/bank are byte angles (0..255, 256 = full circle) +// world coords default to (0,0,0) in metres. +// +// Compares directly to MAME's captured $78..$89. Inputs corresponding +// to the boot Meigs view: yaw=255 (i.e. -1 byte = ~-1.4 deg), pitch=0, +// bank=0, position metres (95, 0, 268). + +#include +#include +#include +#include + +#include "camera.h" +#include "math6502.h" +#include "renderer.h" +#include "sceneryVm.h" + +// Avoid linking aircraft.c just for AC_WORLD_UNITS. +#define AC_WORLD_UNITS(n) ((int32_t)(n) * (1 << CAM_POS_FRACT_BITS)) + +// Stubs for renderer/palette/fs2math symbols pulled in transitively +// from sceneryVm.c. sceneryAttachCamera never invokes them, so the +// stubs only need to satisfy the linker. +#include "fs2math.h" +#include "palette.h" +void rendererDrawLine(RenderStateT *s, int16_t x1, int16_t y1, int16_t x2, int16_t y2) { + (void)s; (void)x1; (void)y1; (void)x2; (void)y2; +} +void rendererDrawColorSpan(RenderStateT *s, int16_t xRight, int16_t length, int16_t y) { + (void)s; (void)xRight; (void)length; (void)y; +} +void rendererFillPolygon(RenderStateT *s, const int16_t *xs, const int16_t *ys, int count) { + (void)s; (void)xs; (void)ys; (void)count; +} +void rendererSetDrawColor(RenderStateT *s, ColorE c) { + (void)s; (void)c; +} +void rendererSetHiresColor(RenderStateT *s, uint8_t code) { + (void)s; (void)code; +} +ColorE paletteFromSceneryCode(uint8_t code) { + (void)code; + return (ColorE)0; +} +int16_t fs2ScaleByAX(int16_t v, int16_t s) { (void)v; (void)s; return 0; } + + +static int16_t readMatrix16(const uint8_t *ram, uint16_t addr) { + uint16_t v = (uint16_t)ram[addr] | ((uint16_t)ram[addr + 1] << 8); + return (int16_t)v; +} + + +static void printMatrix(const uint8_t *ram, const char *tag) { + int16_t m[3][3]; + m[0][0] = readMatrix16(ram, 0x78); + m[0][1] = readMatrix16(ram, 0x7A); + m[0][2] = readMatrix16(ram, 0x7C); + m[1][0] = readMatrix16(ram, 0x7E); + m[1][1] = readMatrix16(ram, 0x80); + m[1][2] = readMatrix16(ram, 0x82); + m[2][0] = readMatrix16(ram, 0x84); + m[2][1] = readMatrix16(ram, 0x86); + m[2][2] = readMatrix16(ram, 0x88); + printf("%s matrix at $78..$89 (post-L6301):\n", tag); + for (int i = 0; i < 3; i++) { + printf(" row %d: %6d %6d %6d\n", i, m[i][0], m[i][1], m[i][2]); + } +} + + +static void printMameRef(void) { + printf("MAME reference (boot frame 13000, Meigs view):\n"); + printf(" row 0: 16382 0 0\n"); + printf(" row 1: 0 32760 100\n"); + printf(" row 2: 0 -401 8190\n"); + printf(" inputs: yaw=$FF93=-109(16b), pitch=0, bank=0,\n"); + printf(" ViewDirection=$0F=15 ($3E=$F0=-22.5deg byte angle)\n"); + printf(" camX=287 camY=804 camAlt=0 (scenery units = m*3)\n"); +} + + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, + "usage: %s [wx_m wy_m wz_m] [vd] [pitchFine]\n", + argv[0]); + fprintf(stderr, + " boot Meigs reference: %s 0 255 0 95 0 268 0 0x93\n", + argv[0]); + return 2; + } + uint8_t yawByte = (uint8_t)strtol(argv[1], NULL, 0); + uint8_t pitchByte = (uint8_t)strtol(argv[2], NULL, 0); + uint8_t bankByte = (uint8_t)strtol(argv[3], NULL, 0); + int32_t wxMetres = (argc > 4) ? (int32_t)strtol(argv[4], NULL, 0) : 0; + int32_t wyMetres = (argc > 5) ? (int32_t)strtol(argv[5], NULL, 0) : 0; + int32_t wzMetres = (argc > 6) ? (int32_t)strtol(argv[6], NULL, 0) : 0; + uint8_t vdByte = (argc > 7) ? (uint8_t)strtol(argv[7], NULL, 0) : 0; + uint8_t pitchFine = (argc > 8) ? (uint8_t)strtol(argv[8], NULL, 0) : 0; + + printf("inputs: yaw=%u pitch=%u bank=%u pos_m=(%d,%d,%d)\n\n", + yawByte, pitchByte, bankByte, wxMetres, wyMetres, wzMetres); + + math6502Init(); + + CameraT cam; + cameraInit(&cam); + cam.yaw = yawByte; + cam.pitch = pitchByte; + cam.bank = bankByte; + cam.pitchFine = pitchFine; + cam.viewDirection = vdByte; + cam.worldX = AC_WORLD_UNITS(wxMetres); + cam.worldY = AC_WORLD_UNITS(wyMetres); + cam.worldZ = AC_WORLD_UNITS(wzMetres); + cameraUpdate(&cam); + + // sceneryAttachCamera writes the matrix through writableRam at + // $78..$89. Allocate a 64K buffer so addressing matches chunk5. + uint8_t *ram = calloc(0x10000, 1); + if (ram == NULL) { return 1; } + + SceneryStateT state; + memset(&state, 0, sizeof(state)); + state.writableRam = ram; + // sceneryAttachCamera reads $4A/$4B/$4D/$4E/$50/$51 if + // SCENERY_USE_RAM_BASE is set; we don't, so leave them zero. + + printf("cam->rot Q1.15:\n"); + for (int i = 0; i < 3; i++) { + printf(" row %d: %6d %6d %6d\n", i, + cam.rot[i][0], cam.rot[i][1], cam.rot[i][2]); + } + printf("\n"); + + sceneryAttachCamera(&state, &cam); + + printMatrix(ram, "PORT"); + printf("\n"); + printMameRef(); + free(ram); + return 0; +} diff --git a/port/tools/nib2dsk.c b/port/tools/nib2dsk.c new file mode 100644 index 0000000..e127bfe --- /dev/null +++ b/port/tools/nib2dsk.c @@ -0,0 +1,236 @@ +// .nib -> .dsk converter for Apple II 5.25" disks. +// +// The .nib format stores 6656 raw nibbles per track (35 tracks total = +// 232,960 bytes) with the sector framing intact: D5 AA 96 marks the +// address field, D5 AA AD marks the data field, and the data field +// holds 342 6-and-2 encoded nibbles plus a checksum that decodes back +// to 256 bytes per sector. +// +// .dsk is just 256 * 16 * 35 = 143,360 bytes of decoded sector data +// in DOS 3.3 sector-interleave order (this tool emits DOS order, +// matching the .DSK conventions on Asimov; pass `--prodos` to emit +// the ProDOS .po block order instead). + +#include +#include +#include +#include + +#define NIB_BYTES_PER_TRACK 6656 +#define DSK_BYTES_PER_TRACK 4096 +#define TRACKS 35 +#define SECTORS 16 + + +// 6-and-2 decode lookup: maps an Apple disk nibble (0x96..0xFF) to its +// 6-bit value (0x00..0x3F) or 0xFF if the nibble is invalid. +static uint8_t decTable[256]; +static uint8_t dosToProDos[16] = { 0, 8, 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15 }; +static uint8_t dosToPhysical[16] = { 0, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 15 }; + + +static void buildDecTable(void); +static int decodeTrack(const uint8_t *nibTrack, uint8_t outSectors[SECTORS][256]); +static int findMarker(const uint8_t *buf, int *pos, int len, uint8_t a, uint8_t b, uint8_t c); + + +static void buildDecTable(void) { + static const uint8_t physicalCodes[64] = { + 0x96, 0x97, 0x9A, 0x9B, 0x9D, 0x9E, 0x9F, 0xA6, + 0xA7, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF, 0xB2, 0xB3, + 0xB4, 0xB5, 0xB6, 0xB7, 0xB9, 0xBA, 0xBB, 0xBC, + 0xBD, 0xBE, 0xBF, 0xCB, 0xCD, 0xCE, 0xCF, 0xD3, + 0xD6, 0xD7, 0xD9, 0xDA, 0xDB, 0xDC, 0xDD, 0xDE, + 0xDF, 0xE5, 0xE6, 0xE7, 0xE9, 0xEA, 0xEB, 0xEC, + 0xED, 0xEE, 0xEF, 0xF2, 0xF3, 0xF4, 0xF5, 0xF6, + 0xF7, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF + }; + for (int i = 0; i < 256; i++) { + decTable[i] = 0xFF; + } + for (int i = 0; i < 64; i++) { + decTable[physicalCodes[i]] = (uint8_t)i; + } +} + + +static int findMarker(const uint8_t *buf, int *pos, int len, uint8_t a, uint8_t b, uint8_t c) { + for (int p = *pos; p + 2 < len; p++) { + if (buf[p] == a && buf[p + 1] == b && buf[p + 2] == c) { + *pos = p + 3; + return 1; + } + } + return 0; +} + + +// Read 8 4-and-4 encoded nibbles -> 4 bytes (volume, track, sector, +// checksum). Returns 1 on success, 0 if there isn't enough buffer. +static int read44(const uint8_t *buf, int *pos, int len, uint8_t out[4]) { + if (*pos + 8 > len) { + return 0; + } + for (int i = 0; i < 4; i++) { + uint8_t hi = buf[*pos + i * 2]; + uint8_t lo = buf[*pos + i * 2 + 1]; + out[i] = (uint8_t)(((hi << 1) | 1) & lo); + } + *pos += 8; + return 1; +} + + +// Decode a 6-and-2 data field at `*pos`. Outputs 256 bytes, advances +// `*pos` past the consumed nibbles. Returns 1 on success, 0 on +// checksum mismatch or truncation. +static int decodeDataField(const uint8_t *buf, int *pos, int len, uint8_t out[256]) { + if (*pos + 343 > len) { + return 0; + } + uint8_t twos[86]; + uint8_t sixes[256]; + + // 86 lower-2-bit nibbles followed by 256 upper-6-bit nibbles + // followed by 1 checksum nibble. Each nibble is XORed with + // the running checksum during decode. + uint8_t running = 0; + for (int i = 85; i >= 0; i--) { + uint8_t nib = decTable[buf[(*pos)++]]; + if (nib == 0xFF) { + return 0; + } + running ^= nib; + twos[i] = running; + } + for (int i = 0; i < 256; i++) { + uint8_t nib = decTable[buf[(*pos)++]]; + if (nib == 0xFF) { + return 0; + } + running ^= nib; + sixes[i] = running; + } + uint8_t cksum = decTable[buf[(*pos)++]]; + if (cksum == 0xFF || running != cksum) { + return 0; + } + + // Combine the 6-bit "sixes" with the 2-bit "twos" (3 sectors of + // bit-pair stream; bit-pair n of a sixes byte goes to byte n, + // n+86, or n+172). + for (int i = 0; i < 256; i++) { + uint8_t two_idx = (uint8_t)(i % 86); + uint8_t two_pos = (uint8_t)(i / 86); + uint8_t low2 = (uint8_t)((twos[two_idx] >> (two_pos * 2)) & 0x03); + // Bit-reverse the 2-bit nibble (Apple writes the high + // bit of each pair first). + low2 = (uint8_t)(((low2 & 1) << 1) | ((low2 >> 1) & 1)); + out[i] = (uint8_t)((sixes[i] << 2) | low2); + } + return 1; +} + + +static int decodeTrack(const uint8_t *nibTrack, uint8_t outSectors[SECTORS][256]) { + int pos = 0; + int decoded = 0; + uint8_t haveSector[SECTORS] = { 0 }; + uint8_t addr[4]; + + while (pos < NIB_BYTES_PER_TRACK && decoded < SECTORS) { + if (!findMarker(nibTrack, &pos, NIB_BYTES_PER_TRACK, 0xD5, 0xAA, 0x96)) { + break; + } + if (!read44(nibTrack, &pos, NIB_BYTES_PER_TRACK, addr)) { + break; + } + if (addr[2] >= SECTORS) { + continue; + } + if (haveSector[addr[2]]) { + continue; + } + if (!findMarker(nibTrack, &pos, NIB_BYTES_PER_TRACK, 0xD5, 0xAA, 0xAD)) { + break; + } + if (!decodeDataField(nibTrack, &pos, NIB_BYTES_PER_TRACK, outSectors[addr[2]])) { + continue; + } + haveSector[addr[2]] = 1; + decoded++; + } + return decoded; +} + + +int main(int argc, char **argv) { + const uint8_t *interleave = dosToPhysical; + int argIndex = 1; + + if (argIndex < argc && strcmp(argv[argIndex], "--prodos") == 0) { + interleave = dosToProDos; + argIndex++; + } + if (argc - argIndex != 2) { + fprintf(stderr, "usage: %s [--prodos] in.nib out.dsk\n", argv[0]); + return 1; + } + + const char *inPath = argv[argIndex]; + const char *outPath = argv[argIndex + 1]; + + FILE *f = fopen(inPath, "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", inPath); + return 1; + } + uint8_t *nib = malloc(TRACKS * NIB_BYTES_PER_TRACK); + if (nib == NULL) { + fclose(f); + return 1; + } + size_t nRead = fread(nib, 1, TRACKS * NIB_BYTES_PER_TRACK, f); + fclose(f); + if (nRead != TRACKS * NIB_BYTES_PER_TRACK) { + fprintf(stderr, "short read: got %zu bytes (expected %d)\n", + nRead, TRACKS * NIB_BYTES_PER_TRACK); + free(nib); + return 1; + } + + buildDecTable(); + + uint8_t (*track)[256] = malloc(SECTORS * 256); + if (track == NULL) { + free(nib); + return 1; + } + FILE *out = fopen(outPath, "wb"); + if (out == NULL) { + fprintf(stderr, "cannot open %s\n", outPath); + free(nib); + free(track); + return 1; + } + + int totalDecoded = 0; + for (int t = 0; t < TRACKS; t++) { + memset(track, 0, SECTORS * 256); + int got = decodeTrack(&nib[t * NIB_BYTES_PER_TRACK], (uint8_t (*)[256])track); + totalDecoded += got; + for (int dosSector = 0; dosSector < SECTORS; dosSector++) { + int physSector = interleave[dosSector]; + fwrite(track[physSector], 1, 256, out); + } + if (got < SECTORS) { + fprintf(stderr, "track %2d: %d/%d sectors decoded\n", t, got, SECTORS); + } + } + fclose(out); + free(nib); + free(track); + + printf("decoded %d / %d sectors -> %s\n", totalDecoded, TRACKS * SECTORS, outPath); + return totalDecoded == TRACKS * SECTORS ? 0 : 1; +} diff --git a/port/tools/prodosextract.c b/port/tools/prodosextract.c new file mode 100644 index 0000000..429974f --- /dev/null +++ b/port/tools/prodosextract.c @@ -0,0 +1,264 @@ +// Minimal ProDOS volume reader. +// +// Walks the volume directory, follows index/master-index blocks for +// sapling/tree files, and writes each file out as a flat binary. Big +// enough to handle the FS2 "san inc pack" image (which ships all the +// scenery as ProDOS files), small enough to live in tools/. + +#include +#include +#include +#include +#include + + +#define BLOCK_SIZE 512 +#define ENTRY_SIZE 39 +#define ENTRIES_PER_BLOCK 13 + + +static const uint8_t *image; +static size_t imageSize; + + +typedef struct EntryT { + uint8_t storageTypeNameLen; + char name[16]; + uint8_t fileType; + uint16_t keyPointer; + uint16_t blocksUsed; + uint32_t eof; + uint16_t auxType; +} EntryT; + + +static int extractFile(const EntryT *e, const char *outDir); +static const uint8_t *blockAt(uint16_t block); +static int parseEntry(const uint8_t *raw, EntryT *out); +static int walkVolume(const char *outDir); +static int writeBlocks(FILE *out, FILE *blockList, uint16_t keyBlock, uint8_t storageType, uint32_t eof); +static int writeSapling(FILE *out, FILE *blockList, uint16_t indexBlock, uint32_t eof); +static int writeTree(FILE *out, FILE *blockList, uint16_t masterBlock, uint32_t eof); + + +static const uint8_t *blockAt(uint16_t block) { + size_t off = (size_t)block * BLOCK_SIZE; + if (off + BLOCK_SIZE > imageSize) { + fprintf(stderr, "block %u out of range\n", block); + return NULL; + } + return image + off; +} + + +static int parseEntry(const uint8_t *raw, EntryT *out) { + memset(out, 0, sizeof(*out)); + out->storageTypeNameLen = raw[0]; + uint8_t storageType = (uint8_t)(raw[0] >> 4); + uint8_t nameLen = (uint8_t)(raw[0] & 0x0F); + if (storageType == 0 || nameLen == 0 || nameLen > 15) { + return 0; + } + memcpy(out->name, &raw[1], nameLen); + out->name[nameLen] = '\0'; + out->fileType = raw[0x10]; + out->keyPointer = (uint16_t)(raw[0x11] | (raw[0x12] << 8)); + out->blocksUsed = (uint16_t)(raw[0x13] | (raw[0x14] << 8)); + out->eof = (uint32_t)(raw[0x15]) | ((uint32_t)raw[0x16] << 8) | ((uint32_t)raw[0x17] << 16); + out->auxType = (uint16_t)(raw[0x1F] | (raw[0x20] << 8)); + return 1; +} + + +// Walk a sapling (storage_type == 2): key_pointer points at one index +// block containing 256 16-bit data-block pointers (lo bytes 0..255, +// then hi bytes 256..511). Sparse data blocks (pointer = 0) are +// represented as zero-filled output bytes. If `blockList` is non-NULL, +// each block number is also appended to that file as a 16-bit +// little-endian word -- producing an in-order list of every ProDOS +// block this file occupies on disk. +static int writeSapling(FILE *out, FILE *blockList, uint16_t indexBlock, uint32_t eof) { + const uint8_t *idx = blockAt(indexBlock); + if (idx == NULL) { + return 0; + } + uint32_t remaining = eof; + for (int i = 0; i < 256 && remaining > 0; i++) { + uint16_t blk = (uint16_t)(idx[i] | (idx[i + 256] << 8)); + uint32_t chunk = remaining > BLOCK_SIZE ? BLOCK_SIZE : remaining; + if (blockList != NULL) { + uint8_t entry[2] = { (uint8_t)(blk & 0xFF), (uint8_t)((blk >> 8) & 0xFF) }; + fwrite(entry, 1, 2, blockList); + } + if (blk == 0) { + static const uint8_t zeros[BLOCK_SIZE] = { 0 }; + fwrite(zeros, 1, chunk, out); + } else { + const uint8_t *data = blockAt(blk); + if (data == NULL) { + return 0; + } + fwrite(data, 1, chunk, out); + } + remaining -= chunk; + } + return 1; +} + + +// Walk a tree (storage_type == 3): key_pointer is a master index +// block. Empirically the master index uses the same lo[0..255] / +// hi[256..511] layout as a regular sapling index block (Beneath Apple +// ProDOS describes a +128 split for master indexes, but the volumes +// I have in hand all use +256 -- the FS2 "san inc pack" image and +// every other ProDOS image I tested). +static int writeTree(FILE *out, FILE *blockList, uint16_t masterBlock, uint32_t eof) { + const uint8_t *master = blockAt(masterBlock); + if (master == NULL) { + return 0; + } + uint32_t remaining = eof; + for (int i = 0; i < 128 && remaining > 0; i++) { + uint16_t indexBlk = (uint16_t)(master[i] | (master[i + 256] << 8)); + uint32_t chunkMax = (uint32_t)256 * BLOCK_SIZE; + uint32_t chunk = remaining > chunkMax ? chunkMax : remaining; + if (indexBlk == 0) { + // Whole subtree is sparse -> output zeros and + // record zero block-numbers for the gap. + for (uint32_t z = 0; z < chunk; z++) { + fputc(0, out); + } + if (blockList != NULL) { + uint32_t blocksInGap = (chunk + BLOCK_SIZE - 1) / BLOCK_SIZE; + for (uint32_t z = 0; z < blocksInGap; z++) { + uint8_t zero[2] = { 0, 0 }; + fwrite(zero, 1, 2, blockList); + } + } + } else { + if (!writeSapling(out, blockList, indexBlk, chunk)) { + return 0; + } + } + remaining -= chunk; + } + return 1; +} + + +static int writeBlocks(FILE *out, FILE *blockList, uint16_t keyBlock, uint8_t storageType, uint32_t eof) { + switch (storageType) { + case 1: { + const uint8_t *data = blockAt(keyBlock); + if (data == NULL) { + return 0; + } + if (blockList != NULL) { + uint8_t entry[2] = { (uint8_t)(keyBlock & 0xFF), (uint8_t)((keyBlock >> 8) & 0xFF) }; + fwrite(entry, 1, 2, blockList); + } + fwrite(data, 1, eof > BLOCK_SIZE ? BLOCK_SIZE : eof, out); + return 1; + } + case 2: + return writeSapling(out, blockList, keyBlock, eof); + case 3: + return writeTree(out, blockList, keyBlock, eof); + default: + fprintf(stderr, "unsupported storage type %u\n", storageType); + return 0; + } +} + + +static int extractFile(const EntryT *e, const char *outDir) { + char outPath[1024]; + char blockListPath[1024]; + snprintf(outPath, sizeof(outPath), "%s/%s", outDir, e->name); + snprintf(blockListPath, sizeof(blockListPath), "%s/%s.blocks", outDir, e->name); + FILE *out = fopen(outPath, "wb"); + FILE *blks = fopen(blockListPath, "wb"); + if (out == NULL || blks == NULL) { + fprintf(stderr, "cannot open %s / %s\n", outPath, blockListPath); + if (out != NULL) fclose(out); + if (blks != NULL) fclose(blks); + return 0; + } + uint8_t storageType = (uint8_t)(e->storageTypeNameLen >> 4); + int ok = writeBlocks(out, blks, e->keyPointer, storageType, e->eof); + fclose(out); + fclose(blks); + printf(" %-16s type=$%02X aux=$%04X blocks=%u eof=%u storage=%u%s\n", + e->name, e->fileType, e->auxType, e->blocksUsed, e->eof, storageType, + ok ? "" : " (FAILED)"); + return ok; +} + + +static int walkVolume(const char *outDir) { + // Volume directory starts at block 2. + uint16_t blockNum = 2; + int isHeader = 1; + int total = 0; + while (blockNum != 0) { + const uint8_t *blk = blockAt(blockNum); + if (blk == NULL) { + return 0; + } + uint16_t nextBlock = (uint16_t)(blk[2] | (blk[3] << 8)); + int offset = 4; + if (isHeader) { + // First entry of block 2 is the volume header, + // not a regular file entry. + uint8_t volNameLen = (uint8_t)(blk[4] & 0x0F); + char volName[16]; + memcpy(volName, &blk[5], volNameLen); + volName[volNameLen] = '\0'; + printf("Volume: /%s/\n", volName); + offset = 4 + ENTRY_SIZE; + isHeader = 0; + } + while (offset + ENTRY_SIZE <= 4 + ENTRIES_PER_BLOCK * ENTRY_SIZE) { + EntryT entry; + if (parseEntry(&blk[offset], &entry)) { + if (extractFile(&entry, outDir)) { + total++; + } + } + offset += ENTRY_SIZE; + } + blockNum = nextBlock; + } + printf("\nextracted %d file(s) -> %s\n", total, outDir); + return total; +} + + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "usage: %s in.po out_dir\n", argv[0]); + return 1; + } + FILE *f = fopen(argv[1], "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", argv[1]); + return 1; + } + fseek(f, 0, SEEK_END); + imageSize = (size_t)ftell(f); + fseek(f, 0, SEEK_SET); + uint8_t *buf = malloc(imageSize); + if (buf == NULL || fread(buf, 1, imageSize, f) != imageSize) { + fprintf(stderr, "could not read %s\n", argv[1]); + fclose(f); + free(buf); + return 1; + } + fclose(f); + image = buf; + + mkdir(argv[2], 0755); + int ok = walkVolume(argv[2]); + free(buf); + return ok > 0 ? 0 : 1; +} diff --git a/port/tools/sampleRow.c b/port/tools/sampleRow.c new file mode 100644 index 0000000..f022d5c --- /dev/null +++ b/port/tools/sampleRow.c @@ -0,0 +1,107 @@ +// sampleRow: print color symbols at sampled X positions for a Y range. +// Reads PPM (P6) images. Used to visually inspect specific rows of a +// rendered scene without ever Read'ing the PNG file (which the user has +// banned for context-corruption reasons). + +#include +#include +#include +#include + + +static int readToken(FILE *f, char *buf, size_t sz) { + int c; + while ((c = fgetc(f)) != EOF) { + if (c == '#') { + while ((c = fgetc(f)) != EOF && c != '\n') {} + continue; + } + if (!isspace(c)) { + break; + } + } + if (c == EOF) { + return -1; + } + size_t i = 0; + do { + if (i + 1 >= sz) { + return -1; + } + buf[i++] = (char)c; + c = fgetc(f); + } while (c != EOF && !isspace(c)); + buf[i] = '\0'; + return (int)i; +} + + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, "usage: %s img.ppm yStart yEnd [scaleY]\n", argv[0]); + return 2; + } + FILE *f = fopen(argv[1], "rb"); + if (f == NULL) { + fprintf(stderr, "cannot open %s\n", argv[1]); + return 1; + } + char tok[32]; + if (readToken(f, tok, sizeof(tok)) < 0 || strcmp(tok, "P6") != 0) { + fprintf(stderr, "not P6\n"); + fclose(f); + return 1; + } + readToken(f, tok, sizeof(tok)); int w = atoi(tok); + readToken(f, tok, sizeof(tok)); int h = atoi(tok); + readToken(f, tok, sizeof(tok)); + unsigned char *p = malloc((size_t)w * h * 3); + if (p == NULL || fread(p, 1, (size_t)w * h * 3, f) != (size_t)w * h * 3) { + fclose(f); + free(p); + return 1; + } + fclose(f); + int yStart = atoi(argv[2]); + int yEnd = atoi(argv[3]); + int scaleY = (argc > 4) ? atoi(argv[4]) : (h / 192); + int sampleW = (w >= 70) ? 70 : w; + int xStep = w / sampleW; + for (int hy = yStart; hy <= yEnd; hy++) { + int sy = hy * scaleY + scaleY/2; + if (sy >= h) { + break; + } + printf("y=%3d: ", hy); + for (int sx = 0; sx < w && sx < sampleW * xStep; sx += xStep) { + unsigned char *px = &p[((size_t)sy * w + sx) * 3]; + int r = px[0]; + int g = px[1]; + int b = px[2]; + char c; + if (r < 30 && g < 30 && b < 30) { + c = '.'; + } else if (b > r + 30 && b > g + 30) { + c = 'B'; + } else if (g > r + 30 && g > b + 30) { + c = 'G'; + } else if (r > 200 && g > 200 && b > 200) { + c = 'W'; + } else if (r > 100 && b > 100 && g < (r + b) / 4) { + c = 'V'; + } else if (r > 200 && g > 100 && b < 80) { + c = 'O'; + } else if (r > 100 && b > 100) { + c = 'M'; + } else if (g > 100 && b > 100) { + c = 'C'; + } else { + c = '?'; + } + putchar(c); + } + putchar('\n'); + } + free(p); + return 0; +} diff --git a/port/tools/sceneryExtract.py b/port/tools/sceneryExtract.py new file mode 100755 index 0000000..c6e9d96 --- /dev/null +++ b/port/tools/sceneryExtract.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +""" +SceneryExtract -- decode FS2 .SD scenery files into a polygon database. + +Walks the chunk5 bytecode interpreter logic offline: starts at sid $2C +(the dispatcher), follows every $0D HEADER (loads sub-sections), every +$18 SUB_INVOKE, and every conditional/unconditional jump. Records: + - polygon-emit ops ($00/$01/$02 xform-A, $40/$41 xform-B, + $32/$33 cached-vertex, $35 cached-plot, $2B curve) + - cache-fill ops ($31, $42) + - station records ($05 ADF, $1D NAV, $1E COM) + - frame-control ops ($07 EnterLocalFrame, $24 PushOriginWithStash) + +Output: JSON containing every visited section with its decoded ops, +plus a flat list of all polygon vertices (for downstream renderers +that don't want to interpret the VM themselves). +""" + +import argparse +import json +import os +import sys + + +# -- file-offset arithmetic ------------------------------------------------ + +SECTION_BYTES = 256 # one chunk5 "sector" = 256 bytes +SECTOR_PER_BLOCK = 2 # 2 sectors per ProDOS 512-byte block +MAX_SID = 0x87 # ComputeBlockFromSector rejects sid >= $88 +DISPATCHER_SID = 0x2C # bootstrap (LA619) sid; common to all files + + +def sidToOffset(sid): + """Return .SD file byte offset for a section id, or None if out of range. + + Empirically verified: FS2.1's dispatcher (sid $2C) lives at file + offset 49152, which matches MAME's captured RAM at $A800. + The formula: + file_offset = (((sid >> 2) + 1) * 16 + (sid & 3)) * 256 + The runtime path (chunk4.s::FetchSectorFromDisk + ReadBlocks via + SmartPort) does additional shifts to convert this into a ProDOS + block# and looks up the actual disk block via an in-memory table + seeded from the ProDOS file index. For an offline extractor + operating on the .SD (which is the file content in logical block + order), the formula above directly indexes the file. + """ + if sid < 0 or sid > MAX_SID: + return None + return (((sid >> 2) + 1) * 16 + (sid & 3)) * SECTION_BYTES + + +# -- opcode metadata ------------------------------------------------------- + +# Each entry: (mnemonic, fixed-byte-count or None for variable) +OPCODES = { + 0x00: ("EmitV1XformAndPlot", 7), # xform-A V1 + plot pixel + 0x01: ("EmitV1Xform80C5", 7), # xform-A V1 silent + 0x02: ("EmitV2Xform80C5", 7), # xform-A V2 + draw + 0x03: ("Call64K_2", 6), # 64K callback #2 + 0x04: ("CullByOutcodeList", None), # variable (terminator-driven) + 0x05: ("ADFRecord", 9), # freq + 24-bit XYZ + 0x06: ("DrawLine", 5), # 2D screen-coord line + 0x07: ("EnterLocalFrame", 14), # set local frame anchor + 0x08: ("Invalid08", 1), + 0x09: ("Skip3", 3), + 0x0A: ("Skip3b", 3), + 0x0B: ("JumpRelative", 3), # 16-bit signed offset + 0x0C: ("Invalid0C", 1), + 0x0D: ("Header", 6), # SECTION-LOAD opcode + 0x0E: ("Call64K", 1), # no-op in 48K mode + 0x0F: ("Invalid0F", 1), + 0x10: ("Invalid10", 1), + 0x11: ("Skip1", 1), + 0x12: ("SetColor", 2), + 0x13: ("JumpIfBeyondXY", 9), # cull XY-plane + 0x14: ("JumpIfBeyondXYZ", 11), # cull XYZ + 0x15: ("Invalid15", 1), + 0x16: ("Invalid16", 1), + 0x17: ("Invalid17", 1), + 0x18: ("SubInvoke", 3), # JSR-style relative + 0x19: ("Return", 1), # RTS + 0x1A: ("WriteWord", 5), # *dst = *src + 0x1B: ("ModeWhite", 1), + 0x1C: ("DayOnly", 1), + 0x1D: ("NAVRecord", 11), # freq + XYZ + 16-bit Z + 0x1E: ("COMRecord", None), # variable; len in cursor[1] + 0x1F: ("Invalid1F", 1), + 0x20: ("CullIfOutside1", 9), + 0x21: ("CullIfOutside2", 15), + 0x22: ("CullIfOutside3", 21), + 0x23: ("JumpIfBitsClear", 7), + 0x24: ("PushOriginWithStash", 8), + 0x25: ("StoreImmWord", 5), + 0x26: ("Invalid26", 1), + 0x27: ("Invalid27", 1), + 0x28: ("JumpIfWordCompare", 9), + 0x29: ("CopyToD2", 1), + 0x2A: ("Invalid2A", 1), + 0x2B: ("EmitCurve", 9), + 0x2C: ("Invalid2C", 1), + 0x2D: ("Invalid2D", 1), + 0x2E: ("Invalid2E", 1), + 0x2F: ("ResetState", 1), + 0x30: ("Invalid30", 1), + 0x31: ("RefreshCachedXform80C5", 8), + 0x32: ("VertexCachedV1", 2), + 0x33: ("VertexCachedV2", 2), + 0x34: ("Invalid34", 1), + 0x35: ("VertexCachedDraw", 2), + 0x40: ("EmitV1Xform7EBC", 5), + 0x41: ("EmitV2Xform7EBC", 5), + 0x42: ("RefreshCachedXform7EBC", 6), +} + + +# -- bytecode decoder ------------------------------------------------------ + +# We DON'T evaluate culls (we want to see ALL polygons). We DO evaluate: +# - $0D HEADER: load and walk the referenced section +# - $18 SUB_INVOKE: walk the referenced sub-record then return +# - $19 RTS: terminate current walk +# - jumps: take fall-through (don't follow conditional branches) +# This biases toward the WORST CASE of geometry the file describes, +# which is what we want for an offline asset extraction. + +class Decoder: + def __init__(self, sd_bytes): + self.sd = sd_bytes + self.sections = {} # sid -> {"raw": bytes, "ops": [...]} + self.visitedHeaders = set() + self.errors = [] + + def readSection(self, sid, count_sectors=1): + """Return up to count_sectors * 256 bytes starting at sid.""" + off = sidToOffset(sid) + if off is None: + return None + end = off + count_sectors * SECTION_BYTES + if end > len(self.sd): + end = len(self.sd) + return self.sd[off:end] + + def decodeSection(self, sid, count_sectors, depth=0, source=None): + """Walk the bytecode for one section, recording ops we encounter.""" + key = (sid, count_sectors) + if key in self.visitedHeaders: + return + self.visitedHeaders.add(key) + + data = self.readSection(sid, count_sectors) + if data is None or len(data) == 0: + self.errors.append(f"section ${sid:02X} unreadable") + return + + ops = [] + secInfo = { + "sid": sid, + "file_offset": sidToOffset(sid), + "count_sectors": count_sectors, + "size_bytes": len(data), + "source": source, + "ops": ops, + } + self.sections[sid] = secInfo + coverage = self._walk(data, sid, ops, depth) + secInfo["coverage_bytes"] = coverage["covered"] + secInfo["unreached_bytes"] = coverage["unreached"] + secInfo["out_of_range_branches"] = coverage["out_of_range"] + + def _readSignedWord(self, data, off): + """Read 16-bit LE signed word, or None if out of range.""" + if off + 1 >= len(data): + return None + v = data[off] | (data[off + 1] << 8) + return v - 0x10000 if v >= 0x8000 else v + + def _opLen(self, data, pc): + """Return byte count for opcode at data[pc], or None to halt walk.""" + if pc >= len(data): + return None + op = data[pc] + if op & 0x80 or op > 0x45: + return None # SceneryStreamEnd (terminator) + meta = OPCODES.get(op) + if meta is None: + return 1 # unknown: skip 1 byte, keep walking + _, n = meta + if n is not None: + return n + # Variable length: + if op == 0x1E: + if pc + 1 >= len(data): + return None + length = data[pc + 1] + return length # COMRecord advances by `length`, NOT length+2 + if op == 0x04: + # CullByOutcodeList: opcode + 2-byte jump + N vertex-index + # bytes + 1-byte terminator (high bit set). Per chunk5.s:2019, + # cursor advances by `4 + N` bytes total. + n = 3 + while pc + n < len(data) and (data[pc + n] & 0x80) == 0: + n += 1 + return n + 1 # include terminator byte + return 1 + + def _walk(self, data, base_sid, ops, depth): + """Walk every reachable byte in this section's bytecode. + + Each pc gets visited at most once (visited set). For conditional + jumps and SubInvoke, both branches are followed (depth-first). + For HEADER, the referenced section is loaded recursively. + """ + visited = set() + worklist = [0] + outOfRangeBranches = 0 + while worklist: + pc = worklist.pop() + if pc in visited or pc < 0 or pc >= len(data): + continue + visited.add(pc) + op = data[pc] + if op & 0x80 or op > 0x45: + continue # terminator: end of this branch + n = self._opLen(data, pc) + if n is None: + continue + opname = OPCODES.get(op, ("Unknown", 1))[0] + entry = { + "pc": pc, + "op": op, + "name": opname, + "bytes": list(data[pc:pc + n]), + } + self._annotate(entry, data, pc, base_sid, depth) + ops.append(entry) + + if op == 0x0D and depth < 6: + # Section load: walk the referenced section. + sid_load = data[pc + 1] + cnt_load = data[pc + 2] + self.decodeSection(sid_load, cnt_load, depth + 1, + source=f"sid${base_sid:02X}@{pc}") + # Continue past the HEADER into the dispatcher. + worklist.append(pc + n) + elif op == 0x18: + # SubInvoke: take BOTH the call target (in-section) AND + # the resume point (pc + 3). The call uses a 16-bit + # signed offset relative to the opcode address. + off = self._readSignedWord(data, pc + 1) + if off is not None: + target = pc + off + if 0 <= target < len(data): + worklist.append(target) + else: + outOfRangeBranches += 1 + worklist.append(pc + n) + elif op == 0x19: + # RTS: end of this sub-record (don't fall through). + continue + elif op in (0x0B,): + # Unconditional relative jump. Follow target only. + off = self._readSignedWord(data, pc + 1) + if off is not None: + target = pc + off + if 0 <= target < len(data): + worklist.append(target) + else: + outOfRangeBranches += 1 + elif op in (0x13, 0x14, 0x20, 0x21, 0x22, 0x23, 0x28): + # Conditional jump: explore BOTH branches so the offline + # walker enumerates every reachable polygon regardless of + # camera state. + off = self._readSignedWord(data, pc + 1) + if off is not None: + target = pc + off + if 0 <= target < len(data): + worklist.append(target) + else: + outOfRangeBranches += 1 + worklist.append(pc + n) # fall-through + elif op == 0x04: + # CullByOutcodeList: jumps to L00A5 (= the address read + # by ReadRelativeAddr from cursor+1..+2) on full-cull, + # else falls through. Take both. + off = self._readSignedWord(data, pc + 1) + if off is not None: + target = pc + off + if 0 <= target < len(data): + worklist.append(target) + else: + outOfRangeBranches += 1 + worklist.append(pc + n) + else: + # Default: continue linearly past this op. + worklist.append(pc + n) + # Mark all bytes consumed by this op as visited so the + # coverage report knows we examined them. + for b in range(pc + 1, min(pc + n, len(data))): + visited.add(b) + # Compute coverage stats. + coveredBytes = len(visited) + return { + "covered": coveredBytes, + "unreached": len(data) - coveredBytes, + "out_of_range": outOfRangeBranches, + } + + def _annotate(self, entry, data, pc, base_sid, depth): + """Decode op-specific fields for nicer JSON. Truncated records (op + spilling past section's allocated bytes) skip annotation rather + than reading beyond the buffer.""" + op = entry["op"] + b = data + # Use the canonical opcode length, not the (possibly truncated) + # captured slice, when deciding bounds. + meta = OPCODES.get(op) + opLen = meta[1] if meta and meta[1] is not None else len(entry["bytes"]) + if pc + opLen > len(data): + return + if op == 0x0D: + entry["sid_load"] = b[pc + 1] + entry["count_sectors"] = b[pc + 2] + entry["dst_rel"] = b[pc + 3] | (b[pc + 4] << 8) + entry["flags"] = b[pc + 5] + elif op == 0x18: + off = b[pc + 1] | (b[pc + 2] << 8) + if off >= 0x8000: off -= 0x10000 + entry["offset_signed"] = off + elif op == 0x12: + entry["color_code"] = b[pc + 1] + elif op == 0x05: # ADF + entry["freq"] = b[pc + 1] | (b[pc + 2] << 8) + entry["x"] = sint24(b, pc + 3) + entry["y"] = sint24(b, pc + 6) + elif op == 0x1D: # NAV + entry["freq"] = b[pc + 1] | (b[pc + 2] << 8) + entry["x"] = sint24(b, pc + 3) + entry["y"] = sint24(b, pc + 6) + entry["z"] = sint16(b, pc + 9) + elif op == 0x1E: # COM/airport record + length = b[pc + 1] + entry["record_length"] = length + if length >= 13 and pc + length <= len(b): + entry["freq"] = b[pc + 2] | (b[pc + 3] << 8) + entry["x"] = sint24(b, pc + 4) + entry["y"] = sint24(b, pc + 7) + entry["z"] = sint24(b, pc + 10) + # Name occupies bytes 13..length-1 (= length - 13 bytes). + # Per chunk5.s::SceneryOpCOMRecord, cursor advances by + # `length` total, so the name is part of the record proper. + if length > 13: + raw = b[pc + 13:pc + length] + # Names are uppercase ASCII; high bits / control + # bytes are field-separators. Stop at the first + # non-printable to avoid pulling in the next record. + chars = [] + for c in raw: + if c < 0x20 or c >= 0x7F: + break + chars.append(chr(c)) + if chars: + entry["name"] = "".join(chars) + elif op in (0x00, 0x01, 0x02): # xform-A vertex (X/Y/Z) + entry["vx"] = sint16(b, pc + 1) + entry["vy"] = sint16(b, pc + 3) + entry["vz"] = sint16(b, pc + 5) + elif op in (0x40, 0x41): # xform-B vertex (X/Z) + entry["vx"] = sint16(b, pc + 1) + entry["vz"] = sint16(b, pc + 3) + elif op == 0x42: # cache-fill xform-B + entry["cache_idx"] = b[pc + 1] + entry["vx"] = sint16(b, pc + 2) + entry["vz"] = sint16(b, pc + 4) + elif op == 0x31: # cache-fill xform-A + entry["cache_idx"] = b[pc + 1] + entry["vx"] = sint16(b, pc + 2) + entry["vy"] = sint16(b, pc + 4) + entry["vz"] = sint16(b, pc + 6) + elif op in (0x32, 0x33, 0x35): + entry["cache_idx"] = b[pc + 1] + elif op == 0x2B: # curve + entry["v1x"] = sint16(b, pc + 1) + entry["v1z"] = sint16(b, pc + 3) + entry["v2x"] = sint16(b, pc + 5) + entry["v2z"] = sint16(b, pc + 7) + elif op in (0x13, 0x14, 0x20, 0x21, 0x22, 0x23, 0x28): + jmp = b[pc + 1] | (b[pc + 2] << 8) + if jmp >= 0x8000: jmp -= 0x10000 + entry["jump_rel"] = jmp + elif op == 0x0B: + jmp = b[pc + 1] | (b[pc + 2] << 8) + if jmp >= 0x8000: jmp -= 0x10000 + entry["jump_rel"] = jmp + elif op == 0x25: + entry["dst"] = b[pc + 1] | (b[pc + 2] << 8) + entry["value"] = b[pc + 3] | (b[pc + 4] << 8) + elif op == 0x1A: + entry["dst"] = b[pc + 1] | (b[pc + 2] << 8) + entry["src"] = b[pc + 3] | (b[pc + 4] << 8) + + +# -- helpers --------------------------------------------------------------- + +def sint16(b, off): + v = b[off] | (b[off + 1] << 8) + if v >= 0x8000: v -= 0x10000 + return v + +def sint24(b, off): + v = b[off] | (b[off + 1] << 8) | (b[off + 2] << 16) + if v >= 0x800000: v -= 0x1000000 + return v + + +# -- driver ---------------------------------------------------------------- + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("sd_file", help="path to A2.SD or FS2.1 file") + p.add_argument("-o", "--output", default=None, + help="output JSON (default: stdout)") + p.add_argument("--summary", action="store_true", + help="print human summary instead of JSON") + args = p.parse_args() + + sd = open(args.sd_file, "rb").read() + if len(sd) != 143360: + print(f"warning: {args.sd_file} is {len(sd)} bytes, expected 143360", + file=sys.stderr) + + dec = Decoder(sd) + # Standard walk: dispatcher + everything HEADER-reachable. + dec.decodeSection(DISPATCHER_SID, 1, source="bootstrap") + # Cross-section completeness: also try every sid in 0..$87 as a + # standalone entry point. Sections that the dispatcher loaded already + # are skipped via decodeSection's visited cache. Sections that start + # with non-bytecode (e.g., 6502 code, sparse zeros, $80 filler) are + # filtered out by checking the first byte before deciding to walk. + for sid in range(MAX_SID + 1): + if sid in dec.sections: + continue + off = sidToOffset(sid) + if off is None or off + 1 >= len(sd): + continue + firstByte = sd[off] + # Skip obvious non-bytecode: terminators ($80+), invalid opcodes, + # and the all-zeros sparse pattern. + if firstByte == 0 or firstByte == 0x80 or firstByte == 0xFF: + continue + if firstByte > 0x45 and firstByte < 0x80: + continue + if firstByte not in OPCODES: + continue + # Walk it; size = 1 sector = 256 bytes (we have no count info). + dec.decodeSection(sid, 1, source="orphan") + + out = { + "source": os.path.basename(args.sd_file), + "size_bytes": len(sd), + "section_count": len(dec.sections), + "sections": list(dec.sections.values()), + "errors": dec.errors, + } + + if args.summary: + printSummary(out) + return + + text = json.dumps(out, indent=2) + if args.output: + with open(args.output, "w") as f: + f.write(text) + print(f"wrote {args.output} ({len(text)} bytes)", file=sys.stderr) + else: + print(text) + + +def printSummary(out): + print(f"file: {out['source']}") + print(f"sections: {out['section_count']}") + print(f"errors: {len(out['errors'])}") + + # Coverage rollup + totalSize = 0 + totalCov = 0 + totalOOR = 0 + for sec in out["sections"]: + totalSize += sec.get("size_bytes", 0) + totalCov += sec.get("coverage_bytes", 0) + totalOOR += sec.get("out_of_range_branches", 0) + pct = 100.0 * totalCov / totalSize if totalSize else 0 + print(f"coverage: {totalCov}/{totalSize} bytes ({pct:.1f}%) reached by walker") + print(f"out-of-range branches: {totalOOR} (jumps that landed outside section data)") + + # Tallies per opcode + op_count = {} + poly_emits = 0 + stations = {"ADF": 0, "NAV": 0, "COM": 0} + cull_ops = 0 + header_count = 0 + for sec in out["sections"]: + for op in sec["ops"]: + op_count[op["name"]] = op_count.get(op["name"], 0) + 1 + if op["op"] in (0x00, 0x01, 0x02, 0x40, 0x41, + 0x32, 0x33, 0x35, 0x2B): + poly_emits += 1 + if op["op"] == 0x05: stations["ADF"] += 1 + if op["op"] == 0x1D: stations["NAV"] += 1 + if op["op"] == 0x1E: stations["COM"] += 1 + if op["op"] in (0x13, 0x14, 0x20, 0x21, 0x22, 0x23, 0x28): + cull_ops += 1 + if op["op"] == 0x0D: header_count += 1 + print(f"polygon emits: {poly_emits}") + print(f"stations: ADF={stations['ADF']} NAV={stations['NAV']} COM={stations['COM']}") + print(f"culls: {cull_ops}") + print(f"sub-section loads ($0D HEADER): {header_count}") + print() + print("opcode counts (top 20):") + for name, n in sorted(op_count.items(), key=lambda x: -x[1])[:20]: + print(f" {name:32s} {n}") + + +if __name__ == "__main__": + main() diff --git a/port/tools/sceneryRender.py b/port/tools/sceneryRender.py new file mode 100644 index 0000000..c08afde --- /dev/null +++ b/port/tools/sceneryRender.py @@ -0,0 +1,389 @@ +#!/usr/bin/env python3 +""" +sceneryRender - render an FS2 scenery snapshot from the extracted database. + +Walks the chunk5 bytecode using the polygon-rendering subset of opcodes +($07 EnterLocalFrame, $24 PushOriginWithStash, $12 SetColor, $40/$41 +xform-B emit, $01/$02 xform-A emit, $31/$42 cache fill, $32/$33/$35 +cached vertex). Uses MAME's captured matrix for the camera transform so +the output is bit-equivalent to running the original on real hardware. + +Usage: + sceneryRender.py FS2.1.json [--out path.ppm] +""" + +import argparse +import json +import os +import struct +import sys + +# Viewport (Apple II hires above the panel). +NATIVE_WIDTH = 280 +NATIVE_HEIGHT = 192 +VIEWPORT_BOTTOM = 99 # Last row of the scenery viewport. + +# Camera state captured from MAME at the Meigs boot moment. +# $5C/$5D = camera X (scenery units), $64/$65 = camera Z, $5E/$5F = altitude. +CAMERA_X_SCENERY = 287 +CAMERA_Y_SCENERY = 25 # = altitude in metres for MAME boot +CAMERA_Z_SCENERY = 804 + +# 3x3 view matrix at boot. Captured from MAME runtime ZP $78-$89. +# Q1.14 entries (signed 16-bit, bit 14 = 1.0). +MATRIX = ( + (16382, 0, 0), + ( 0, 32760, 100), + ( 0, -401, 8190), +) + +# Palette codes -> 24-bit RGB (chunk5 ToHiresColorTable mapping). +COLORS = { + 0x00: (0, 0, 0), + 0x01: (63, 125, 47), # ground-day green + 0x02: (42, 93, 168), # water blue + 0x03: (63, 125, 47), + 0x04: (42, 93, 168), + 0x05: (0, 0, 0), + 0x06: (139, 134, 128), # runway grey + 0x07: (160, 96, 64), # building tan/red + 0x08: (0, 0, 0), + 0x09: (224, 224, 224), + 0x0A: (156, 122, 79), + 0x0B: (42, 85, 32), + 0x0C: (110, 90, 63), + 0x0D: (240, 240, 240), # white + 0x0E: (200, 224, 240), + 0x0F: (176, 168, 120), # CITY tan +} +SKY_COLOR = (107, 163, 220) +GROUND_COLOR = (63, 125, 47) +DEFAULT_LINE_COLOR = (240, 240, 240) + + +# -- bytecode walker that renders --------------------------------------------- + +class State: + def __init__(self): + # Camera-vs-section delta ($66/$67=X, $68/$69=Y, $6A/$6B=Z). + # Per port: at boot, $66=$6A=0, $68 = camera Y in scenery units. + self.deltaX = 0 + self.deltaY = CAMERA_Y_SCENERY + self.deltaZ = 0 + # Cached vertex pool ($0140 + idx*8). Each entry: (x, y, z, outcode). + self.cache = [None] * 64 + # V1 / V2 holding cells. + self.v1 = (0, 0, 0, 0) # (x, y, z, outcode) + self.v2 = (0, 0, 0, 0) + # Color (palette code). + self.colorCode = 0x0D # white default + # Day-only suppression (chunk5 $1B clears, $1C sets at night). + self.dayOnlySkip = False + # Polygon-state $D3 (= outcode AND of accumulated primary verts). + self.d3 = 0 + # All emitted segments: (x1,y1,z1, x2,y2,z2, color_code). + self.segments = [] + # All cached-vertex emits (for stats). + self.cacheFills = 0 + + +# -- transform & projection --------------------------------------------------- + +def transform7EBC(state, vx, vz): + """xform-B: subtract camera delta then multiply by view matrix. + + chunk5 TransformVertex7EBC reads X and Z from the stream (no Y; Y comes + from the section base, accumulated by EnterLocalFrame). The result is + the camera-relative (X, Y, Z) ready for projection. + """ + dx = vx - state.deltaX + dy = -state.deltaY # local Y = 0 - camera Y + dz = vz - state.deltaZ + return _matmul(dx, dy, dz) + + +def transform80C5(state, vx, vy, vz): + """xform-A: 6-byte X/Y/Z stream; Y is per-vertex.""" + dx = vx - state.deltaX + dy = vy - state.deltaY + dz = vz - state.deltaZ + return _matmul(dx, dy, dz) + + +def _matmul(dx, dy, dz): + # Q1.14 matrix * Q0 vector. Output Q0 (world units). + # chunk5 normalises by >> 14 (or 16 for some paths); use >> 14 to match. + rx = (MATRIX[0][0] * dx + MATRIX[0][1] * dy + MATRIX[0][2] * dz) >> 14 + ry = (MATRIX[1][0] * dx + MATRIX[1][1] * dy + MATRIX[1][2] * dz) >> 14 + rz = (MATRIX[2][0] * dx + MATRIX[2][1] * dy + MATRIX[2][2] * dz) >> 14 + return (rx, ry, rz) + + +def classifyOutcode(v): + """6-bit frustum outcode (bit 7 = behind-camera valid flag).""" + x, y, z = v + code = 0 + if z <= 0: + code |= 0x20 + return code | 0x80 + if x < -z: code |= 0x01 + if x > z: code |= 0x02 + if y < -z: code |= 0x04 + if y > z: code |= 0x08 + return code | 0x80 + + +def project(v): + x, y, z = v + if z <= 0: + return None + qx = (x * 128) // z + qy = (y * 128) // z + cx = NATIVE_WIDTH // 2 + cy = VIEWPORT_BOTTOM // 2 + sx = cx + (qx * (NATIVE_WIDTH // 2)) // 0x7F + sy = cy - (qy * (VIEWPORT_BOTTOM // 2)) // 0x7F + return (sx, sy) + + +def clipNearPlane(a, b): + if a[2] > 0 and b[2] > 0: + return a, b + if a[2] <= 0 and b[2] <= 0: + return None + behind, front = (a, b) if a[2] <= 0 else (b, a) + denom = front[2] - behind[2] + if denom <= 0: + return None + t = ((1 - behind[2]) << 8) // denom + if t < 0: t = 0 + if t > 256: t = 256 + bx = behind[0] + ((front[0] - behind[0]) * t) // 256 + by = behind[1] + ((front[1] - behind[1]) * t) // 256 + return (a, (bx, by, 1)) if b is behind else ((bx, by, 1), b) + + +# -- bytecode interpreter ---------------------------------------------------- + +def runSection(secOps, state): + """Walk a section's already-decoded ops in pc order. The extractor's + walker followed both branches of every conditional, so we just iterate + in pc order and process the rendering-relevant ops. + """ + for op in sorted(secOps, key=lambda o: o['pc']): + code = op['op'] + if code == 0x12: # SetColor + state.colorCode = op['color_code'] + elif code == 0x1B: # ModeWhite + state.dayOnlySkip = False + state.colorCode = 0x0D + elif code == 0x1C: # DayOnly + pass # daytime render: don't suppress + elif code == 0x07: # EnterLocalFrame + # Layout: $07, base, x*3, y*3, z*3, dx*2, dy*2 = 14 bytes. + # For now: extract section base and shift camera delta. + b = op['bytes'] + if len(b) >= 8: + bx = _signed16(b[2], b[3]) + by = _signed16(b[4], b[5]) + bz = _signed16(b[6], b[7]) + state.deltaX += bx + state.deltaY += by + state.deltaZ += bz + elif code == 0x24: # PushOriginWithStash + # Layout: $24, dx*2, dy*2, dz*2 = 8 bytes (approx). + # Pushes a new origin onto the section stack. + b = op['bytes'] + if len(b) >= 8: + bx = _signed16(b[2], b[3]) + by = _signed16(b[4], b[5]) + bz = _signed16(b[6], b[7]) + state.deltaX += bx + state.deltaY += by + state.deltaZ += bz + elif code == 0x40: # EmitV1Xform7EBC silent + if 'vx' not in op or 'vz' not in op: continue + v = transform7EBC(state, op['vx'], op['vz']) + state.v1 = (*v, classifyOutcode(v)) + elif code == 0x41: # EmitV2Xform7EBC + draw + if 'vx' not in op or 'vz' not in op: continue + v = transform7EBC(state, op['vx'], op['vz']) + state.v2 = (*v, classifyOutcode(v)) + _emitSegment(state) + state.v1 = state.v2 + elif code == 0x01: # EmitV1Xform80C5 silent + if not all(k in op for k in ('vx','vy','vz')): continue + v = transform80C5(state, op['vx'], op['vy'], op['vz']) + state.v1 = (*v, classifyOutcode(v)) + elif code == 0x02: # EmitV2Xform80C5 + draw + if not all(k in op for k in ('vx','vy','vz')): continue + v = transform80C5(state, op['vx'], op['vy'], op['vz']) + state.v2 = (*v, classifyOutcode(v)) + _emitSegment(state) + state.v1 = state.v2 + elif code == 0x42: # cache fill xform-B + if 'cache_idx' not in op or 'vx' not in op: continue + v = transform7EBC(state, op['vx'], op['vz']) + state.cache[op['cache_idx']] = (*v, classifyOutcode(v)) + state.cacheFills += 1 + elif code == 0x31: # cache fill xform-A + if not all(k in op for k in ('cache_idx','vx','vy','vz')): continue + v = transform80C5(state, op['vx'], op['vy'], op['vz']) + state.cache[op['cache_idx']] = (*v, classifyOutcode(v)) + state.cacheFills += 1 + elif code == 0x32: # cached V1 (silent) + if 'cache_idx' not in op: continue + cv = state.cache[op['cache_idx']] + if cv is not None: + state.v1 = cv + elif code == 0x33: # cached V2 + draw V1->V2 + if 'cache_idx' not in op: continue + cv = state.cache[op['cache_idx']] + if cv is not None: + state.v2 = cv + _emitSegment(state) + state.v1 = state.v2 + elif code == 0x35: # cached + plot pixel + if 'cache_idx' not in op: continue + cv = state.cache[op['cache_idx']] + if cv is not None: + state.v1 = cv + + +def _emitSegment(state): + if state.dayOnlySkip: + return + v1 = state.v1 + v2 = state.v2 + # Outcode AND -> entirely off-screen on one side. + if (v1[3] & v2[3] & 0x3F) != 0: + return + state.segments.append((v1[0], v1[1], v1[2], v2[0], v2[1], v2[2], state.colorCode)) + + +def _signed16(lo, hi): + v = lo | (hi << 8) + return v - 0x10000 if v >= 0x8000 else v + + +# -- framebuffer + draw ------------------------------------------------------ + +class FB: + def __init__(self): + self.pix = bytearray(NATIVE_WIDTH * NATIVE_HEIGHT * 3) + + def fillRow(self, y, color): + for x in range(NATIVE_WIDTH): + self.setPixel(x, y, color) + + def setPixel(self, x, y, color): + if 0 <= x < NATIVE_WIDTH and 0 <= y < NATIVE_HEIGHT: + i = (y * NATIVE_WIDTH + x) * 3 + self.pix[i] = color[0] + self.pix[i+1] = color[1] + self.pix[i+2] = color[2] + + def line(self, x1, y1, x2, y2, color): + # Bresenham, clipped to viewport. + dx = abs(x2 - x1); sx = 1 if x1 < x2 else -1 + dy = -abs(y2 - y1); sy = 1 if y1 < y2 else -1 + err = dx + dy + steps = 0 + while True: + self.setPixel(x1, y1, color) + if x1 == x2 and y1 == y2: break + e2 = 2 * err + if e2 >= dy: err += dy; x1 += sx + if e2 <= dx: err += dx; y1 += sy + steps += 1 + if steps > 2000: # safety cap + break + + def writePpm(self, path): + with open(path, 'wb') as f: + f.write(b'P6\n%d %d\n255\n' % (NATIVE_WIDTH, NATIVE_HEIGHT)) + f.write(bytes(self.pix)) + + +# -- main -------------------------------------------------------------------- + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("db", help="path to *.json (e.g. extracted_db/FS2.1.json)") + p.add_argument("--out", default="port/screenshots/db_render.ppm") + p.add_argument("--cam-x", type=int, default=CAMERA_X_SCENERY) + p.add_argument("--cam-z", type=int, default=CAMERA_Z_SCENERY) + p.add_argument("--cam-y", type=int, default=CAMERA_Y_SCENERY) + p.add_argument("--all-sections", action="store_true", + help="walk every section, not just dispatcher-cull-passing") + args = p.parse_args() + + db = json.load(open(args.db)) + print(f"Loaded {len(db['sections'])} sections from {db['source']}", + file=sys.stderr) + + # Find dispatcher and decide which sections to render. + disp = next(s for s in db['sections'] if s['sid'] == 0x2C) + sectionsToRender = [] + if args.all_sections: + sectionsToRender = list(db['sections']) + else: + ops = sorted(disp['ops'], key=lambda o: o['pc']) + i = 0 + while i < len(ops): + op = ops[i] + if op['op'] == 0x13 and i + 1 < len(ops) and ops[i+1]['op'] == 0x0D: + b = op['bytes'] + bound = b[3] | (b[4] << 8) + refY = _signed16(b[5], b[6]) + refX = _signed16(b[7], b[8]) + if (abs(refX - args.cam_x) <= bound + and abs(refY - args.cam_z) <= bound): + sid = ops[i+1]['bytes'][1] + sec = next((s for s in db['sections'] if s['sid'] == sid), None) + if sec is not None: + sectionsToRender.append(sec) + print(f" cull pass: load sid ${sid:02X}", file=sys.stderr) + i += 2 + continue + i += 1 + # Always include the dispatcher so any inline polygons render too. + sectionsToRender.insert(0, disp) + + print(f"Rendering {len(sectionsToRender)} sections", file=sys.stderr) + + # Render. + fb = FB() + # Sky/ground fill at horizon row (= cy = VIEWPORT_BOTTOM/2). + for y in range(NATIVE_HEIGHT): + if y <= VIEWPORT_BOTTOM // 2: + fb.fillRow(y, SKY_COLOR) + elif y < VIEWPORT_BOTTOM: + fb.fillRow(y, GROUND_COLOR) + else: + fb.fillRow(y, (0, 0, 0)) # panel area + + state = State() + for sec in sectionsToRender: + runSection(sec['ops'], state) + + print(f"Emitted {len(state.segments)} 3D segments", file=sys.stderr) + + # Project & draw. + drawn = 0 + for v1x, v1y, v1z, v2x, v2y, v2z, colCode in state.segments: + clip = clipNearPlane((v1x, v1y, v1z), (v2x, v2y, v2z)) + if clip is None: continue + a, b = clip + sa = project(a); sb = project(b) + if sa is None or sb is None: continue + col = COLORS.get(colCode & 0x0F, DEFAULT_LINE_COLOR) + fb.line(sa[0], sa[1], sb[0], sb[1], col) + drawn += 1 + + print(f"Drew {drawn} 2D segments to {args.out}", file=sys.stderr) + fb.writePpm(args.out) + + +if __name__ == "__main__": + main() diff --git a/res/README.md b/res/README.md new file mode 100644 index 0000000..667007a --- /dev/null +++ b/res/README.md @@ -0,0 +1,4 @@ + + +* `loading_panel.bin` - an 8k high-res bitmap loaded at $4000 and displayed while the rest of the program loads. The top half has "LOADING... VERSION 2.0". The bottom half is the aircraft instrument panel bitmap shown throughout the simulation. This is later copied to $2000 to allow page-flipping during simulation. + diff --git a/res/loading_panel.bin b/res/loading_panel.bin new file mode 100644 index 0000000..ca86461 --- /dev/null +++ b/res/loading_panel.bin @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c257ad24f7d8f54f0cc6ce4a853e6df87f383dd99088100084ab613db983422c +size 8192 diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..1d247d1 --- /dev/null +++ b/src/README.md @@ -0,0 +1,68 @@ + +The loader in @qkumba's version of FS2 loads and decompresses several +chunks of data/code into memory before jumping into the FS2 code. + +The chunks are: + +* Chunk 1: $4000-$5fff - from `../res/loading_panel.bin` +* Chunk 2: $f600-$fbff - `chunk2.s` +* Chunk 3: $d300-$f3ff - `chunk3.s` +* Chunk 4: $0200-$25ff - `chunk4.s` +* Chunk 5: $6000-$b3df - `chunk5.s` + +## Memory Map + +``` + Main +$FFFF +-------------+ + | chunk2 | + |.............| +$F000 | chunk3 | + | | + | | +$E000 +-------------+ +-------------+ +Bank2 |.............| | Course Plot | Bank1 + | ??? | | Data | +$D000 +-------------+ +-------------+ + FS2 runs with LC Bank2 active! + +$C000 +-------------+ + | ??? | + |.............| +$B000 | chunk5 | + | | + | | +$A000 | | + | | + | | +$9000 | | + | | + | | +$8000 | | + | | + | | +$7000 | | + | | + | | +$6000 +-------------+ + |.HiRes.Pg.2..| + |.............| +$5000 |.............| + |.............| +$4000 +-------------+ + |.HiRes.Pg.1..| + |.............| +$3000 |.............| + |.............| + |.............| +$2000 +-------------+ + | chunk4 | + | | +$1000 | | $400-$7FF - I/O screen holes ($X7X and $XFX) + | | $3Fx - interrupt vectors +$0200 +-------------+ + |.Stack.......| +$0100 +-------------+ + | Zero Page | +$0000 +-------------+ +``` diff --git a/src/asm.cfg b/src/asm.cfg new file mode 100644 index 0000000..07f4d90 --- /dev/null +++ b/src/asm.cfg @@ -0,0 +1,13 @@ +# Configuration for assembler programs which don't need a special setup + +FEATURES { + STARTADDRESS: default = $4000; +} +MEMORY { + MAIN: file = %O, define = yes, start = %S, size = $20000; # 128k; custom loader handles details + BSS: file = "", start = $0000, size = $10000; +} +SEGMENTS { + CODE: load = MAIN, type = rw; + BSS: load = BSS, type = bss; +} diff --git a/src/chunk2.s b/src/chunk2.s new file mode 100644 index 0000000..f3ceb4a --- /dev/null +++ b/src/chunk2.s @@ -0,0 +1,679 @@ + .org $f600 + +;;; Some functionality that is only present in the "64k" mode +;;; of the original FS2 (per Appendix 3): +;;; * Altimeter 3rd hand. +;;; * Course plotter system. +;;; * Improved demo mode. + +;;; ============================================================ +;;; Course Plotting + +msg_courseplotter: + MESSAGE $0C, $0A, "**** COURSE PLOTTER SYSTEM ****" + MESSAGE $14, $2D, " " + MESSAGE $1C, $06, "A. BEGIN NORMAL COURSE RECORDING" + MESSAGE $24, $06, "B. DISPLAY COURSE PLOT" + MESSAGE $2C, $06, "C. BOOT DOS" + MESSAGE $34, $06, "D. BEGIN PRECISION RECORDING" + MESSAGE $3C, $06, "E. TURN OFF COURSE PLOTTER" + MESSAGE $50, $0A, "SELECT OPTION, THEN PRESS ESC TO" + MESSAGE $56, $0A, "RETURN TO FLIGHT, SLEW, OR RADAR" + MESSAGE $5C, $0A, "MODE TO VIEW OR RECORD COURSE." + .byte $00, $00 + +CoursePlotterSampleRate: + .byte 10 +CoursePlotterSampleCounter: + .byte 1 ; reset to above; sample at 0 +CoursePlotterState: + .byte 0 ; 0 = off; 1 = record; 2 = display +CoursePlotterRecordPos: + .word 0 ; pointer to data pos ($D000...$DFFF) +CoursePlotterSampleRange: + .byte 0 ; 6 = normal, 4 = precision +CoursePlotterAnyData: + .byte 0 ; 0 = not data, 1 = some data + +CoursePlottingMenu: + jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_courseplotter + jsr TogglePause + cmp #'A' + beq BeginNormalCourseRecording + cmp #'B' + beq DisplayCoursePlot + cmp #'C' + beq BootDOS + cmp #'D' + beq BeginPrecisionRecording + cmp #'E' + beq TurnOffCoursePlotter + cmp #$1B ; Escape + bne CoursePlottingMenu + rts + +BeginNormalCourseRecording: + ldx #$06 ; range + lda #$0A ; rate + bne BeginRecordingCommon ; always + +DisplayCoursePlot: + lda CoursePlotterAnyData + beq CoursePlottingMenu + lda #$02 + sta CoursePlotterState + jmp CoursePlottingMenu + +BootDOS: + ;; Don't reboot immediately - patch `MaybeBootDOS` (an RTS by + ;; default) into a NOP so the next pass through the simulator + ;; main loop will fall through to the boot vector. Returning to + ;; the menu lets the user press ESC to actually exit, at which + ;; point the patched `MaybeBootDOS` triggers the reboot. + lda BootSlot + lsr a + lsr a + lsr a + lsr a + ora #$C0 + sta RebootVector+1 + lda #$00 + sta RebootVector + lda #OPC_NOP + sta MaybeBootDOS ; override RTS + jmp CoursePlottingMenu + +TurnOffCoursePlotter: + lda #$00 + sta CoursePlotterState + jmp CoursePlottingMenu + +BeginPrecisionRecording: + ldx #$04 ; range + lda #$02 ; rate + +BeginRecordingCommon: + stx CoursePlotterSampleRange + sta CoursePlotterSampleRate + lda #$01 + sta CoursePlotterState + jsr EnableLCBank1 + LDAX #$D000 + STAX $B8 + ldy #$00 + lda #$07 + sta ($B8),y + iny + lda CoursePlotterSampleRange + sta ($B8),y + iny + + ldx #$00 +: lda $5A,x + sta ($B8),y + inx + iny + cpx #$0C + bne :- + + lda #$1C + sta ($B8),y + iny + lda #$01 + sta ($B8),y + iny + lda #$00 + sta ($B8),y + iny + sta ($B8),y + iny + sta ($B8),y + iny + sta ($B8),y + iny + sta ($B8),y + iny + sta ($B8),y + iny + lda #$79 + sta ($B8),y + tya + clc + adc $B8 + sta CoursePlotterRecordPos + lda $B9 + sta CoursePlotterRecordPos+1 + lda #$01 + sta CoursePlotterAnyData + jsr EnableLCBank2 + jmp CoursePlottingMenu + +;;; ============================================================ +;;; Course Plotting - Per-Frame Update +;;; +;;; Called from the simulator main loop (only in 64K mode, via the +;;; `P64K_9` patch). Dispatches based on `CoursePlotterState`: +;;; 0 = off (no-op) +;;; 1 = recording (sample state into the buffer at $D000) +;;; 2 = displaying the previously recorded course + +UpdateCoursePlotter: + lda CoursePlotterState + bne :+ + rts +: + cmp #$01 ; recording? + beq MaybeRecord ; yes, maybe grab a sample + cmp #$02 ; displaying? + beq DisplayCoursePlotData + rts + +DisplayCoursePlotData: + LDAX #$D000 + STAX $8B + jsr EnableLCBank1 + jsr L6006 + jsr EnableLCBank2 + rts + +MaybeRecord: + dec CoursePlotterSampleCounter + beq :+ + rts +: + ;; Is there room? + lda CoursePlotterSampleRate + sta CoursePlotterSampleCounter + lda #<$DFE0 + sec + sbc CoursePlotterRecordPos + lda #>$DFE0 + sbc CoursePlotterRecordPos+1 + bpl :+ + rts ; nope +: + jsr EnableLCBank1 + LDAX CoursePlotterRecordPos + STAX $B8 + ldy #$00 + lda #$02 + sta ($B8),y + iny + lda $5B + sec + sbc $D003 + sta $B7 + lda $5C + sbc $D004 + sta $BC + lda $5D + sbc $D005 + sta $BD + jsr AdjustForRange + lda $B7 + sta ($B8),y + iny + lda $BC + sta ($B8),y + iny + lda $5F + sec + sbc $D007 + sta $B7 + lda $60 + sbc $D008 + sta $BC + lda $61 + sbc $D009 + sta $BD + jsr AdjustForRange + lda $B7 + sta ($B8),y + iny + lda $BC + sta ($B8),y + iny + lda $63 + sec + sbc $D00B + sta $B7 + lda $64 + sbc $D00C + sta $BC + lda $65 + sbc $D00D + sta $BD + jsr AdjustForRange + lda $B7 + sta ($B8),y + iny + lda $BC + sta ($B8),y + iny + lda #$79 + sta ($B8),y + tya + clc + adc $B8 + sta CoursePlotterRecordPos + lda $B9 + adc #$00 + sta CoursePlotterRecordPos+1 + jsr EnableLCBank2 + rts + +EnableLCBank1: + lda #$01 + sta LCBank1Flag ; set + lda LCBANK1 + lda LCBANK1 + rts + +EnableLCBank2: + lsr LCBank1Flag ; clear + lda LCBANK2 + lda LCBANK2 + rts + +AdjustForRange: + lda CoursePlotterSampleRange + cmp #$04 ; reduced range? + beq :+ + lsr $BD + ror $BC + ror $B7 + lsr $BD + ror $BC + ror $B7 + lsr $BD + ror $BC + ror $B7 + lsr $BD + ror $BC + ror $B7 +: rts + +;;; ============================================================ +;;; Altimeter - Third Hand (10k) +;;; +;;; Drawn as one of nine pixel lists (PLF8F9..PLF96B), corresponding +;;; to altitudes 0, 10K, 20K, ..., 80K feet. The hand is drawn via +;;; XOR, so the previously drawn position is remembered and erased +;;; before the new position is drawn. + + ;; Index 0..8 of the previously drawn hand position, or $FF + ;; if nothing has been drawn yet. +Altimeter10KHandIndex: + .byte $FF + + ;; Table of PixelLists for altimeter's "third hand" +PixelListAltimeter10KTable: + .addr PLF8F9 + .addr PLF906 + .addr PLF919 + .addr PLF926 + .addr PLF933 + .addr PLF944 + .addr PLF94F + .addr PLF95C + .addr PLF96B + +PLF8F9: .byte 6, $80,$69, $80,$6A, $82,$6A, $80,$6B, $81,$6B, $82,$6B + +PLF906: .byte 9, $8C,$6B, $8A,$6C, $89,$6C, $89,$6D, $88,$6D, $8A,$6D, $89,$6E + .byte $88,$6E, $87,$6E + +PLF919: .byte 6, $8E,$6D, $8D,$6E, $8D,$6F, $8E,$6F, $8D,$70, $8B,$6F + +PLF926: .byte 6, $93,$72, $92,$73, $94,$71, $92,$73, $91,$73, $90,$73 + +PLF933: .byte 8, $95,$78, $98,$78, $96,$79, $95,$79, $94,$79, $94,$7A, $95,$7A, $91,$79 + +PLF944: .byte 5, $95,$7D, $96,$7D, $94,$7D, $94,$7C, $94,$7E + +PLF94F: .byte 6, $95,$84, $94,$84, $94,$83, $95,$83, $91,$83, $61,$82 + +PLF95C: .byte 7, $93,$8A, $92,$8A, $92,$89, $91,$89, $90,$89, $91,$8A, $94,$8B + +PLF96B: .byte 7, $8C,$8E, $8D,$8E, $8E,$8E, $8C,$8D, $8B,$8D, $8A,$8D, $8D,$8F + + ;; Draw's altimeter's "third hand" (10k, arrow by outside) +.proc UpdateAltimeter10K + lda $60 ; high byte of altitude + clc + adc #$02 ; round + ldx #$FF +: inx + sec + sbc #$06 ; ~10K feet per increment + bcs :- + cpx #$09 + bcc :+ + ldx #$08 ; clamp to last position +: txa + cmp Altimeter10KHandIndex + bne redraw ; position changed + rts ; same position - nothing to do + +redraw: ldx Altimeter10KHandIndex + sta Altimeter10KHandIndex + pha ; save new index + bmi :+ ; first call ($FF) - skip erase + txa ; old index + jsr drawHand ; XOR-erase the old hand +: pla ; new index +drawHand: + asl a + tax + lda PixelListAltimeter10KTable,x + sta PixelListData + lda PixelListAltimeter10KTable+1,x + ldx #$00 + ldy #$00 + jmp DrawPixelListHelper +.endproc + +;;; ============================================================ +;;; Wind & Turbulence (64K only) +;;; +;;; `ComputeWindComponents` (called every other frame) selects a wind +;;; layer based on the current altitude and resolves it into X and Y +;;; (north/east) component deltas at $09A2/$09A3 and $09A4/$09A5. +;;; +;;; `ApplyWind` (called every frame) subtracts those components from +;;; the live position vectors and adds a turbulence kick that pulses +;;; off the low UpdateCounter bits. + + ;; Byte 1 of the most-recently-selected wind layer record. + ;; Bit 0 selects the turbulence kick; the byte as a whole is + ;; used to scale a frame's worth of wind influence. +WindLayerByte1: + .byte $00 + +WindReturn: + rts + +ApplyWind: + lda OnGroundFlag + bne WindReturn ; on ground - wind is irrelevant + + ;; Subtract X (north?) wind component from position $5A-$5D, + ;; sign-extending the 16-bit wind through the upper bytes. + ldx #$00 + lda $09A3 ; wind X high byte + bpl :+ + dex +: stx $A5 ; sign-extension byte + SUB16C $5A, $09A2, $5A + lda $5C + sbc $A5 + sta $5C + lda $5D + sbc $A5 + sta $5D + + ;; Same for Y (east?) component. + ldx #$00 + lda $09A5 + bpl :+ + dex +: stx $A5 + SUB16C $62, $09A4, $62 + lda $64 + sbc $A5 + sta $64 + lda $65 + sbc $A5 + sta $65 + + ;; Scale a position-derived value by the wind layer parameter + ;; and accumulate into $09AF/$09B0. + lda $62 + sta $C3 + lda #$00 + sta $C2 + ldx WindLayerByte1 + jsr ScaleC2ByAX + clc + adc $09AF + sta $09AF + txa + adc $09B0 + sta $09B0 + + ;; Turbulence kick: when WindLayerByte1 has bit 0 set, the kick + ;; cycles through {0, +1, 0, -1} as UpdateCounter rolls over, + ;; otherwise it stays zero. + lda WindLayerByte1 + and #$01 + beq turbZero + lda UpdateCounter + and #$C0 + beq turbNeg ; bits 7,6 = 00 + cmp #$80 + beq turbPos ; bits 7,6 = 10 +turbZero: ; bits 7,6 = 01 or 11 + lda #$00 + ldx #$00 + beq turbStore ; always +turbPos: + lda #$00 + ldx #$01 + jmp turbScale +turbNeg: + lda #$00 + ldx #$FF +turbScale: + sta $C2 + stx $C3 + lda $09DE + ldx $09DF + jsr ScaleC2ByAX +turbStore: + sta $08A1 + stx $08A2 + rts + +;;; Pick a wind layer record based on altitude ($5F/$60), then resolve +;;; its (speed, direction) into X/Y components at $09A2/$09A3 and +;;; $09A4/$09A5. Three altitude thresholds split the sky into four +;;; bands; the surface band reuses `WindSpeed` as a 4-byte record. + +ComputeWindComponents: + lda $5F + cmp WindAlt1 + lda $60 + sbc WindAlt1+1 + bmi useSurface + + lda $5F + cmp WindAlt2 + lda $60 + sbc WindAlt2+1 + bmi useLayer1 + + lda $5F + cmp WindAlt3 + lda $60 + sbc WindAlt3+1 + bmi useLayer2 + + LDAX #WindLayer3 ; highest band + jmp resolve + +useLayer2: + LDAX #WindLayer2 + jmp resolve + +useLayer1: + LDAX #WindLayer1 + jmp resolve + +useSurface: + LDAX #WindSpeed ; surface band - reuses WindSpeed/Dir + inc $09A6 + +resolve: + STAX $BE ; record pointer + ldy #$01 + lda ($BE),y ; record[1] - turbulence/scale byte + sta WindLayerByte1 + ldy #$03 + lda ($BE),y ; record[3] - direction angle + clc + adc $0847 + lsr $09A6 + bcc :+ ; surface band gets an extra offset + clc + adc $0849 +: pha + jsr L1768 ; sin (or cos) of angle + stx $A5 + pla + jsr L1763 ; cos (or sin) of angle + stx $A7 + ldy #$00 + lda ($BE),y ; record[0] - magnitude + pha + tax + ldy $A5 + jsr L1818 + ldx $09DF + jsr L180C + sta $09A2 + sty $09A3 ; X component + pla + tax + ldy $A7 + jsr L1818 + ldx $09DF + jsr L180C + sta $09A4 + sty $09A5 ; Y component + rts + +;;; ============================================================ +;;; Demo Mode + +DemoModeParam1: .byte $01 ; constant +DemoModeParam2: .byte $06 ; constant +DemoModeParam3: .byte $02 + +.proc DemoMode64K + lda YokeVertPos + cmp #$18 + bcs :+ + jsr YokeUp +: lda $60 + cmp DemoModeParam1 + bcs :+ + jsr MoreThrottle + jmp Common +: + cmp DemoModeParam2 + bcc Common + jsr LessThrottle + +Common: + lda #$24 + sta $08FF + lda #$03 + sta $0900 + lda #$1F + sta $08FB + lda #$01 + sta $08FC + ldx #$00 + jsr ComputeStationDelta + jsr LA4D2 + rol $BE + lda $BF + rol a + cmp DemoModeParam3 + bcc Settle + lda #$01 + sta DemoModeParam3 + lda $B7 + eor #$80 + tax + lda $B6 + sbc $09E4 + txa + sbc $09E5 + bpl :+ + + ;; Maybe turn left + lda $09B0 + eor #$80 + cmp #$72 + bcc Center + jsr YokeLeft + jsr YokeLeft + rts +: + ;; Maybe turn right + lda $09B0 + eor #$80 + cmp #$8E + bcs Center + jsr YokeRight + jsr YokeRight + rts + + ;; Near the target station -- skip turn correction, decay + ;; yoke X toward neutral by one count (+1 if neg, -1 if pos), + ;; recenter vertically, and exit. +Settle: lda #$02 + sta DemoModeParam3 + ldx $09B0 + beq Exit + bpl :+ + inx + inx +: dex + stx $09B0 +Center: + jsr YokeCenter + +Exit: rts +.endproc + +;;; ============================================================ +;;; Trailing data ($FB7C..$FBFF) - 132 bytes of dead pad. +;;; +;;; Pattern: 16-byte blocks of `$FF,$FF,$00,$00` quartets with $76 at +;;; offset 11 of each block (except the first): +;;; +;;; $FF,$FF,$00,$00 $FF,$FF,$00,$00 $FF,$FF,$00,$76 $FF,$FF,$00,$00 +;;; +;;; Verified unused: no immediate `lda #$FB`, no indirect pointer +;;; construction, no patch-table reference, no scenery-dispatch reach +;;; (the dispatcher cursor walks $A800-$BFFF only). The matching +;;; pattern at the start of chunk3 ($D300..$D3CF, 208 bytes) is the +;;; same kind of dead pad. Both are 1984-era residue captured when the +;;; original Sublogic assembler emitted the binary image; the $76 +;;; bytes are likely leftover stack-frame markers. +Chunk2TrailingData: + .refto Chunk2TrailingData + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $76 + + .assert * = $FC00, error, "EOF mismatch" + +;;; $FC00 is used for instrument save buffers diff --git a/src/chunk3.s b/src/chunk3.s new file mode 100644 index 0000000..c5f96ec --- /dev/null +++ b/src/chunk3.s @@ -0,0 +1,3178 @@ + .org $d300 + +;;; Some functionality that is only present in the "64k" mode +;;; of the original FS2 (per Appendix 3): +;;; * Instrument lights function at night. The panel can be shut off. +;;; * Reality mode effects. +;;; * ADF functions. +;;; * Wingtip generation in side and back views. +;;; * North and East readouts in slew mode. +;;; * Bomb sight in war game. +;;; * War report. +;;; * COM Radio +;;; * Airplane image in center of the radar screen. + +L00BA := $00BA + +;;; Uninitialized memory (filler matching the chunk dump - same +;;; $FF,$FF,$00,$00 + sparse $76 pattern as the trailing block in +;;; chunk2). 208 bytes of leading dead space before code begins. + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $76 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $76 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $76 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $76 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $76 + +;;; ============================================================ +;;; Scenery Loader Dispatch (64K only) +;;; +;;; In 48K mode, the disk-loader entry points (`L1EAD`..`L1EC1` in +;;; chunk4) are simple `JMP $20xx` thunks into the original loader. +;;; In 64K mode, the patch table at the end of chunk5 rewrites those +;;; thunks to jump here instead, so all subsequent loader calls go +;;; through the chunk3 implementations below. The 64K loader keeps +;;; its state in `L1E01` (sector counter), `L1E03` (data pointer), +;;; `L1E09` (open/active flag) and `L1E0B` (block offset). + +SceneryLoaderEntry1: ; L1EAD -> here in 64K + jmp SceneryReadUntilC0 + +SceneryLoaderEntry2: ; L1EB0 -> here in 64K + jmp SceneryReadFixed + +SceneryLoaderEntry3: ; L1EB3 -> here in 64K + jmp SceneryReadNoOp + +SceneryLoaderEntry4: ; L1EB6 -> here in 64K + jmp SceneryReadDecoded + +SceneryLoaderEntry5: ; L1EB9 -> here in 64K + jmp SceneryBuffer1 ; called once buffer is loaded + +SceneryLoaderEntry6: ; L1EBC -> here in 64K + jmp SceneryLoaderInit + +SceneryLoaderEntry7: ; L1EC1 -> here in 64K + jmp SceneryBuffer2 ; called once buffer is loaded + +;;; ============================================================ + +SceneryLoaderInit: + lda #$00 + sta L1E09 ; mark loader inactive + lda #$20 + sta L1E0B ; reset block offset + bit SceneryBuffer4 + ldx BootSlot + lda $D088,x ; touch slot ROM + clc + rts + +;;; Read a fixed-size run of bytes from the loader stream into the +;;; destination at L1E03, advancing L1E03 as it goes. Caller arrives +;;; with X = number of 256-byte pages to copy. +SceneryReadFixed: + jsr SceneryEnsureOpen + bcs SceneryReadFixed_Exit + jsr PopulateA5ThruA8From1E03 +: lda ($A5),y + sta ($A7),y + iny + bne :- + inc $A6 + inc $A8 + inc L1E03+1 + dex + bne :- + jsr SceneryAdvanceSector + jsr LD5C8 + lda #$00 + sta L1E09 +SceneryReadFixed_Exit: + .refto SceneryReadFixed_Exit + .refto LD41E +LD41E: rts + +;;; Read bytes into an indirect destination ($A7) until the source +;;; pointer ($A5/$A6) crosses into ROM space ($Cx). Used for scenery +;;; chunks that include their own end markers. +SceneryReadUntilC0: + jsr SceneryEnsureOpen + bcs SceneryReadUntilC0_Exit + jsr PopulateA5ThruA8From1E03 + stx $A0 + ldx #$00 + ;; Inner loop: copy one byte; if source crossed $Cx, finalize. +SceneryReadUntilC0_Loop: + .refto SceneryReadUntilC0_Loop + .refto LD42B +LD42B: lda $A6 + and #$F0 + cmp #$C0 + beq SceneryReadUntilC0_Done + lda ($A7,x) + sta ($A5,x) + inc $A7 + bne LD43D ; no carry into $A8 + inc $A8 +LD43D: inc $A5 + bne LD443 ; no carry into $A6 + inc $A6 +LD443: iny + bne LD42B + dec $A0 + bne LD42B + ;; Source pointer reached $Cx: rewind sector pointer and exit. +SceneryReadUntilC0_Done: + .refto SceneryReadUntilC0_Done + .refto LD44A +LD44A: jsr SceneryAdvanceSector + lda $A5 + sta L1E03 + lda $A5+1 + sta L1E03+1 + clc +SceneryReadUntilC0_Exit: + .refto SceneryReadUntilC0_Exit + .refto LD458 +LD458: rts + +;;; Open the scenery stream if not already active. Returns C=0 on +;;; success, C=1 if the underlying read failed. +SceneryEnsureOpen: + jsr ComputeBlockFromSector + bcs SceneryMarkClosed +SceneryReopen: + jsr FetchSectorFromDisk + lda #$FF + sta L1E09 + rts + + .byte $02 + sta $B6 +SceneryReadDecodedRetry: + jsr SceneryBuffer3 + bcc SceneryMarkOpen + dec $B6 + beq SceneryReloadAndRetry + bpl SceneryReadDecodedRetry + bmi SceneryMarkClosed +SceneryReloadAndRetry: + jsr SceneryBuffer2 + jmp SceneryReadDecodedRetry + +SceneryMarkOpen: + lda #$FF + sta L1E09 + clc + rts + +SceneryMarkClosed: + lda #$00 + sta L1E09 + sec + rts + +SceneryAdvanceSector: + inc L1E01 + bne :+ + inc $1E02 +: rts + +SceneryReadNoOp: + clc + rts + + .res 95, 0 + +;;; Compute the next disk block offset from the sector counter and +;;; (if still in range) re-open the loader. Returns C=1 once the +;;; counter exceeds $46 sectors (~end-of-stream). +SceneryComputeBlockOffset: + lda L1E01 + asl a + clc + adc #$02 + cmp #$46 + bcs :+ + sta $1E0B + jsr SceneryReopen +: rts + +;;; Read one sector's worth of data into the $3B60 staging buffer +;;; using the 6+2 nibble decoder, then advance the loader state. +SceneryReadDecoded: + jsr SceneryComputeBlockOffset + bcs SceneryReadDecodedDone + jsr SceneryNibbleDecode + lda #$00 + sta L1E09 + jsr LD5C8 + bcs SceneryReadDecodedDone + inc L1E01 + bne SceneryReadDecodedSave + inc $1E02 +SceneryReadDecodedSave: + lda $A5 + sta L1E03 + lda $A5+1 + sta L1E03+1 + clc +SceneryReadDecodedDone: + rts + +;;; 6+2 nibble decode a 256-byte block from ($A5)... into $3B60..$3DFE. +;;; Each source byte is split into a 6-bit half (low 6 bits, ORed with +;;; $AA) and a 2-bit half (high 2 bits, ORed with $AA). EOR-folded +;;; checksum byte is finally written to $3B5E/$3B5F. +SceneryNibbleDecode: + jsr PopulateA5ThruA8From1E03 + lda #<$3B60 + sta $A7 + lda #>$3B60 + sta $A8 + sty $B6 +nibLoop: + lda ($A5),y + eor $B6 + sta $B6 ; running EOR checksum + lda ($A5),y + lsr a ; 6-bit half + ora #$AA + sta ($A7),y + inc $A7 + bne :+ + inc $A8 +: lda ($A5),y ; 2-bit half + ora #$AA + sta ($A7),y + inc $A7 + bne :+ + inc $A8 +: inc $A5 + bne :+ + inc $A6 +: lda $A8 + cmp #$3E + bcc nibLoop + lda $B6 ; flush checksum + ora #$AA + sta $3B5F + lda $B6 + lsr a + ora #$AA + sta $3B5E + rts + +;;; ============================================================ + +ReadBlockDataBuffer: + .res 506, 0 +LD5C8 := ReadBlockDataBuffer + 83 ; executable! + + ;; Scratch buffers used by the scenery loader. The first three + ;; are jump targets - sized for code that gets relocated/loaded + ;; into them at runtime. The fourth is the loader's working + ;; sector buffer (touched by `bit` in `SceneryLoaderInit`). +SceneryBuffer1: + .res 70, 0 +SceneryBuffer3: + .res 424, 0 +SceneryBuffer2: + .res 21, 0 +SceneryBuffer4: + .res 115, 0 + +;;; ============================================================ +;;; Automatic Direction Finder (ADF) + + ;; Set to $01 by the original; the first call to + ;; `UpdateADFIndicator` shifts it to $00 and triggers the + ;; one-time panel draw via `InitADFPanel`. +ADFNeedsInit: .byte $01 + + ;; $00 = no station tuned; $01 = station selected and its + ;; coords are in $08FA..$0900. Set by the lookup in + ;; `LookupADFStation`; cleared when the user starts editing + ;; the frequency. +ADFStationActive: brk + + ;; Two packed BCD bytes used as the ADF station identifier + ;; when scanning the station table. ADFFreqLowPacked has the + ;; mid + low frequency nibbles in the high/low half-bytes; + ;; ADFFreqHighDigit has the high frequency digit. +ADFFreqLowPacked: brk +ADFFreqHighDigit: brk + + ;; Last needle angle drawn on the panel; XOR-erased before the + ;; new angle is drawn. $FF means "not drawn yet"; reset to + ;; $0A in `InitADFPanel`. +ADFLastDrawnAngle: .byte $FF + +msg_adf_frequency: + MESSAGE $88, $67, " 287", str_adf_frequency + +;;; Update the ADF instrument on the panel +.proc UpdateADFIndicator + lda ADFMode + beq UADF_exit + lsr ADFNeedsInit + bcc :+ ; not first frame, skip panel init + jsr InitADFPanel +: lda ADFStationActive + beq UADF_exit + ldx #$00 + jsr ComputeStationDelta + lda $B6 + sbc $70 + sta $C2 + sta $BE + lda $B7 + sbc $71 + clc + adc #$40 + clc + adc $0849 + clc + adc $0847 + lsr a + sta $C3 + sta $BF + ror $C2 + ror $BE + LDAX #$58 + jsr ScaleC2ByAX + eor #$FF + clc + adc #$58 + ldx ADFLastDrawnAngle + bmi UADF_exit ; first call: skip undraw + cmp ADFLastDrawnAngle + sta ADFLastDrawnAngle + beq UADF_exit ; no change: skip + txa + ldx #4 ; needle index + jsr DrawIndicatorDialNeedle ; XOR-undraw old angle + lda ADFLastDrawnAngle + ldx #4 ; needle index + jsr DrawIndicatorDialNeedle ; XOR-draw new angle + jsr DrawADFHeadingDigits +UADF_exit: + .refto UADF_exit + .refto LDA52 +LDA52: rts + +;;; Convert the ADF needle angle (in $BE/$BF) into degrees, format +;;; as a 3-digit string, and redraw the heading message. +DrawADFHeadingDigits: + lda $BE + sta $C2 + lda $BF + clc + adc #$20 + and #$7F + sta $C3 + LDAX #$168 + jsr ScaleC2ByAX + STAX ValueForString + CALLAX Set3DigitString, str_adf_heading + JUMPAX DrawMessageOrange, msg_adf_heading +.endproc + +;;; Used for drawing the ADF (turned on in Edit mode), like this: +;;; ┌---- +;;; | ADF +;;; | 237 +;;; | | +;;; | +;;; |- - +;;; | +;;; | | +;;; + +DrawADFPanel: + MESSAGE $93, $4B, " " ; start erasing VOR2 + MESSAGE $98, $4B, " " ; continue + MESSAGE $9C, $55, " " ; continue (right bit under trim) + MESSAGE $9D, $4D, "$ " ; start left edge, more erasing + MESSAGE $A2, $4E, " " ; more erasing + MESSAGE $A7, $4E, " " ; more erasing + MESSAGE $AC, $4D, "$ " ; restart left edge, more erasing + MESSAGE $B1, $4D, "$ " ; more left edge, more erasing + MESSAGE $B6, $4D, "$ " ; more left edge, more erasing + MESSAGE $BB, $4B, " " + + MESSAGE $93, $4D, "<----" ; back up to draw top row + MESSAGE $98, $4D, "$ " ; more left edge + MESSAGE $A2, $4D, "$" ; more left edge + MESSAGE $A7, $4D, "$" ; more left edge + + MESSAGE $97, $4D, "$ ADF" ; back up for the label + MESSAGE $A3, $4D, "$ $ " ; left edge, top marker + MESSAGE $AF, $4D, "$- -" ; left edge, left/right markers + MESSAGE $BB, $4D, "$ $ " ; left edge, bottom marker + + MESSAGE $82, $68, "ADF " ; replace NAV2 label in radio panel + MESSAGE $89, $69, " " ; clear out NAV2 numbers + MESSAGE $88, $69, " " ; more for good measure? + .byte 0, 0 ; sentinel + +msg_adf_heading: + MESSAGE $9D, $55, "237", str_adf_heading + +;;; One-time draw of the ADF panel (frame, label, frequency, heading +;;; and an initial needle at index 10). +InitADFPanel: + CALLAX DrawMultiMessage, DrawADFPanel + lda #$0A + sta ADFLastDrawnAngle + ldx #$04 ; needle index + jsr DrawIndicatorDialNeedle + CALLAX DrawMessageOrange, msg_adf_heading + CALLAX DrawMessageOrange, msg_adf_frequency + rts + +;;; Match the user's tuned frequency against the next station record +;;; pointed to by ($8B). On a hit, copy the station's 6-byte position +;;; (3 little-endian words at offsets 3..8) into $08FA..$0900 and +;;; raise `ADFStationActive`. Falls through to a common tail that jumps +;;; to L6018 on the way out. +LookupADFStation: + lda $08A9 + beq LookupADFStationDone + lda #$00 + sta $08A8 + lda ADFFreqLowPacked + ldy #$01 + cmp ($8B),y + bne LookupADFStationDone + lda ADFFreqHighDigit + iny + cmp ($8B),y + bne LookupADFStationDone + lda #$00 + sta $08F9 + sta $08FD + iny + lda ($8B),y + sta $08FA + iny + lda ($8B),y + sta $08FB + iny + lda ($8B),y + sta $08FC + iny + lda ($8B),y + sta $08FE + iny + lda ($8B),y + sta $08FF + iny + lda ($8B),y + sta $0900 + lda #$01 + sta ADFStationActive +LookupADFStationDone: + lda #$09 + jmp L6018 + +;;; Pressed Ctrl+A / A key. If ADF is not yet engaged (= $08F1 = 0) +;;; or the current InputMode is outside the ADF digit range $0D..$0F, +;;; reset to $0D (= first ADF digit). Otherwise advance to the next +;;; ADF digit (= InputMode + 1). +ADFKeyboardHook: + lda $08F1 + beq ADFKeyHook_FirstDigit + lda $FA + cmp #$0D + bcc ADFKeyHook_FirstDigit + cmp #$10 + bcs ADFKeyHook_FirstDigit + adc #$01 + bne ADFKeyHook_StoreMode ; always +ADFKeyHook_FirstDigit: + .refto ADFKeyHook_FirstDigit + .refto LDBAC +LDBAC: lda #$0D ; ADF +ADFKeyHook_StoreMode: + .refto ADFKeyHook_StoreMode + .refto LDBAE +LDBAE: jmp SetInputModeAndCounter + +;;; KeyDecrease (`,`) patched entry: handles ADF-digit-down and the +;;; mixture-decrease shared with chunk5's main KeyDecrease. Returns +;;; A = current InputMode so the chunk5 caller can continue dispatch. +KeyDecreasePatch: + ldx ADFMode + beq KeyDecPatch_NotADF + lda InputMode + cmp #$0D ; ADF + bcc KeyDecPatch_NotADF + cmp #$10 + bcs KeyDecPatch_NotADF + sec + sbc #$0D + tax ; X = digit slot (0..2) + lda str_adf_frequency+1,x + cmp #'0' + bne KeyDecPatch_DigitDown + lda #'9'+1 ; wrap '0' -> '9' (after sbc 1 below) +KeyDecPatch_DigitDown: + .refto KeyDecPatch_DigitDown + .refto LDBCD +LDBCD: sec + sbc #$01 + jsr StoreADFDigitAndRedraw +KeyDecPatch_NotADF: + .refto KeyDecPatch_NotADF + .refto LDBD3 +LDBD3: lda InputMode + cmp #$03 ; mixture mode? + bne KeyPatch_ExitWithMode + ldx $0A49 ; mixture index + inx + cpx #$08 + beq KeyPatch_ExitWithMode + bne KeyPatch_StoreMixture ; always + +;;; KeyIncrease (`.`) patched entry: symmetric to KeyDecreasePatch. +KeyIncreasePatch: + ldx ADFMode + beq KeyIncPatch_NotADF + lda InputMode + cmp #$0D ; ADF + bcc KeyIncPatch_NotADF + cmp #$10 + bcs KeyIncPatch_NotADF + sec + sbc #$0D + tax + lda str_adf_frequency+1,x + cmp #'9' + bne KeyIncPatch_DigitUp + lda #'0'-1 +KeyIncPatch_DigitUp: + .refto KeyIncPatch_DigitUp + .refto LDBFF +LDBFF: clc + adc #$01 + jsr StoreADFDigitAndRedraw +KeyIncPatch_NotADF: + .refto KeyIncPatch_NotADF + .refto LDC05 +LDC05: lda $FA + cmp #$03 ; mixture mode? + bne KeyPatch_ExitWithMode + ldx $0A49 + dex + bmi KeyPatch_ExitWithMode +KeyPatch_StoreMixture: + .refto KeyPatch_StoreMixture + .refto LDC11 +LDC11: txa + jsr UpdateMixtureControlIndicator +KeyPatch_ExitWithMode: + .refto KeyPatch_ExitWithMode + .refto LDC15 +LDC15: lda $FA + rts + +;;; Caller passes A = ASCII digit, X = digit slot (0..2). Stores it +;;; into the visible ADF frequency string, redraws the message, and +;;; recomputes the packed-BCD station ID used by `LookupADFStation` +;;; to scan the scenery. +StoreADFDigitAndRedraw: + sta str_adf_frequency+1,x + CALLAX DrawMessageOrange, msg_adf_frequency + lda str_adf_frequency+2 + asl a + asl a + asl a + asl a + sta $B7 ; mid nibble + lda str_adf_frequency+3 + and #$0F ; low nibble + ora $B7 + sta ADFFreqLowPacked + lda str_adf_frequency+1 + and #$0F ; high nibble + sta ADFFreqHighDigit + +;;; Clear the "station tuned" flag and request a fresh lookup pass +;;; against the scenery file. Reached as a fall-through from +;;; `StoreADFDigitAndRedraw` and as the 64K patch target for the +;;; main-loop `P64K_1` slot. +RequestADFStationLookup: + ldx #$00 + stx ADFStationActive + inx + stx $08A8 + jmp LDC15 + +;;; ============================================================ +;;; Reality Mode + + ;; Reality-mode failure procedures. One is selected at random + ;; from `(UpdateCounter+altHi) & $0E` and dispatched after the + ;; "AIRCRAFT PROBLEM" message. Each clears an instrument bit + ;; in `InstrumentOperationalFlags` or sets engine-fault bits + ;; in $0991. +FailureProcTable: + .addr FailInstrumentBit0 ; AND $FE + .addr SetEngineFault01 ; ORA $03 -> $0991 + .addr FailInstrumentBit2 ; AND $FB + .addr FailInstrumentBit3 ; AND $F7 + .addr SetEngineFault23 ; ORA $0C -> $0991 + .addr FailInstrumentBit5 ; AND $DF + .addr FailInstrumentBit6 ; AND $BF + .addr FailInstrumentBit7 ; AND $7F + +.proc RealityModeHook + lda RealityMode + beq RealityModeOff + lda $0937 + bne RMH_problemCheck + ;; ---- Elevator trim "drift" toward $0A5D auto-trim target --- + lda YokeVertPos + tax + sec + sbc $0A5D + bvc RMH_trimSignOK + bpl RMH_trimAddTwo + bmi RMH_trimSubFour +RMH_trimSignOK: + .refto RMH_trimSignOK + .refto LDC71 +LDC71: beq RMH_trimDone + bmi RMH_trimAddTwo +RMH_trimSubFour: + .refto RMH_trimSubFour + .refto LDC75 +LDC75: txa + sec + sbc #$04 + tax +RMH_trimAddTwo: + .refto RMH_trimAddTwo + .refto LDC7A +LDC7A: txa + clc + adc #$02 + sta YokeVertPos +RMH_trimDone: + .refto RMH_trimDone + .refto LDC81 +LDC81: jsr RefreshElevatorIndicator +RMH_problemCheck: + .refto RMH_problemCheck + .refto LDC84 + ;; ---- Aircraft-problem trigger: every 10 ticks, with a chance + ;; depending on (cam X + cam Z) vs ReliabilityFactor. +LDC84: dec $08BC + bpl RMH_damageAccum + lda #$0A + sta $08BC + lda $63 + adc $5B + cmp #100 + bcs RMH_damageAccum + cmp ReliabilityFactor + bcc RMH_damageAccum + jsr TriggerAircraftProblem +RMH_damageAccum: + .refto RMH_damageAccum + .refto LDC9E + ;; ---- Damage accumulator slew toward target. $099E/$099F is + ;; the current accumulator, $09A0 is the target. Slew by 2 per + ;; tick (or -2 if shrinking). +LDC9E: lda $099E + cmp $09A0 + sta $B6 + lda $099F + sec + sbc $09A0 + bmi RMH_damageUp + ora $B6 + beq RMH_dirtTickInc + lda #$FE ; -2 (16-bit -$0002) + ldx #$FF + bne RMH_damageApply +RMH_damageUp: + .refto RMH_damageUp + .refto LDCB9 +LDCB9: lda #$02 ; +2 + ldx #$00 +RMH_damageApply: + .refto RMH_damageApply + .refto LDCBD +LDCBD: clc + adc $099E + sta $099E + txa + adc $099F + sta $099F +RMH_dirtTickInc: + .refto RMH_dirtTickInc + .refto LDCCB + ;; ---- Time-on-ground / wear counter $08AB/$08AC += 2 ---- +LDCCB: lda $08AB + clc + adc #$02 + sta $08AB + bcc RMH_exit + inc $08AC +RMH_exit: + .refto RMH_exit + .refto LDCD9 +LDCD9: rts + +;;; Reality mode is off: zero out the failure-related accumulators +;;; ($099E/$099F damage counters, $08AB/$08AC time-on-ground). +RealityModeOff: + lda #$00 + sta $099E + sta $099F + sta $08AB + sta $08AC + rts + +;;; Trigger a random aircraft problem: clear the viewports, print +;;; "AIRCRAFT PROBLEM !!!!", pick one of the eight handlers in +;;; `FailureProcTable` based on `(UpdateCounter + altLow) & $0E`, +;;; and tail-jump into it. +TriggerAircraftProblem: + jsr ClearViewportsToBlack + CALLAX DrawMessageOrange, msg_problem + lda UpdateCounter + adc $5F + and #$0E + tax + lda FailureProcTable,x + sta L00BA + lda FailureProcTable+1,x + sta L00BA+1 + jmp (L00BA) +.endproc + +FailInstrumentBit0: + lda #$FE +AndInstrumentFlags: + and InstrumentOperationalFlags + sta InstrumentOperationalFlags + rts + +SetEngineFault01: + lda #$03 +OraEngineFaults: + ora $0991 + sta $0991 + rts + +FailInstrumentBit2: + lda #$FB + bne AndInstrumentFlags ; always +FailInstrumentBit3: + lda #$F7 + bne AndInstrumentFlags ; always +SetEngineFault23: + lda #$0C + bne OraEngineFaults ; always +FailInstrumentBit5: + lda #$DF + bne AndInstrumentFlags ; always +FailInstrumentBit6: + lda #$BF + bne AndInstrumentFlags ; always +FailInstrumentBit7: + lda #$7F + bne AndInstrumentFlags ; always + +;;; ============================================================ +;;; North and East readouts in slew mode + +msg_north: + MESSAGE $02, $0A, " 00000 NORTH " +msg_east: + MESSAGE $02, $4C, " 00000 EAST " + +.proc DrawSlewOverlays + lda ShowSlewDigits + and #$01 + beq :+ + LDAX NorthPosition + STAX $B6 + + ;; Draw "00000 NORTH" (slew mode) + CALLAX DrawMessage, msg_north + + LDAX EastPosition + STAX $B6 + + ;; Draw "00000 EAST" (slew mode) + CALLAX DrawMessage, msg_east + +: rts +.endproc + +;;; ============================================================ + +;;; Refresh the three "panel light" colour bytes ($0917..$0919) every +;;; frame. In daylight (`$083C` bit 0 = 1) or whenever `PanelLights` +;;; are explicitly on, copy the live values from $089B..$089D. At +;;; night with the panel switched off, fall back to a dim default +;;; (#$10/#$80/#$01) so the cockpit gauges go dark. +UpdateInstrumentLights: + lda $083C + lsr a + bcs useLiveLights + lda #$10 + sta $0917 + lda #$80 + sta $0918 + lda #$01 + sta $0919 + lda PanelLights + beq :+ +useLiveLights: + lda $089B + ldx $089C + sta $0917 + stx $0918 + lda $089D + sta $0919 +: rts + +msg_mountain_crash: MESSAGE $32, $14, "MOUNTAIN CRASH" +msg_crash: MESSAGE $32, $14, "CRASH" +msg_building_crash: MESSAGE $32, $14, "BUILDING CRASH" +msg_splash: MESSAGE $32, $14, "SPLASH!" +msg_problem: MESSAGE $32, $14, "AIRCRAFT PROBLEM !!!!" + +;;; Address table +crash_msg_table: + .addr msg_crash + .addr msg_mountain_crash + .addr msg_crash + .addr msg_building_crash + +SplashCounter: + brk + +;;; Driven from the main loop. Either announces a crash that already +;;; happened (`$0834` non-zero) or, while on the ground, watches for +;;; the aircraft to settle on a water tile and prints "SPLASH!" after +;;; 5 frames of soaking. +HandleCrashOrSplash: + lda $0834 ; crash code + bne ShowCrashMessage + lda OnGroundFlag + bne CheckSplash + rts + +ShowCrashMessage: + and #$06 + tay + lda crash_msg_table,y + ldx crash_msg_table+1,y + ldy #$00 + sty $0834 +DrawCrashAndAbort: + jsr DrawMessageWhite + ldx #$FF + jsr Delay + ldx #$01 + jmp L9093 + +CheckSplash: + lda $2A36 ; current scenery tile byte + cmp #$55 ; water tile? + bne ResetSplashCounter + lda $083C + and #$01 + beq ResetSplashCounter ; only at night/dusk? + lda $0937 + ora RadarView + ora ViewDirection + bne ResetSplashCounter + inc SplashCounter + lda SplashCounter + cmp #$05 + bne SplashCheckDone + LDAX #msg_splash + jmp DrawCrashAndAbort + +ResetSplashCounter: + lda #$00 + sta SplashCounter +SplashCheckDone: + rts + +;;; Day-phase schedule: four 8-byte rows, one per season quadrant. +;;; Each row holds (Min/Hour) pairs for dawn-start, sunrise, sunset +;;; and dusk-end. Bytes interpreted as code by the disassembler are +;;; really data; see `ComputeDayPhase` for the indexing. +DayPhaseTable: + brk ; row 0: dawn-start min + .byte $06 ; row 0: dawn-start hour + .byte $1E ; sunrise min + .byte $06 ; sunrise hour + brk ; sunset min + .byte $13 ; sunset hour + .byte $1E ; dusk-end min + .byte $13 ; dusk-end hour + + brk + .byte $07 + asl a:$07,x + ora ($1E),y + ora ($0000),y + asl $1E + asl $0000 + .byte $13 + asl a:$13,x + ora $1E + ora $0000 + ora $1E,x + .byte $15 + +;;; Recompute the current day phase from `Hours`/`Minutes` against the +;;; per-quadrant `DayPhaseTable`. Result in `$083C` ($01=dusk-night, +;;; $02=twilight, $04=full daylight). Patches the daylight color into +;;; `$0884` based on `Season`. +ComputeDayPhase: + ;; The disassembler split the actual `lda $0956` (bytes + ;; $AD,$56,$09) across the .byte and the lsr below. The real + ;; instruction stream is `lda $0956 ; and #$03 ; asl asl asl + ;; tax`, i.e. fetch a per-frame value, mask to a 0..3 quadrant + ;; and use it as the row offset into `DayPhaseTable`. + .byte $AD + lsr $09,x + and #$03 ; quadrant 0..3 + asl a + asl a + asl a ; * 8 = row offset + tax + lda Minutes + sec + sbc DayPhaseTable + lda Hours + sbc DayPhaseTable+1,x + bmi phaseNight ; before dawn-start + lda Minutes + sec + sbc DayPhaseTable+2 + lda Hours + sbc DayPhaseTable+3,x + bmi phaseTwilight ; before sunrise (dawn) + lda Minutes + sec + sbc DayPhaseTable+4 + lda Hours + sbc DayPhaseTable+5,x + bmi phaseDay ; daylight + lda Minutes + sec + sbc DayPhaseTable+6 + lda Hours + sbc DayPhaseTable+7,x + bmi phaseTwilight ; before dusk-end +phaseNight: + lda #$04 + bne storePhase +phaseTwilight: + lda #$02 + bne storePhase +phaseDay: + lda #$01 +storePhase: + sta $083C + ldx Season + lda #$01 +: sta $0884 + asl a + dex + bne :- + rts + +;;; ============================================================ + +;;; Data for drawing wingtips / tail +;;; Word-sized values (x, w, dx, dw) are 8.8 fixed point; +;;; X values are in "color pixels" (0...139) +;;; Format: +;;; byte 0 = w (8.8 hi, lo is 0) +;;; byte 1/2 = dw (8.8, lo/hi) +;;; byte 3 = y start +;;; byte 4 = x (8.8 hi, lo is 0) +;;; byte 5/6 = dx (8.8, lo/hi) +;;; byte 7 = height + +.macro TRAPEZOID_RECORD xstart, dx, width, dw, ystart, height + .byte width + .word dw + .byte ystart + .byte xstart + .word dx + .byte height +.endmacro + +WingOrTailTrapezoids: + TRAPEZOID_RECORD $00, $0000, $00, $0000, $00, $00 ; front (nothing) + TRAPEZOID_RECORD $8B, $0000, $14, $0100, $46, $1E ; front right (wingtip) + TRAPEZOID_RECORD $78, $0080, $14, $01C0, $3C, $28 ; right (wingtip) + TRAPEZOID_RECORD $70, $0000, $02, $0000, $3C, $28 ; back right (tail) + TRAPEZOID_RECORD $48, $0000, $02, $0000, $3C, $28 ; back (tail) + TRAPEZOID_RECORD $20, $0000, $02, $0000, $3C, $28 ; back left (tail) + TRAPEZOID_RECORD $28, $0140, $14, $01C0, $3C, $28 ; left (wingtip) + TRAPEZOID_RECORD $14, $0100, $14, $0100, $46, $1E ; front left (wingtip) + TRAPEZOID_RECORD $00, $0000, $00, $0000, $00, $00 ; down (nothing) + +;;; Data for airplane image (radar view) and bomb sight (WW1 mode) +;;; Format is: addr lo, addr hi, or mask + +.macro OVERLAY_RECORD addr, mask + .addr addr + .byte mask +.endmacro + +ImageOverlays: +BombSightOverlayPixels: + OVERLAY_RECORD $2B14, $7F + OVERLAY_RECORD $2B13, $7F + OVERLAY_RECORD $2B15, $7F + OVERLAY_RECORD $2714, $0C + OVERLAY_RECORD $2314, $0C + OVERLAY_RECORD $3E94, $3F + OVERLAY_RECORD $3A94, $0C + OVERLAY_RECORD $2F14, $0C + OVERLAY_RECORD $3314, $0C + OVERLAY_RECORD $3714, $3F + OVERLAY_RECORD $3B14, $0C + OVERLAY_RECORD $3F14, $0C + OVERLAY_RECORD $3694, $0C + OVERLAY_RECORD $3294, $3F + OVERLAY_RECORD $2394, $3F + .byte $00, $00 ; sentinel + +AirplaneOverlayPixels: + OVERLAY_RECORD $2B14, $7F + OVERLAY_RECORD $2714, $0C + OVERLAY_RECORD $2314, $0C + OVERLAY_RECORD $2F14, $0C + OVERLAY_RECORD $3314, $0C + OVERLAY_RECORD $3714, $0C + OVERLAY_RECORD $3B14, $3F + OVERLAY_RECORD $2B13, $78 + OVERLAY_RECORD $2B15, $03 + .byte $00, $00 ; sentinel + +.proc DrawViewOverlays + lda RadarView + beq NotRadar + ldx #AirplaneOverlayPixels - ImageOverlays + bne Skip ; always + +NotRadar: + lda ViewDirection + bpl DrawWingsOrTailOverlays ; anything but down + lda #$10 + ldx WW1AceMode + beq DrawWingsOrTailOverlays + + ;; Draw airplane image (radar view) or bomb sight (WW1 mode) + ldx #BombSightOverlayPixels - ImageOverlays +Skip: ldy #$00 + +Loop: lda ImageOverlays,x + sta $B8 + inx + lda ImageOverlays,x + beq Exit ; sentinel + sta $B9 + inx + lda ImageOverlays,x + pha + ora ($B8),y + sta ($B8),y + + lda $B9 ; do page 2 as well + clc + adc #$20 + sta $B9 + + pla + ora ($B8),y + sta ($B8),y + + inx + bne Loop +Exit: rts + +.proc DrawWingsOrTailOverlays + asl a ; *= 4 + asl a ; `ViewDirection` already scaled by 2 + pha + jsr DrawWingsOrTail + pla + cmp #$40 + beq Exit +;;; Shared early-exit RTS used by the trapezoid setup below (= sentinel +;;; trapezoid array entry hit) and the post-overlay return. +DrawWingsOrTail_Exit: + .refto DrawWingsOrTail_Exit + .refto LDFAF +LDFAF: rts + +TrapezoidX: .word 0 ; "color pixel" right pos in 8.8 form +TrapezoidDX: .word 0 ; pos delta (8.8) +TrapezoidW: .word 0 ; "color pixel" width in 8.8 form +TrapezoidDW: .word 0 ; width delta (8.8) +TrapezoidY: .byte 0 ; top +TrapezoidH: .byte 0 ; height + +DrawWingsOrTail: + pha + lda #$09 + sta $0876 + jsr L601E + pla + tax + lda #$00 + sta TrapezoidW ; low byte (fraction) of 8.8 fixed + sta TrapezoidX ; point values start at 0 + lda WingOrTailTrapezoids,x + sta TrapezoidW+1 ; high byte - integer part + beq LDFAF ; exit if sentinel + + lda WingOrTailTrapezoids+1,x + sta TrapezoidDW + lda WingOrTailTrapezoids+2,x + sta TrapezoidDW+1 + lda WingOrTailTrapezoids+3,x + sta TrapezoidY + lda WingOrTailTrapezoids+4,x + sta TrapezoidX+1 ; high byte - integer part + lda WingOrTailTrapezoids+5,x + sta TrapezoidDX + lda WingOrTailTrapezoids+6,x + sta TrapezoidDX+1 + lda WingOrTailTrapezoids+7,x + sta TrapezoidH +Loop: + lda TrapezoidX+1 + sta $27 ; "color pixel" (0...139) right edge + ldy TrapezoidY + lda HiresTableLo,y + sta HiresRowPtr + lda HiresTableHi,y + sta HiresRowPtr+1 + lda TrapezoidW+1 ; A = "color pixel" (0...139) + jsr DrawColorSpanRelay + + ;; Update pos and width by delta, 8.8 fixed point math. + ;; (Which is just 16-bit addition.) + ADD16C TrapezoidX, TrapezoidDX, TrapezoidX + ADD16C TrapezoidW, TrapezoidDW, TrapezoidW + + inc TrapezoidY + dec TrapezoidH + + bne Loop + rts +.endproc +.endproc + +;;; ============================================================ + + ;; Decoded from the magneto state by `SetMagnetoFromA`: + ;; $00 $00 -> off + ;; $01 $00 -> left only + ;; $00 $01 -> right only + ;; $01 $01 -> both +LeftMagnetoOn: .byte $01 +RightMagnetoOn: .byte $01 + + ;; Engine "spin-up" counter: decremented each frame after a + ;; restart until it reaches zero, smoothing the RPM ramp. +EngineSpinupCounter: brk + + ;; CHT-gauge slew accumulator: fraction in $LE049, integer in + ;; $099C. Slewed by $0A per call from UEM_chtSlewTarget. +CHTSlewFraction: + .refto CHTSlewFraction + .refto LE049 +LE049: brk + + ;; Oil-temp slew accumulator: fraction in $LE04A, integer in + ;; $099D. Slewed by $64 per call from UEM_oilSlewTarget. +OilTempSlewFraction: + .refto OilTempSlewFraction + .refto LE04A +LE04A: brk + + ;; RPM lookup tables indexed by `(L0A11+1) >> 2` (clamped to + ;; $1F). The two curves describe how visible RPM tracks the + ;; idle/run state of the engine. +RPMCurveIdle: + .byte $0D, $0F, $11, $13, $15, $16, $18, $19 + .byte $1B, $1C, $1D, $1E, $1F, $20, $21, $22 + .byte $23, $24, $25, $26, $27, $28, $29, $2A + .byte $2B, $2C, $2D, $2E, $2F, $30, $31, $32 +RPMCurveRun: + .byte $00, $03, $07, $0B, $0F, $12, $13, $14 + .byte $15, $18, $1B, $1E, $21, $24, $26, $28 + .byte $2A, $2C, $2D, $2F, $30, $32, $34, $35 + .byte $37, $39, $3B, $3D, $3F, $41, $43, $46 + +;;; ============================================================ +;;; Engine model (64K only) +;;; +;;; In 64K mode the patch table redirects the simple 48K engine +;;; routine at `LA55A` to `UpdateEngineWithMagneto` here, which adds +;;; magneto, mixture and prop-autorotation behaviour on top of the +;;; baseline RPM update. + +;;; Magneto state messages + +msg_O: MESSAGE $9A, $76, "O" ; off +msg_L: MESSAGE $9A, $76, "L" ; left on +msg_R: MESSAGE $9A, $76, "R" ; right on +msg_B: MESSAGE $9A, $76, "B" ; both on +msg_S: MESSAGE $9A, $76, "S" ; start + +UpdateEngineWithMagneto: + ;; Reality mode + at-least-one-magneto check. WW1 mode and a + ;; "carb heat full" condition ($0A49 == 7) bypass the test. + lda WW1AceMode + bne UEMcheckFuel + lda RealityMode + beq UEMcheckFuel + lda $0A49 + cmp #$07 + beq UEMengineOff + lda LeftMagnetoOn + and $0999 + sta $B7 + lda RightMagnetoOn + and $099A + ora $B7 + bne UEMcheckFuel +UEMengineOff: + jsr StopEngine + jmp UEMupdateRPM + +UEMcheckFuel: + lda $0998 + bne UEMcheckMixture + lda $0994 + beq UEMengineOff + bne UEMrealityRun +UEMcheckMixture: + lda $0997 + beq UEMengineOff +UEMrealityRun: + lda RealityMode + ora EngineOnFlag + beq UEMforceOn + lda EngineOnFlag + bne UEMupdateRPM + lda L0A11+1 + cmp #$25 + bcs UEMforceOn + + ;; Engine off and prop spinning slowly: maybe transition out + ;; of the start position and into "both magnetos on". + lda MagnetoState + cmp #4 ; start + bne UEMupdateRPM + ldx #$64 + jsr Delay + ldx #3 ; both + jsr SetMagnetoState + lda Season + cmp #$01 ; winter cold-start: occasional misfire + bne UEMforceOn + lda UpdateCounter + and #$04 + beq UEMupdateRPM +UEMforceOn: + lda #$01 + sta EngineOnFlag +UEMupdateRPM: + lda L0A11+1 + lsr a + lsr a + cmp #$1F + bcc :+ + lda #$1F +: tax + lda RPMCurveRun,x + sta L00BA + ;; ---- Phase 1: RPM curve selection ------------------------ + ;; Compute the target RPM index by combining the throttle ($0A6F), + ;; the carb-heat penalty (signal $0A58 reduces throttle by a + ;; fixed bias), the spinup offset (EngineSpinupCounter ticks + ;; while the prop is windmilling up), the mixture-fault clamp + ;; ($0991 & $03 = "lean too far"), and the single-magneto + ;; degradation. Result in Y = curve index, looked up in + ;; RPMCurveIdle to give the smooth target RPM in $0990. + ldy $0A6F + lda EngineOnFlag + beq UEM_engineOff ; engine stopped -> Y = 0 + lda $0A58 + beq UEM_spinupMerge ; carb heat off -> no penalty + lda #$F8 ; carb-heat bias: -$08 + ldx EngineSpinupCounter + beq UEM_spinupOffset + lda #$E0 ; spinup-in-progress: bigger penalty + dec EngineSpinupCounter +UEM_spinupOffset: + .refto UEM_spinupOffset + .refto LE13A +LE13A: clc + adc $0A6F + tay + bpl UEM_spinupMerge +UEM_engineOff: + .refto UEM_engineOff + .refto LE141 +LE141: ldy #$00 +UEM_spinupMerge: + .refto UEM_spinupMerge + .refto LE143 +LE143: lda $0991 + and #$03 ; mixture fault flags + beq UEM_magnetoCheck + tya + cmp #$10 + bcc UEM_clampMixture + lda #$10 ; mixture fault: clamp throttle to $10 +UEM_clampMixture: + .refto UEM_clampMixture + .refto LE151 +LE151: tay +UEM_magnetoCheck: + .refto UEM_magnetoCheck + .refto LE152 +LE152: lda LeftMagnetoOn + and $0999 ; mag1 enabled & functional? + beq UEM_magnetoDegrade + lda RightMagnetoOn + and $099A ; mag2 enabled & functional? + bne UEM_curveLookup ; both -> full RPM +UEM_magnetoDegrade: + .refto UEM_magnetoDegrade + .refto LE162 + ;; Only one magneto good: drop curve index by 4 (~ -100 RPM). +LE162: tya + sec + sbc #$04 + tay + bpl UEM_curveLookup + ldy #$00 +UEM_curveLookup: + .refto UEM_curveLookup + .refto LE16B +LE16B: tya + lsr a + lsr a ; Y /= 4 (curve has 32 entries for 0..$80 throttle) + tay + lda RPMCurveIdle,y + ldx EngineOnFlag + bne UEM_storeRPM + lda #$00 ; engine off -> target = 0 +UEM_storeRPM: + .refto UEM_storeRPM + .refto LE179 +LE179: sta $0990 + cmp #$0E + bcs UEM_scaleDrag + lda #$00 ; very low RPM -> treat as 0 for drag calc +UEM_scaleDrag: + .refto UEM_scaleDrag + .refto LE182 +LE182: sta $C3 + lda #$00 + sta $C2 + ;; Propeller drag: $0A0D/$0A0E = (RPM<<8) * $07D0 * $09A9 + LDAX #$7D0 + jsr ScaleC2ByAXIntoC2 + LDAX $09A9 + jsr ScaleC2ByAX + sta $0A0D + stx $0A0E + lda L00BA ; cap RPM at the run-curve max + cmp $0990 + bmi UEM_realityFloor + sta $0990 +UEM_realityFloor: + .refto UEM_realityFloor + .refto LE1A8 + ;; ---- Phase 2: Reality-mode idle floor -------------------- + ;; In game mode, force RPM to at least $0D so the engine + ;; never sounds dead at idle. Reality mode lets it drop. +LE1A8: lda RealityMode + bne UEM_chtGauge + lda $0990 + cmp #$0D + bcs UEM_chtGauge + lda #$0D + sta $0990 +UEM_chtGauge: + .refto UEM_chtGauge + .refto LE1B9 + ;; ---- Phase 3: cylinder-head temperature gauge ($099C) ---- + ;; $FC bit 6 enables temperature gauge updates (= every other + ;; tick or so). Target value depends on engine on/off and + ;; mixture fault flag $0991 & $01. +LE1B9: lda $FC + and #$40 + beq UEM_fuelGate + ldx #$00 ; engine off: target X = 0 + lda EngineOnFlag + beq UEM_chtSlewTarget + ldx #$06 ; engine on, no mixture fault: target = 6 + lda $0991 + and #$01 + beq UEM_chtSlewTarget + ldx #$0A ; mixture fault: target hotter ($0A) +UEM_chtSlewTarget: + .refto UEM_chtSlewTarget + .refto LE1D1 +LE1D1: lda LE049 + cpx $099C + beq UEM_oilGauge ; already at target + bcs UEM_chtUp + sbc #$0A ; cool down: slew by $0A per call + sta LE049 + lda $099C + sbc #$00 + jmp UEM_chtStore + +UEM_chtUp: + .refto UEM_chtUp + .refto LE1E8 +LE1E8: adc #$0A + sta LE049 + lda $099C + adc #$00 +UEM_chtStore: + .refto UEM_chtStore + .refto LE1F2 +LE1F2: sta $099C +UEM_oilGauge: + .refto UEM_oilGauge + .refto LE1F5 + ;; ---- Phase 4: oil-temperature gauge ($099D) -------------- + ;; Same shape as the CHT slew, slewing $64 per call so the + ;; oil temp ramps in slower. +LE1F5: ldx #$00 + lda EngineOnFlag + beq UEM_oilSlewTarget + ldx #$06 + lda $0991 + and #$01 + beq UEM_oilSlewTarget + ldx #$02 +UEM_oilSlewTarget: + .refto UEM_oilSlewTarget + .refto LE207 +LE207: lda LE04A + cpx $099D + beq UEM_fuelGate + bcs UEM_oilUp + sbc #$64 + sta LE04A + lda $099D + sbc #$00 + jmp UEM_oilStore + +UEM_oilUp: + .refto UEM_oilUp + .refto LE21E +LE21E: adc #$64 + sta LE04A + lda $099D + adc #$00 +UEM_oilStore: + .refto UEM_oilStore + .refto LE228 +LE228: sta $099D +UEM_fuelGate: + .refto UEM_fuelGate + .refto LE22B + ;; ---- Phase 5: fuel consumption ($0992/$0993/$0994 = left, + ;; $0995/$0996/$0997 = right). Only runs every 32 ticks + ;; (InputTickCounter & $1F == 0) and only if the engine is on. + ;; WW1 ace mode quadruples the burn rate. +LE22B: lda InputTickCounter + and #$1F + bne UEM_exit + lda EngineOnFlag + beq UEM_exit + ldx #$F2 ; base burn-rate table index + lda WW1AceMode + beq UEM_fuelLookup + txa + asl a ; WW1 mode: index *= 4 + asl a + tax +UEM_fuelLookup: + .refto UEM_fuelLookup + .refto LE241 +LE241: ldy $0A6F + jsr L180C + sta $A5 + sty $A6 + ;; Left-tank fuel leak ($0991 & $04)? + lda $0991 + and #$04 + beq UEM_leftTankBurn + dec $0994 + bpl UEM_leftTankBurn + inc $0994 ; clamp at 0 +UEM_leftTankBurn: + .refto UEM_leftTankBurn + .refto LE25A +LE25A: lda $0998 ; tank-select: 0 = left + bne UEM_rightTankLeak + lda $A5 + clc + adc $0992 + sta $0992 + tya + bpl UEM_exit + adc $0993 + sta $0993 + lda #$FF + adc $0994 + bmi UEM_rightTankLeak + sta $0994 +UEM_rightTankLeak: + .refto UEM_rightTankLeak + .refto LE27B +LE27B: lda $0991 + and #$08 + beq UEM_rightTankBurn + dec $0997 + bpl UEM_rightTankBurn + inc $0997 +UEM_rightTankBurn: + .refto UEM_rightTankBurn + .refto LE28A +LE28A: lda $0998 + beq UEM_exit + .byte $A5 ; opcode for "lda zp" -- absorbs the next + lda $18 ; byte ($18 == EngineOnFlag's $18 entry) + adc $0995 ; so the byte stream reads as lda $A5 (=$A5) + sta $0995 ; -- a clever way to share the next add. + lda $A6 + bpl UEM_exit + adc $0996 + sta $0996 + lda #$FF + adc $0997 + bmi UEM_exit + sta $0997 +UEM_exit: + .refto UEM_exit + .refto LE2AC +LE2AC: rts + +StopEngine: + lda #$00 + sta EngineOnFlag + rts + +;;; Decode the magneto state passed in A into the per-magneto bits +;;; (LeftMagnetoOn / RightMagnetoOn) and store the encoded state. +;;; A = $00 off, $01 left, $02 right, $03 both, $04 start. +SetMagnetoFromA: + ldy #$00 + sty $FA + lsr a + sta RightMagnetoOn + rol a + and #$01 + sta LeftMagnetoOn +SetMagnetoState: + stx MagnetoState + jmp DrawCarbHeatAndLights + +;;; Message table for magneto state + +MagnetoStateMessageTable: + .addr msg_O + .addr msg_L + .addr msg_R + .addr msg_B + .addr msg_S + +DrawMagnetoStateHook: + jsr DrawMessageWhite + lda MagnetoState + asl a + tax + lda MagnetoStateMessageTable,x + pha + lda MagnetoStateMessageTable+1,x + tax + pla + jsr DrawMessageWhite + rts + +;;; ============================================================ + +;;; 64k replacement for `SelectRadarView`. When InputMode == $03 +;;; (magnetos), 4 key picks magneto state "both"; otherwise it toggles +;;; into Radar View just like the 48K version. +SelectRadarViewPatch: + lda $FA + cmp #$03 + bne SRVPatch_NotMagneto + lda #$03 ; both magnetos + ldx #$03 + jmp ApplyMagnetoState + +SRVPatch_NotMagneto: + .refto SRVPatch_NotMagneto + .refto LE2F3 +LE2F3: ldx #$02 ; Radar View + stx $FA +SRVPatch_JumpToCommon: + .refto SRVPatch_JumpToCommon + .refto LE2F7 +LE2F7: jmp L9100 + +;;; ============================================================ + +;;; 64k replacement for `Select3DView`. When InputMode == $03 (magnetos), +;;; 5 key picks magneto state "start"; otherwise it switches to 3D view. +Select3DViewPatch: + lda $FA + cmp #$03 + bne S3DPatch_NotMagneto + lda #$03 + ldx #$04 ; start magneto + jmp ApplyMagnetoState + +S3DPatch_NotMagneto: + .refto S3DPatch_NotMagneto + .refto LE307 +LE307: ldx #$01 ; 3D View + lda RadarView + bne LE2F7 + stx $FA + rts + +;;; ============================================================ + +;;; Inputs: +;;; X = delay length +.proc Delay + ldy #$FF +: inc $A5 + dec $A5 + dey + bne :- + dex + bne Delay + rts +.endproc + +;;; ============================================================ +;;; COM Radio (ATIS Messages) + +;;; Full message example: +;;; +;;; OHARE INTERNATIONAL AIRPORT +;;; INFORMATION BRAVO 13:00 ZULU +;;; WEATHER - VISIBILITY +;;; 10 - TEMPERATURE 53 - +;;; WIND 00 AT 0 - ALTIMETER +;;; 29.95 - LANDING AND DEPARTING +;;; RUNWAY 04 - ADVISE CONTROLLER +;;; ON INITIAL CONTACT YOU HAVE +;;; BRAVO..... +;;; +;;; A byte $80+ encodes using a chunk via the `ChunkOffsetTable` +;;; e.g. $41 $42 $20 $85 $20 $43 $44 $2E $2E $00 would emit +;;; "AB INFORMATION CD..". So the above message is encoded as: +;;; $4F $48 $41 $52 $45 $20 $49 $4E $54 $45 $52 $4E $41 +;;; $54 $49 $4F $4E $41 $4C $20 $41 $49 $52 $50 $4F $52 +;;; $54 $85 $42 $52 $41 $56 $4F $82 $80 $8B $89 $84 $8A +;;; $88 $86 $87 $8C $59 $4F $55 $20 $48 $41 $56 $45 $20 +;;; $42 $52 $41 $56 $4F $2E $2E $2E $2E $2E $00 + +;;; Message fragment offsets from `MessageChunks` + +ChunkOffsetTable: + .byte mc0 - MessageChunks + .byte mc1 - MessageChunks + .byte mc2 - MessageChunks + .byte mc3 - MessageChunks + .byte mc4 - MessageChunks + .byte mc5 - MessageChunks + .byte mc6 - MessageChunks + .byte mc7 - MessageChunks + .byte mc8 - MessageChunks + .byte mc9 - MessageChunks + .byte mcA - MessageChunks + .byte mcB - MessageChunks + .byte mcC - MessageChunks + +MessageChunks: + +mc0: .byte "WEATHER - ", 0 + +mc1: .byte "OBSERVATION", 0 + +mc2: +str_time_digits: .byte "12:00 ZULU", 0 + +mc3: .byte 0 + +mc4: .byte "TEMPERATURE " +str_temp_digits: .byte "75 - ", 0 + +mc5: .byte "INFORMATION", 0 + +mc6: .byte "LANDING AND DEPARTING RUNWAY " +str_runway_digits: .byte "31 - ", 0 + +mc7: .byte "ADVISE CONTROLLER", 0 + +mc8: .byte "ALTIMETER 29.95 -", 0 + +mc9: .byte "VISIBILITY 10 - ", 0 + +mcA: .byte "WIND " +str_wind_dir_digits: .byte "330 AT " +str_wind_speed_digits: .byte "27 - ", 0 + +mcB: .byte " " +str_ceiling_include: .byte " " ; set to $0 to skip ceiling + .byte "MEASURED CEILING " +str_ceiling_digits: .byte "00600 OVERCAST - ", 0 + +mcC: .byte "ON INITIAL CONTACT", 0 + +.proc UpdateCOMMessageChunks + ldx Season + lda SeasonTempTable,x + clc + adc BaseTemp + jsr ATo2Digits + sta str_temp_digits ; temperature 10s digit + stx str_temp_digits+1 ; temperature 1s digit + + lda #<360 + sta $C2 + lda #>360 + sta $C2+1 + LDAX WindDirection + jsr MultiplyAXByC2 + txa + ldx $C9 + jsr AXTo3Digits + sta str_wind_dir_digits+1 ; wind direction 10s digit + stx str_wind_dir_digits+2 ; wind direction 1s digit + cpy #$30 ; suppress leading '0' on 100s digit + bne UCM_storeWind100s + ldy #$20 ; -> space +UCM_storeWind100s: + .refto UCM_storeWind100s + .refto LE444 +LE444: sty str_wind_dir_digits ; wind direction 100s digit + + lda WindSpeed + jsr ATo2Digits + cmp #$30 ; suppress leading '0' on wind-speed 10s + bne UCM_storeWind10 + lda #$20 +UCM_storeWind10: + .refto UCM_storeWind10 + .refto LE453 +LE453: sta str_wind_speed_digits ; wind speed 10s digit + stx str_wind_speed_digits+1 ; wind speed 1s digit + + lda Hours + clc + adc $0854 + cmp #$18 ; wrap 24-hour clock + bcc UCM_storeHours + sec + sbc #24 +UCM_storeHours: + .refto UCM_storeHours + .refto LE467 +LE467: jsr ATo2Digits + STAX str_time_digits ; start of "12:00 ZULU" + + lda #<$0866 + sta $C2 + lda #>$0866 + sta $C3 + LDAX $0856 + jsr MultiplyAXByC2 + ldy #$00 ; exclude ceiling message + sty str_ceiling_include + txa + ldx $C9 + bne UCM_ceilingHasDigits + tay + beq UCM_pickRunway ; no ceiling -> skip digits, jump to runway +UCM_ceilingHasDigits: + .refto UCM_ceilingHasDigits + .refto LE48E +LE48E: ldy #' ' ; include ceiling message + sty str_ceiling_include + jsr AXTo3Digits + cpy #$30 ; suppress leading zeros on ceiling 10000s + bne UCM_storeCeiling + ldy #$20 + cmp #$30 + bne UCM_storeCeiling + lda #$20 +UCM_storeCeiling: + .refto UCM_storeCeiling + .refto LE4A2 +LE4A2: sty str_ceiling_digits ; ceiling 10000s digit + sta str_ceiling_digits+1 ; ceiling 1000s digit + stx str_ceiling_digits+2 ; ceiling 100s digit +UCM_pickRunway: + .refto UCM_pickRunway + .refto LE4AB +LE4AB: lda WindDirection+1 + lsr a ; /= 64 + lsr a + lsr a + lsr a + lsr a + lsr a + tax + lda RunwaysTable,x + jsr ATo2Digits + sta str_runway_digits ; runway 10s digit + stx str_runway_digits+1 ; runway 1s digit + rts +.endproc + +;;; ============================================================ + + +TmpStr: .res 3, 0 + +ATo2Digits: + ldx #$00 +AXTo3Digits: + STAX $B6 + CALLAX Set3DigitString, TmpStr + ldy TmpStr+0 + lda TmpStr+1 + ldx TmpStr+2 + rts + +;;; ============================================================ + +;;; Output message 1 character at a time, constructing a temporary +;;; MESSAGE at $B8 (row, col, character, null) + + MsgPtr := $B6 + + CharMsg := $B8 + CharMsgRow := CharMsg+0 + CharMsgCol := CharMsg+1 + CharMsgChar := CharMsg+2 + CharMsgNull := CharMsg+3 + +ATISReturn: + rts + +;;; Type out the COM-radio ATIS message one character at a time, with +;;; an inter-character delay set by `ATISPacing`. Bytes >= $80 expand +;;; via `ChunkOffsetTable` into one of the canned `mc?` fragments. +;;; Used as the 64K replacement for `LA60C`. +.proc DrawATISMessage + lda $0913 + bne ATISReturn ; another draw is already in progress + inc $0913 + sta CharMsgNull ; A = $00 + lda #$03 + sta CharMsgRow + sta CharMsgCol + jsr ClearViewportsToBlack + LDAX $092A ; points at airport name + STAX MsgPtr +IncPtr: + ldy #$00 + lda (MsgPtr),y + inc MsgPtr + bne LE504 + inc MsgPtr+1 +LE504: tay + bne :+ + rts +: + bpl DoChar ; simple char + lda MsgPtr+1 + pha + lda MsgPtr + pha + lda ChunkOffsetTable-$80,y ; ignore high bit + clc + adc #MessageChunks + adc #$00 + sta MsgPtr+1 + lda #$20 + jsr LE504 + jsr AdvanceCursor + pla + sta MsgPtr + pla + sta MsgPtr+1 +IncRelay: + bne IncPtr ; always + +DoChar: + cmp #' ' + beq SkipDraw + + sta CharMsgChar + CALLAX DrawMessageWhite, CharMsg + + ;; Delay for a few milliseconds + lda #$00 + sec + sbc ATISPacing + tax +l1: ldy #$78 +l2: dey + bne l2 + dex + bne l1 + + jsr Advance + bne IncRelay ; always + +SkipDraw: + jsr AdvanceCursor + bne IncRelay ; always + +AdvanceCursor: + lda CharMsgCol + cmp #100 + bcc Advance + + lda CharMsgRow ; wrap, maybe clear + adc #7 + cmp #90 + bcc SetRow + jsr ClearViewportsToBlack + lda #$03 +SetRow: + sta CharMsgRow + lda #$03 + bne SetCol ; always + +Advance: + lda CharMsgCol + clc + adc #4 +SetCol: + sta CharMsgCol + rts +.endproc + +;;; ============================================================ + +;;; Instrument Save Record: +;;; byte 0 = XStart +;;; byte 1 = bit0 = XStart high byte low bit +;;; bit1-7 = YSize +;;; byte 2 = YStart +;;; byte 3 = XSize +;;; byte 4/5 = storage buffer + +;;; The macro takes care of the funky order and bit-sharing +.macro SAVE_RECORD xstart, ystart, xsize, ysize, bufptr + .byte xstart & $FF, (ysize << 1) | (xstart >> 8), ystart, xsize + .addr bufptr +.endmacro + +;;; Group 1: 8 instrument-save records (large panels - artificial +;;; horizon, attitude indicator, big gauges, ATIS, ...). Indexed by +;;; the $0977 / InstrumentOperationalFlags / $0917 triple. +InstrumentSaveRecords_Group1: + .refto InstrumentSaveRecords_Group1 + .refto LE575 +LE575: + SAVE_RECORD $00, $68, $33, $2D, LE800 + SAVE_RECORD $03, $96, $2D, $29, $E91F + SAVE_RECORD $35, $96, $2D, $29, $EA06 + SAVE_RECORD $68, $68, $33, $2D, $EAED + SAVE_RECORD $34, $68, $33, $2D, $EC0C + SAVE_RECORD $9C, $6A, $2F, $29, $ED2B + SAVE_RECORD $DC, $B3, $15, $05, $EE1C + SAVE_RECORD $6B, $96, $2D, $29, $EE2A + +;;; Group 2: 8 instrument-save records (smaller cockpit gauges / +;;; magneto / mixture indicators). +InstrumentSaveRecords_Group2: + .refto InstrumentSaveRecords_Group2 + .refto LE5A5 +LE5A5: + SAVE_RECORD $CE, $8F, $16, $07, $EF11 + SAVE_RECORD $F3, $82, $22, $05, $EF25 + SAVE_RECORD $CF, $74, $1C, $05, $EF3B + SAVE_RECORD $CF, $66, $1A, $05, $EF4D + SAVE_RECORD $31, $64, $03, $06, $EF5E + SAVE_RECORD $D0, $A2, $45, $07, $EF61 + SAVE_RECORD $D6, $AA, $3E, $07, $EF9E + SAVE_RECORD $CE, $65, $02, $02, $FC00 +;;; Group 3: 4 instrument-save records (remaining instruments and +;;; one large fill area at $FC4E). +InstrumentSaveRecords_Group3: + .refto InstrumentSaveRecords_Group3 + .refto LE5D5 +LE5D5: + SAVE_RECORD $DA, $BA, $3B, $06, $FC01 + SAVE_RECORD $CF, $82, $1D, $05, $FC2E + SAVE_RECORD $F6, $74, $14, $05, $FC41 + SAVE_RECORD $9C, $95, $30, $2B, $FC4E + +.proc HideOrShowInstruments + lda $0977 + and InstrumentOperationalFlags + and $0917 + cmp $FB + beq :+ + sta $9E + ldx $FB + stx $A0 + sta $FB + CALLAX HideOrShow8Instruments, LE575 +: + lda $0978 + and $0915 + and $0918 + cmp $FC + beq :+ + sta $9E ; flags + ldx $FC + stx $A0 ; alt flags + sta $FC + CALLAX HideOrShow8Instruments, LE5A5 +: + lda $0979 + and $0916 + and $0919 + cmp $FD + beq :+ + sta $9E ; flags + ldx $FD + stx $A0 ; alt flags + sta $FD + CALLAX HideOrShow8Instruments, LE5D5 +: + rts +.endproc + +;;; Hide/Show 8 instruments +;;; Inputs: +;;; $BC = record list ptr (see above) +;;; $9E = flags for hide show +;;; $A0 = alt flags for hide/show + +.proc HideOrShow8Instruments + RecPtr := $BC + Count := $A2 + Flags := $9E + AltFlags := $A0 + + STAX RecPtr ; record ptr + lda #8 ; number of instruments + sta Count +Loop: + lda AltFlags + and #$01 + sta $B6 ; pass alt flag + lda Flags + and #$01 + sta $B7 ; pass flag + LDAX RecPtr + STAX $B8 ; pass record ptr + + jsr HideOrShowInstrument + + lsr Flags ; next bits + lsr AltFlags + + lda RecPtr + clc + adc #6 ; next record + sta RecPtr + bcc :+ + inc RecPtr+1 +: + dec Count + bne Loop + rts +.endproc + +;;; ============================================================ + +;;; Inputs: +;;; $B6 = no-op if equal to $B7 +;;; $B7 = hide (zero) or show (non-zero) +;;; $B8-B9 = pointer to save info record (see above) + +HideOrShow: .byte 0 ; passed in $B7 +XStart: .word 0 ; passed in ($B8) record +YStart: .byte 0 ; passed in ($B8) record +YSize: .byte 0 ; passed in ($B8) record +XSize: .byte 0 ; passed in ($B8) record +XCoord: .word 0 ; temp +XCount: .byte 0 ; temp +BitNumber: .byte 0 ; temp + +BufPtr := L00BA + +.proc HideOrShowInstrument + + lda $B7 + cmp $B6 + bne :+ + rts +: + sta HideOrShow + ldy #$00 + lda ($B8),y + sta XStart + iny + lda ($B8),y + sta YSize + lsr YSize ; YSize is in bits 1-7 + and #$01 + sta XStart+1 + iny + lda ($B8),y + sta YStart + iny + lda ($B8),y + sta XSize + iny + lda ($B8),y + sta BufPtr + iny + lda ($B8),y + sta BufPtr+1 + + lda #0 + sta BitNumber + + YCoord := YStart + +YLoop: lda XSize + sta XCount + LDAX XStart + STAX XCoord + +XLoop: lda HideOrShow + beq DoHide + + ;; Show + jsr LoadPixelState + ldx XCoord+1 + stx $A5 ; x coord hi + ldx XCoord ; x coord lo + ldy YCoord ; y coord + jsr MaybeXORPixel + jmp Next + + ;; Hide +DoHide: lda XCoord+1 + sta $A5 + ldx XCoord + ldy YCoord + jsr ErasePixelReturnIfSet + jsr SavePixelState + +Next: inc XCoord + bne :+ + inc XCoord+1 +: dec XCount + bne XLoop + + inc YCoord + dec YSize + bne YLoop + lda HideOrShow + bne Done + + ;; Pad to 8 bits + ldx BitNumber + ldy #0 + lda (BufPtr),y +PadLoop: + inx + cpx #9 + beq :+ + rol a + jmp PadLoop +: + sta (BufPtr),y +Done: rts +.endproc + +;;; ============================================================ + +;;; Clear a pixel, but return whether or not it was set +;;; Inputs: +;;; Y = y coord +;;; X = x coord lo +;;; $A5 = x coord hi +;;; Output: +;;; A = 1 if pixel was set, 0 otherwise +.proc ErasePixelReturnIfSet + jsr CalcPixelAddrAndMask + and ($B8),y + beq EPS_stash + iny ; pixel is set +EPS_stash: + .refto EPS_stash + .refto LE72A +LE72A: sty $A7 ; stash ("pixel was set") + txa + eor #$FF ; invert mask + ldy #$00 + and ($B8),y ; clear the pixel + sta ($B8),y + tax ; TODO: Use two pointers! + lda $B9 + clc + adc #$20 + cmp #$60 + bcc :+ + sec + sbc #$40 +: sta $B9 + txa + sta ($B8),y ; update the other graphics page + lda $A7 ; unstash ("pixel was set") + rts +.endproc + +;;; ============================================================ + +;;; XOR pixel on both hires screens +;;; A = toggle pixel? +;;; Y = y coord +;;; X = x coord lo +;;; $A5 = x coord hi +.proc MaybeXORPixel + pha + jsr CalcPixelAddrAndMask + pla + ora #$00 ; TODO: Skip this??? + beq MXP_noOp ; no-op + txa ; A = bit mask + eor ($B8),y ; invert + jmp MXP_store ; ... and store + +MXP_noOp: + .refto MXP_noOp + .refto LE759 +LE759: lda ($B8),y +MXP_store: + .refto MXP_store + .refto LE75B +LE75B: sta ($B8),y + tax ; stash... + lda $B9 ; TODO: Use two pointers! + clc + adc #$20 + cmp #$60 + bcc :+ + sec + sbc #$40 +: sta $B9 + txa ; unstash... + sta ($B8),y ; and update other graphics page + rts +.endproc + +;;; ============================================================ + +;;; Map pixel to row/byte/mask +;;; Input: +;;; Y = y coord +;;; X = x coord lo +;;; $A5 = x coord hi +;;; Output: +;;; $B8-B9 = byte ptr (NOTE: not row ptr!) +;;; Y = 0 +;;; X = A = bit mask +.proc CalcPixelAddrAndMask + lda HiresTableHi,y + sta $B9 + lda HiresTableLo,y + sta $B8 + ;; Pick the byte-offset table based on X hi byte (= page). + lda HiresPixelToByteTable,x + ldy $A5 + beq CPAM_addByteOff + lda HiresPixelToByteTable+256,x +CPAM_addByteOff: + .refto CPAM_addByteOff + .refto LE784 +LE784: clc + adc $B8 + sta $B8 + bcc CPAM_pickMask + inc $B9 +CPAM_pickMask: + .refto CPAM_pickMask + .refto LE78D +LE78D: lda HiresPixelToBitMaskTable,x + ldy $A5 + beq CPAM_storeMask + lda HiresPixelToBitMaskTable+242,x +CPAM_storeMask: + .refto CPAM_storeMask + .refto LE797 +LE797: tax + ldy #$00 + rts +.endproc + +;;; ============================================================ + +.proc SavePixelState + sta $A5 ; pixel was set + + inc BitNumber + lda BitNumber + cmp #9 + bne :+ + lda #1 + sta BitNumber + inc BufPtr + bne :+ + inc BufPtr+1 +: + lda $A5 + ldy #$00 ; Pixel was set + ror a + lda (BufPtr),y + rol a + sta (BufPtr),y + rts +.endproc + +;;; ============================================================ + +.proc LoadPixelState + inc BitNumber + lda BitNumber + cmp #9 + bne :+ + lda #1 + sta BitNumber + inc BufPtr + bne :+ + inc BufPtr+1 +: + ldy #$00 + lda (BufPtr),y + rol a + sta (BufPtr),y + rol a + and #$01 + rts +.endproc + +;;; ============================================================ + + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + .byte $FE + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + inc $FFFF,x + brk + brk + +;;; ============================================================ + +;;; Save buffer for instrument bitmaps - $E800 through $F000 +;;; Any data here is assumed to be garbage; the first save-record in +;;; InstrumentSaveRecords_Group1 (= the artificial horizon area) +;;; writes its backing store here on a hide/show transition. + +InstrumentSaveBuffer: + .refto InstrumentSaveBuffer + .refto LE800 +LE800: + .byte $A9, $D5, $8D, $FE, $FF, $A9, $EF, $8D + .byte $FF, $FF, $4C, $31, $EA, $00, $60, $00 + .byte $36, $30, $05, $FF, $FF, $09, $0A, $04 + .byte $0B, $89, $1F, $43, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $D3, $AA, $80, $80, $80, $80, $C0 + .byte $9B, $84, $80, $80, $80, $80, $A0, $D8 + .byte $81, $80, $80, $80, $80, $80, $D0, $CD + .byte $80, $80, $80, $80, $80, $E4, $86, $80 + .byte $80, $86, $80, $90, $80, $80, $C0, $81 + .byte $A0, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $C0, $FF, $FF + .byte $FF, $87, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $E0, $88, $B2, $D8 + .byte $F1, $B6, $DB, $87, $DB, $D5, $AA, $D5 + .byte $AA, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $80, $80, $80, $80, $80 + .byte $80, $80, $80, $82, $D5, $80, $80, $80 + .byte $80, $F0, $91, $88, $80, $80, $80, $80 + .byte $90, $88, $C1, $8D, $80, $80, $80, $E0 + .byte $C6, $98, $84, $80, $80, $80, $C0, $B0 + .byte $B6, $E7, $E7, $83, $83, $90, $B6, $C3 + .byte $99, $80, $A0, $80, $C6, $80, $C4, $80 + .byte $C4, $81, $B0, $B6, $83, $80, $80, $A0 + .byte $83, $80, $80, $93, $80, $DB, $81, $E4 + .byte $80, $98, $88, $95, $A2, $D4, $C0, $81 + .byte $C2, $E0, $D8, $8D, $86, $83, $D4, $80 + .byte $D1, $A2, $A5, $80, $80, $00, $04, $0B + .byte $00, $A9, $02, $00, $A9, $01, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $EA, $7A, $EA + .byte $9E, $EA, $E4, $EA, $48, $EA, $FE, $EB + .byte $41, $EB, $75, $EB, $9D, $EC, $20, $EC + .byte $71, $EC, $B9, $ED, $47, $ED, $B3, $EE + .byte $FD, $08, $8D, $0D, $E8, $68, $8D, $10 + .byte $E8, $8E, $0E, $E8, $8C, $0F, $E8, $BA + .byte $8E, $11, $E8, $20, $67, $EF, $20, $74 + .byte $EF, $A0, $00, $20, $7F, $EF, $99, $17 + .byte $E8, $AA, $CA, $F0, $0E, $C8, $C0, $FF + .byte $B0, $09, $20, $7F, $EF, $99, $17, $E8 + .byte $4C, $52, $EA, $A0, $01, $B9, $17, $E8 + .byte $C9, $0E, $90, $03, $4C, $49, $EA, $0A + .byte $AA, $BD, $15, $EA, $48, $E8, $BD, $15 + .byte $EA, $48, $60, $A0, $00, $A9, $04, $99 + .byte $16, $E9, $C8, $B9, $17, $E8, $99, $16 + .byte $E9, $C8, $68, $99, $16, $E9, $C8, $68 + .byte $48, $99, $16, $E9, $88, $B9, $16, $E9 + .byte $48, $20, $4F, $EF, $4C, $49, $EA, $A0 + .byte $00, $A9, $03, $99, $16, $E9, $C8, $B9 + .byte $17, $E8, $99, $16, $E9, $C8, $B9, $17 + .byte $E8, $C9, $41, $D0, $06, $AD, $0D, $E8 + .byte $4C, $DC, $EA, $C9, $58, $D0, $06, $AD + .byte $0E, $E8, $4C, $DC, $EA, $C9, $59, $D0 + .byte $06, $AD, $0F, $E8, $4C, $DC, $EA, $C9 + .byte $53, $D0, $06, $AD, $11, $E8, $4C, $DC + .byte $EA, $AD, $10, $E8, $99, $16, $E9, $20 + .byte $4F, $EF, $4C, $49, $EA, $A0, $00, $A9 + .byte $03, $99, $16, $E9, $C8, $B9, $17, $E8 + .byte $99, $16, $E9, $C8, $A9, $00, $99, $16 + .byte $E9, $20, $4F, $EF, $4C, $49, $EA, $A0 + .byte $02, $B9, $17, $E8, $C8, $C9, $41, $D0 + .byte $09, $B9, $17, $E8, $8D, $0D, $E8, $4C + .byte $49, $EA, $C9, $58, $D0, $09, $B9, $17 + .byte $E8, $8D, $0E, $E8, $4C, $49, $EA, $C9 + .byte $59, $D0, $09, $B9, $17, $E8, $8D, $0F + .byte $E8, $4C, $49, $EA, $C9, $53, $D0, $09 + .byte $B9, $17, $E8, $8D, $11, $E8, $4C, $49 + .byte $EA, $B9, $17, $E8, $8D, $10, $E8, $4C + .byte $49, $EA, $A0, $02, $B9, $17, $E8, $85 + .byte $57, $C8, $B9, $17, $E8, $85, $56, $C8 + .byte $B9, $17, $E8, $AA, $18, $69, $02, $A0 + .byte $00, $99, $16, $E9, $C8, $B9, $17, $E8 + .byte $99, $16, $E9, $A0, $00, $B1, $56, $C8 + .byte $C8, $99, $16, $E9, $88, $CA, $D0, $F5 + .byte $20, $4F, $EF, $4C, $49, $EA, $A0, $02 + .byte $B9, $17, $E8, $85, $57, $C8, $B9, $17 + .byte $E8, $85, $56, $A0, $00, $B9, $17, $E8 + .byte $8D, $12, $E8, $A2, $04, $A0, $00, $BD + .byte $17, $E8, $91, $56, $C8, $E8, $EC, $12 + .byte $E8, $90, $F4, $4C, $49, $EA, $A2, $02 + .byte $BD, $17, $E8, $85, $57, $A9, $00, $85 + .byte $56, $E8, $BD, $17, $E8, $A8, $E8, $BD + .byte $17, $E8, $8D, $E0, $EB, $E8, $BD, $17 + .byte $E8, $8D, $E4, $EB, $A2, $00, $BD, $17 + .byte $E8, $8D, $12, $E8, $38, $E9, $07, $8D + .byte $FC, $EB, $A9, $02, $48, $A2, $06, $B1 + .byte $56, $DD, $17, $E8, $F0, $14, $A2, $06 + .byte $C8, $D0, $02, $E6, $57, $A5, $57, $C9 + .byte $99, $90, $EC, $C0, $99, $90, $E8, $4C + .byte $0E, $EC, $E8, $EC, $12, $E8, $B0, $03 + .byte $4C, $D8, $EB, $68, $C9, $FC, $B0, $17 + .byte $AA, $98, $38, $E9, $99, $9D, $16, $E9 + .byte $E8, $A5, $57, $E9, $00, $9D, $16, $E9 + .byte $E8, $8A, $48, $4C, $D6, $EB, $68, $A0 + .byte $00, $99, $16, $E9, $C8, $B9, $17, $E8 + .byte $99, $16, $E9, $20, $4F, $EF, $4C, $49 + .byte $EA, $A0, $02, $B9, $17, $E8, $8D, $64 + .byte $EC, $C8, $B9, $17, $E8, $8D, $6A, $EC + .byte $C8, $B9, $17, $E8, $85, $57, $C8, $B9 + .byte $17, $E8, $85, $56, $A0, $08, $B9, $17 + .byte $E8, $85, $59, $C8, $B9, $17, $E8, $85 + .byte $58, $A0, $00, $B1, $56, $91, $58, $C6 + .byte $56, $A5, $56, $C9, $FF, $D0, $02, $C6 + .byte $57, $C6, $58, $A5, $58, $C9, $FF, $D0 + .byte $02, $C6, $59, $A9, $99, $C5, $57, $90 + .byte $E2, $A9, $99, $C5, $56, $90, $DC, $4C + .byte $49, $EA, $A0, $02, $B9, $17, $E8, $85 + .byte $57, $C8, $B9, $17, $E8, $85, $56, $C8 + .byte $B9, $17, $E8, $8D, $AE, $EC, $C8, $B9 + .byte $17, $E8, $8D, $B4, $EC, $C8, $B9, $17 + .byte $E8, $85, $59, $C8, $B9, $17, $E8, $85 + .byte $58, $A0, $00, $B1, $56, $91, $58, $E6 + .byte $56, $D0, $02, $E6, $57, $E6, $58, $D0 + .byte $02, $E6, $59, $A5, $57, $C9, $01, $90 + .byte $EA, $A5, $56, $C9, $00, $90, $E4, $4C + .byte $49, $EA, $A0, $02, $B9, $17, $E8, $85 + .byte $57, $C8, $B9, $17, $E8, $85, $56, $C8 + .byte $B9, $17, $E8, $8D, $F8, $EC, $C8, $B9 + .byte $17, $E8, $8D, $FE, $EC, $C8, $B9, $17 + .byte $E8, $85, $59, $C8, $B9, $17, $E8, $85 + .byte $58, $A0, $00, $B1, $56, $D1, $58, $D0 + .byte $26, $E6, $56, $D0, $02, $E6, $57, $E6 + .byte $58, $D0, $02, $E6, $59, $A5, $57, $C9 + .byte $99, $90, $E8, $A5, $56, $C9, $99, $90 + .byte $E2, $A2, $02, $A9, $FF, $9D, $16, $E9 + .byte $E8, $9D, $16, $E9, $4C, $34, $ED, $48 + .byte $A2, $02, $A5, $57, $9D, $16, $E9, $E8 + .byte $A5, $56, $9D, $16, $E9, $E8, $68, $9D + .byte $16, $E9, $E8, $A5, $59, $9D, $16, $E9 + .byte $E8, $A5, $58, $9D, $16, $E9, $E8, $B1 + .byte $58, $9D, $16, $E9, $E8, $8A, $A2, $00 + .byte $9D, $16, $E9, $E8, $BD, $17, $E8, $9D + .byte $16, $E9, $20, $4F, $EF, $4C, $49, $EA + .byte $A2, $00, $BD, $13, $E8, $95, $56, $E8 + .byte $E0, $04, $90, $F6, $AE, $11, $E8, $9A + .byte $A0, $00, $B9, $17, $E8, $99, $16, $E9 + .byte $C8, $B9, $17, $E8, $99, $16, $E9, $C8 + .byte $B9, $17, $E8, $48, $C8, $B9, $17, $E8 + .byte $48, $88, $AD, $F0, $03, $99, $17, $E8 + .byte $C8, $AD, $F1, $03, $99, $17, $E8, $A9 + .byte $98, $8D, $F0, $03, $A9, $ED, $8D, $F1 + .byte $03, $AE, $0E, $E8, $AC, $0F, $E8, $AD + .byte $10, $E8, $48, $AD, $0D, $E8, $28, $60 + .byte $EA, $4C, $A4, $EF, $BA, $8E, $11, $E8 + .byte $88, $B9, $17, $E8, $8D, $F0, $03, $C8 + .byte $B9, $17, $E8, $8D, $F1, $03, $20, $4F + .byte $EF, $4C, $49, $EA, $A2, $7C, $A9, $C0 + .byte $8D, $12, $E8, $A9, $00, $85, $56, $85 + .byte $57, $A8, $B1, $56, $85, $58, $C8, $D0 + .byte $03, $20, $C7, $EE, $B1, $56, $C5, $58 + .byte $F0, $1D, $A5, $58, $C9, $83, $D0, $0B + .byte $48, $20, $97, $EF, $CA, $D0, $03, $20 + .byte $C1, $EE, $68, $20, $97, $EF, $CA, $D0 + .byte $03, $20, $C1, $EE, $4C, $C2, $ED, $C8 + .byte $D0, $03, $20, $C7, $EE, $B1, $56, $C5 + .byte $58, $F0, $33, $A5, $58, $C9, $83, $D0 + .byte $16, $48, $20, $97, $EF, $CA, $D0, $03 + .byte $20, $C1, $EE, $68, $48, $20, $97, $EF + .byte $CA, $D0, $03, $20, $C1, $EE, $68, $48 + .byte $20, $97, $EF, $CA, $D0, $03, $20, $C1 + .byte $EE, $68, $20, $97, $EF, $CA, $D0, $03 + .byte $20, $C1, $EE, $4C, $C2, $ED, $A9, $03 + .byte $85, $59, $C8, $D0, $03, $20, $C7, $EE + .byte $B1, $56, $C5, $58, $F0, $4F, $A9, $83 + .byte $20, $97, $EF, $CA, $D0, $03, $20, $C1 + .byte $EE, $A5, $59, $C9, $83, $D0, $23, $A9 + .byte $40, $20, $97, $EF, $CA, $D0, $03, $20 + .byte $C1, $EE, $A5, $58, $20, $97, $EF, $CA + .byte $D0, $03, $20, $C1, $EE, $A9, $83, $20 + .byte $97, $EF, $CA, $D0, $03, $20, $C1, $EE + .byte $A9, $43, $20, $97, $EF, $CA, $D0, $03 + .byte $20, $C1, $EE, $A5, $58, $20, $97, $EF + .byte $CA, $D0, $03, $20, $C1, $EE, $A9, $00 + .byte $85, $59, $4C, $C2, $ED, $A5, $59, $18 + .byte $69, $01, $B0, $05, $85, $59, $4C, $32 + .byte $EE, $A9, $83, $20, $97, $EF, $CA, $D0 + .byte $03, $20, $C1, $EE, $A5, $59, $20, $97 + .byte $EF, $CA, $D0, $03, $20, $C1, $EE, $A5 + .byte $58, $20, $97, $EF, $CA, $D0, $03, $20 + .byte $C1, $EE, $A9, $01, $85, $59, $4C, $32 + .byte $EE, $20, $7F, $EF, $A2, $80, $60, $AD + .byte $12, $E8, $38, $E9, $01, $F0, $0B, $8D + .byte $12, $E8, $A5, $57, $18, $69, $01, $85 + .byte $57, $60, $68, $68, $A5, $59, $F0, $1B + .byte $A9, $83, $20, $97, $EF, $CA, $D0, $03 + .byte $20, $C1, $EE, $A5, $59, $20, $97, $EF + .byte $CA, $D0, $03, $20, $C1, $EE, $A5, $58 + .byte $20, $97, $EF, $4C, $49, $EA, $A9, $C0 + .byte $8D, $12, $E8, $A9, $00, $85, $56, $85 + .byte $57, $A8, $20, $7F, $EF, $C9, $83, $F0 + .byte $0B, $91, $56, $C8, $D0, $03, $20, $37 + .byte $EF, $4C, $0A, $EF, $20, $7F, $EF, $C9 + .byte $83, $F0, $EE, $AA, $20, $7F, $EF, $91 + .byte $56, $C8, $D0, $05, $48, $20, $37, $EF + .byte $68, $CA, $D0, $F3, $4C, $0A, $EF, $AD + .byte $12, $E8, $38, $E9, $01, $F0, $0B, $8D + .byte $12, $E8, $A5, $57, $18, $69, $01, $85 + .byte $57, $60, $68, $68, $4C, $49, $EA, $A0 + .byte $00, $B9, $16, $E9, $AA, $20, $7F, $EF + .byte $C9, $FF, $F0, $0A, $B9, $16, $E9, $20 + .byte $97, $EF, $C8, $CA, $D0, $EF, $60, $A2 + .byte $00, $B5, $56, $9D, $13, $E8, $E8, $E0 + .byte $04, $90, $F6, $60, $A9, $03, $8D, $B0 + .byte $C0, $A9, $11, $8D, $B0, $C0, $60, $A9 + .byte $70, $2C, $B0, $C0, $D0, $0C, $A9, $01 + .byte $2C, $B0, $C0, $F0, $F2, $AD, $B1, $C0 + .byte $18, $60, $AD, $B1, $C0, $38, $60, $48 + .byte $A9, $02, $2C, $B0, $C0, $F0, $FB, $68 + .byte $8D, $B1, $C0, $60, $A5, $45, $8D, $0D + .byte $E8, $A5, $46, $8D, $0E, $E8, $A5, $47 + .byte $8D, $0F, $E8, $A5, $48, $8D, $10, $E8 + .byte $A5, $49, $18, $69, $04, $8D, $11, $E8 + .byte $A0, $02, $A5, $3A, $38, $E9, $02, $99 + .byte $16, $E9, $C8, $A5, $3B, $E9, $00, $99 + .byte $16, $E9, $4C, $9C, $ED, $85, $45, $28 + .byte $86, $46, $84, $47, $08, $68, $85, $48 + .byte $BA, $86, $49, $D8, $68, $85, $3A, $68 + .byte $85, $3B, $6C, $F0, $03, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $FF, $00, $FE, $FF, $FF, $00, $00 + +;;; ============================================================ +;;; "War Report" for "World War 1 Ace" mode +;;; The whole display is rendered, then the stats are re-rendered +;;; in orange. + +.assert * = $F000, error, .sprintf("placement, %04X", *) + +msg_war_report: MESSAGE $00, $18, "***** WAR REPORT *****" + MESSAGE $0C, $04, "ENEMY PLANES SHOT DOWN = " +msg_wr2: MESSAGE $0C, $68, "000", str_enemy_shot_down + MESSAGE $12, $04, "BOMB HITS = " +msg_wr3: MESSAGE $12, $34, "000", str_bomb_hits + MESSAGE $18, $04, "AIRCRAFT DAMAGE BY ENEMY = " +msg_wr4: MESSAGE $18, $70, "000", str_damage_by_enemy + MESSAGE $24, $04, "ENEMY STATUS: 0=SHOT DOWN" + MESSAGE $2A, $3C, "1=RETURNING OR HOME" + MESSAGE $30, $3C, "2=ATTACKING" + + MESSAGE $3C, $04, "ENEMY 1 = " +msg_wr5: MESSAGE $3C, $2C, "0", str_enemy1_status + + MESSAGE $3C, $3C, "ENEMY 2 = " +msg_wr6: MESSAGE $3C, $64, "0", str_enemy2_status + + MESSAGE $42, $04, "ENEMY 3 = " +msg_wr7: MESSAGE $42, $2C, "0", str_enemy3_status + + MESSAGE $42, $3C, "ENEMY 4 = " +msg_wr8: MESSAGE $42, $64, "0", str_enemy4_status + + MESSAGE $48, $04, "ENEMY 5 = " +msg_wr9: MESSAGE $48, $2C, "0", str_enemy5_status + + MESSAGE $48, $3C, "ENEMY 6 = " +msg_wr10: MESSAGE $48, $64, "0", str_enemy6_status + + MESSAGE $54, $00, "PRESS ANY KEY TO RESUME BATTLE" + .byte 0, 0 ; sentinel + +;;; Shared early-exit RTS for ShowWarReport (= when $08C8 = 0 = no +;;; war-report request pending). +ShowWarReport_Exit: + .refto ShowWarReport_Exit + .refto LF13D +LF13D: rts + +;;; Compose and display the WW1-ace "war report" overlay: enemy +;;; counters, bomb hits, status digits, and a "PRESS ANY KEY" +;;; prompt. Triggered when $08C8 is set; clears the flag, fills +;;; the message strings from the per-enemy state cells at $A972.. +;;; $A9FE, then calls DrawMultiMessage and TogglePauseRelay. +ShowWarReport: + .refto ShowWarReport + lda $08C8 + beq LF13D + lda #$00 + sta $08C8 + lda $0898 + sta $B6 + lda #$00 + sta $B7 + CALLAX Set3DigitStringRelay, str_enemy_shot_down + lda $A81B + sta $B6 + lda #$00 + sta $B7 + CALLAX Set3DigitStringRelay, str_bomb_hits + lda $08A4 + sta $B6 + lda #$00 + sta $B7 + CALLAX Set3DigitStringRelay, str_damage_by_enemy + lda $A972 + ora #$30 + sta str_enemy1_status + lda $A98E + ora #$30 + sta str_enemy2_status + lda $A9AA + ora #$30 + sta str_enemy3_status + lda $A9C6 + ora #$30 + sta str_enemy4_status + lda $A9E2 + ora #$30 + sta str_enemy5_status + lda $A9FE + ora #$30 + sta str_enemy6_status + jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_war_report + CALLAX DrawMessageOrange, msg_wr2 + CALLAX DrawMessageOrange, msg_wr3 + CALLAX DrawMessageOrange, msg_wr4 + CALLAX DrawMessageOrange, msg_wr5 + CALLAX DrawMessageOrange, msg_wr6 + CALLAX DrawMessageOrange, msg_wr7 + CALLAX DrawMessageOrange, msg_wr8 + CALLAX DrawMessageOrange, msg_wr9 + CALLAX DrawMessageOrange, msg_wr10 + jsr TogglePauseRelay + rts + +;;; ============================================================ + + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + .byte $76, $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $2F + rti + +;;; ============================================================ +;;; SceneryRotatedTransform scratch matrix (= 30 bytes, LF242..LF25F) +;;; +;;; This region is overwritten on every call to +;;; `SceneryRotatedTransform` (just below) with the rotated transform +;;; coefficients. The bytes that appear in the original binary look +;;; like uninitialised "garbage" -- the disassembler renders them as +;;; mixed words/bytes/instructions, but their values don't matter +;;; because the routine always writes them before reading. Layout +;;; used at runtime: +;;; +;;; LF242/LF243 = -(c*dx + s*dy) (negated rotated X) +;;; LF244/LF245 = (c*dx + s*dy) (rotated X) +;;; LF247/LF248 = (s*dx + c*dy) (rotated Y) +;;; LF249/LF24A = -(s*dx + c*dy) (negated rotated Y) +;;; LF24C/LF24D = -(rotated X) - 1 (= eor #$FF + 1 form) +;;; LF24E/LF24F = rotated X - 1 (= eor #$FF form) +;;; LF251/LF252 = -(rotated Y) - 1 +;;; LF253/LF254 = rotated Y - 1 +;;; LF258..LF25F = 4 matrix elements (sin*x, sin*y and their negatives) +;;; +;;; The trailing $41 / "and #$1B" / $2B / $19 bytes are leftover stack +;;; or zero-page padding from the original assemble; they get clobbered +;;; on the first call. +SceneryRotScratch: + .refto SceneryRotScratch + .refto LF242 +LF242: .word 0 +LF244: brk +LF245: brk + .byte $41 +LF247: .word 0 +LF249: .word 0 + .byte $41 +LF24C: brk +LF24D: brk +LF24E: brk +LF24F: brk + .byte $41 +LF251: brk +LF252: brk +LF253: brk +LF254: brk + and #$1B + .byte $2B +LF258: brk +LF259: brk +LF25A: brk +LF25B: brk +LF25C: brk +LF25D: brk +LF25E: brk +LF25F: brk + .byte $19 + +;;; Scenery 64K-mode opcode $03 implementation. Reads four bytes +;;; from the stream: +;;; byte 1 = rotation angle (offset by $0847 + $0849 then negated) +;;; bytes 2-3 = X displacement +;;; bytes 4-5 = Y displacement +;;; Produces a 2D rotation matrix that transforms (X, Y) into +;;; aircraft-relative coordinates, stashed at LF244..LF25F. Falls +;;; through into the rest of the scenery interpreter via the next +;;; record. +SceneryRotatedTransform: + iny + lda ($8B),y + clc + adc $0849 + clc + adc $0847 + eor #$FF + clc + adc #$41 + sta $F2 + jsr L1763 + sta $A9 + stx $AA + lda $F2 + jsr L1768 + sta $BE + stx $BF + sta $C4 + stx $C5 + ldy #$02 + lda ($8B),y + sta $3E + sta $C2 + iny + lda ($8B),y + sta $3F + sta $C3 + jsr ScaleC2ByC4 + sta $18 + stx $19 + sta LF25A + stx LF25B + eor #$FF + clc + adc #$01 + sta LF25E + bne LF2AE + dex +LF2AE: txa + eor #$FF + sta LF25F + lda $A9 + ldx $AA + sta $C4 + stx $C5 + lda $3E + ldx $3F + sta $C2 + stx $C3 + jsr ScaleC2ByC4 + sta $1B + stx $1C + sta LF258 + stx LF259 + eor #$FF + clc + adc #$01 + sta LF25C + bne LF2DC + dex +LF2DC: txa + eor #$FF + sta LF25D + ldy #$04 + lda ($8B),y + sta $9A + sta $C2 + iny + lda ($8B),y + sta $9B + sta $C3 + lda $BE + ldx $BF + sta $C4 + stx $C5 + jsr ScaleC2ByC4 + sta $1E + stx $1F + lda $9A + ldx $9B + sta $C2 + stx $C3 + lda $A9 + ldx $AA + sta $C4 + stx $C5 + jsr ScaleC2ByC4 + sta $A9 + stx $AA + clc + adc $18 + sta LF244 + txa + adc $19 + sta LF245 + eor #$FF + sta LF24F + lda LF244 + eor #$FF + clc + adc #$01 + sta LF24E + bne LF338 + inc LF24F +LF338: ADD16C $1B, $1E, LF247 + eor #$FF + sta LF252 + lda LF247 + eor #$FF + clc + adc #$01 + sta LF251 + bne LF35C + inc LF252 +LF35C: SUB16C $1B, $1E, LF242 + eor #$FF + sta LF24D + lda LF242 + eor #$FF + clc + adc #$01 + sta LF24C + bne LF380 + inc LF24D +LF380: SUB16C $18, $A9, LF249 + eor #$FF + sta LF254 + lda LF249 + eor #$FF + clc + adc #$01 + sta LF253 + bne LF3A4 + inc LF254 +LF3A4: lda $8B + pha + lda $8C + pha + lda #$40 + sta $8B + lda #$F2 + sta $8C + lda #$00 + sta $0876 + lda #$00 + sta $0877 + jsr ProcessScenery + pla + sta $8C + pla + sta $8B + lda #$06 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $0E callback (64K only). Pulls a two-byte +;;; argument out of the record into $A5/$A6 (probably a relative +;;; pointer) and tail-jumps into the chunk5 opcode handler at +;;; `SceneryJumpToFetched`. +SceneryOp64KCallback: + iny + lda ($8B),y + sta $A5 + iny + lda ($8B),y + sta $A6 + jmp SceneryJumpToFetched + + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + .byte $76, $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + .byte $76, $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + .byte $76 + + .assert * = $F400, error, .sprintf("EOF mismatch, %04X", *) diff --git a/src/chunk4.s b/src/chunk4.s new file mode 100644 index 0000000..6dacc50 --- /dev/null +++ b/src/chunk4.s @@ -0,0 +1,3875 @@ + .org $200 + +;;; chunk5 references +LA851 := $A851 ; into middle of message??? + + sty $C2 + .byte $D2 + cmp $CE,x + ldy #$D5 + sta $FF8D + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + ora ($02,x) + .byte $04 + php + bpl L0256 + rti + + .byte $80 + .byte $80 + rti + + .byte $20 + .byte $10 + .byte $08 + .byte $04 + .byte $02 + .byte $01 + +;;; Pixel list for elevator control position +PLElevatorControlPositionIndicator: + .byte 6, 0,0, 1,0, 0,1, 1,1, 0,2, 1,2 + + brk + brk + brk + brk + brk + brk + brk + brk + brk +L0256: brk + brk + brk + brk + brk + brk + brk + +;;; Pixel list for aileron indicator and rudder indicator +PLAileronAndRudderIndicators: + .byte 9, 0,0, 1,0, 2,0, 3,0, 4,0, 1,1, 2,1, 3,1, 2,2 + +;;; Pixel list for slip/skid indicator +PLSlipSkidIndicator: + .byte 12, 1,0, 2,0, 0,1, 1,1, 2,1, 3,1, 0,2, 1,2, 2,2, 3,2, 1,3, 2,3 + +;;; Pixel list for fuel/oil gauges +PLFuelAndOilGauges: + .byte 12, 0,0, 0,1, 0,2, 0,3, 0,4, 0,5, 1,0, 1,1, 1,2, 1,3, 1,4, 1,5 + +IndicatorDialNeedleLastValue: + .byte 0, 0, 0, 0, 0 + +;;; Table used by `DrawIndicatorDialNeedle`, indexed by needle +IndicatorDialNeedleX: + .byte $82 + .byte $82 + .byte $1A + .byte $82 + .byte $B4 + +;;; Table used by `DrawIndicatorDialNeedle`, indexed by needle +IndicatorDialNeedleY: + .byte $7E + .byte $7E + .byte $7E + .byte $AA + .byte $AF + +;;; Table used by `DrawIndicatorDialNeedle`, indexed by needle +;;; 0 = skinny, 1 = thick +IndicatorDialNeedleStyle: + .byte 0, 1, 0, 1, 1 + +;;; Thin needle data - table of addrs indexed by needle pos +IndicatorDialThinNeedleTable: + .addr ND0314, ND031B, ND0322, ND032D, ND033A, ND0349, ND035C, ND0371 + .addr ND0388, ND03A1, ND03BC, ND0400, ND041F, ND0440, ND0463, ND0488 + .addr ND04AF, ND04D6, ND04FF, ND0528, ND0553, ND057E, ND05A9 + + .addr ND05D4 + +;;; Thick needle data - table of addrs indexed by needle pos +IndicatorDialThickNeedleTable: + .addr ND05D4, ND05DD, ND05E8, ND05F5, ND0604, ND0613, ND0624, ND0637 + .addr ND064C, ND0663, ND067C, ND0695, ND06B0, ND06CD, ND06EC, ND070B + .addr ND072C, ND074F, ND0772, ND0795, ND07BA, ND0E50, ND0E75 + +;;; Dial needle pixel lists +ND0314: .byte $01, $0a, $00, $15, $01, $0a, $ff +ND031B: .byte $01, $05, $00, $10, $01, $14, $ff +ND0322: .byte $01, $03, $00, $09, $01, $0c, $06, $0c, $10, $05, $ff +ND032D: .byte $01, $02, $00, $07, $01, $0a, $05, $0a, $0b, $08, $11, $04, $ff +ND033A: .byte $00, $03, $01, $05, $01, $08, $04, $08, $08, $07, $0d, $05, $11, $03, $ff +ND0349: .byte $00, $03, $01, $04, $01, $06, $03, $06, $06, $06, $09, $05, $0c, $04, $0f, $03, $12, $02, $ff +ND035C: .byte $00, $02, $01, $03, $01, $05, $03, $05, $05, $05, $08, $04, $0a, $04, $0c, $04, $0f, $03, $11, $02, $ff +ND0371: .byte $00, $02, $01, $03, $01, $05, $03, $04, $05, $04, $07, $04, $09, $04, $0b, $03, $0d, $03, $0f, $03, $11, $02, $ff +ND0388: .byte $00, $03, $00, $05, $00, $06, $01, $06, $03, $06, $05, $05, $07, $04, $09, $04, $0b, $03, $0d, $02, $0f, $02, $11, $01, $ff +ND03A1: .byte $00, $03, $00, $04, $00, $05, $01, $06, $03, $05, $04, $05, $06, $04, $08, $03, $09, $03, $0b, $03, $0c, $03, $0e, $02, $10, $01, $ff +ND03BC: .byte $00, $03, $00, $04, $00, $05, $01, $05, $03, $04, $04, $04, $05, $04, $07, $03, $08, $03, $09, $03, $0b, $02, $0c, $02, $0d, $02, $0f, $01, $ff + + +;;; Probably garbage until vectors + + jmp $B7B5 + + lda $9D0F + ldy $9D0E + rts + + lda $AAC2 + ldy $AAC1 + rts + + jmp LA851 ; ??? + + nop + nop + + .byte $4c ; JMP ??? + + .addr $ED98 ; BRK vector + .addr ResetInterruptHandler ; Reset vector + .byte $02 ; PWRUP byte + jmp L1F89 ; Applesoft & vector + jmp L1F89 ; Monitor Ctrl-Y vector + jmp ResetInterruptHandler ; NMI vector + .addr ResetInterruptHandler ; IRQ vector + + + ;; Text page 1 - and screen holes! + +;;; Dial needle pixel lists +ND0400: .byte $00, $03, $00, $04, $00, $05, $01, $05, $02, $04, $03, $04, $05, $03, $06, $03, $07, $03, $08, $03, $09, $03, $0a, $02, $0c, $01, $0d, $01, $0e, $01, $ff +ND041F: .byte $00, $03, $00, $04, $00, $04, $01, $04, $02, $04, $03, $04, $04, $03, $05, $03, $06, $03, $07, $03, $08, $02, $09, $02, $0a, $02, $0b, $02, $0c, $01, $0d, $01, $ff +ND0440: .byte $00, $03, $00, $04, $00, $04, $01, $04, $02, $04, $03, $03, $03, $04, $04, $03, $05, $03, $06, $03, $07, $02, $08, $02, $09, $02, $09, $02, $0a, $02, $0b, $01, $0c, $01, $ff +ND0463: .byte $00, $03, $00, $04, $00, $04, $01, $04, $01, $04, $02, $04, $03, $03, $04, $03, $04, $03, $05, $03, $06, $02, $07, $02, $07, $02, $08, $02, $09, $01, $0a, $01, $0a, $01, $0b, $01, $ff +ND0488: .byte $00, $01, $00, $03, $01, $02, $01, $03, $02, $02, $02, $03, $03, $02, $04, $02, $04, $02, $05, $02, $05, $02, $06, $02, $06, $02, $07, $02, $08, $01, $08, $02, $09, $01, $09, $02, $0a, $01, $ff +ND04AF: .byte $00, $01, $00, $03, $01, $02, $01, $03, $02, $02, $02, $03, $03, $02, $03, $02, $04, $02, $04, $02, $05, $02, $05, $02, $06, $02, $06, $02, $07, $01, $07, $02, $08, $01, $08, $02, $09, $01, $ff +ND04D6: .byte $00, $01, $00, $03, $00, $03, $01, $03, $01, $03, $02, $02, $02, $03, $03, $02, $03, $02, $04, $02, $04, $02, $04, $02, $05, $02, $05, $02, $06, $01, $06, $02, $07, $01, $07, $01, $08, $01, $08, $01, $ff +ND04FF: .byte $00, $01, $00, $03, $00, $03, $01, $02, $01, $03, $01, $03, $02, $02, $02, $02, $02, $03, $03, $02, $03, $02, $03, $02, $04, $01, $04, $02, $04, $02, $05, $01, $05, $01, $05, $02, $06, $01, $06, $01, $ff +ND0528: .byte $01, $01, $00, $03, $00, $03, $01, $02, $01, $02, $01, $03, $01, $03, $02, $02, $02, $02, $02, $02, $02, $02, $03, $02, $03, $02, $03, $02, $03, $02, $04, $01, $04, $01, $04, $02, $04, $02, $05, $01, $05, $01, $ff +ND0553: .byte $01, $01, $00, $03, $00, $03, $00, $03, $01, $02, $01, $02, $01, $03, $01, $03, $01, $03, $02, $02, $02, $02, $02, $02, $02, $02, $03, $01, $03, $01, $03, $01, $03, $02, $03, $02, $04, $01, $04, $01, $04, $01, $ff +ND057E: .byte $01, $01, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $02, $02, $01, $02, $01, $02, $01, $02, $01, $02, $01, $ff +ND05A9: .byte $01, $01, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $00, $03, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $01, $ff +ND05D4: .byte $07, $08, $00, $12, $04, $0d, $0a, $05, $ff +ND05DD: .byte $07, $07, $00, $10, $03, $0f, $07, $0a, $0b, $04, $ff +ND05E8: .byte $00, $01, $00, $0e, $02, $0e, $05, $0d, $09, $08, $0c, $03, $ff +ND05F5: .byte $00, $01, $00, $07, $02, $0c, $04, $0c, $06, $0c, $09, $07, $0b, $03, $ff +ND0604: .byte $00, $04, $01, $09, $03, $0b, $05, $0a, $07, $09, $09, $07, $0b, $06, $ff +ND0613: .byte $00, $03, $01, $06, $03, $08, $05, $09, $06, $09, $08, $08, $0a, $06, $0c, $05, $ff +ND0624: .byte $00, $02, $01, $04, $03, $06, $04, $08, $05, $09, $07, $08, $08, $08, $09, $07, $0b, $06, $ff +ND0637: .byte $00, $02, $01, $03, $02, $04, $04, $05, $05, $06, $06, $07, $07, $07, $08, $07, $0a, $05, $0b, $05, $ff +ND064C: .byte $00, $01, $01, $02, $02, $03, $03, $04, $04, $05, $05, $06, $07, $06, $08, $06, $09, $05, $0a, $05, $0c, $03, $ff +ND0663: .byte $00, $01, $01, $02, $02, $03, $03, $03, $04, $04, $05, $05, $05, $07, $06, $07, $07, $07, $08, $06, $09, $06, $0c, $03, $ff +ND067C: .byte $00, $01, $01, $02, $02, $02, $03, $03, $04, $04, $05, $04, $05, $06, $06, $06, $07, $06, $08, $05, $09, $05, $0b, $03, $ff +ND0695: .byte $00, $01, $01, $02, $01, $03, $02, $03, $03, $04, $04, $04, $04, $05, $05, $06, $06, $06, $07, $05, $07, $06, $08, $05, $0a, $03, $ff +ND06B0: .byte $00, $01, $01, $01, $01, $02, $02, $02, $03, $02, $03, $04, $04, $04, $04, $05, $05, $05, $06, $05, $06, $05, $07, $05, $08, $04, $0a, $02, $ff +ND06CD: .byte $00, $01, $01, $01, $01, $02, $02, $02, $02, $03, $03, $04, $04, $04, $04, $05, $05, $05, $05, $06, $06, $05, $06, $05, $07, $05, $08, $04, $0a, $02, $ff +ND06EC: .byte $00, $01, $01, $01, $01, $02, $02, $02, $02, $03, $03, $03, $03, $03, $04, $03, $04, $04, $05, $04, $05, $05, $06, $04, $06, $05, $07, $04, $09, $02, $ff +ND070B: .byte $00, $01, $00, $02, $01, $02, $01, $02, $02, $02, $02, $03, $03, $03, $03, $04, $03, $05, $04, $04, $04, $05, $05, $05, $05, $05, $06, $04, $07, $03, $09, $01, $ff +ND072C: .byte $00, $01, $00, $02, $01, $01, $01, $02, $01, $03, $02, $03, $02, $03, $02, $04, $02, $05, $03, $05, $03, $05, $03, $06, $04, $05, $04, $05, $05, $04, $06, $03, $08, $01, $ff +ND074F: .byte $00, $01, $00, $02, $00, $02, $01, $02, $01, $02, $01, $03, $01, $04, $02, $03, $02, $04, $02, $04, $02, $05, $03, $04, $03, $05, $03, $05, $04, $04, $05, $03, $07, $01, $ff +ND0772: .byte $00, $01, $00, $02, $00, $02, $00, $03, $01, $02, $01, $03, $01, $03, $01, $04, $01, $04, $01, $05, $02, $04, $02, $05, $02, $05, $02, $05, $03, $04, $04, $03, $06, $01, $ff +ND0795: .byte $01, $01, $01, $01, $01, $02, $01, $02, $01, $03, $01, $03, $01, $04, $02, $03, $02, $03, $02, $04, $02, $04, $02, $05, $02, $05, $02, $05, $03, $03, $03, $03, $04, $01, $04, $01, $ff +ND07BA: .byte $01, $01, $01, $01, $01, $02, $01, $02, $01, $02, $01, $03, $01, $03, $01, $03, $01, $03, $01, $04, $01, $04, $01, $04, $01, $05, $01, $05, $02, $03, $02, $03, $03, $01, $03, $01, $ff + + ror $6A6C + pla + ror $64 + adc ($5E,x) + .byte $5A + eor $50,x + .byte $4B + .byte $47 + .byte $43 + rol $3539,x + and ($2C),y + rol $22 + .byte $1F + .byte $1C + .byte $1A + ldy #$C6 + .byte $FF + brk + brk + .byte $FF + .byte $FF + brk + brk + + ;; $800 - $16 bytes patched in Color or B&W choice +ColorOrBWModePatch: + .res $16, 0 + + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + ora ($00,x) + brk + brk +RadarView: .byte 0 ; $01 if in Radar View mode + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + ora ($00,x) + brk + brk + brk + brk + ora ($00,x) + ora ($00,x) + ora ($00,x) + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + cmp $D5,x + rol a + rol a + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + .byte $FF + .byte $FF + .byte $FF + .byte $0C + sta $24 + brk + brk + brk + brk + brk + +EditModeFlag: .byte 0 + +ModeLibraryAction: + .byte 0 +;;; 1 = read +;;; 2 = save + + brk + brk + brk + brk + brk + +;;; Reboot vector (used for "Boot DOS" from Course Plotter) +RebootVector: .addr 0 +RebootFlag: .byte 1 + +LCBank1Flag: .byte 0 + + brk + .byte $9A + .byte $0D + +ShowSlewDigits: .byte 1 ; low bit is flag + + .byte 0 + brk + brk + brk + brk + brk + brk + brk + brk + ora ($00,x) + +WW1AceScore: .byte 0 + + brk + brk + brk + brk + brk + +Has64K: .byte 1 + + .byte 0 + .byte 0 + + ;; $8C9 + MESSAGE $9B, $68, "BOMBS: 5 " ; modified in chunk5 + WW1AceBombsStr := *-3 + brk + brk + + ;; $8D7 + MESSAGE $9B, $68, "SCORE:000" ; modified in chunk5 + WW1AceScoreStr := *-4 + + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + .byte $04 + brk + +Seconds: .byte 0 + +InputCounter: .byte 0 ; ??? + + ora ($00,x) + .byte $03 + brk + bpl L08F8 +L08F8: bpl L08FA +L08FA: brk + asl $DD,x + brk + brk + sed + .byte $03 + brk + brk + asl $DD,x + brk + brk + asl $04,x + brk + brk + +RunwaysTable: .byte 0, 0, 0 + +SeasonTempTable: .byte 0, 0, 0, 0 + + brk + brk + +InstrumentOperationalFlags: + .byte $FF + + .byte $FF + .byte $0F + .byte $FF + .byte $FF + .byte $0F + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk +DemoMode: .byte 0 + brk + brk + brk + +BaseTemp: .byte 0 + + .byte $0C, $50, $A7 + brk + +;;; ============================================================ +;;; Current Flight Parameters + +SoundMode: .byte 0 + +AutoCoordinationMode: + .byte 1 + +SlewMode: .byte $00 + +RealityMode: .byte 0 + + brk + +WW1AceMode: .byte $00 + +ATISPacing: .byte $C8 + +NorthPosition: .word 17188 +EastPosition: .word 16671 +Altitude: .word 0 + +;;; TODO: These are locations set by Edit Mode, but not otherwise +;;; directly referenced. They must be copied elsewhere afterwards. + +EMPitch: .word 0 + .refto EMPitch +EMBank: .word 0 + .refto EMBank +EMHeading: .word 0 + .refto EMHeading +EMAirspeed: .word 0 + .refto EMAirspeed +EMThrottle: .word 0 + .refto EMThrottle +EMRudder: .word 32767 + .refto EMRudder +EMAilerons: .word 32767 + .refto EMAilerons +EMFlaps: .word 0 + .refto EMFlaps +EMElevators: .word 32767 + .refto EMElevators + +Hours: .byte 8 +Minutes: .byte 0 + +Season: .byte 4 + + brk + brk + brk + brk + brk + brk + brk + brk + ;; Three altitude thresholds (16-bit, low/high) that split the + ;; sky into four wind bands: surface, Layer1, Layer2, Layer3. + ;; Compared against the high bytes of altitude ($5F/$60). +WindAlt1: .word 0 +WindAlt2: .word 0 +WindAlt3: .word 0 + + ;; Wind layer records: byte 0 = magnitude, byte 1 = scale/ + ;; turbulence, byte 2 = unused, byte 3 = direction angle. The + ;; surface band reuses `WindSpeed`/`WindDirection` as such a + ;; record (see `ComputeWindComponents` in chunk2). The records + ;; are stored highest-band-first: alt >= WindAlt3 picks Layer3, + ;; WindAlt2 <= alt < WindAlt3 picks Layer2, etc. +WindLayer3: .word 0 + .word 0 +WindLayer2: .word 0 + .word 0 +WindLayer1: .word 0 + .word 0 + +WindSpeed: .word 0 + +WindDirection: .word 0 + +ReliabilityFactor: .byte 100 + + brk + .byte $FF + .byte $FF + .byte $0F + +JoystickMode: .byte 0 + +ADFMode: .byte 0 + +;;; ============================================================ + + asl a + ora ($01,x) + brk + brk + .byte $FF + brk + .byte $FF + brk + .byte $FF + .byte $02 + brk + brk + .byte $FF + .byte $FF + .byte $FF + brk + brk + brk + brk + ora $00,y + brk +;;; Left-fuel-tank quantity (24-bit; $L0992 lo, $L0993 mid, $L0994 hi). +;;; Burned down by the fuel-consumption phase of UpdateEngineWithMagneto. +FuelTankLeft: + .refto FuelTankLeft + .refto L0994 +L0994: ora $00,y ; .byte $19, $00, $00 +;;; Right-fuel-tank quantity (24-bit, same layout). +FuelTankRight: + .refto FuelTankRight + .refto L0997 +L0997: ora $0100,y ; .byte $19, $00, $01 + .byte 1 +EngineOnFlag: .byte 1 +;;; Cylinder-head-temperature gauge value (= slow-slewed target of +;;; UEM_chtSlewTarget). +CylinderHeadTemp: + .refto CylinderHeadTemp + .refto L099C +L099C: brk +;;; Oil-temperature gauge value. +OilTemp: + .refto OilTemp + .refto L099D +L099D: brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + .byte $FF + .byte $7F + brk + brk + brk + brk + brk + brk + bit $6001 + ora #$30 + adc $00,x + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk +L09DA: dey + .byte $13 + brk + brk + brk + brk + brk + brk + .byte $FF + +OnGroundFlag: .byte $FF +;;; $FF when on ground, $00 otherwise + + brk + brk + brk + brk + brk + brk + brk + brk + bne L09F5 + bne L09F7 + brk + brk + cld + .byte $59 + brk +L09F5: brk + brk +L09F7: brk + brk + brk + brk + bpl L0A24 + brk + rti + + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + ;; Live airspeed in 16-bit form. The high byte is what gauges + ;; / RPM curve / stall envelope test against. Note that the + ;; visible RPM is derived from airspeed via `RPMCurveRun`, + ;; not stored separately. +Airspeed: + .refto Airspeed +L0A11: .word 0 + brk + brk + brk + brk + cpy $09 + brk + brk + brk + brk + brk + brk + brk + brk + .byte $54 + .byte $F2 + rts + +L0A24: beq L09DA + .byte $E2 + bmi L0A9E + brk + ora $1E + brk + tay + adc ($B8,x) + .byte $0B + ldy #$0F +L0A33: brk + brk + brk + ldy $00,x + brk + brk + brk + +IndicatorDialNeedlePos: .byte 0 +IndicatorDialNeedleIndex: .byte 0 + +;;; Inner-loop scratch slot used by DrawIndicatorDialNeedle. Counts +;;; the pixels remaining in the current pixel-list segment. +PixelListSegmentCount: + .refto PixelListSegmentCount + .refto L0A3D +L0A3D: brk + brk +PixelListXHi: .byte 0 + php + php + + ;; ----- "Last drawn" state cells for instrument indicators. + ;; Each Update*Indicator routine compares its target value + ;; with the corresponding cell, XOR-undraws the previous + ;; needle when they differ, then XOR-draws the new needle + ;; and stores the new value back here. + +LastDrawnElevatorPos: + .refto LastDrawnElevatorPos + .refto L0A42 +L0A42: brk +LastDrawnAileronPos: + .refto LastDrawnAileronPos + .refto L0A43 +L0A43: brk +LastDrawnSlipSkid: + .refto LastDrawnSlipSkid + .refto L0A44 +L0A44: brk +LastDrawnRudderPos: + .refto LastDrawnRudderPos + .refto L0A45 +L0A45: brk +LastDrawnThrottle: + .refto LastDrawnThrottle + .refto L0A46 +L0A46: brk +LastDrawnFlaps: + .refto LastDrawnFlaps + .refto L0A47 +L0A47: brk +LastDrawnTrim: + .refto LastDrawnTrim + .refto L0A48 +L0A48: brk +LastDrawnMixture: + .refto LastDrawnMixture + .refto L0A49 +L0A49: brk +LastDrawnFuelLeft: + .refto LastDrawnFuelLeft + .refto L0A4A +L0A4A: brk +LastDrawnFuelRight: + .refto LastDrawnFuelRight + .refto L0A4B +L0A4B: brk +LastDrawnOilTemp: + .refto LastDrawnOilTemp + .refto L0A4C +L0A4C: brk +LastDrawnOilPressure: + .refto LastDrawnOilPressure + .refto L0A4D +L0A4D: brk + ;; ----- DrawPixelList scratch cells (used as locals). +PixelListXLo: + .refto PixelListXLo + .refto L0A4E +L0A4E: brk +PixelListY: + .refto PixelListY + .refto L0A4F +L0A4F: brk +PixelListDataOffset: + .refto PixelListDataOffset + .refto L0A50 +L0A50: brk + brk + brk + +YokeHorizPos: .byte 0 + + .byte $64 + brk + brk + brk + brk + brk + brk + +YokeVertPos: .byte 0 + + brk + +ElevatorTrim: .byte 0 + + brk + brk + brk + +PanelLights: .byte $FF + +MagnetoState: .byte 3 +;;; 0 = off +;;; 1 = left +;;; 2 = right +;;; 3 = both +;;; 4 = start + + .byte 1 + .byte 0 + +RudderPos: .byte 0 + + brk + +SlewRollRate: .byte 0 + brk +SlewYawRate: .byte 0 + brk +SlewPitchRate: .byte 0 + brk +SlewAltRate: .byte 0 + brk + brk + +ViewDirection: .byte 0 +;;; $00 = front +;;; $02 = front right +;;; $04 = right +;;; $06 = back right +;;; $08 = back +;;; $0A = back left +;;; $0C = left +;;; $0E = front left +;;; $FF = down + + .byte $01 + +WarDeclared: + .byte $00 + + .byte $FF + +ZoomLevel: .faraddr $0003FF + + rts + + rti + + cpy #$E0 + .byte $F4 + ora ($41,x) + rti + + .byte $1F + .byte $F4 + ora ($FF,x) + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + rti + + .byte $F4 + ora ($40,x) + .byte $1F + eor ($F4,x) + ora ($C0,x) + cpx #$FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + rti + + rti + + .byte $1F + .byte $0C + .byte $FE + .byte $41 +L0A9E: cpy #$E0 + .byte $0C + inc $FFFF,x + .byte $FF + .byte $FF + .byte $FF + .byte $FF + rti + + .byte $0C + inc $E0C0,x + eor ($0C,x) + inc $1F40,x + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + +;;; ============================================================ + +;;; Twelve 64-byte vertex-storage columns used by chunk5's scenery +;;; interpreter and clipping/projection pipeline. Each is indexed by +;;; the per-record vertex counter `$B5` (primary) or `$B6` (secondary). +;;; +;;; Primary set (`$0AB8..$0BF7`): per-vertex (xLo, xHi, yLo, yHi, +;;; zLo, zHi). Populated by `EmitPrimaryVertex` from the scenery +;;; stream and by `ProcessVertex1`/`ProcessVertex2` when a vertex +;;; survives transform+classify. +;;; Secondary set (`$0C38..$0DB7`): same layout, used as the second +;;; buffer of clipped/transformed vertices. +;;; +;;; The first ~24 bytes of each column hold startup data that is +;;; overwritten before the scenery interpreter ever reads from these +;;; arrays. +;;; +;;; (Defined here so chunk5 can reference them; the L`xxxx` labels +;;; remain for backward-compat with existing cross-chunk callers.) + +PrimVertXLo := L0AB8 +PrimVertXHi := L0AF8 +PrimVertYLo := L0B38 +PrimVertYHi := L0B78 +PrimVertZLo := L0BB8 +PrimVertZHi := L0BF8 + +SecVertXLo := L0C38 +SecVertXHi := L0C78 +SecVertYLo := L0CB8 +SecVertYHi := L0CF8 +SecVertZLo := L0D38 +SecVertZHi := L0D78 + +L0AB8: + .byte $BE, $AB, $AE, $BA, $EA, $AA, $EE, $AA + .byte $AE, $EB, $AE, $AA, $AE, $EB, $BB, $AA + .byte $AE, $EB, $BA, $AA, $BE, $EA, $EA, $EE + .byte $BB, $BA, $BA, $FE, $AE, $EA, $AF, $FE + .byte $EF, $EA, $FA, $FB, $FA, $EE, $AE, $BF + .byte $FE, $FE, $EA, $FE, $AF, $FE, $AA, $EA + .byte $AA, $AE, $EE, $BE, $AE, $EA, $AA, $97 + .byte $97, $97, $97, $97, $97, $97, $97, $97 + +L0AF8: + .byte $97, $97, $97, $97, $97, $97, $97, $97 + .byte $AA, $D5, $DA, $EB, $97, $D7, $CB, $BF + .byte $F3, $AB, $AE, $B7, $96, $96, $96, $96 + .byte $AE, $9D, $EF, $BE, $AA, $AA, $BA, $AB + .byte $AA, $EF, $FF, $AA, $FF, $AA, $AA, $FF + .byte $AA, $FF, $FF, $AA, $FF, $AA, $AA, $FF + .byte $AA, $FF, $FF, $AA, $FF, $AA, $AA, $FF + .byte $AA, $FF, $FF, $BE, $FA, $BF, $EE, $EE + +L0B38: + .byte $FF, $AA, $BB, $BA, $AE, $AF, $FE, $AB + .byte $FE, $AB, $BA, $EA, $EA, $AB, $AA, $AF + .byte $EB, $FA, $FA, $FB, $EA, $FA, $BA, $AB + .byte $EA, $FB, $BA, $AB, $FF, $BE, $AA, $BE + .byte $BA, $EA, $AF, $AA, $AF, $AA, $AF, $FB + .byte $BA, $BE, $FA, $BF, $FB, $AA, $FA, $BA + .byte $AB, $FA, $BA, $AF, $AB, $BF, $FE, $FA + .byte $FE, $FF, $FE, $BA, $FA, $BA, $BE, $FB + +L0B78: + .byte $FE, $EE, $AB, $EE, $FB, $AB, $FA, $AA + .byte $EA, $BE, $FE, $AE, $FE, $AA, $EA, $BE + .byte $FE, $AF, $BE, $AB, $AB, $AF, $EA, $BB + .byte $AF, $EB, $BB, $AF, $AB, $AA, $AB, $BA + .byte $EA, $BA, $FF, $EA, $EA, $FA, $BA, $AA + .byte $BE, $BB, $BB, $AA, $BF, $BB, $BB, $AA + .byte $BB, $BB, $BB, $EE, $EA, $AB, $BB, $AE + .byte $EA, $BF, $AA, $BA, $EA, $AB, $AE, $AA + +L0BB8: + .byte $EE, $AB, $FB, $AE, $FE, $AA, $BA, $AA + .byte $EE, $FF, $BA, $AA, $EE, $FF, $BA, $AA + .byte $EE, $FF, $EE, $AA, $EE, $FA, $EA, $EA + .byte $AE, $AA, $FF, $AB, $AF, $AF, $FE, $BB + .byte $AF, $FA, $AE, $BF, $BA, $BB, $AB, $EA + .byte $BF, $AF, $EE, $AF, $FA, $EF, $AF, $BA + .byte $EF, $BA, $FA, $AB, $AE, $FA, $97, $97 + .byte $97, $97, $97, $97, $97, $97, $97, $97 + +L0BF8: + .byte $97, $97, $97, $97, $97, $97, $97, $97 + .byte $AA, $AA, $EB, $AF, $9B, $97, $9F, $E6 + .byte $FA, $B3, $9A, $F3, $96, $96, $96, $9B + .byte $96, $9D, $AE, $FA, $AE, $AF, $AF, $AF + .byte $AA, $AA, $FF, $AA, $BF, $FF, $AA, $FF + .byte $AA, $AA, $FF, $AA, $FF, $FF, $AA, $FF + .byte $AA, $AA, $FF, $AA, $FF, $AA, $AA, $FF + .byte $AA, $AA, $FF, $EF, $BE, $EF, $FE, $FE + +L0C38: + .byte $FA, $AF, $AA, $BA, $EE, $AE, $BF, $BA + .byte $FB, $AF, $EA, $AA, $EA, $EA, $AA, $FE + .byte $BA, $FB, $FA, $BA, $EA, $FA, $AF, $EA + .byte $AB, $BA, $AA, $AF, $FF, $FE, $AE, $AF + .byte $EA, $EE, $BE, $AA, $BE, $AF, $BF, $FB + .byte $AA, $BA, $AA, $FA, $EE, $EA, $AF, $BB + .byte $AA, $AA, $AA, $AE, $AA, $FE, $AB, $AB + .byte $AA, $FA, $AA, $EA, $AF, $BF, $EB, $BA + +L0C78: + .byte $AB, $AA, $AB, $AF, $BE, $FF, $AF, $FB + .byte $BA, $AF, $AB, $FA, $AB, $EA, $FA, $EA + .byte $EA, $FE, $FF, $AA, $FE, $FB, $BA, $FE + .byte $BE, $BB, $FA, $FE, $FE, $AF, $FE, $FB + .byte $AB, $FE, $FF, $FF, $FA, $BB, $FA, $BA + .byte $AA, $AA, $AB, $BA, $AF, $AA, $AB, $BA + .byte $BE, $AA, $AB, $EA, $EA, $FE, $AB, $EA + .byte $EA, $AA, $AA, $FE, $FA, $AA, $EA, $FA + +L0CB8: + .byte $EB, $BA, $BF, $FE, $BF, $FE, $AA, $BB + .byte $BB, $BA, $AA, $BF, $BB, $BA, $AA, $BA + .byte $BB, $BA, $EA, $BF, $EA, $FB, $FA, $AA + .byte $EB, $EA, $FB, $AA, $FA, $FB, $BA, $FE + .byte $FA, $AB, $AA, $EA, $AE, $AB, $FF, $BF + .byte $BB, $AF, $AF, $AF, $AF, $AF, $BE, $AB + .byte $FE, $BA, $AF, $AB, $AA, $AB, $97, $97 + .byte $97, $97, $97, $97, $97, $97, $97, $97 + +L0CF8: + .byte $97, $97, $97, $97, $97, $97, $97, $97 + .byte $AA, $AD, $AE, $9E, $97, $9A, $AC, $D7 + .byte $E9, $B2, $AE, $FA, $96, $96, $96, $9D + .byte $96, $9D, $AE, $BA, $AF, $BE, $EA, $BF + .byte $FF, $AA, $FF, $AA, $AA, $FF, $AA, $FF + .byte $FF, $AA, $FF, $AA, $AA, $FF, $AA, $FF + .byte $FF, $AA, $FF, $AA, $AA, $AA, $AA, $FF + .byte $AA, $AA, $FF, $BF, $FA, $EB, $EE, $EF + +L0D38: + .byte $FF, $BE, $AF, $AA, $EF, $BA, $EF, $AA + .byte $AF, $BE, $FB, $BA, $EE, $EA, $EA, $AB + .byte $EA, $FB, $AA, $AF, $AA, $EB, $FB, $FA + .byte $EA, $AA, $AE, $BF, $BA, $BF, $AF, $AA + .byte $FE, $AF, $FF, $AA, $FE, $BE, $BA, $EE + .byte $FB, $EA, $FB, $AB, $FA, $FA, $FB, $AE + .byte $EA, $AE, $FE, $AB, $AF, $AB, $BF, $AA + .byte $EA, $FA, $AE, $FA, $FB, $AA, $AB, $AA + +L0D78: + .byte $AA, $AB, $AF, $BF, $FA, $AA, $AE, $EE + .byte $EA, $EA, $FE, $BB, $AA, $FA, $FF, $AF + .byte $EA, $AB, $BB, $AA, $BF, $BF, $AE, $AB + .byte $EA, $BE, $BB, $BA, $BF, $EE, $BF, $FB + .byte $FF, $BF, $EE, $EA, $FE, $AB, $BB, $FE + .byte $BA, $AA, $FE, $FA, $BA, $AA, $FE, $FA + .byte $BA, $AA, $FE, $EA, $AF, $AB, $FE, $EA + .byte $AE, $FF, $EA, $AB, $FB, $AA, $BA, $BA + +;;; ============================================================ + +;;; 0DB8: + .byte $00, $37, $50, $41, $32, $2A, $1E, $1B + .byte $19, $19, $64, $32, $16, $12, $0F, $0F + .byte $0F, $0F, $0F, $0F, $78, $78, $3C, $27 + .byte $0F, $18, $15, $12, $0F, $0F, $00, $78 + .byte $46, $2D, $23, $19, $14, $12, $11, $11 + .byte $06, $F8, $FF, $FD, $06, $F9, $FF, $FD + .byte $06, $FA, $FF, $FD, $06, $FB, $FF, $FD + .byte $07, $FC, $00, $FC, $07, $FD, $00, $FC + +;;; 0DF8: + .byte $07, $FE, $00, $FC, $07, $FF, $00, $FC + .byte $07, $00, $00, $FC, $07, $01, $00, $FC + .byte $07, $02, $00, $FC, $07, $03, $00, $FC + .byte $07, $04, $00, $FC, $07, $05, $01, $FD + .byte $06, $06, $01, $FD, $06, $07, $01, $FD + .byte $FF, $AA, $FF, $FF, $AA, $FF, $AA, $AA + .byte $FF, $AA, $FF, $BA, $AA, $FF, $AA, $AA + .byte $AA, $AA, $FF, $FB, $BF, $EE, $FF, $FF + +;;; 0E38: + + .byte $FB + ldx $AEBF + tax + inc $ABAA,x + ldx $AABA,y + .byte $FB + tsx + inc $AFAF,x + .byte $FA + inc $FBBB,x + tsx + ldx $FEBE,y + +;;; ============================================================ + +;;; Dial needle pixel lists +ND0E50: .byte $01, $01, $01, $01, $01, $01, $01, $02, $01, $02, $01, $02, $01, $02, $00, $04, $00, $04, $00, $04, $00, $04, $00, $05, $00, $05, $00, $05, $01, $03, $01, $03, $02, $01, $02, $01, $ff +ND0E75: .byte $01, $01, $01, $01, $01, $01, $01, $01, $01, $02, $01, $02, $01, $02, $00, $03, $00, $03, $00, $03, $00, $04, $00, $04, $00, $04, $00, $04, $00, $03, $01, $02, $01, $01, $01, $01, $ff + +;;; HBASH table + +HiresTableHi: + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $20,$24,$28,$2c,$30,$34,$38,$3c + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $21,$25,$29,$2d,$31,$35,$39,$3d + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $22,$26,$2a,$2e,$32,$36,$3a,$3e + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + .byte $23,$27,$2b,$2f,$33,$37,$3b,$3f + + +;;; HBASHL table + +HiresTableLo: + .byte $00,$00,$00,$00,$00,$00,$00,$00 + .byte $80,$80,$80,$80,$80,$80,$80,$80 + .byte $00,$00,$00,$00,$00,$00,$00,$00 + .byte $80,$80,$80,$80,$80,$80,$80,$80 + .byte $00,$00,$00,$00,$00,$00,$00,$00 + .byte $80,$80,$80,$80,$80,$80,$80,$80 + .byte $00,$00,$00,$00,$00,$00,$00,$00 + .byte $80,$80,$80,$80,$80,$80,$80,$80 + .byte $28,$28,$28,$28,$28,$28,$28,$28 + .byte $a8,$a8,$a8,$a8,$a8,$a8,$a8,$a8 + .byte $28,$28,$28,$28,$28,$28,$28,$28 + .byte $a8,$a8,$a8,$a8,$a8,$a8,$a8,$a8 + .byte $28,$28,$28,$28,$28,$28,$28,$28 + .byte $a8,$a8,$a8,$a8,$a8,$a8,$a8,$a8 + .byte $28,$28,$28,$28,$28,$28,$28,$28 + .byte $a8,$a8,$a8,$a8,$a8,$a8,$a8,$a8 + .byte $50,$50,$50,$50,$50,$50,$50,$50 + .byte $d0,$d0,$d0,$d0,$d0,$d0,$d0,$d0 + .byte $50,$50,$50,$50,$50,$50,$50,$50 + .byte $d0,$d0,$d0,$d0,$d0,$d0,$d0,$d0 + .byte $50,$50,$50,$50,$50,$50,$50,$50 + .byte $d0,$d0,$d0,$d0,$d0,$d0,$d0,$d0 + .byte $50,$50,$50,$50,$50,$50,$50,$50 + .byte $d0,$d0,$d0,$d0,$d0,$d0,$d0,$d0 + +HiresPixelToByteTable: + .byte $00, $00, $00, $00, $00, $00, $00 + .byte $01, $01, $01, $01, $01, $01, $01 + .byte $02, $02, $02, $02, $02, $02, $02 + .byte $03, $03, $03, $03, $03, $03, $03 + .byte $04, $04, $04, $04, $04, $04, $04 + .byte $05, $05, $05, $05, $05, $05, $05 + .byte $06, $06, $06, $06, $06, $06, $06 + .byte $07, $07, $07, $07, $07, $07, $07 + .byte $08, $08, $08, $08, $08, $08, $08 + .byte $09, $09, $09, $09, $09, $09, $09 + .byte $0A, $0A, $0A, $0A, $0A, $0A, $0A + .byte $0B, $0B, $0B, $0B, $0B, $0B, $0B + .byte $0C, $0C, $0C, $0C, $0C, $0C, $0C + .byte $0D, $0D, $0D, $0D, $0D, $0D, $0D + .byte $0E, $0E, $0E, $0E, $0E, $0E, $0E + .byte $0F, $0F, $0F, $0F, $0F, $0F, $0F + .byte $10, $10, $10, $10, $10, $10, $10 + .byte $11, $11, $11, $11, $11, $11, $11 + .byte $12, $12, $12, $12, $12, $12, $12 + .byte $13, $13, $13, $13, $13, $13, $13 + .byte $14, $14, $14, $14, $14, $14, $14 + .byte $15, $15, $15, $15, $15, $15, $15 + .byte $16, $16, $16, $16, $16, $16, $16 + .byte $17, $17, $17, $17, $17, $17, $17 + .byte $18, $18, $18, $18, $18, $18, $18 + .byte $19, $19, $19, $19, $19, $19, $19 + .byte $1A, $1A, $1A, $1A, $1A, $1A, $1A + .byte $1B, $1B, $1B, $1B, $1B, $1B, $1B + .byte $1C, $1C, $1C, $1C, $1C, $1C, $1C + .byte $1D, $1D, $1D, $1D, $1D, $1D, $1D + .byte $1E, $1E, $1E, $1E, $1E, $1E, $1E + .byte $1F, $1F, $1F, $1F, $1F, $1F, $1F + .byte $20, $20, $20, $20, $20, $20, $20 + .byte $21, $21, $21, $21, $21, $21, $21 + .byte $22, $22, $22, $22, $22, $22, $22 + .byte $23, $23, $23, $23, $23, $23, $23 + .byte $24, $24, $24, $24, $24, $24, $24 + .byte $25, $25, $25, $25, $25, $25, $25 + .byte $26, $26, $26, $26, $26, $26, $26 + .byte $27, $27, $27, $27, $27, $27, $27 + +HiresPixelToBitMaskTable: + .repeat 38 + .byte $01, $02, $04, $08, $10, $20, $40 + .endrepeat + +ColorPixelToByteTable: + .byte $00, $00, $00, $00 + .byte $01, $01, $01 + .byte $02, $02, $02, $02 + .byte $03, $03, $03 + .byte $04, $04, $04, $04 + .byte $05, $05, $05 + .byte $06, $06, $06, $06 + .byte $07, $07, $07 + .byte $08, $08, $08, $08 + .byte $09, $09, $09 + .byte $0A, $0A, $0A, $0A + .byte $0B, $0B, $0B + .byte $0C, $0C, $0C, $0C + .byte $0D, $0D, $0D + .byte $0E, $0E, $0E, $0E + .byte $0F, $0F, $0F + .byte $10, $10, $10, $10 + .byte $11, $11, $11 + .byte $12, $12, $12, $12 + .byte $13, $13, $13 + .byte $14, $14, $14, $14 + .byte $15, $15, $15 + .byte $16, $16, $16, $16 + .byte $17, $17, $17 + .byte $18, $18, $18, $18 + .byte $19, $19, $19 + .byte $1A, $1A, $1A, $1A + .byte $1B, $1B, $1B + .byte $1C, $1C, $1C, $1C + .byte $1D, $1D, $1D + .byte $1E, $1E, $1E, $1E + .byte $1F, $1F, $1F + .byte $20, $20, $20, $20 + .byte $21, $21, $21 + .byte $22, $22, $22, $22 + .byte $23, $23, $23 + .byte $24, $24, $24, $24 + .byte $25, $25, $25 + .byte $26, $26, $26, $26 + .byte $27, $27, $27 + .assert * - ColorPixelToByteTable = 140, error, "size" + +AltColorPixelToByteTable: + .byte $00, $00, $00 + .byte $01, $01, $01, $01 + .byte $02, $02, $02 + .byte $03, $03, $03, $03 + .byte $04, $04, $04 + .byte $05, $05, $05, $05 + .byte $06, $06, $06 + .byte $07, $07, $07, $07 + .byte $08, $08, $08 + .byte $09, $09, $09, $09 + .byte $0A, $0A, $0A + .byte $0B, $0B, $0B, $0B + .byte $0C, $0C, $0C + .byte $0D, $0D, $0D, $0D + .byte $0E, $0E, $0E + .byte $0F, $0F, $0F, $0F + .byte $10, $10, $10 + .byte $11, $11, $11, $11 + .byte $12, $12, $12 + .byte $13, $13, $13, $13 + .byte $14, $14, $14 + .byte $15, $15, $15, $15 + .byte $16, $16, $16 + .byte $17, $17, $17, $17 + .byte $18, $18, $18 + .byte $19, $19, $19, $19 + .byte $1A, $1A, $1A + .byte $1B, $1B, $1B, $1B + .byte $1C, $1C, $1C + .byte $1D, $1D, $1D, $1D + .byte $1E, $1E, $1E + .byte $1F, $1F, $1F, $1F + .byte $20, $20, $20 + .byte $21, $21, $21, $21 + .byte $22, $22, $22 + .byte $23, $23, $23, $23 + .byte $24, $24, $24 + .byte $25, $25, $25, $25 + .byte $26, $26, $26 + .byte $27, $27, $27, $27 + .assert * - AltColorPixelToByteTable = 140, error, "size" + +PixelToBitNumberTable: + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + .byte 0, 1, 2, 3, 4, 5, 6 + +;;; Bit-permutation table. Bit-positions 0..7 ordered as +;;; (0,2,4,6,1,3,5,7) -- the same shuffled order Apple II hires +;;; horizontal-pixel masks lay down to expose left/right halves +;;; of each color group. Used by message-rendering routines. +BitPermutationTable: + .refto BitPermutationTable + .refto L13EE +L13EE: .byte 0, 2, 4, 6, 1, 3, 5, 7 + +;;; Pixel and/or masks + +OrMaskTable1: + .byte $01, $04, $10, $40 + +OrMaskTable2: + .byte $02, $08, $20, $01, $04, $10, $40, $02, $08, $20 + +AndMaskTable1: + .byte $FE, $FB, $EF, $BF + +AndMaskTable2: + .byte $FD, $F7, $DF, $FE, $FB, $EF, $BF, $FD, $F7, $DF + + ;; Hires color table: 4 colour-encoding bytes for each colour + ;; group (Black, Magenta/Green base, Orange/Blue base, White). + ;; The two tables differ in the byte ordering used by + ;; `MapColorAndPrepRowRoutine` to set up the line/span draw + ;; opcodes for either even or odd column phases. +HiresColorTableEven: + .refto HiresColorTableEven + .refto L1412 +L1412: .byte $00, $2A, $55, $7F + .refto L1412 +HiresColorTableOdd: + .refto HiresColorTableOdd + .refto L1416 +L1416: .byte $00, $55, $2A, $7F + .refto L1416 + + ;; 16-bit cosine table: 64 entries of `cos(a) * $7FFF` for + ;; `a = 0, 1, ..., 63 / 256 of a turn`. Used by `L17BC` to + ;; resolve sine/cosine of byte-encoded angles via two-point + ;; linear interpolation. + ;; L141A,Y / L141B,Y -> cos(Y/2) low/high + ;; L141C,Y / L141D,Y -> cos((Y/2)+1) low/high +CosTable16: + .refto CosTable16 +L141A: .byte $FF +L141B: .byte $7F +L141C: .byte $F5 +L141D: .byte $7F + .byte $D7 + .byte $7F + ldx $7F + adc ($7F,x) + ora #$7F + .byte $9C + ror $7E1C,x + .byte $89 + adc $7CE3,x + and #$7C + .byte $5C + .byte $7B + .byte $7C + .byte $7A + .byte $89 + adc $7883,y + .byte $6B + .byte $77 + rti + + ror $03,x + adc $B5,x + .byte $73 + .byte $54 + .byte $72 + sbc ($70,x) + lsr $C96F,x + adc $6C23 + jmp ($796A) + + .byte $67 + dec $E766 + .byte $64 + sbc ($62),y + .byte $EB + rts + + dec $5E,x + .byte $B3 + .byte $5C + sta ($5A,x) + .byte $42 + cli + .byte $F4 + eor $9A,x + .byte $53 + .byte $33 + eor ($BF),y + lsr $4C3F + .byte $B3 + eor #$1C + .byte $47 + .byte $7A + .byte $44 + cmp $1641 + .byte $3F + lsr $3C,x + sty $B939 + rol $DE,x + .byte $33 + .byte $FB + bmi L148D + rol $2B1F + rol $28 + .byte $27 + and $23 + .byte $22 + ora $0B1F,y + .byte $1C + sbc $E118,y +L148D: ora $C7,x + .byte $12 + .byte $AB + .byte $0F + sty $6A0C + ora #$47 + asl $24 + .byte $03 + brk + brk + .byte $DC + .byte $FC + + ;; Edge masks used by `TidySkyGroundEdgeInRow` to whiten one + ;; or two pixels around the sky/ground transition column, + ;; keeping the colour-clash artefacts off the horizon line. +L149E: ; left-edge mask, indexed by colour pixel + .byte $3F, $7C, $70, $40, $7E, $78, $60 + +L14A5: ; right-edge mask, indexed by colour pixel + .byte $00, $01, $07, $1F, $00, $03, $0F + +;;; Pixel list for throttle control indicator +PLThrottleIndicator: + .byte 4, 0,0, 1,0, 0,1, 1,1 + +;;; Pixel list for flaps/trim/mixture indicator +PLFlapsTrimMixtureIndicator: + .byte 4, 0,0, 2,0, 0,1, 2,1 + +;;; Map 3 character bits to pixels (doubled) + +CharBitsToPixelsTable: + .byte %000000 + .byte %000011 + .byte %001100 + .byte %001111 + .byte %110000 + .byte %110011 + .byte %111100 + .byte %111111 + +LeftMaskTable: + .byte %10000000 + .byte %10000001 + .byte %10000011 + .byte %10000111 + .byte %10001111 + .byte %10011111 + .byte %10111111 + +RightMaskTable: + .byte %11111110 + .byte %11111100 + .byte %11111000 + .byte %11110000 + .byte %11100000 + .byte %11000000 + .byte %10000000 + +;;; Character Bitmaps +;;; 16 bits encode 5x3 pixels; the bits are shifted out to the right +;;; 3 at a time. Covers ASCII code point $20 (' ') through $5A ('Z') + +CharBitmapTable: + .word $0000, $2092, $002d, $5f7d, $2492, $588d, $0000, $0012 ; !"#$%&' + .word $2922, $4494, $55d5, $25d2, $2400, $01c0, $2000, $4889 ; ()*+,-./ + .word $7b6f, $1249, $79cf, $73cf, $13ed, $73e7, $7be4, $124f ; 01234567 + .word $7bef, $13ef, $0410, $2410, $24c0, $0e38, $0000, $246a ; 89:;<=>? + .word $0000, $5f6a, $6bae, $3923, $6b6e, $79e7, $49e7, $7b27 ; @ABCDEFG + .word $5bed, $2492, $7249, $5bad, $7924, $5b7d, $57f5, $7b6f ; HIJKLMNO + .word $49ef, $1f6f, $5def, $73e7, $2497, $7b6d, $256d, $5f6d ; PQRSTUVW + .word $5aad, $24ad, $788f ; XYZ + +;;; NOTE: Some characters are repurposed for box drawing (e.g. ADF) +;;; < is ┌ +;;; $ is │ + +;;; Inputs are 16-bit signed numbers; scales X by (Y / $7FFF) +;;; Input: ZP locations specified by X, Y +;;; Output: ZP location specified by A +;;; Uses $C2-C6, $A5, $A7 +.proc ZPScale + ;; $154A + + ;; $C2-C3 = $00,X + ;; $C4-C5 = $00,Y + ;; $00,A = (result of jsr ScaleC2ByC4) + sta $A5 + lda $00,x + sta $C2 + lda $01,x + sta $C3 + lda $00,y + sta $C4 + lda $01,y + sta $C5 + jsr ScaleC2ByC4 + ldy $A5 + sta $00,y + stx $01,y + rts + +ScaleC2ByC4: + lda $C2 ; If $C2-C3 is zero... + ora $C3 + beq L1575 + lda $C4 ; Or $C4-C5 is zero... + ora $C5 + bne L1577 +;;; Either operand is zero -- return zero. Used as the tail RTS. +ScaleC2ByC4_ReturnZero: + .refto ScaleC2ByC4_ReturnZero + .refto L1575 +L1575: tax ; just return 0 in X + rts + +;;; Both operands non-zero. Determine result sign by XOR-ing the high +;;; bytes of $C2/$C3 and $C4/$C5; that final sign goes in Y. Then +;;; convert $C2/$C3 (multiplier) and $C4/$C5 (multiplicand) to their +;;; absolute values to drive an unsigned multiply. +ScaleC2ByC4_BothNonZero: + .refto ScaleC2ByC4_BothNonZero + .refto L1577 +L1577: lda $C3 + eor $C5 + tay + lda $C3 + bmi L158E + lda #$FF + eor $C2 + sta $C2 + lda #$FF + eor $C3 + sta $C3 + bmi L159C +;;; $C2/$C3 is positive: ones-complement style negation handled above +;;; if it was actually positive; here we just decrement to convert +;;; from sign-magnitude to two's-complement (-1 bias). +ScaleC2ByC4_AbsC2: + .refto ScaleC2ByC4_AbsC2 + .refto L158E +L158E: lda $C2 + bne L159A + dec $C3 + bmi L159A + inc $C3 + bmi L159C +ScaleC2ByC4_DecC2: + .refto ScaleC2ByC4_DecC2 + .refto L159A +L159A: dec $C2 +;;; Now convert $C4/$C5 (multiplicand) to absolute value. If negative, +;;; SUB16 from $0000 (= negate). Then start the unrolled multiply. +ScaleC2ByC4_AbsC4: + .refto ScaleC2ByC4_AbsC4 + .refto L159C +L159C: lda $C5 + bpl :+ + SUB16C #$00, $C4, $C4 + bpl :+ + dec $C4 + dec $C5 +: + lsr $C2 + bcc :+ + lda #$00 +: + lsr a + ror $C2 + bcs :+ + adc $C5 +: + lsr a + ror $C2 + bcs :+ + adc $C5 +: + lsr a + ror $C2 + bcs :+ + adc $C5 +: + lsr a + ror $C2 + bcs :+ + adc $C5 +: + lsr a + ror $C2 + bcs :+ + adc $C5 +: + lsr a + ldx #$00 + stx $A7 + lsr $C2 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C2 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + lsr $C3 + bcs :+ + tax + lda $A7 + adc $C4 + sta $A7 + txa + adc $C5 +: + lsr a + ror $A7 + cpy #$00 + bpl :+ + sta $A8 + lda #$00 + sec + sbc $A7 + sta $A7 + lda #$00 + sbc $A8 +: + tax + lda $A7 + rts +.endproc +ScaleC2ByC4 := ZPScale::ScaleC2ByC4 + +;;; ============================================================ + +.proc ScaleC2ByAX + sta $C4 + stx $C5 + jmp ZPScale::ScaleC2ByC4 +.endproc + +.proc ScaleC2ByAXIntoC2 + sta $C4 + stx $C5 + jsr ZPScale::ScaleC2ByC4 + sta $C2 + stx $C3 + rts +.endproc + +;;; ============================================================ + +;;; Signed 16-bit divide: AX = val1 / val2 (`val1 = $C2/$C3`, +;;; `val2 = $C4/$C5`). Sign of the result reflects the XOR of the +;;; two input signs. Caller passes the dividend in A (high) and X +;;; (low); divisor must already be in val2. +DivideSigned16: + .refto DivideSigned16 +.proc L16A2 + val1 := $C2 + val2 := $C4 + + stx val1 + sta val1+1 + eor val2+1 + pha ; stash sign-mismatch for later + + ;; If `val1` is negative, invert it + lda val1+1 + bpl :+ + SUB16C #$00, val1, val1 +: + ;; If `val2` is negative, invert it + lda val2+1 + bpl :+ + SUB16C #$00, val2, val2 +: + ;; Now both `val1` and `val2` are positive... + ldx val1 + stx $A7 + lda val1+1 + ldy #$0F ; number of bits +;;; DivideSigned16 main bit loop. 15 iterations of shift-subtract: +;;; for each bit, trial-subtract divisor from running remainder. +;;; If no borrow: bit=1, stay positive (L16E0). +;;; If borrow: bit=0, switch to negative (L16EC) which trial-adds +;;; on subsequent iterations until the remainder goes positive again. +DivideSigned16_PosLoop: + .refto DivideSigned16_PosLoop + .refto L16D3 +L16D3: tax + lda $A7 + sec + sbc val2 + sta $A7 + txa + sbc val2+1 + bmi L16EC +DivideSigned16_PosBitOne: + .refto DivideSigned16_PosBitOne + .refto L16E0 +L16E0: rol val1 + rol val1+1 + asl $A7 + rol a + dey + bne L16D3 + beq L1706 +DivideSigned16_NegLoop: + .refto DivideSigned16_NegLoop + .refto L16EC +L16EC: asl val1 + rol val1+1 + asl $A7 + rol a + dey + beq L1706 + tax + lda $A7 + clc + adc val2 + sta $A7 + txa + adc val2+1 + bmi L16EC + sec + bcs L16E0 +;;; Done with all 15 bits. Final shift-up + sign correction. +DivideSigned16_FinalShift: + .refto DivideSigned16_FinalShift + .refto L1706 +L1706: asl val1 + rol val1+1 + bpl L1710 + dec val1 + dec val1+1 +DivideSigned16_PostShift: + .refto DivideSigned16_PostShift + .refto L1710 +L1710: + ;; Invert `val1` if signs mismatched + pla + bpl :+ + SUB16C #$00, val1, val1 +: + ;; Add 35 to $32-$33 ??? + lda $32 + clc + adc #$23 + sta $32 + bcc :+ + inc $33 +: + rts +.endproc + +;;; Divide A,X by 2, signed +;;; Trashes Y +.proc AXDiv2 + tay + txa + cmp #$80 + ror a + tax + tya + ror a + rts +.endproc + +;;; 16-bit unsigned multiply, A,X by $C2-C3 +;;; Inputs: A,X, $C2-C3 +;;; Output: A,X +;;; Uses $C4-C9 +.proc MultiplyAXByC2 + sta $C4 + stx $C5 + lda #$00 + sta $C8 + sta $C9 + ldx #$10 ; bits +;;; MultiplyAXByC2 main loop. 16 iterations of standard shift-add +;;; multiplication: shift multiplier ($C2/$C3) right; if bit was 1 +;;; add multiplicand ($C4/$C5) to accumulator ($C8/$C9). Then shift +;;; accumulator right. +MultiplyAXByC2_Loop: + .refto MultiplyAXByC2_Loop + .refto L1741 +L1741: lsr $C3 + ror $C2 + bcc L1752 + lda $C8 + clc + adc $C4 + sta $C8 + lda $C9 + adc $C5 +MultiplyAXByC2_NoAdd: + .refto MultiplyAXByC2_NoAdd + .refto L1752 +L1752: ror a + sta $C9 + ror $C8 + ror $C7 + ror $C6 + dex + bne L1741 + lda $C7 + ldx $C8 + rts +.endproc + +;;; sin and cos of a byte-encoded angle (256 = 360 deg). Both return +;;; their 16-bit signed result in AX. +;;; +;;; L1763: A = sin(byteAngle) +;;; L1768: A = sin(byteAngle - 0x40) = -cos(byteAngle) +L1763: +SinByteAngle: + .refto SinByteAngle + ldx #$00 + jmp L177B + +L1768: +SinShiftedByteAngle: + .refto SinShiftedByteAngle + ldx #$00 + jmp L1778 + + and #$80 + tax + tya + jmp L177B + + and #$80 + tax + tya +;;; Sub $40 from angle (= shift by -90 deg) before computing sin = +;;; -cos. Tail-falls into the unsigned-angle entry below. +SinByteAngle_ShiftedEntry: + .refto SinByteAngle_ShiftedEntry + .refto L1778 +L1778: sec + sbc #$40 +;;; Compute sin of byte-angle in A, sign-extension in X (= $00 for +;;; positive, $FF for negative). Quadrant 4 (= angle bit 7 set, i.e. +;;; >= 128) flips to negative quadrants. +SinByteAngle_AngleEntry: + .refto SinByteAngle_AngleEntry + .refto L177B +L177B: ora #$00 + bmi L17A5 +;;; Quadrants 0-1 (= angle in [0, 127]). Reflect quadrant 1 into +;;; quadrant 0 (= angle [64, 127] becomes [0, 63] reversed). +SinByteAngle_Q01: + .refto SinByteAngle_Q01 + .refto L177F +L177F: cmp #$40 + bmi L17BC + sta $BC + stx $BD + lda #$00 + sec + sbc $BD + tax + lda #$80 + sbc $BC + jsr L17BC + sta $BC + stx $BD + lda #$00 + sec + sbc $BC + tay + lda #$00 + sbc $BD + tax + tya + rts + +;;; Quadrants 2-3 (= angle bit 7 set, [128, 255]). Negate angle (= +;;; reflect into [0, 127] = quadrants 0-1) and recurse with sign-flip. +SinByteAngle_Q23: + .refto SinByteAngle_Q23 + .refto L17A5 +L17A5: eor #$FF + tay + txa + eor #$FF + tax + tya + inx + bne L177F + clc + adc #$01 + bpl L177F + lda #$7F + ldx #$FF + jmp L177F + +;;; Look up sin in the 64-entry sin table at L141A. Three branches: +;;; X==0 (= unsigned <64): direct table lookup +;;; X has high bit set: interpolate between current and next entry +;;; otherwise: average two adjacent table entries +SinByteAngle_TableLookup: + .refto SinByteAngle_TableLookup + .refto L17BC +L17BC: asl a + tay + txa + beq L17DA + and #$7F + bne L17E1 + lda L141A,y + clc + adc L141C,y + sta $BC + lda L141B,y + adc L141D,y + lsr a + tax + lda $BC + ror a + rts + +;;; Direct table lookup (= no interpolation needed, fractional bits = 0). +SinByteAngle_DirectLookup: + .refto SinByteAngle_DirectLookup + .refto L17DA +L17DA: lda L141A,y + ldx L141B,y + rts + +;;; Linear interpolation between L141A,y and L141C,y (= adjacent +;;; sin-table entries) using X >> 1 as the fractional weight. +SinByteAngle_Interpolate: + .refto SinByteAngle_Interpolate + .refto L17E1 +L17E1: lda L141A,y + sta $BC + lda L141B,y + sta $BD + lda L141C,y + sec + sbc $BC + sta $C2 + lda L141D,y + sbc $BD + sta $C3 + txa + lsr a + tax + lda #$00 + jsr ScaleC2ByAX + clc + adc $BC + tay + txa + adc $BD + tax + tya + rts + +;;; Signed multiply-and-halve: result = (Y * X) / 2 (signed). Used by +;;; the renderer when it needs a one-bit shift built in. +L180C: +MultiplyXYAndHalve: + .refto MultiplyXYAndHalve + jsr L1818 + pha + tya + rol a + tya + ror a + tay + pla + ror a + rts + +;;; Signed 7x7 -> 14-bit multiply. Inputs Y, X. Result in AX (signed +;;; 16-bit), with $C2/$C4/$C5 used as scratch. +L1818: +MultiplyXY: + .refto MultiplyXY + tya + sta $A5 + bpl L1822 + sec + sbc #$01 + bmi L1824 +MultiplyXY_AbsY: + .refto MultiplyXY_AbsY + .refto L1822 +L1822: eor #$FF +MultiplyXY_StoreYAbs: + .refto MultiplyXY_StoreYAbs + .refto L1824 +L1824: sta $C4 + txa + bpl L1832 + eor #$FF + clc + adc #$01 + bpl L1832 + lda #$7F +;;; Store |X| as multiplier in $C2, then run 7 iterations of unrolled +;;; shift-add multiplication. Each iteration: ROR $C4 (= shift product +;;; right + capture next bit of multiplier into bit 7), if bit was 1 +;;; add multiplier to running total in A. +MultiplyXY_StoreXAbs: + .refto MultiplyXY_StoreXAbs + .refto L1832 +L1832: sta $C2 + ror $C4 + bcc L183A + lda #$00 +MultiplyXY_Iter1: + .refto MultiplyXY_Iter1 + .refto L183A +L183A: lsr a + ror $C4 + bcs L1841 + adc $C2 +MultiplyXY_Iter2: + .refto MultiplyXY_Iter2 + .refto L1841 +L1841: lsr a + ror $C4 + bcs L1848 + adc $C2 +MultiplyXY_Iter3: + .refto MultiplyXY_Iter3 + .refto L1848 +L1848: lsr a + ror $C4 + bcs L184F + adc $C2 +MultiplyXY_Iter4: + .refto MultiplyXY_Iter4 + .refto L184F +L184F: lsr a + ror $C4 + bcs L1856 + adc $C2 +MultiplyXY_Iter5: + .refto MultiplyXY_Iter5 + .refto L1856 +L1856: lsr a + ror $C4 + bcs L185D + adc $C2 +MultiplyXY_Iter6: + .refto MultiplyXY_Iter6 + .refto L185D +L185D: lsr a + ror $C4 + bcs L1864 + adc $C2 +;;; Final shift extracts bit 0; result is in (A, $C4) = upper byte + +;;; lower byte. Apply sign correction by negating if X and Y had +;;; different signs ($A5 was the saved Y sign). +MultiplyXY_FinalShift: + .refto MultiplyXY_FinalShift + .refto L1864 +L1864: lsr a + ror $C4 + sta $C5 + txa + eor $A5 + bpl L187B + lda #$00 + sec + sbc $C4 + tax + lda #$00 + sbc $C5 + tay + txa + rts + +;;; Same-sign result: copy unsigned product directly to AX. +MultiplyXY_SameSign: + .refto MultiplyXY_SameSign + .refto L187B +L187B: ldy $C5 + lda $C4 + rts + + +;;; Negate signed 16-bit value in AX. +L1880: +NegateAX: + .refto NegateAX + eor #$FF + tay + txa + eor #$FF + tax + iny + tya + bne L188C + inx +NegateAX_Exit: + .refto NegateAX_Exit + .refto L188C +L188C: rts + +;;; ============================================================ + +;;; 188D: UpdateAltimeterIndicator +.proc UpdateAltimeterIndicator + lda L0A11+1 + tax + lsr a + lsr a + lsr a + lsr a + tay + lda ($B8),y + sta $BC + iny + lda ($B8),y + sec + sbc $BC + tay + txa + and #$0F + asl a + asl a + asl a + tax + jsr L180C + asl a + sta $C2 + tya + rol a + clc + adc $BC + sta $C3 + rts + +Init2: ldx #$00 ; needle index + lda IndicatorDialNeedleLastValue,x + cmp $28 + bne L18C0 + rts + +L18C0: jsr DrawIndicatorDialNeedle + ldx #$01 ; needle index + lda IndicatorDialNeedleLastValue,x + jsr DrawIndicatorDialNeedle +Init: lda $29 + ldx #$01 ; needle index + jsr DrawIndicatorDialNeedleHelper + lda $28 + ldx #$00 +.endproc + +;;; ============================================================ + +;;; some common code + +DrawIndicatorDialNeedleHelper: + sta IndicatorDialNeedleLastValue,x + jsr DrawIndicatorDialNeedle + +HandyRTS: + rts + +;;; ============================================================ + +.proc UpdateAirspeedIndicator + lda $FB + lsr a + bcc HandyRTS + ldx #$02 + lda IndicatorDialNeedleLastValue,x + cmp L0A33 + beq HandyRTS + sta $B6 + jsr L190C + + lda L0A33 + sec + sbc $B6 + tax + cpx #$80 + ror a + cpx #$80 + ror a + tax + bpl UASI_NoExtra + dex +UASI_NoExtra: + .refto UASI_NoExtra + .refto L1902 +L1902: inx + txa + clc + adc $B6 +Init: ldx #$02 ; needle index + sta IndicatorDialNeedleLastValue,x + +UASI_WrapIfNeeded: + .refto UASI_WrapIfNeeded + .refto L190C +L190C: cmp #$58 + bcc UASI_DoDraw + sbc #$58 +UASI_DoDraw: + .refto UASI_DoDraw + .refto L1912 +L1912: jmp DrawIndicatorDialNeedle +.endproc + +;;; ============================================================ + +.proc UpdateVerticalSpeedIndicator + ldx #$03 ; needle index + lda IndicatorDialNeedleLastValue,x + cmp $2A + beq HandyRTS + php + pha + jsr DrawIndicatorDialNeedle + pla + plp + bcc UVSI_AddOne + sec + sbc #$02 +UVSI_AddOne: + .refto UVSI_AddOne + .refto L192A +L192A: clc + adc #$01 +Init: ldx #$03 ; needle index + bne DrawIndicatorDialNeedleHelper + ;; fall through to `DrawIndicatorDialNeedle` +.endproc + +;;; ============================================================ + +;;; Generic indicator logic +;;; Inputs: +;;; A = ??? +;;; X = needle index (0=alt1, 1=alt2, 2=airspeed, 3=vertical, 4=ADF) + +.proc DrawIndicatorDialNeedle + sta IndicatorDialNeedlePos + stx IndicatorDialNeedleIndex ; needle index + + NeedlePosTable := $94 + + + ;; Defaults for thin needle + lda #IndicatorDialThinNeedleTable + sta NeedlePosTable+1 + + lda IndicatorDialNeedleStyle,x + beq DIN_SetCenter + ;; thick needle + lda #IndicatorDialThickNeedleTable + sta NeedlePosTable+1 + +DIN_SetCenter: + .refto DIN_SetCenter + .refto L194C +L194C: + lda IndicatorDialNeedleX,x + sta $A5 + lda IndicatorDialNeedleY,x + sta $A7 + + ;; Determine quadrant: 0..$15 = upper-left, $16..$2B = upper-right + ;; ($16-..flipped), $2C..$41 = lower-left, $42..$57 = lower-right. + ;; Each quadrant configures the pixel-emit loop differently via + ;; SetUpFor{INX,DEX}{ADC,SBC}/{INC,DEC}. + lda IndicatorDialNeedlePos + cmp #$42 + bcs DIN_LowerRight ; (L198F) + cmp #$2C + bcs DIN_LowerLeft ; (L1980) + cmp #$16 + bcs DIN_UpperRight ; (L1971) + + ;; Quadrant 0 (= upper-left): X increases, Y decreases. + jsr SetUpForINXAndADC + jsr SetUpForDEC + lda IndicatorDialNeedlePos + jmp DIN_LookupTable + + ;; Quadrant 1 (= upper-right): X decreases, Y decreases. +DIN_UpperRight: + .refto DIN_UpperRight + .refto L1971 +L1971: jsr SetUpForDEXAndSBC + jsr SetUpForDEC + lda #$2C + sec + sbc IndicatorDialNeedlePos + jmp DIN_LookupTable + + ;; Lower-half-of-dial entry (= needle pointing top-left). + ;; Walks pixel-list with X decreasing, Y increasing. +DIN_LowerLeft: + .refto DIN_LowerLeft + .refto L1980 +L1980: jsr SetUpForDEXAndSBC + jsr SetUpForINC + lda IndicatorDialNeedlePos + sec + sbc #$2C + jmp DIN_LookupTable + + ;; Lower-half-of-dial entry (= needle pointing top-right). + ;; Walks pixel-list with X increasing, Y increasing. +DIN_LowerRight: + .refto DIN_LowerRight + .refto L198F +L198F: jsr SetUpForINXAndADC + jsr SetUpForINC + lda #$58 + sec + sbc IndicatorDialNeedlePos + + data_ptr := $96 + + ;; Convert adjusted pos into an index into NeedlePosTable + ;; (table of pixel-list pointers per dial position). +DIN_LookupTable: + .refto DIN_LookupTable + .refto L199B +L199B: asl a + tay + lda (NeedlePosTable),y + sta data_ptr + iny + lda (NeedlePosTable),y + sta data_ptr+1 + jmp DIN_OuterLoop + +SetUpForINXAndADC: + lda #OPC_INX + sta L1A32 + lda $A5 + clc + adc #$FE + sta $E6 + lda #OPC_CLC + sta L1A01 + lda #OPC_ADC_izy + sta L1A04 + dec $A5 + rts + +SetUpForDEXAndSBC: + lda #OPC_DEX + sta L1A32 + lda $A5 + clc + adc #$EC + sta $E6 + lda #OPC_SEC + sta L1A01 + lda #OPC_SBC_izy + sta L1A04 + inc $A5 + rts + +SetUpForDEC: + lda #OPC_DEC_zp + sta L1A38 + lda $A7 + clc + adc #$EC + sta $E7 + inc $A7 + rts + +SetUpForINC: + lda #OPC_INC_zp + sta L1A38 + dec $A7 + lda $A7 + sta $E7 + rts + +;;; Pixel-list drawing inner loop. Each pixel-list entry is two bytes: +;;; the X step and the count of pixels to paint at that step. Sentinel +;;; byte (= high bit set) terminates. The four sites L1A01/L1A04/ +;;; L1A32/L1A38 are self-modified by the SetUpFor* helpers above to +;;; pick the column step (CLC/ADC vs SEC/SBC), the X delta (INX/DEX), +;;; and the row delta (INC/DEC) for whichever quadrant of the dial +;;; we're drawing. +DIN_OuterLoop: + .refto DIN_OuterLoop + .refto L19F6 + + data_offset := $E7 + +L19F6: ldy #$00 + sty data_offset +DIN_LoopTop: + .refto DIN_LoopTop + .refto L19FA +L19FA: ldy data_offset + lda (data_ptr),y + bpl DIN_NewSegment ; high bit as sentinel + rts + +DIN_NewSegment: + .refto DIN_NewSegment + .refto L1A01 +L1A01: clc ; self-modified (CLC for ADC / SEC for SBC) + lda $A5 +DIN_AccStep: + .refto DIN_AccStep + .refto L1A04 +L1A04: adc ($96),y ; self-modified (ADC for INX path / SBC for DEX) + sta $E6 + iny + lda ($96),y + sta L0A3D ; pixel count + iny + sty $E7 + ldx $A7 + lda HiresTableLo,x + sta $9A + sta $BA + lda HiresTableHi,x + sta $9B + clc + adc HiresPageDelta + sta $BB ; alt-page row ptr + ldx $E6 +DIN_PaintByte: + .refto DIN_PaintByte + .refto L1A26 +L1A26: ldy HiresPixelToByteTable,x + lda HiresPixelToBitMaskTable,x ; mask + eor ($9A),y ; XOR pixel on primary page + sta ($9A),y + sta ($BA),y ; mirror to alt page +DIN_StepX: + .refto DIN_StepX + .refto L1A32 +L1A32: inx ; self-modified (INX or DEX) + dec L0A3D + bne L1A26 +DIN_StepRow: + .refto DIN_StepRow + .refto L1A38 +L1A38: inc $A7 ; self-modified (INC or DEC) + jmp L19FA +.endproc + +;;; ============================================================ + +.proc UpdateElevatorPositionIndicator + pha + lda L0A42 + jsr Draw + pla +Init: sta L0A42 +Draw: + .refto Draw + .refto L1A48 +L1A48: ldx #$64 + clc + adc #$9F + tay + lda #PLElevatorControlPositionIndicator + jmp DrawPixelListHelper +.endproc + +;;; ============================================================ + +.proc UpdateAileronPositionIndicator + pha + lda L0A43 + jsr Draw + pla +Init: sta L0A43 +Draw: + .refto Draw + .refto L1A62 +L1A62: clc + adc #$55 + tax + ldy #$94 + ;; fall through to `DrawAileronOrRudderIndicator` +.endproc + +DrawAileronOrRudderIndicator: + lda #PLAileronAndRudderIndicators +DrawPixelListHelper: + sta PixelListData+1 + lda #$00 + sta PixelListXHi + jmp DrawPixelList + +;;; ============================================================ + +.proc UpdateRudderPositionIndicator + pha + lda L0A45 + jsr Draw + pla +Init: sta L0A45 +Draw: + .refto Draw + .refto L1A83 +L1A83: clc + adc #$55 + tax + ldy #$BC ; rudder + jmp DrawAileronOrRudderIndicator +.endproc + +;;; ============================================================ + +.proc UpdateThrottleIndicator + pha + lda L0A46 + jsr Draw + pla +Init: sta L0A46 +Draw: + .refto Draw + .refto L1A97 +L1A97: eor #$FF + clc + clc ; ??? + adc #$10 + ldx #$CA + clc + adc #$AF + tay + lda #PLThrottleIndicator + jmp DrawPixelListHelper +.endproc + +;;; ============================================================ + +.proc UpdateFlapsIndicator + pha + lda L0A47 + jsr Draw + pla +Init: sta L0A47 +Draw: + .refto Draw + .refto L1AB7 +L1AB7: ldx #$C8 + clc + adc #$66 + jmp DrawFTM_Helper +.endproc + +.proc UpdateTrimIndicator + pha + lda L0A48 + jsr Draw + pla +Init: sta L0A48 +Draw: + .refto Draw + .refto L1ACA +L1ACA: ldx #$C8 + clc + adc #$8E + jmp DrawFTM_Helper +.endproc + +.proc UpdateMixtureControlIndicator + .refto UpdateMixtureControlIndicator + + pha + lda L0A49 + jsr Draw + pla +Init: sta L0A49 +Draw: + .refto Draw + .refto L1ADD +L1ADD: ldx #$D0 + clc + adc #$AF + ;; fall through to `DrawFTM_Helper` +.endproc + +;;; Shared tail for Flaps, Trim, and Mixture indicators. +DrawFTM_Helper: + .refto DrawFTM_Helper + .refto L1AE2 +L1AE2: tay + lda #PLFlapsTrimMixtureIndicator + jmp DrawPixelListHelper + +;;; ============================================================ + +.proc UpdateSlipSkidIndicator + cmp L0A44 + bne Different + rts + +Different: + .refto Different + .refto L1AF2 +L1AF2: pha + lda L0A44 + jsr Draw + pla +Init: sta L0A44 +Draw: + .refto Draw + .refto L1AFD +L1AFD: clc + adc #$0E + tax + ldy #$B4 + lda #PLSlipSkidIndicator + jmp DrawPixelListHelper +.endproc + +;;; ============================================================ + +.scope UpdateFuelTankGauges +Left: + lda L0994 + lsr a + cmp L0A4A + bne LeftDifferent +NoChangeExit: + .refto NoChangeExit + .refto L1B15 +L1B15: rts + +LeftDifferent: + .refto LeftDifferent + .refto L1B16 +L1B16: pha + lda L0A4A + jsr DrawAtX + pla +InitLeft: + sta L0A4A + jmp DrawAtX + +Right: + lda L0997 + lsr a + cmp L0A4B + beq NoChangeExit + pha + lda L0A4B + clc + adc #$1E + jsr DrawAtX + pla +InitRight: + sta L0A4B + clc + adc #$1E ; Tank 2 X offset +DrawAtX: + .refto DrawAtX + .refto L1B3E +L1B3E: clc + adc #$E8 ; Tank 1 X position + tax + lda #$00 + adc #$00 + sta PixelListXHi + ldy #$A2 ; Fuel tank Y position + ;; fall through to `DrawFuelOrOilGauge` +.endscope + +.proc DrawFuelOrOilGauge + lda #PLFuelAndOilGauges + sta PixelListData+1 + jmp DrawPixelList +.endproc + +;;; ============================================================ + +.scope UpdateOilTempAndPressureGauges +Temp: + lda L099C + cmp L0A4C + bne TempDifferent +NoChangeExit: + .refto NoChangeExit + .refto L1B5E +L1B5E: rts + +TempDifferent: + .refto TempDifferent + .refto L1B5F +L1B5F: pha + lda L0A4C + jsr DrawAtX + pla +InitTemp: + sta L0A4C + jmp DrawAtX + +Pressure: + lda L099D + cmp L0A4D + beq NoChangeExit + pha + lda L0A4D + clc + adc #$1E + jsr DrawAtX + pla +InitPressure: + sta L0A4D + clc + adc #$1E ; Oil pressure X position +DrawAtX: + .refto DrawAtX + .refto L1B86 +L1B86: clc + adc #$E8 ; Oil temp X position + tax + lda #$00 + adc #$00 + sta PixelListXHi + ldy #$AA ; Oil Y position + jmp DrawFuelOrOilGauge +.endscope + +;;; ============================================================ + +PLFuelTankIndicator: + .byte 4, 0,0, 3,0, 0,2, 3,2 + +.proc UpdateFuelTankIndicator + ldx #$FB + ldy #$A4 + lda #PLFuelTankIndicator + jmp DrawPixelListHelper +.endproc + +;;; ============================================================ + +;;; Inputs: $9C points at data; first byte is length (copied to $F1), +;;; rest is X,Y pixel offsets (single byte each) +;;; X = x pos (low byte) +;;; Y = y pos +;;; PixelListXHi = x pos (high byte) + +.proc DrawPixelList + data_ptr := PixelListData + data_offset := L0A50 + data_count := $F1 + hires_ptr1 := $8E + hires_ptr2 := $BA + x_pos := L0A4E + y_pos := L0A4F + + x_hi := PixelListXHi ; hi byte of X coord + + stx x_pos + sty y_pos + ldy #$00 + lda (data_ptr),y + sta data_count + iny + sty data_offset + +loop: + ldy data_offset + lda (data_ptr),y ; X coord in data + clc + adc x_pos + tax ; low byte of X + lda x_hi + adc #$00 + sta x_hi ; hi byte of X + iny + lda (data_ptr),y ; Y coord in data + iny + sty data_offset + clc + adc y_pos + tay + lda HiresTableHi,y + sta hires_ptr1+1 + clc ; TODO: Just EOR with %01100000 + adc HiresPageDelta + sta hires_ptr2+1 + lda HiresTableLo,y + sta hires_ptr1 + sta hires_ptr2 + + ;; x > 256 + lda x_hi + beq low + ldy HiresPixelToByteTable+256,x + lda (hires_ptr1),y + eor HiresPixelToBitMaskTable+242,x + jmp write +low: + ldy HiresPixelToByteTable,x + lda (hires_ptr1),y + eor HiresPixelToBitMaskTable,x +write: + sta (hires_ptr1),y + sta (hires_ptr2),y + dec data_count + bne loop + rts +.endproc + +;;; ============================================================ + +;;; Draw message to both hires screens +;;; A,X = message (row, col, null-terminated string) + +.proc DrawMessage + msg_ptr := $BE + color_mask1 = $90 + color_mask2 = $91 + msg_row = $C0 + msg_col = $C1 + + ;; Entry point used for North/East in Slew Mode + sta msg_ptr + stx msg_ptr+1 + clc + adc #$02 + bcc :+ + inx +: sta $B8 ; points at string + stx $B9 + ldy #$00 + lda $B7 + bmi :+ + lda #$20 + sta ($B8),y + bne L1C39 ; always + +: lda #$2D ; '-' sign for negative slew offset + sta ($B8),y + SUB16C #$00, $B6, $B6 + bmi DM_AllMinuses ; if still negative -> value was out of range + + ;; Prepare 5-digit number for display +DM_BuildDigits: + .refto DM_BuildDigits + .refto L1C39 +L1C39: LDAX #10000 + ldy #$01 + jsr DivideByAXAndSetDigitY + LDAX #1000 + ldy #$02 + jsr DivideByAXAndSetDigitY + LDAX #100 + ldy #$03 + jsr DivideByAXAndSetDigitY + LDAX #10 + ldy #$04 + jsr DivideByAXAndSetDigitY + lda ValueForString + ora #'0' + ldy #$05 + sta ($B8),y + + bne DM_DrawString ; always + ;; Out-of-range / overflow: render the whole 5-digit field as "-----". +DM_AllMinuses: + .refto DM_AllMinuses + .refto L1C67 +L1C67: lda #$2D + ldy #$05 +DM_FillLoop: + .refto DM_FillLoop + .refto L1C6B +L1C6B: sta ($B8),y + dey + bne DM_FillLoop +DM_DrawString: + .refto DM_DrawString + .refto L1C70 +L1C70: jmp L1CA3 + +;;; ============================================================ + +;;; Partial uint16-to-string; calculate high digit for $B6-7 / A,X +;;; Inputs: A,X = divisor (e.g. 100) +;;; Y = index of digit to set +;;; $B6-B7 = value +;;; $B8 = ptr to string +;;; Output: Character at ($B8),Y set; $B6 has remainder +;;; Uses: $A5-$A7 + +.proc DivideByAXAndSetDigitY + sta $A5 + stx $A6 + sty $A7 + + ldx #'0' + lda z:ValueForString +loop: sec + sbc $A5 + tay + lda z:ValueForString+1 + sbc $A6 + bmi L1C90 + inx + sta z:ValueForString+1 + sty z:ValueForString + tya + jmp loop + +L1C90: ldy $A7 + txa + sta ($B8),y + rts +.endproc + +;;; ============================================================ + +;;; Color mask is white +DrawMessageWhite: + sta msg_ptr + stx msg_ptr+1 +DrawMessageWhite_NoPtr: + .refto DrawMessageWhite_NoPtr + .refto L1C9A +L1C9A: lda #$7F + tax + bne DrawMessage_StoreMask ; always + +;;; Color mask is orange +DrawMessageOrange: + sta msg_ptr + stx msg_ptr+1 +DrawMessageOrange_NoPtr: + .refto DrawMessageOrange_NoPtr + .refto L1CA3 +L1CA3: lda #$D5 + ldx #$AA + bne DrawMessage_StoreMask ; always + +;;; Unused ??? + lda #$AA + ldx #$D5 + +DrawMessage_StoreMask: + .refto DrawMessage_StoreMask + .refto L1CAD +L1CAD: sta color_mask1 + stx color_mask2 + jsr GetMessageColRow + sta msg_row + stx msg_col + jmp NextCharacter + +;;; Assuming pointer is at start of message, returns A=row, X=col +.proc GetMessageColRow + jsr GetMessageByte + pha + jsr GetMessageByte + tax + pla + rts +.endproc + +;;; Returns byte from message, incrementing `msg_ptr` +.proc GetMessageByte + ldy #$00 + lda (msg_ptr),y + inc msg_ptr + bne :+ + inc msg_ptr+1 +: rts +.endproc + +;;; A = character ASCII code - $20 + +DrawCharacter: + char_word = $A5 + char_rows = $A7 + + asl a ; *= 2 + tax + lda CharBitmapTable,x + sta char_word + lda CharBitmapTable+1,x + sta char_word+1 + lda #5 ; rows + sta char_rows + + ldx msg_col + ldy PixelToBitNumberTable,x + ldx L13EE,y + stx $A9 + lda LeftMaskTable,x + sta $AD + lda RightMaskTable,x + sta $AE + ldy msg_col + lda ColorPixelToByteTable,y + lsr a + lda color_mask1 + ldy color_mask2 + bcc L1D06 + + sta $BC + sty $BD + bne L1D0A ; always +L1D06: + sta $BD + sty $BC +L1D0A: + + hires_ptr1 := $8E + hires_ptr2 := $3C + + ldy msg_row + ldx msg_col + lda HiresTableHi,y + sta hires_ptr1+1 + clc ; TODO: Just EOR with %01100000 + adc #$20 + cmp #$60 + bcc :+ + sbc #$40 +: sta hires_ptr2+1 + lda HiresTableLo,y + sta hires_ptr1 + sta hires_ptr2 + ldy ColorPixelToByteTable,x + + ;; Shift 3 bits out + lda #$00 + lsr char_word+1 + ror char_word + rol a + lsr char_word+1 + ror char_word + rol a + lsr char_word+1 + ror char_word + rol a + tax + lda CharBitsToPixelsTable,x + ldx #$00 + stx $AB + ldx $A9 + jmp L1D49 + +;;; Inner loop of text drawing + +L1D46: asl a + rol $AB +L1D49: dex + bpl L1D46 + clc + adc #$80 + ora #$80 + and $BC + sta $AC + lda $AB + rol a + ora #$80 + and $BD + sta $AB + lda $AD + and (HiresRowPtr),y + ora $AC + sta (HiresRowPtr),y + sta ($3C),y + iny + lda $AE + and (HiresRowPtr),y + ora $AB + sta (HiresRowPtr),y + sta ($3C),y + inc msg_row + dec $A7 + bne L1D0A + lda msg_col + clc + adc #$04 + sta msg_col + lda msg_row + sec + sbc #$05 + sta msg_row + +NextCharacter: + jsr GetMessageByte + sec + sbc #$20 + bmi DrawMessage_Exit ; end of string + jmp DrawCharacter + +;;; Draws multiple message, until sentinel is reached +DrawMultiMessage: + sta msg_ptr + stx msg_ptr+1 +DMM_Top: + .refto DMM_Top + .refto L1D96 +L1D96: ldy #$00 + lda (msg_ptr),y + bne DMM_DrawOne + iny + lda (msg_ptr),y + beq DrawMessage_Exit +DMM_DrawOne: + .refto DMM_DrawOne + .refto L1DA1 +L1DA1: jsr L1C9A + jmp L1D96 + +DrawMessage_Exit: + .refto DrawMessage_Exit + .refto L1DA7 +L1DA7: rts +.endproc + +DrawMessageOrange := DrawMessage::DrawMessageOrange +DrawMessageWhite := DrawMessage::DrawMessageWhite +DrawMultiMessage := DrawMessage::DrawMultiMessage +DivideByAXAndSetDigitY := DrawMessage::DivideByAXAndSetDigitY + +;;; Clear viewport on both hires screens +.proc ClearViewportsToBlack + ptr1 := $8E + ptr2 := $3C + + ;; Loop over all viewport rows + ldx #0 ; row + + ;; Hires table may be set for page1 or page2; + ;; set ptr1 to current and ptr2 to alt regardless +loop: lda HiresTableHi,x + sta ptr1+1 + clc ; TODO: Just EOR with %01100000 + adc #$20 + cmp #$60 + bcc :+ + sbc #$40 +: sta ptr2+1 + lda HiresTableLo,x + sta ptr1 + sta ptr2 + + ;; Clear current row to black + ldy #39 + lda #0 +: sta (ptr1),y + sta (ptr2),y + dey + bpl :- + + ;; Next row + inx + cpx #100 ; viewport rows + bne loop + + rts +.endproc + +;;; 1DD2 ??? + + bmi :+ + sec + .byte $02 +: ldx #$02 + .byte $A7 + .byte $02 + ldy $B102 + .byte $02 + .byte $DF + .byte $07 + txs + asl $0F5A + .byte $1A + bpl $1E01 + ora ($32),y + ora ($24),y + .byte $12 + .byte $3C + .byte $12 + iny + .byte $12 + .byte $54 + .byte $13 + inc $F613 + .byte $13 + .byte $04 + .byte $14 + .byte $12 + .byte $14 + asl $14,x + .byte $12 + .byte $14 + .byte $13 + .byte $14 + brk + + ;; Scenery loader state. The 64K loader's chunk3 code reads + ;; and writes these every time it streams a new sector in. + + ;; 16-bit sector counter; see `SceneryAdvanceSector` and the + ;; offset calc in `SceneryComputeBlockOffset`. +L1E01: .byte $20 + brk + + ;; 16-bit pointer to the next destination/source byte for the + ;; loader; advanced by every successful read. +L1E03: .word $0005 + brk + brk + ;; Set to $01 by `Apply64KPatchTable` once 64K-mode init has + ;; finished. Used to gate optional 64K-only behaviour. +L1E07: .byte $00 + +BootSlot: + .byte $60 ; high nibble = boot slot + + ;; Loader open/active flag: $00 = closed, $FF = open. +L1E09: .byte 0 + .byte $20 + + ;; Block offset within the current track; reset to $20 by + ;; `SceneryLoaderInit`. +L1E0B: .byte $20 +L1E0C: ora ($D9,x) + brk + brk + brk + sta ($00),y + .byte $8F + brk + sta ($00),y + cpx $00 + .byte $8F + brk + .byte $DF + brk + .byte $DF + brk + .byte $DF + brk + .byte $DF + brk + stx $8F00 + brk + .byte $DF + brk + .byte $8B + brk + stx $E200 + brk + stx $DC00 + brk + dec $DE00,x + brk + .byte $8B + brk + sta $8C00 + brk + dec $8D00,x + brk + dec $DB00,x + brk + dec $DE00,x + brk + sta $8A00 + brk + cmp $8D00,x + brk + .byte $8B + brk + .byte $DF + brk + stx $97,y + txs + .byte $9B + sta $9F9E,x + ldx $A7 + .byte $AB + ldy $AEAD + .byte $AF + .byte $B2 + .byte $B3 + ldy $B5,x + ldx $B7,y + lda $BBBA,y + ldy $BEBD,x + .byte $BF + .byte $CB + cmp $CFCE + .byte $D3 + dec $D7,x + cmp $DBDA,y + .byte $DC + cmp $DFDE,x + sbc $E6 + .byte $E7 + sbc #$EA + .byte $EB + cpx $EEED + .byte $EF + .byte $F2 + .byte $F3 + .byte $F4 + sbc $F6,x + .byte $F7 + sbc $FBFA,y + .byte $FC + sbc $FFFE,x + ora ($30,x) + plp + bit $20 + asl $1C1D,x + .byte $1C + .byte $1C + .byte $1C + .byte $1C + bvs $1ECF + rol $22 + .byte $1F + asl $1C1D,x + .byte $1C + .byte $1C + .byte $1C + .byte $1C + +;;; Scenery-loader thunks. As shipped these jump into the 48K loader +;;; living at $20xx. In 64K mode the patch table at the end of chunk5 +;;; rewrites every JMP target to a `SceneryLoaderEntryN` in chunk3. + +SceneryLoaderThunk1: + .refto SceneryLoaderThunk1 + .refto L1EAD +L1EAD: jmp $203A ; becomes SceneryLoaderEntry1 +SceneryLoaderThunk2: + .refto SceneryLoaderThunk2 + .refto L1EB0 +L1EB0: jmp $2015 ; becomes SceneryLoaderEntry2 +SceneryLoaderThunk3: + .refto SceneryLoaderThunk3 + .refto L1EB3 +L1EB3: jmp $20AF ; becomes SceneryLoaderEntry3 +SceneryLoaderThunk4: + .refto SceneryLoaderThunk4 + .refto L1EB6 +L1EB6: jmp $2122 ; becomes SceneryLoaderEntry4 +SceneryLoaderThunk5: + .refto SceneryLoaderThunk5 + .refto L1EB9 +L1EB9: jmp $238A ; becomes SceneryLoaderEntry5 +SceneryLoaderThunk6: + .refto SceneryLoaderThunk6 + .refto L1EBC +L1EBC: jmp $2000 ; becomes SceneryLoaderEntry6 + + sec + rts + +SceneryLoaderThunk7: + .refto SceneryLoaderThunk7 + .refto L1EC1 +L1EC1: jmp $2578 ; becomes SceneryLoaderEntry7 + +;;; Loader "succeeded with no-op" return - used as a default tail. +LoaderNoOpSuccess: + clc + rts + +;;; Read the next disk block into the staging buffer. Computes the +;;; ProDOS block number from the sector counter `L1E01`, swaps in the +;;; loader's zero-page workspace, and runs `ReadBlocks`. Restores ZP +;;; on the way back out. +FetchSectorFromDisk: + jsr SwapZP + lda #$00 + sta $53 + lda L1E01 + lsr a + lsr a + clc + adc #$01 + asl a + asl a + asl a + rol $53 + asl a + rol $53 + ldy $53 + tax + tya + lsr a + sta $5A + txa + ror a + jsr ReadBlocks + +;;; ============================================================ +;;; Zero page preservation code + +.proc SwapZP + ldx #$1F +: lda $40,x + ldy ZPSwapStorage,x + sta ZPSwapStorage,x + sty $40,x + dex + bpl :- + clc + rts +.endproc + +;;; ============================================================ + +.proc ReadBlocks + sta $5E + lda #>$2600 + sta $45 + lda #<$2600 + sta $44 + lda #$10 + sta $53 + ;; Per-block loop top: if the device-side block index hasn't + ;; changed since the last read, skip the swap; otherwise issue + ;; the read into the standard buffer. +LoopTop: + .refto LoopTop + .refto L1F08 +L1F08: ldy $5A + cpy $59 + beq SkipDeviceRead + sty $59 + ldx $5E + inx + stx $5F + lda $45 + pha + lda $44 + pha + ldy $5A + ldx ReadBlockDataBuffer+512,y + lda ReadBlockDataBuffer+768,y + jsr DoReadBlockWithStandardDataBuffer + pla + sta $44 + pla + sta $45 +SkipDeviceRead: + .refto SkipDeviceRead + .refto L1F2C +L1F2C: ldy $5E + inc $5E + bne L1F34 + inc $5A +L1F34: ldx ReadBlockDataBuffer,y + lda ReadBlockDataBuffer+256,y + pha + ora ReadBlockDataBuffer,y + tay + pla + dey + iny + beq L1F52 + jsr DoReadBlock +L1F47: inc $45 + inc $45 + dec $53 + dec $53 + bne L1F08 + rts + +L1F52: sta ($44),y + inc $45 + sta ($44),y + dec $45 + iny + bne L1F52 + beq L1F47 + +DoReadBlockWithStandardDataBuffer: + ldy #ReadBlockDataBuffer + sty $45 + + ;; X,A = block number, $44-45 = data buffer +DoReadBlock: + ldy #$01 ; ReadBlock + sty SPCommandNumber + stx RBBlockNumber + sta RBBlockNumber+1 + jsr L1F85 + lda $45 + sta RBDataBuffer+1 + lda $44 + sta RBDataBuffer + + jsr $C70D ; SmartPort entry point (self-modified) +SPCommandNumber: + .byte 1 ; command (ReadBlock) + .addr RBParams ; params + +L1F85: lda #$04 + sta $49 + +;;; Swap A pages of memory at ($48,$49) with the stash at $FF00. +;;; Each call moves up to 256 * A bytes back and forth, which is how +;;; the simulator preserves machine state across error/Applesoft/ +;;; reset entries. Reused as the BRK / Ctrl-Y / IRQ vector handler so +;;; the user can fall back into Applesoft for inspection without +;;; losing the running game state. +SwapPagesWithStash: + .refto SwapPagesWithStash +L1F89: ldx #$00 + stx $48 + sta $4A +SwapPagesWithStash_PageStart: + .refto SwapPagesWithStash_PageStart + .refto L1F8F +L1F8F: ldy #$78 +SwapPagesWithStash_ByteLoop: + .refto SwapPagesWithStash_ByteLoop + .refto L1F91 +L1F91: lda ($48),y + pha + lda $FF00,x + sta ($48),y + pla + sta $FF00,x + inx + tya + eor #$80 + tay + bmi L1F91 + iny + bpl L1F91 + inc $49 + dec $4A + bne L1F8F + rts + + ;; ReadBlock parameter list +RBParams: + .byte $03 ; parameter count + .byte $01 ; unit number +RBDataBuffer: + .addr $95DF ; data buffer (lo, hi) +RBBlockNumber: + .byte $02, $00, $00 ; block number (lo, mid, hi) +.endproc + +;;; Stash +L1F89 := ReadBlocks::L1F89 + +;;; ============================================================ + + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + brk + +PopulateA5ThruA8From1E03: + lda L1E03 + sta $A5 + lda L1E03+1 + sta $A6 + lda #$00 + sta $A7 + lda L1E0C + asl a + asl a + adc #$26 + sta $A8 + ldx #$04 + ldy #$00 + rts + +;;; Convert the loader's sector counter `L1E01` into a (block, offset) +;;; pair stored at `L1E0B/L1E0C`. Returns C=1 (failure) once the +;;; sector counter exceeds $87 - the end-of-stream marker. +ComputeBlockFromSector: + lda L1E01 + cmp #$88 + bcs :+ + lsr a ; A /= 4 (two-step), saving low bit + php + lsr a + sta L1E0B + lda #$00 + rol a + plp + rol a + sta L1E0C + inc L1E0B + asl L1E0B +: rts + + brk + brk + brk + brk + + .assert * = $2000, error, "EOF mismatch" + + .byte $80 + .incbin "../res/loading_panel.bin", 1, 1535 diff --git a/src/chunk5.s b/src/chunk5.s new file mode 100644 index 0000000..5af8934 --- /dev/null +++ b/src/chunk5.s @@ -0,0 +1,13320 @@ + .org $6000 + +L003C := $003C +L00A5 := $00A5 + +;;; Possible chunk5 references - overlays? +LB808 := $B808 +LB849 := $B849 +LB9C3 := $B9C3 +LB9CE := $B9CE + +;;; ============================================================ +;;; chunk5 entry-point trampolines ($6000..$6028) +;;; +;;; Each three-byte slot is a public JMP into a chunk5 routine, +;;; called by chunk2/chunk3 (which can't see internal labels here). +;;; In 64K mode some are runtime-patched to redirect to chunk3 code. + +L6000: jmp SetupViewProjection + +FlipPagesFillViewportRelay: + jmp FlipPagesFillViewport + + ;; Called from chunk2 (DisplayCoursePlotData) and main loop +L6006: jmp ProcessScenery + + jmp DrawColorLine + + jmp PlotColorPixel + + jmp SetPixelDrawMode + +L6012: jmp UpdateTurnCoordinator + + jmp L84F4 + + ;; Called by chunk3 (LookupADFStation tail) +L6018: jmp SceneryOpAdvanceAndContinue + + ;; Called by chunk3 +DrawColorSpanRelay: jmp DrawColorSpan + + ;; Called by chunk3 +L601E: jmp MapColorAndPrepRowRoutine + +;;; Scenery opcode $05: ADF station record. In 48K mode this is a +;;; no-op that just advances past the 9-byte record. In 64K mode the +;;; first instruction is patched to `JMP LookupADFStation` (chunk3). +SceneryOpADFRecord: + lda #$09 ; 64k: Patched to JMP `LookupADFStation` + jmp SceneryOpAdvanceAndContinue + +L6026: rts ; 64k: Patched to JMP `UpdateCOMMessageChunks` + + rts + + rts + +;;; Scenery opcode $1D: NAV-station record. The record encodes the +;;; station's two-byte tuning frequency followed by an 8-byte +;;; descriptor (X, Y, Z, ranges...). Tries each of the user's two +;;; tuned NAV frequencies ($08F7 = NAV1, $08F5 = NAV2) against the +;;; record, copying the descriptor into $08F9/$0901 on a hit. +SceneryOpNAVRecord: + lda $08F3 + beq L6049 ; nothing tuned this frame + lda #$00 + sta $08F2 + ldy #$08 + LDAX $08F7 ; NAV1 frequency + jsr MatchNAVFreq + ldy #$00 + LDAX $08F5 ; NAV2 frequency + jsr MatchNAVFreq +L6049: lda #$0B ; record length + jmp SceneryOpAdvanceAndContinue + +;;; Match the AX = tuned frequency against ($8B)+1/+2 and copy the +;;; following 8 bytes into $08F9+Y on a hit. Y selects NAV1 ($00) or +;;; NAV2 ($08) destination slot. Sets/ORs the active bit into $08F4 +;;; so the panel knows which gauges to drive. +MatchNAVFreq: + sty $B6 + ldy #$01 + cmp ($8B),y + bne L607C + txa + iny + cmp ($8B),y + bne L607C + ldx $B6 + lda #$08 + sta $B6 + iny +MatchNAVFreq_CopyLoop: + .refto MatchNAVFreq_CopyLoop + .refto L6063 +L6063: lda ($8B),y + sta $08F9,x + iny + inx + dec $B6 + bne L6063 + lda #$01 ; NAV1 active bit + cpx #$10 + beq L6076 + lda #$02 ; NAV2 active bit +MatchNAVFreq_SetActiveBit: + .refto MatchNAVFreq_SetActiveBit + .refto L6076 +L6076: ora $08F4 + sta $08F4 +MatchNAVFreq_Exit: + .refto MatchNAVFreq_Exit + .refto L607C +L607C: rts + +;;; Scenery opcode $1E: COM/airport record. Holds a record-length +;;; byte followed by COM frequency, position, and an inline airport +;;; name string. On a frequency match copies the position and +;;; latches the name pointer at $092A, then triggers the ATIS +;;; renderer. +SceneryOpCOMRecord: + iny + lda ($8B),y ; record length + pha + iny + lda $090A + beq L60BD + lda #$00 + sta $0909 + lda $089F ; tuned COM frequency low + iny + cmp ($8B),y + bne L60BD + lda $08A0 ; tuned COM frequency high + iny + cmp ($8B),y + bne L60BD + iny + ldx #$08 +SceneryOpCOMRecord_CopyDescriptorLoop: + .refto SceneryOpCOMRecord_CopyDescriptorLoop + .refto L609F +L609F: lda ($8B),y + iny + sta $0905,y + dex + bne L609F + lda $8B + clc + adc #$0D ; skip header to airport name + sta $092A + lda #$00 + sta $0913 + adc $8C + sta $092B + jsr L6026 ; trigger ATIS message draw +SceneryOpCOMRecord_AdvanceAndContinue: + .refto SceneryOpCOMRecord_AdvanceAndContinue + .refto L60BD +L60BD: pla ; restore record length + clc + adc $8B + sta $8B + bcc L60C7 + inc $8C +L60C7: jmp SceneryInterpreterEntry + + lda #$04 + ldx #$A5 + sta $3E +L60D0: iny + lda ($8B),y + sta $00,x + inx + dec $3E + bne L60D0 + rts + +;;; Scenery opcode $0D: section-header / demand-load trigger. +;;; 1. Copies a 5-byte parameter block from the record into +;;; $08E5..$08E9 (scale, range, tile delta, slot index). +;;; 2. If the high bit of $08E9 is clear, cumulatively advances the +;;; saved section-base cursor $08E7 by the current cursor $8B +;;; (= the next-section's load address grows by the bytes we just +;;; walked through). +;;; 3. Masks the low 2 bits of $08E9 to pick a cache slot (0..3) and +;;; calls `SceneryHeaderLoadIfMiss` via the trampoline at L8776 +;;; / `SceneryHeaderLoadTrampoline`: on a cache miss it +;;; invalidates newer slots and runs `SceneryHeaderRunSection` +;;; (which DMA-fetches the section bytecode from disk into the +;;; address held in $08E7); on a hit it returns immediately. +SceneryOpHeader: + ldy #$05 +: lda ($8B),y + sta $08E4,y + dey + bne :- + lda $08E9 + bmi L60FD + pha + + ADD16 $08E7, $8B, $08E7 + + pla +SceneryOpHeader_RunSetup: + .refto SceneryOpHeader_RunSetup + .refto L60FD +L60FD: and #$03 + sta $08E9 + jsr L8776 + lda #$06 ; record length + jmp SceneryOpAdvanceAndContinue + +;;; ============================================================ +;;; View Projection Setup +;;; +;;; Build the 2D-projection coefficients used by the polygon +;;; rasteriser based on the current camera. Three branches: +;;; 1. RadarView -> top-down orthographic, fixed pitch. +;;; 2. ViewDirection negative -> back-view (looking aft). +;;; 3. ViewDirection 0..n -> side/forward view at scaled angle. +;;; All three branches converge at `L61F0` which builds the rotation +;;; matrix into $CB/$CD/$CF, then derives a 3x3 transform via a +;;; cascade of `ZPScale` calls and a final 16-bit normalisation. + +SetupViewProjection: + lda RadarView + beq L612D + lda #$00 + sta $72 + lda #$40 + sta $73 + lda #$00 + sta $76 + lda #$00 + sta $77 + sec + sbc $70 + sta $74 + lda #$00 + sbc $71 + sta $75 + jmp L61F0 + +SetupViewProjection_NotRadar: + .refto SetupViewProjection_NotRadar + .refto L612D +L612D: lda ViewDirection + bpl L6155 + SUB16C #$00, $70, $74 + lda #$00 + sta $76 + lda #$00 + sta $77 + lda $6C + sta $72 + lda $6D + clc + adc #$40 + sta $73 + jmp L61F0 + +SetupViewProjection_SideOrForwardView: + .refto SetupViewProjection_SideOrForwardView + .refto L6155 +L6155: lda ViewDirection + asl a + asl a + asl a + asl a + sta $3E + LDAX $6E + STAX $B6 + LDAX $6C + STAX $C0 + lda $71 + sta $3D + lda $B7 + clc + adc #$40 + bmi L617C + jmp L6199 + +SetupViewProjection_NegateAngle: + .refto SetupViewProjection_NegateAngle + .refto L617C +L617C: lda #$00 + ldx #$80 + sec + sbc $C0 + tay + txa + sbc $C1 + tax + tya + STAX $C0 + lda $B7 + eor #$80 + sta $B7 + lda $3D + eor #$80 + sta $3D +SetupViewProjection_BuildXZBasis: + .refto SetupViewProjection_BuildXZBasis + .refto L6199 +L6199: lda $3D + clc + adc $3E + sta $77 + lda $3E + jsr L1763 + sta $BA + stx $BB + lda $3E + jsr L1768 + sta $BE + stx $BF + + ldx #$C0 + ldy #$BA + lda #$98 + jsr ZPScale + + ldx #$B6 + ldy #$BE + lda #$AD + jsr ZPScale + + SUB16 $98, $AD, $72 + + ldx #$B6 + ldy #$BA + lda #$98 + jsr ZPScale + + ldx #$C0 + ldy #$BE + lda #$AD + jsr ZPScale + + ADD16 $AD, $98, $74 + +;;; Common branch where all 3 view-direction paths converge. Builds +;;; the rotation matrix at $CB/$CD/$CF from the synthesised pitch/yaw +;;; values in $72/$74/$76, then derives the 3x3 transform via a +;;; cascade of ZPScale calls and a final 16-bit normalisation. +SetupViewProjection_BuildMatrix: + .refto SetupViewProjection_BuildMatrix + .refto L61F0 +L61F0: lda $73 + clc + adc #$40 + bpl L6210 + SUB16C #$8000, $72, $72 + lda $75 + eor #$80 + sta $75 + lda $77 + eor #$80 + sta $77 +SetupViewProjection_NormaliseAngle: + .refto SetupViewProjection_NormaliseAngle + .refto L6210 +L6210: ldx $72 + lda $73 + jsr L1778 + STAX $CB + ldx $74 + lda $75 + jsr L1778 + STAX $CD + ldx $76 + lda $77 + jsr L1778 + STAX $CF + ldx $72 + lda $73 + jsr L177B + sta $18 + stx $19 + ldx $74 + lda $75 + jsr L177B + sta $D4 + stx $D5 + ldx $76 + lda $77 + jsr L177B + sta $D6 + stx $D7 + + ldx #$D6 + ldy #$D4 + lda #$D8 + jsr ZPScale + + ldx #$CF + ldy #$CD + lda #$1B + jsr ZPScale + + ldx #$D6 + ldy #$CD + lda #$DD + jsr ZPScale + + ldx #$CF + ldy #$D4 + lda #$DF + jsr ZPScale + + ldx #$CB + ldy #$1B + lda #$E1 + jsr ZPScale + + ldx #$CB + ldy #$DF + lda #$1E + jsr ZPScale + + ldx #$DD + ldy #$CB + lda #$4A + jsr ZPScale + + ldx #$CB + ldy #$D8 + lda #$4D + jsr ZPScale + + ADD16 $D8, $E1, $78 + SUB16 $1E, $DD, $7A + + ldx #$CF + ldy #$18 + lda #$7C + jsr ZPScale + + ldx #$CD + ldy #$18 + lda #$7E + jsr ZPScale + + ldx #$18 + ldy #$D4 + lda #$80 + jsr ZPScale + + SUB16C #$00, $CB, $82 + SUB16 $4A, $DF, $84 + ADD16 $1B, $4D, $86 + + ldx #$D6 + ldy #$18 + lda #$88 + jsr ZPScale + + ldx #$0C +;;; Final matrix normalisation: arithmetic-shift each 16-bit matrix +;;; entry at $78..$89 right by 1 (= halve), with the row-3 entries +;;; ($7D/$7F/$81 ...) shifted twice (= halve x 2) so all entries +;;; are in the same Q1.15 fixed-point domain. Walks X = $0C down to +;;; $00 in steps of $06 (= 3 axes per pass for 3 rows). +SetupViewProjection_NormaliseMatrix: + .refto SetupViewProjection_NormaliseMatrix + .refto L6301 +L6301: lda $79,x + rol a + ror $79,x + ror $78,x + lda $7D,x + rol a + php + ror $7D,x + ror $7C,x + plp + ror $7D,x + ror $7C,x + txa + sec + sbc #$06 + tax + bpl L6301 + rts + +;;; Recompute the section base accumulator at $4A..$52 from the matrix +;;; row-2 entries ($7E/$80/$82) scaled by $68/$69 (= camera Y delta). +;;; Each axis: (matrix * scale) >> 15 stored as 24-bit MID/HI/LO at +;;; $4A..$4C (X), $4D..$4F (Y), $50..$52 (Z). Called from L6D28 +;;; FrameSetupEpilogue when the cached scale at $35/$36 differs from +;;; current $68/$69. MAME-patched binary writes to $2A..$2F as 16-bit +;;; pairs instead. +RecomputeSectionBase: + .refto RecomputeSectionBase + .refto L631D +L631D: LDAX $7E + STAX $C2 + jsr ScaleSignedC2 + lda $C7 + sta $4C + LDAX $C8 + STAX $4A + LDAX $80 + STAX $C2 + jsr ScaleSignedC2 + lda $C7 + sta $4F + LDAX $C8 + STAX $4D + LDAX $82 + STAX $C2 + jsr ScaleSignedC2 + lda $C7 + sta $52 + LDAX $C8 + STAX $50 + rts + +;;; Multiply ($C2/$C3) signed * ($68/$69) and double; result in +;;; $C7/$C8/$C9 (24-bit). Used by RecomputeSectionBase to compute one +;;; axis at a time. +ScaleSignedC2: + .refto ScaleSignedC2 + .refto L6363 +L6363: lda $C3 + eor $69 + php + lda $C3 + bpl L6379 + SUB16C #$00, $C2, $C2 +ScaleSignedC2_DoMultiply: + .refto ScaleSignedC2_DoMultiply + .refto L6379 +L6379: LDAX $68 + jsr MultiplyAXByC2 + asl $C7 + rol $C8 + rol $C9 + plp + bmi L639C + SUB16C #$00, $C7, $C7 + lda #$00 + sbc $C9 + sta $C9 +ScaleSignedC2_Exit: + .refto ScaleSignedC2_Exit + .refto L639C +L639C: rts + +;;; ============================================================ + +;;; Flip pages and fill viewport with sky/ground. Also updates artificial horizon. + +.proc FlipPagesFillViewport + txa + bne L63BE + + ;; Point `HiresTableHi` at "other" page + + ldx #$C0 +FlipPagesFillViewport_RotateRowTable: + .refto FlipPagesFillViewport_RotateRowTable + .refto L63A2 +L63A2: lda HiresTableHi-1,x + clc + adc HiresPageDelta + sta HiresTableHi-1,x + dex + bne L63A2 + lda HiresPageDelta + eor #$C0 + sta HiresPageDelta + + ;; Flip displayed pages + bpl L63BB + sta LOWSCR + bmi L63BE ; always +FlipPagesFillViewport_FlipToHires: + .refto FlipPagesFillViewport_FlipToHires + .refto L63BB +L63BB: sta HISCR +;;; After page flip: snapshot $8B/$8C cursor to $35/$36, install +;;; EmitClippedLine tail JMP target ($0D = high byte of $6BC0 area +;;; that interpreter expects), set $EA = $FF (= sentinel row), then +;;; build a synthetic scenery cursor pointing at the horizon polygon +;;; at $0AB9..$0AC2. Save $66/$67 (= camera X) to $02/$03 and $6A/$6B +;;; (= camera Z) to $04/$05, ZERO all six camera ZPs and the entire +;;; $4A..$52 base accumulator, then run the interpreter. After it +;;; returns, restore $66/$6A and the cursor. +FlipPagesFillViewport_RunHorizonPoly: + .refto FlipPagesFillViewport_RunHorizonPoly + .refto L63BE +L63BE: + + LDAX $8B + STAX $35 + lda #$0D + sta L6B2C + lda #$FF + sta $EA + lda $77 + clc + adc #$20 + lsr a + lsr a + and #$F0 + clc + adc #$78 + sta $8B + lda #$0A + adc #$00 + sta $8C + LDAX $66 + STAX $02 + LDAX $6A + STAX $04 + lda #$00 + sta $66 + sta $67 + sta $6A + sta $6B + sta $4A + sta $4B + sta $4C + sta $4D + sta $4E + sta $4F + sta $50 + sta $51 + sta $52 + jsr SceneryInterpreterEntry + LDAX $02 + STAX $66 + LDAX $04 + STAX $6A + lda #$20 + sta L6B2C + LDAX $35 + STAX $8B + LDAX $E9 + STAX $D1 + LDAX $EB + STAX $DA + + lda $0882 ; override ground color ??? + beq L644A ; black + cmp #$FF + beq L644A ; white + lda #$2A ; green (even col) +FlipPagesFillViewport_StoreGroundColor: + .refto FlipPagesFillViewport_StoreGroundColor + .refto L644A +L644A: sta FillColor + + lda $0880 ; override sky color ??? + beq L6457 ; black + cmp #$FF + beq L6457 ; white + lda #$D5 ; blue (even col) +FlipPagesFillViewport_StoreSkyColor: + .refto FlipPagesFillViewport_StoreSkyColor + .refto L6457 +L6457: sta AltFillColor + beq L6467 ; black + cmp #$FF + beq L6467 ; white + + lda FillColor + beq L6467 ; black + cmp #$FF + bne L646D ; white +FlipPagesFillViewport_PrepRow: + .refto FlipPagesFillViewport_PrepRow + .refto L6467 +L6467: and #$03 + tax ; hires color + jsr SetEvenAndOddColorsAndPrepRowRoutine + +;;; Determine the screen-Y range affected by the horizon polygon's +;;; transformed line endpoints. $EA/$EC are post-clip Y values (= the +;;; sky/ground transition extents). Tracks min in $EF, max in $F0. +;;; If $EA == $FF (= no line emit happened), use full viewport rows. +FlipPagesFillViewport_DetermineExtents: + .refto FlipPagesFillViewport_DetermineExtents + .refto L646D +L646D: lda #$00 + sta $F0 + lda #$63 + sta $EF + lda $EA + bpl L6492 + dec $F0 + lda $73 + bpl L6482 + jsr SwapFillColors +FlipPagesFillViewport_FillFullViewport: + .refto FlipPagesFillViewport_FillFullViewport + .refto L6482 +L6482: jmp L6509 + +;;; Helper: track min/max screen-Y for the horizon line endpoint A. +;;; Updates $EF (= min) and $F0 (= max). Returns via RTS. +TrackHorizonExtent: + .refto TrackHorizonExtent + .refto L6485 +L6485: cmp $EF + bcs L648B + sta $EF +TrackHorizonExtent_TestMax: + .refto TrackHorizonExtent_TestMax + .refto L648B +L648B: cmp $F0 + bcc L6491 + sta $F0 +TrackHorizonExtent_Exit: + .refto TrackHorizonExtent_Exit + .refto L6491 +L6491: rts + +;;; Sky/ground horizon-line was emitted ($EA != $FF). Compute the +;;; min/max row range and decide which rows are pure sky, pure ground, +;;; or mixed (= contain the diagonal transition). +FlipPagesFillViewport_HorizonEmitted: + .refto FlipPagesFillViewport_HorizonEmitted + .refto L6492 +L6492: lda $EA + jsr L6485 + lda $EC + jsr L6485 + lda $E9 + beq L64BD + lda $EA + beq L64C0 + lda $EB + beq L64C0 + lda $EC + beq L64BD + lda $E9 + cmp #$8B + bne L64B9 + lda $EC + bne L64C0 + jmp L64BD + +;;; Typical view: +;;; +;;; +----------------------------------------+ +;;; | |\ +;;; | | } solid sky +;;; |. . . . . . . . . . . . . . . . . . . . |/ +;;; | ______,-----|\ +;;; | ______,------' | } mix of sky/ground +;;; |______,------' |/ +;;; |. . . . . . . . . . . . . . . . . . . . |\ +;;; | | } solid ground +;;; | |/ +;;; +----------------------------------------+ +;;; +;;; NOTE: Depending on pitch/roll, this could be inverted! + +;;; Fill rows of viewport *above* the sky/ground diagonal +FlipPagesFillViewport_AboveDiag_TestSwap: + .refto FlipPagesFillViewport_AboveDiag_TestSwap + .refto L64B9 +L64B9: lda $EB + beq L64C0 +FlipPagesFillViewport_AboveDiag_DoSwap: + .refto FlipPagesFillViewport_AboveDiag_DoSwap + .refto L64BD +L64BD: jsr SwapFillColors +FlipPagesFillViewport_AboveDiag_Fill: + .refto FlipPagesFillViewport_AboveDiag_Fill + .refto L64C0 +L64C0: ldy $EF + beq L64CC + sty $F1 + dey + sty $E7 + jsr FillViewportRows + +;;; Fill rows of viewport with the sky/ground diagonal +FlipPagesFillViewport_DiagFill_TestSwap: + .refto FlipPagesFillViewport_DiagFill_TestSwap + .refto L64CC +L64CC: lda $E9 + bne L64D9 + lda $EC + cmp $EA + bcs L64E3 + jmp L64E6 + +FlipPagesFillViewport_DiagFill_TestSwap2: + .refto FlipPagesFillViewport_DiagFill_TestSwap2 + .refto L64D9 +L64D9: lda $EB + bne L64E6 + lda $EA + cmp $EC + bcc L64E6 +FlipPagesFillViewport_DiagFill_DoSwap: + .refto FlipPagesFillViewport_DiagFill_DoSwap + .refto L64E3 +L64E3: jsr SwapFillColors +FlipPagesFillViewport_DiagFill_Fill: + .refto FlipPagesFillViewport_DiagFill_Fill + .refto L64E6 +L64E6: jsr FillMixedViewportRows + +;;; Fill rows of viewport *below* the sky/ground diagonal + lda $F0 + cmp #99 ; viewport rows + beq L6515 + lda $E9 + bne L64FC + lda $EC + cmp $EA + bcc L6506 + jmp L6509 + +FlipPagesFillViewport_BelowDiag_TestSwap: + .refto FlipPagesFillViewport_BelowDiag_TestSwap + .refto L64FC +L64FC: lda $EB + bne L6509 + lda $EA + cmp $EC + bcs L6509 +FlipPagesFillViewport_BelowDiag_DoSwap: + .refto FlipPagesFillViewport_BelowDiag_DoSwap + .refto L6506 +L6506: jsr SwapFillColors +FlipPagesFillViewport_BelowDiag_Fill: + .refto FlipPagesFillViewport_BelowDiag_Fill + .refto L6509 +L6509: lda #99 + sta $E7 + sec + sbc $F0 + sta $F1 + jsr FillViewportRows + +;;; Finish up - update artificial horizon too +FlipPagesFillViewport_FullSky: + .refto FlipPagesFillViewport_FullSky + .refto L6515 +L6515: + jsr UpdateArtificialHorizon + rts +.endproc + +;;; ============================================================ + +.proc SwapFillColors + ldx FillColor + ldy AltFillColor + stx AltFillColor + sty FillColor + rts +.endproc + +;;; ============================================================ + +;;; Make sky/ground edge white, to avoid color clash/black pixels +.proc TidySkyGroundEdgeInRow + stx TmpStash ; self-modified opcode (RTS / STX) + ldy AltColorPixelToByteTable,x + lda PixelToBitNumberTable+4,x + tax + lda L149E,x + ora (HiresRowPtr),y + sta (HiresRowPtr),y + cpy #39 ; max col + bcs :+ + iny + lda L14A5,x + ora (HiresRowPtr),y + sta (HiresRowPtr),y +: + ldx TmpStash + rts +.endproc + +;;; ============================================================ + +;;; Inputs: +;;; $E7 = bottom-most row (start) +;;; $F1 = number of rows +.proc FillViewportRows + ldy $E7 + lda HiresTableLo,y + sta HiresRowPtr + lda HiresTableHi,y + sta HiresRowPtr+1 + lda FillColor + beq L6555 ; black, so use our optimized routine + cmp #$FF + bne L6567 ; not white, so use generic routine + + ;; Optimized routine when fill is solid black or white +FillViewportRows_SolidStart: + .refto FillViewportRows_SolidStart + .refto L6555 +L6555: ldy #39 ; columns +FillViewportRows_SolidLoop: + .refto FillViewportRows_SolidLoop + .refto L6557 +L6557: sta (HiresRowPtr),y + dey + sta (HiresRowPtr),y + dey + sta (HiresRowPtr),y + dey + sta (HiresRowPtr),y + dey + bpl L6557 + bmi L656A ; always + +;;; Color fill: call the unrolled helper which alternates between the +;;; even and odd column color patterns to avoid NTSC color fringing. +FillViewportRows_ColorFill: + .refto FillViewportRows_ColorFill + .refto L6567 +L6567: jsr DrawSkyGroundRowUnrolledHelper + +;;; Loop tail: $E7 is current row (decrement), $F1 is rows remaining. +FillViewportRows_Tail: + .refto FillViewportRows_Tail + .refto L656A +L656A: dec $E7 + dec $F1 + bne FillViewportRows ; loop + rts +.endproc + +;;; ============================================================ + +;;; Used for edge-to-edge fills of sky/ground +;;; The EOR #$7F are modified to be LDA #$xx at the transition point +;;; e.g. from the inital blue to green + +DrawSkyGroundRowUnrolled: + sta (HiresRowPtr),y + iny + eor #$7F ; self-modified + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + iny + eor #$7F + sta (HiresRowPtr),y + rts + +;;; DrawSkyGroundRow entry when the transition column is exactly $27 +;;; (= one past the last column). Skip the row split and just fall +;;; through to the right-side fill helper that emits a span if the +;;; FillColor is non-trivial. +DrawSkyGroundRow_NoSplit: + .refto DrawSkyGroundRow_NoSplit + .refto L6637 +L6637: lda FillColor + beq L669D + cmp #$FF + beq L669D +DrawSkyGroundRowUnrolledHelper: + ldy #$00 + jmp DrawSkyGroundRowUnrolled + +;;; Modify the line fill to transition sky/ground at appropriate column + +DrawSkyGroundRow: + ldy $E7 ; self-modified; turned into JMP AltDrawSkyGroundRow + lda HiresTableLo,y + sta HiresRowPtr + lda HiresTableHi,y + sta HiresRowPtr+1 + stx $B2 + + ;; Compute transition point (sky->ground or ground->sky), + ;; and modify `DrawSkyGroundRowUnrolled` to switch colors. + lda ColorPixelToByteTable,x + cmp #$27 + beq L6637 + asl a + asl a + adc ColorPixelToByteTable,x + tay ; Y is offset into `DrawSkyGroundRowUnrolled` + ror a + lda AltFillColor + bcs :+ ; even or odd column? + eor #$7F ; flip if needed +: sta DrawSkyGroundRowUnrolled+4,y + lda #OPC_LDA_imm + sta DrawSkyGroundRowUnrolled+3,y + sty TmpStash ; stash for later + ldy #$00 + lda FillColor + + jsr DrawSkyGroundRowUnrolled + + ldy TmpStash + lda #OPC_EOR_imm ; flip even/odd + sta DrawSkyGroundRowUnrolled+3,y + lda #$7F ; mask + sta DrawSkyGroundRowUnrolled+4,y + lda AltFillColor + beq L668B + cmp #$FF + bne L6695 +;;; Right-of-transition fill: emit a DrawColorSpan from B2+1 to col $8B +;;; (= the right side of the row past the sky/ground transition). +DrawSkyGroundRow_FillRightSide: + .refto DrawSkyGroundRow_FillRightSide + .refto L668B +L668B: lda #$8B + sta $27 + sec + sbc $B2 + jsr DrawColorSpan +DrawSkyGroundRow_TestFillColor: + .refto DrawSkyGroundRow_TestFillColor + .refto L6695 +L6695: lda FillColor + beq L669D + cmp #$FF + bne L66A4 +;;; Left-of-transition fill: emit a DrawColorSpan from col 0 to B2. +DrawSkyGroundRow_FillLeftSide: + .refto DrawSkyGroundRow_FillLeftSide + .refto L669D +L669D: lda $B2 + sta $27 + jsr DrawColorSpan +DrawSkyGroundRow_Exit: + .refto DrawSkyGroundRow_Exit + .refto L66A4 +L66A4: ldx $B2 + rts + +;;; ============================================================ + + brk + +;;; ============================================================ +;;; Scenery Interpreter Opcode Table +;;; +;;; Drives the scenery streaming engine: each scenery record's first +;;; byte (0..$45) is doubled and used as an index here. Anything from +;;; $46 upward, or with bit 7 set, terminates the stream. +;;; +;;; Many slots point at `SceneryOpInvalid` (the "unknown opcode" +;;; trap). Recognised opcodes are documented inline beside their +;;; targets - see `SceneryInterpreterStep` below for the dispatcher. +SceneryOpcodeTable: + .addr SceneryOpEmitV1XformAndPlot ; $00 plot-vertex + .addr SceneryOpEmitV1Xform80C5 ; $01 emit V1 xform-A + .addr SceneryOpEmitV2Xform80C5 ; $02 emit V2 xform-A + .addr SceneryOpCall64K_2 ; $03 64K callback #2 + .addr SceneryOpCullByOutcodeList ; $04 cull by outcode AND + .addr SceneryOpADFRecord ; $05 ADF station record + .addr SceneryOpDrawLine ; $06 + .addr SceneryOpEnterLocalFrame ; $07 enter sub-record frame + .addr SceneryOpInvalid ; $08 + .addr SceneryOpSkip3 ; $09 + .addr SceneryOpSkip3 ; $0A + .addr SceneryOpJumpRelative ; $0B + .addr SceneryOpInvalid ; $0C + .addr SceneryOpHeader ; $0D scenery header + .addr SceneryOpCall64K ; $0E 64K-only callback + .addr SceneryOpInvalid ; $0F + .addr SceneryOpInvalid ; $10 + .addr SceneryOpSkip1 ; $11 + .addr SceneryOpSetColor ; $12 + .addr SceneryOpJumpIfBeyondXY ; $13 + .addr SceneryOpJumpIfBeyondXYZ ; $14 + .addr SceneryOpInvalid ; $15 + .addr SceneryOpInvalid ; $16 + .addr SceneryOpInvalid ; $17 + .addr SceneryOpSubInvoke ; $18 recursive subrecord + .addr SceneryOpReturn ; $19 no-op + .addr SceneryOpWriteWord ; $1A *dst = *src (word) + .addr SceneryOpModeWhite ; $1B + .addr SceneryOpDayOnly ; $1C + .addr SceneryOpNAVRecord ; $1D NAV station record + .addr SceneryOpCOMRecord ; $1E COM/airport record + .addr SceneryOpInvalid ; $1F + .addr SceneryOpCullIfOutside1 ; $20 1-axis cull + .addr SceneryOpCullIfOutside2 ; $21 2-axis cull + .addr SceneryOpCullIfOutside3 ; $22 3-axis cull + .addr SceneryOpJumpIfBitsClear ; $23 mask & *ptr == 0 -> jump + .addr SceneryOpPushOriginWithStash ; $24 push frame + stash + .addr SceneryOpStoreImmWord ; $25 *dst = imm16 + .addr SceneryOpInvalid ; $26 + .addr SceneryOpInvalid ; $27 + .addr SceneryOpJumpIfWordCompare ; $28 cmp *p1 vs *p2 + .addr SceneryOpCopyToD2 ; $29 + .addr SceneryOpInvalid ; $2A + .addr SceneryOpEmitCurve ; $2B 8-segment curve + .addr SceneryOpInvalid ; $2C + .addr SceneryOpInvalid ; $2D + .addr SceneryOpInvalid ; $2E + .addr SceneryOpResetState ; $2F + .addr SceneryOpInvalid ; $30 + .addr SceneryOpRefreshCachedXform80C5 ; $31 refresh-cache xform-A + .addr SceneryOpVertexCachedV1 ; $32 cached V1 + .addr SceneryOpVertexCachedV2 ; $33 cached V2 + .addr SceneryOpInvalid ; $34 + .addr SceneryOpVertexCachedDraw ; $35 cached + plot + .addr SceneryOpInvalid ; $36 + .addr SceneryOpInvalid ; $37 + .addr SceneryOpInvalid ; $38 + .addr SceneryOpInvalid ; $39 + .addr SceneryOpInvalid ; $3A + .addr SceneryOpInvalid ; $3B + .addr SceneryOpInvalid ; $3C + .addr SceneryOpInvalid ; $3D + .addr SceneryOpInvalid ; $3E + .addr SceneryOpInvalid ; $3F + .addr SceneryOpEmitV1Xform7EBC ; $40 emit V1 xform-B + .addr SceneryOpEmitV2Xform7EBC ; $41 emit V2 xform-B + .addr SceneryOpRefreshCachedXform7EBC ; $42 refresh-cache xform-B + .addr SceneryOpInvalid ; $43 + .addr SceneryOpInvalid ; $44 + .addr SceneryOpInvalid ; $45 + +;;; ============================================================ + +;;; ============================================================ +;;; Scenery Interpreter +;;; +;;; The active scenery record stream is at ($8B). Each iteration of +;;; `SceneryInterpreterStep` reads one byte, treats it as an opcode, +;;; and dispatches through `SceneryOpcodeTable`. Most opcode handlers +;;; tail-jump back here (often via `SceneryOpAdvanceAndContinue`) +;;; until the stream terminates. + + ;; Per-frame entry from main loop: snapshot the lookup-flag + ;; gates ($08F2/$0909/$08A8) into their "in-progress" mirrors + ;; ($08F3/$090A/$08A9), so any matching opcodes encountered + ;; while interpreting the stream can latch their results. +ProcessScenery: + lda $08F2 + sta $08F3 + lda $0909 + sta $090A + lda $08A8 + sta $08A9 + +SceneryInterpreterEntry: + lda #$00 + sta $08C4 + sta $8A +SceneryInterpreterStep: + lda $33 + cmp #$20 + bcc :+ + jsr L8752 +: ldy #$00 + lda ($8B),y ; opcode + bmi SceneryStreamEnd ; bit 7 set ($80..$FF) -> end + cmp #$46 + bmi SceneryDispatch ; $00..$45 -> dispatch + ;; ($46..$7F) also falls through to SceneryStreamEnd: the + ;; opcode table only covers $00..$45, so any byte in [$46..$7F] + ;; is also a stream terminator (neither dispatched nor returned). +SceneryStreamEnd: + lda #$00 + sta $090A ; clear in-progress flags + sta $08F3 + sta $08A9 + rts + +SceneryOpInvalid: + lda #$90 ; unknown opcode: call SwapPagesWithStash to + jsr L1F89 ; stash state, then fall through into + ; SceneryDispatch with garbage in A -- the + ; engine's "recover and keep running" path. +SceneryDispatch: + asl a + tax + lda SceneryOpcodeTable,x + sta L00A5 + lda SceneryOpcodeTable+1,x + sta L00A5+1 + jmp (L00A5) + +;;; Opcode $18: subroutine call. The next two bytes are a relative +;;; offset; push the current pointer, jump to the target, recurse, and +;;; restore on return. +SceneryOpSubInvoke: + lda $8B + clc + adc #$03 + pha + lda $8C + adc #$00 + pha + inc $08B5 + iny + lda ($8B),y + clc + adc $8B + tax + iny + lda ($8B),y + adc $8C + sta $8C + stx $8B + jsr SceneryInterpreterStep + dec $08B5 + pla + sta $8C + pla + sta $8B + jmp SceneryInterpreterStep + +;;; Opcodes $09/$0A: skip a 3-byte record. +SceneryOpSkip3: + lda #$03 + jmp SceneryOpAdvanceAndContinue + +;;; Opcode $0E: 64K-only call into `SceneryOp64KCallback` in chunk3. +;;; On 48K it falls through to `SceneryOpReturn` which is just an +;;; `rts` -- this is NOT a "skip and continue" no-op; rts pops one +;;; level of dispatcher recursion. If $0E is encountered at the top +;;; level, the rts returns to the loop that called `ProcessScenery`. +;;; If reached from inside a `$18 SubInvoke` recursion, the rts +;;; returns to the parent section. Either way, the cursor is NOT +;;; advanced past the $0E byte by this handler. +SceneryOpCall64K: + lda Has64K + beq SceneryOpReturn + jmp SceneryOp64KCallback + +;;; Opcode $19: explicit section-end (rts). Pops one level of +;;; recursion just like the $0E 48K path. +SceneryOpReturn: + rts + +;;; Opcode $2F: arm polygon-fill mode for the next $29 SceneryOpCopyToD2. +;;; Sets $2C = $FF (polygon-mode flag = true) and $B5 = 0 (vertex +;;; output count = 0). Subsequent $00/$01/$40 vertex emits APPEND +;;; into the polygon buffer instead of drawing a line; the closing +;;; $29 then runs the 4-pass clipper + scan-fill. Falls through into +;;; SceneryOpSkip1 below for the 1-byte advance. +SceneryOpResetState: + lda #$FF + sta $2C + lda #$00 + sta $B5 + + +;;; Opcode $11: skip a 1-byte record. +SceneryOpSkip1: + lda #$01 + jmp SceneryOpAdvanceAndContinue + +;;; Opcode $29: copy 9 bytes of saved state from $07.. into $D2.., +;;; then advance and continue with extended drawing. +SceneryOpCopyToD2: + lda $2C + bne Op29PolygonFillBranch + ;; Line-emit branch ($2C clear): copy 9 bytes of saved V1 shadow + ;; from $07.. into V2 slot $D2.., then emit a single line v1->v2. +Op29LineEmitBranch: + .refto Op29LineEmitBranch + ldx #$09 +Op29LineEmitBranch_CopyLoop: + .refto Op29LineEmitBranch_CopyLoop + .refto L67CE +L67CE: lda $07,x + sta $D2,x + dex + bne L67CE + lda #$01 + jsr AddTo8B + jmp EmitClippedLine + +;;; Polygon-fill branch ($2C set): if D3 (= polygon-outcode AND) shows +;;; the polygon is fully visible AND the vertex count $B5 is non-zero, +;;; invoke the scan-fill kernel. Always clears $2C at the end so +;;; subsequent $40/$41 emits resume drawing lines. +Op29PolygonFillBranch: + .refto Op29PolygonFillBranch + .refto L67DD +L67DD: lda $D3 + bmi L67E8 + dec $B5 + bmi L67E8 + jsr PolygonScanFillSetup +Op29PolygonFillBranch_ClearAndAdvance: + .refto Op29PolygonFillBranch_ClearAndAdvance + .refto L67E8 +L67E8: lda #$00 + sta $2C + lda #$01 + jmp SceneryOpAdvanceAndContinue + +;;; ============================================================ + +.proc AddTo8B + clc + adc $8B + sta $8B + bcc :+ + inc $8C +: rts +.endproc + +;;; ============================================================ + + lda #$02 + + ;; Common opcode tail: A holds the record's byte count, so + ;; advance the scenery pointer past it and resume the + ;; interpreter loop. +SceneryOpAdvanceAndContinue: + jsr AddTo8B + jmp SceneryInterpreterEntry + +;;; ============================================================ + + +;;; Process vertex 2 ($D4..$D9): apply the currently-selected vertex +;;; transform (jsr target self-modified via `L6806`), then either +;;; * project it to screen if `$2C` says we need 2D coords, or +;;; * append the transformed copy to the primary vertex array +;;; (advancing `$B5`, capped at 60 vertices). +ProcessVertex2: + .refto ProcessVertex2 +L6803: ldy #$D4 +L6806 := *+1 + jsr L7EBC ; self-modified jsr (transform-A or transform-B) + lda $2C + beq L6839 + ldy $B5 + cpy #$3C ; cap at 60 vertices + bcs L6843 + inc $B5 + lda $D4 + sta PrimVertXLo,y + lda $D5 + sta PrimVertXHi,y + lda $D6 + sta PrimVertYLo,y + lda $D7 + sta PrimVertYHi,y + lda $D8 + sta PrimVertZLo,y + lda $D9 + sta PrimVertZHi,y + and $D3 ; accumulate outcode AND across the polygon + sta $D3 + jmp L6843 + +;;; ProcessVertex2 line-mode branch (= $2C clear): classify the +;;; transformed V2 against the frustum, and if no axis is fully clipped +;;; ($D3 still 0) project to screen coords. Otherwise just exit so the +;;; caller's polygon outcode stays. +ProcessVertex2_LineMode: + .refto ProcessVertex2_LineMode + .refto L6839 +L6839: jsr ClassifyVertex2 + lda $D3 + bne L6843 + jsr ProjectV2ToScreen +;;; ProcessVertex2 exit. Shared exit point reached from polygon-mode +;;; vertex-cap-hit branch, polygon-mode store-complete branch, and +;;; line-mode classify+project branch. +ProcessVertex2_Exit: + .refto ProcessVertex2_Exit + .refto L6843 +L6843: rts + +;;; ============================================================ + +;;; Process vertex 1 ($CB..$D0): companion to `ProcessVertex2`. If we +;;; need 2D coords we classify and project; otherwise we append to +;;; the primary vertex array. Note this path overwrites `$D3` rather +;;; than ANDing — it's the first vertex of the sub-polygon. +ProcessVertex1: + .refto ProcessVertex1 +L6844: ldy #$CB +L6847 := *+1 + jsr L7EBC ; self-modified jsr (transform-A or transform-B) + lda $2C + beq L6874 + ldy $B5 + inc $B5 + lda $CB + sta PrimVertXLo,y + lda $CC + sta PrimVertXHi,y + lda $CD + sta PrimVertYLo,y + lda $CE + sta PrimVertYHi,y + lda $CF + sta PrimVertZLo,y + lda $D0 + sta PrimVertZHi,y + sta $D3 ; seed outcode AND with this vertex's zHi + jmp L6843 + +;;; ProcessVertex1 line-mode branch (= $2C clear): classify and project +;;; V1 to screen coords if it's in-frustum. +ProcessVertex1_LineMode: + .refto ProcessVertex1_LineMode + .refto L6874 +L6874: jsr ClassifyVertex1 + lda $CA + bne L687E + jsr ProjectV1ToScreen +;;; Save vertex 1 + outcode + screen coords to the shadow registers +;;; at $07..$10 so the caller can recover them after the next call +;;; trashes $C9..$D2. +SnapshotVertex1ToShadow: + .refto SnapshotVertex1ToShadow + .refto L687E +L687E: ldx #$09 +SnapshotVertex1ToShadow_Loop: + .refto SnapshotVertex1ToShadow_Loop + .refto L6880 +L6880: lda $C9,x + sta $07,x + dex + bne L6880 + rts + +;;; Scenery opcode $01: select transform-A ($80C5) for vertex 1, run +;;; `ProcessVertex1` from the scenery stream, then resume. +SceneryOpEmitV1Xform80C5: + ldx #TransformVertex80C5 + jmp L6893 + +;;; Scenery opcode $40: select transform-B ($7EBC) for vertex 1, run +;;; `ProcessVertex1` from the scenery stream, then resume. +SceneryOpEmitV1Xform7EBC: + ldx #TransformVertex7EBC +EmitV1Common: + .refto EmitV1Common + .refto L6893 +L6893: stx L6847 + sta L6847+1 + jsr L6844 +;;; Continue dispatcher after a vertex emit completes WITHOUT drawing +;;; a line. Reached when the V1 emit ($01/$40) finishes (= they always +;;; take this path) and when V2 emit ($02/$41) finds polygon-mode set +;;; ($2C != 0) so the vertex was appended to the polygon buffer +;;; instead of triggering a line draw. +ResumeAfterVertexEmit: + .refto ResumeAfterVertexEmit + .refto L689C +L689C: jmp SceneryInterpreterStep + +;;; Scenery opcode $02: select transform-A ($80C5) for vertex 2, run +;;; `ProcessVertex2` from the scenery stream. If still inside the +;;; frustum (`$2C` set), continue; otherwise drop into the polygon +;;; emitter at `EmitClippedLine`. +SceneryOpEmitV2Xform80C5: + ldx #TransformVertex80C5 + jmp L68AA + +;;; Scenery opcode $41: select transform-B ($7EBC) for vertex 2. +SceneryOpEmitV2Xform7EBC: + ldx #TransformVertex7EBC +EmitV2Common: + .refto EmitV2Common + .refto L68AA +L68AA: stx L6806 + sta L6806+1 + jsr L6803 + lda $2C + bne L689C + jmp EmitClippedLine + +;;; Scenery opcode $32: read a vertex index from the stream, point +;;; ($2D) at the cached vertex (8 bytes per slot at $0140+idx*8). If +;;; the cached vertex is still useable (`$2C` set after lookup) just +;;; copy it into the primary array via `EmitPrimaryVertex`. Otherwise +;;; fall through to load the cached 8-byte record into vertex 1 +;;; ($CB..$D2) and its shadow ($09..$10), with the high bit of $D2 +;;; selecting the outcode source. +SceneryOpVertexCachedV1: + jsr L6987 + lda $2C + beq L68C7 + jsr L6919 + jmp L68E4 + +VertexCachedV1_LineMode: + .refto VertexCachedV1_LineMode + .refto L68C7 +L68C7: ldy #$07 + lda #$00 + sta $CA + sta $08 +VertexCachedV1_CopyLoop: + .refto VertexCachedV1_CopyLoop + .refto L68CF +L68CF: lda ($2D),y + sta $CB,y + sta $09,y + dey + bpl L68CF + lda $D2 + bpl L68E4 + lda $D1 + sta $CA + sta $08 +VertexCachedV1_Advance: + .refto VertexCachedV1_Advance + .refto L68E4 +L68E4: lda #$01 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $33: same as op $32 but for vertex 2 ($D4..$DB) +;;; and tail-jumps to the polygon emitter (`EmitClippedLine`) instead of the +;;; opcode-advance path. +SceneryOpVertexCachedV2: + jsr L6987 + lda $2C + beq L68FB + jsr L6919 + lda #$01 + jsr AddTo8B + jmp SceneryInterpreterStep + +VertexCachedV2_LineMode: + .refto VertexCachedV2_LineMode + .refto L68FB +L68FB: lda #$00 + sta $D3 + ldy #$07 +VertexCachedV2_CopyLoop: + .refto VertexCachedV2_CopyLoop + .refto L6901 +L6901: lda ($2D),y + sta $D4,y + dey + bpl L6901 + lda $DB + bpl L6911 + lda $DA + sta $D3 +VertexCachedV2_Emit: + .refto VertexCachedV2_Emit + .refto L6911 +L6911: lda #$01 + jsr AddTo8B + jmp EmitClippedLine + +;;; Append the next 6 bytes from ($2D) into the six "primary" +;;; vertex columns at PrimVertXLo/PrimVertXHi/PrimVertYLo/PrimVertYHi/PrimVertZLo/PrimVertZHi (indexed +;;; by `$B5`, which is auto-incremented). The last byte's outcodes +;;; are ANDed into `$D3` so the caller can quickly tell whether the +;;; whole sub-polygon is offscreen. +EmitPrimaryVertex: + .refto EmitPrimaryVertex +L6919: ldy #$00 + ldx $B5 + inc $B5 + lda ($2D),y + sta PrimVertXLo,x + iny + lda ($2D),y + sta PrimVertXHi,x + iny + lda ($2D),y + sta PrimVertYLo,x + iny + lda ($2D),y + sta PrimVertYHi,x + iny + lda ($2D),y + sta PrimVertZLo,x + iny + lda ($2D),y + sta PrimVertZHi,x + and $D3 ; accumulate outcode AND across the polygon + sta $D3 + rts + +;;; Scenery opcode $31: load a stored vertex (via `L6987`), apply +;;; transform-A, and write the transformed result back to the stored +;;; slot — but only if classification leaves a non-zero outcode. +;;; `$DB..$E2` snapshots vertex 2's prior state so we can restore it +;;; afterwards (this op is a "transform-and-cache" that must not +;;; perturb the in-flight polygon). +SceneryOpRefreshCachedXform80C5: + ldx #TransformVertex80C5 + jmp L6952 + +;;; Scenery opcode $42: same as op $31 with transform-B. +SceneryOpRefreshCachedXform7EBC: + ldx #TransformVertex7EBC +RefreshCachedXform_Common: + .refto RefreshCachedXform_Common + .refto L6952 +L6952: stx L6806 + sta L6806+1 + jsr L6987 + ldx #$09 +RefreshCachedXform_SnapshotV2: + .refto RefreshCachedXform_SnapshotV2 + .refto L695D +L695D: lda $D2,x ; snapshot vertex 2 state + sta $DB,x + dex + bne L695D + lda #$FF + sta $DB + jsr L6803 + lda $D3 + beq L6971 + sta $DA +RefreshCachedXform_WriteCache: + .refto RefreshCachedXform_WriteCache + .refto L6971 +L6971: ldy #$07 +RefreshCachedXform_WriteLoop: + .refto RefreshCachedXform_WriteLoop + .refto L6973 +L6973: lda $D4,y ; write transformed vertex back to cache + sta ($2D),y + dey + bpl L6973 + ldx #$09 +RefreshCachedXform_RestoreV2: + .refto RefreshCachedXform_RestoreV2 + .refto L697D +L697D: lda $DB,x ; restore vertex 2 from snapshot + sta $D2,x + dex + bne L697D + jmp SceneryInterpreterStep + +;;; Read the next byte of the scenery stream as a vertex index, then +;;; set ($2D,$2E) = $0140 + index*8 (eight bytes per stored vertex). +;;; The caller follows up by reading 8 bytes through ($2D),Y to pull +;;; that vertex's components. +SetVertexPointerFromStream: + .refto SetVertexPointerFromStream +L6987: iny + lda ($8B),y +;;; Set ($2D,$2E) = $0140 + A*8 (= 8 bytes per cached vertex slot). +;;; Entry point used when the vertex index is already in A (= called +;;; from $04 SceneryOpCullByOutcodeList). Caller must NOT advance $8B +;;; — the index byte was inline in a different record. +SetVertexPointerFromIdx: + .refto SetVertexPointerFromIdx + .refto L698A +L698A: ldx #$00 + stx $2E + asl a + rol $2E + asl a + rol $2E + asl a + rol $2E + adc #$40 + sta $2D + lda $2E + adc #$01 + sta $2E + lda #$01 + jsr AddTo8B + rts + +;;; Scenery opcode $35: load a cached vertex into vertex 1 ($CB..$D2), +;;; recover its outcode from $D2's high bit, then continue at L69D5 +;;; (which decides whether the vertex is on-screen and plots a single +;;; pixel via PlotColorPixel if so). +SceneryOpVertexCachedDraw: + jsr L6987 + lda #$01 + jsr AddTo8B + ldy #$07 + lda #$00 + sta $CA +VertexCachedDraw_LoadLoop: + .refto VertexCachedDraw_LoadLoop + .refto L69B5 +L69B5: lda ($2D),y + sta $CB,y + dey + bpl L69B5 + lda $D2 + bpl L69D5 + lda $D1 + sta $CA + jmp L69D5 + +;;; Scenery opcode $00: select transform-A for vertex 1, run +;;; `ProcessVertex1`, then if the vertex is in-frustum project to +;;; screen and plot a single pixel. +SceneryOpEmitV1XformAndPlot: + ldx #TransformVertex80C5 + stx L6847 + sta L6847+1 + jsr L6844 +;;; Common tail for $00 EmitV1XformAndPlot and $35 VertexCachedDraw: +;;; if V1 outcode is 0 (= in frustum), project and plot a pixel via +;;; PlotColorPixel ($7963). Otherwise just resume dispatcher. +EmitV1AndPlotTail: + .refto EmitV1AndPlotTail + .refto L69D5 +L69D5: lda $CA + beq L69DC + jmp SceneryInterpreterStep + +EmitV1AndPlotTail_DoPlot: + .refto EmitV1AndPlotTail_DoPlot + .refto L69DC +L69DC: jsr ProjectV1ToScreen + tax + jsr L7963 +EmitV1AndPlotTail_Resume: + .refto EmitV1AndPlotTail_Resume + .refto L69E3 +L69E3: jmp SceneryInterpreterStep + +;;; Scenery opcode $2B: read two consecutive 4-byte vertices from the +;;; stream (xform-B = XZ only; Y comes from the section base), then +;;; draw a curve between them as 8 line segments by linear midpoint +;;; subdivision. Total record = opcode + 4-byte V1 + 4-byte V2 = 9 +;;; bytes (one byte of the V2 stream overlaps V1's last byte via +;;; `dec $8B` rewind so the dispatcher only advances by 9, not 10). +SceneryOpEmitCurve: + ldx #TransformVertex7EBC + stx L6806 + stx L6847 + sta L6806+1 + sta L6847+1 + jsr L6844 ; read & transform vertex 1 + dec $8B ; back the stream pointer up by 1 + lda $8B ; so vertex 2 starts at the same opcode + cmp #$FF + bne L6A03 + dec $8C +EmitCurve_ReadV2: + .refto EmitCurve_ReadV2 + .refto L6A03 +L6A03: jsr L6803 ; read & transform vertex 2 + lda $CA + and $D3 ; both vertices outside same half-space? + bne L69E3 ; -> cull whole curve +;;; Align the V1/V2 shift exponents so they're at the same scale before +;;; subdivision. Halve V1 (HalveTriple16 at $CB) or V2 (at $D4) until +;;; their auto-scale exponents ($2F) match. $08EE holds V1's exponent +;;; from the first transform. +EmitCurve_AlignExponents: + .refto EmitCurve_AlignExponents + .refto L6A0C +L6A0C: lda $2F ; align the coords' shift exponents + cmp $08EE + beq L6A2A + bcs L6A20 + ldx #$CB + jsr L6ACD ; HalveTriple16 vertex 1 + dec $08EE + jmp L6A0C + +EmitCurve_HalveV2: + .refto EmitCurve_HalveV2 + .refto L6A20 +L6A20: ldx #$D4 + jsr L6ACD ; HalveTriple16 vertex 2 + dec $2F + jmp L6A0C + +;;; Compute step = (V2 - V1) / 16 into ($9E, $A0, $A2) and patch the +;;; EmitClippedLine tail to RTS so the segment loop can call repeatedly. +EmitCurve_BuildStep: + .refto EmitCurve_BuildStep + .refto L6A2A +L6A2A: SUB16 $D4, $CB, $9E ; ($9E,$A0,$A2) = (V2 - V1) / 16 + SUB16 $D6, $CD, $A0 + SUB16 $D8, $CF, $A2 + ldx #$9E + jsr L6ACD + jsr L6ACD + jsr L6ACD + jsr L6ACD + lda #$60 + sta L6B4F + lda #$08 ; 8 segments + sta $0A51 +;;; Per-segment loop: classify+project the current V1 and V2 = V1+step, +;;; emit the line, then advance V1 by step. Iterates 8 times via $0A51. +EmitCurve_SegmentLoop: + .refto EmitCurve_SegmentLoop + .refto L6A69 +L6A69: jsr L6874 ; classify+project endpoint 1 + + ADD16 $CB, $9E, $D4 ; vertex 2 = vertex 1 + step + ADD16 $CD, $A0, $D6 + ADD16 $CF, $A2, $D8 + jsr L6839 ; classify+project endpoint 2 + jsr EmitClippedLine ; emit segment + ADD16 $CB, $9E, $CB ; advance vertex 1 by step + ADD16 $CD, $A0, $CD + ADD16 $CF, $A2, $CF + dec $0A51 + bne L6A69 + lda #$4C + sta L6B4F + jmp SceneryInterpreterStep + +;;; Arithmetic-shift-right of three consecutive 16-bit signed values +;;; starting at $00,X. Used to scale x/y/z coords down (e.g. when +;;; clipping or zooming) without disturbing their sign. +HalveTriple16: + .refto HalveTriple16 +L6ACD: lda $01,x + rol a + ror $01,x + ror $00,x + lda $03,x + rol a + ror $03,x + ror $02,x + lda $05,x + rol a + ror $05,x + ror $04,x + rts + +;;; Emit a single clipped, projected line segment from vertex 1 to +;;; vertex 2. Used by the polygon/line opcodes ($02/$33/$41/$2B etc.). +;;; +;;; If both vertices' outcodes share a half-plane the segment is +;;; trivially culled (`L6B2F`). Otherwise, vertex 1 (and/or 2) is +;;; clipped to the frustum, projected to screen, and `DrawColorLine` +;;; fills it in. Cleanup at L6B2F restores vertex 1 ($C9..$D2) from +;;; the unclipped vertex 2 shadow at $DB so the next segment can +;;; chain off this endpoint without re-transforming. +;;; +;;; The trailing `jmp` at `L6B4F` is self-modified by +;;; `SceneryOpEmitCurve`: during curve emission it's patched to +;;; `RTS` (#$60) so `EmitClippedLine` returns; afterwards it's +;;; restored to `JMP` (#$4C) so normal callers tail-jump back into +;;; the interpreter. +EmitClippedLine: + lda $CA + and $D3 + bne L6B2F ; both ends share a half-plane -> cull + lda $CA + beq L6AF7 + jsr ClipBothVerticesToFrustum + inc $08C4 + lda $CA + bne L6B2F +;;; EmitClippedLine V2 clip phase: snapshot V2 ($D2..) into shadow ($DB..) +;;; then clip V2 to the frustum. Marker $8A=1 means "V2 was clipped" +;;; so the cleanup path knows to restore V1 from the snapshot. +EmitClippedLine_ClipV2: + .refto EmitClippedLine_ClipV2 + .refto L6AF7 +L6AF7: lda $D3 + beq L6B0D + ldx #$07 +EmitClippedLine_SnapshotV2Loop: + .refto EmitClippedLine_SnapshotV2Loop + .refto L6AFD +L6AFD: lda $D2,x + sta $DB,x ; snapshot vertex 2 before clipping + dex + bne L6AFD + jsr ClipVertex2ToFrustum + inc $8A + lda $D3 + bne L6B2F +;;; Project V1 to screen if it was just clipped (= $08C4 set). +EmitClippedLine_ProjectV1IfNeeded: + .refto EmitClippedLine_ProjectV1IfNeeded + .refto L6B0D +L6B0D: lda $08C4 + beq L6B15 + jsr ProjectV1ToScreen +;;; Project V2 to screen if it was just clipped (= $8A set). +EmitClippedLine_ProjectV2IfNeeded: + .refto EmitClippedLine_ProjectV2IfNeeded + .refto L6B15 +L6B15: lda $8A + beq L6B1C + jsr ProjectV2ToScreen +;;; Load both endpoints' screen coords into $E9/$EA (V1) and $EB/$EC (V2) +;;; for the DrawColorLine call. +EmitClippedLine_PrepareEndpoints: + .refto EmitClippedLine_PrepareEndpoints + .refto L6B1C +L6B1C: LDAX $D1 + STAX $E9 + LDAX $DA + STAX $EB +EmitClippedLine_DoDraw: + .refto EmitClippedLine_DoDraw + .refto L6B2C +L6B2C: jsr DrawColorLine +;;; EmitClippedLine cleanup tail. Reached either via successful +;;; DrawColorLine fall-through OR via cull-jump from the cull tests +;;; at the top of EmitClippedLine. Restores V1 ($C9..$D2) from the +;;; appropriate snapshot: +;;; $8A != 0 (= V2 was clipped): restore V1 from v2-clip shadow $DB.. +;;; $8A == 0 (= V2 unchanged): restore V1 from current V2 ($D2..) +;;; Then clears $8A and $08C4, falls into EmitClippedLineTail. +EmitClippedLineCleanup: + .refto EmitClippedLineCleanup + .refto L6B2F +L6B2F: lda $8A ; cleanup tail + beq L6B3F + ldx #$07 +EmitClippedLineCleanup_FromShadow: + .refto EmitClippedLineCleanup_FromShadow + .refto L6B35 +L6B35: lda $DB,x ; restore vertex 1 from v2-clip shadow + sta $C9,x + dex + bne L6B35 + jmp L6B48 + +EmitClippedLineCleanup_FromV2: + .refto EmitClippedLineCleanup_FromV2 + .refto L6B3F +L6B3F: ldx #$09 +EmitClippedLineCleanup_FromV2Loop: + .refto EmitClippedLineCleanup_FromV2Loop + .refto L6B41 +L6B41: lda $D2,x ; or restore v1 from current v2 state + sta $C9,x + dex + bne L6B41 +EmitClippedLineCleanup_ClearFlags: + .refto EmitClippedLineCleanup_ClearFlags + .refto L6B48 +L6B48: lda #$00 + sta $8A + sta $08C4 +;;; EmitClippedLine tail. The JMP byte ($4C) here is self-modified by +;;; SceneryOpEmitCurve to RTS ($60) during 8-segment curve emission so +;;; the curve loop can call EmitClippedLine repeatedly without each +;;; call re-entering the interpreter. After the curve completes, the +;;; byte is restored to $4C. In MAME-patched chunk5 the corresponding +;;; byte is RTS by default, with the dispatcher driving re-entry from +;;; the caller side instead of self-looping. +EmitClippedLineTail: + .refto EmitClippedLineTail + .refto L6B4F +L6B4F: jmp SceneryInterpreterStep ; self-modified ($4C/$60: JMP/RTS) + +;;; Scenery opcode $06: copy four bytes (x0,y0,x1,y1) from the +;;; record into $E9..$EC and draw the line via `DrawColorLine`. +SceneryOpDrawLine: + iny + lda ($8B),y + sta $E8,y + cpy #$04 + bne SceneryOpDrawLine + jsr DrawColorLine + lda #$05 ; record length + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $24: push the current world origin onto the +;;; "saved camera" registers ($18/$1B/$1E) and then transform it by +;;; the sub-record's local frame. Used to enter a nested scenery +;;; frame; matching pop must follow. RadarView path uses the radar +;;; viewpoint at $0A73 instead of the live camera $5E. +SceneryOpPushOriginWithStash: + iny + lda ($8B),y + pha + LDAX $5A + STAX $18 + LDAX $62 + STAX $1E + lda RadarView + beq L6B8A + LDAX $0A73 + STAX $1B + jmp L6B92 + +PushOriginWithStash_NonRadar: + .refto PushOriginWithStash_NonRadar + .refto L6B8A +L6B8A: LDAX $5E + STAX $1B +PushOriginWithStash_RunChain: + .refto PushOriginWithStash_RunChain + .refto L6B92 +L6B92: lda #$FF + sta $AD ; "stash mode" flag + pla + tax + jsr L6BB0 + lda #$08 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $07: enter a sub-record's local coordinate frame +;;; without stashing the parent frame. The first byte selects one of +;;; ~5 axis-permutation variants (identity / x-scaled / xy-swap / +;;; y-scaled / passthrough). Length 14. +SceneryOpEnterLocalFrame: + iny + lda ($8B),y + tax + lda #$00 + sta $AD ; "no stash" flag + jsr L6BB0 + lda #$0E + jmp SceneryOpAdvanceAndContinue + +;;; Compute camera-vs-section-anchor delta. Shared body of $07 +;;; EnterLocalFrame and $24 PushOriginWithStash. Reads anchor coords +;;; from the stream, subtracts from aircraft full position +;;; ($5A/$5B, $5E/$5F, $62/$63), and stores deltas in $66/$67 (X), +;;; $68/$69 (Y), $6A/$6B (Z) plus scratch slots $18/$19, $1B/$1C, +;;; $1E/$1F. Stash flag at $AD ($00 = full chain, $FF = stashed mode +;;; that skips the $5A/$5E/$62 SBCs since the caller pre-populated +;;; the scratch slots from full position). Variant byte from caller's +;;; X selects axis-permutation cascade (0=L6CCE, 2=L6C6E, 4=L6C89, +;;; 5+=L6C53, others=L6D28 epilogue directly). +ComputeSectionCameraDelta: + .refto ComputeSectionCameraDelta + .refto L6BB0 +L6BB0: sec + lda $AD + bne L6BC3 + lda $5A + iny + sbc ($8B),y + sta $18 + lda $5B + iny + sbc ($8B),y + sta $19 +;;; ComputeSectionCameraDelta X-axis SBC (= shared by both stash and +;;; non-stash entry paths). Computes $66/$67 = $5C/$5D - stream[3..4]. +ComputeSectionCameraDelta_X: + .refto ComputeSectionCameraDelta_X + .refto L6BC3 +L6BC3: lda $5C + iny + sbc ($8B),y + sta $66 + lda $5D + iny + sbc ($8B),y + sta $67 + sec + lda RadarView + beq L6BFE + lda $AD + bne L6BEB + lda $0A73 + iny + sbc ($8B),y + sta $1B + lda ZoomLevel + iny + sbc ($8B),y + sta $1C +;;; ComputeSectionCameraDelta Y-axis SBC, radar branch: $68/$69 from +;;; ZoomLevel+1/+2 minus stream bytes. +ComputeSectionCameraDelta_Y_Radar: + .refto ComputeSectionCameraDelta_Y_Radar + .refto L6BEB +L6BEB: lda ZoomLevel+1 + iny + sbc ($8B),y + sta $68 + lda ZoomLevel+2 + iny + sbc ($8B),y + sta $69 + jmp L6C1E + +;;; ComputeSectionCameraDelta Y-axis non-radar branch. +ComputeSectionCameraDelta_Y_NonRadar: + .refto ComputeSectionCameraDelta_Y_NonRadar + .refto L6BFE +L6BFE: lda $AD + bne L6C10 + lda $5E + iny + sbc ($8B),y + sta $1B + lda $5F + iny + sbc ($8B),y + sta $1C +ComputeSectionCameraDelta_Y_NonRadarStash: + .refto ComputeSectionCameraDelta_Y_NonRadarStash + .refto L6C10 +L6C10: lda $60 + iny + sbc ($8B),y + sta $68 + lda $61 + iny + sbc ($8B),y + sta $69 +;;; ComputeSectionCameraDelta Z-axis SBC. Non-stash mode reads aircraft +;;; Z lo/mid into $1E/$1F as scratch first, then $6A/$6B = $64/$65 - +;;; stream is the upper Z delta. +ComputeSectionCameraDelta_Z: + .refto ComputeSectionCameraDelta_Z + .refto L6C1E +L6C1E: sec + lda $AD + bne L6C31 + lda $62 + iny + sbc ($8B),y + sta $1E + lda $63 + iny + sbc ($8B),y + sta $1F +ComputeSectionCameraDelta_Z_Upper: + .refto ComputeSectionCameraDelta_Z_Upper + .refto L6C31 +L6C31: lda $64 + iny + sbc ($8B),y + sta $6A + lda $65 + iny + sbc ($8B),y + sta $6B + dex + dex + bmi L6C53 + bne L6C48 + jmp L6CCE + +;;; Variant dispatch (= post dex,dex). X=2 -> swap, X=4 -> asl-4, +;;; anything else -> straight to epilogue (= no axis permutation). +FrameVariantDispatch: + .refto FrameVariantDispatch + .refto L6C48 +L6C48: cpx #$02 + beq L6C6E + cpx #$04 + beq L6C89 + jmp L6D28 + +;;; Variant 5+ axis cascade: copy scratch slots verbatim into camera +;;; delta (= identity / passthrough). Used when the variant byte was +;;; 0 or 1 before the dex,dex (= bmi taken). +FrameVariantPassthrough: + .refto FrameVariantPassthrough + .refto L6C53 +L6C53: LDAX $18 + STAX $66 + LDAX $1B + STAX $68 + LDAX $1E + STAX $6A + jmp L6D28 + +;;; Variant 2 axis cascade: swap lo/hi between scratch slot and camera +;;; delta. Used when variant byte was 4 (= dex,dex left X=2). +FrameVariantSwap: + .refto FrameVariantSwap + .refto L6C6E +L6C6E: ldx $19 + ldy $66 + stx $66 + sty $67 + ldx $1C + ldy $68 + stx $68 + sty $69 + ldx $1F + ldy $6A + stx $6A + sty $6B + jmp L6D28 + + ;; Called from chunk2 +;;; Variant 4 axis cascade: ASL each scratch hi byte 4 times into +;;; camera delta (= effectively shift delta by 4 with high nibble of +;;; scratch entering low). Used when variant byte was 6. +FrameVariantAsl4: + .refto FrameVariantAsl4 + .refto L6C89 +L6C89: lda $19 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + lda $1C + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + lda $1F + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B + jmp L6D28 + +;;; Variant 0 axis cascade: ROL-shift hi-bytes into camera delta with +;;; bit-stuffing from scratch lo bytes. Used when variant byte was 2 +;;; (= dex,dex left X=0). +FrameVariantRolShift: + .refto FrameVariantRolShift + .refto L6CCE +L6CCE: lda $66 + sta $67 + lda $19 + sta $66 + lda $18 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + asl a + rol $66 + rol $67 + lda $68 + sta $69 + lda $1C + sta $68 + lda $1B + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + asl a + rol $68 + rol $69 + lda $6A + sta $6B + lda $1F + sta $6A + lda $1E + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B + asl a + rol $6A + rol $6B +;;; $07/$24 frame-setup epilogue. After the variant cascade has set +;;; $66/$67/$68/$69/$6A/$6B (= camera-vs-section delta), forces +;;; $68/$69 to be at least 1 (avoids divide-by-zero downstream), then +;;; checks the cached scale at $35/$36. If $68/$69 changed, calls +;;; RecomputeSectionBase ($631D) to refresh $4A..$52. MAME-patched +;;; binary uses cache at $38/$39 instead. +FrameSetupEpilogue: + .refto FrameSetupEpilogue + .refto L6D28 +L6D28: lda $69 + bne L6D32 + lda $68 + bne L6D32 + inc $68 +FrameSetupEpilogue_CacheCheck: + .refto FrameSetupEpilogue_CacheCheck + .refto L6D32 +L6D32: lda $68 + cmp $35 + sta $35 + bne L6D40 + lda $69 + cmp $36 + beq L6D43 +FrameSetupEpilogue_FireRecompute: + .refto FrameSetupEpilogue_FireRecompute + .refto L6D40 +L6D40: jsr L631D +FrameSetupEpilogue_UpdateCache: + .refto FrameSetupEpilogue_UpdateCache + .refto L6D43 +L6D43: lda $69 + sta $36 + rts + +;;; Scenery opcode $0B: relative jump. Reads a 16-bit offset from +;;; the stream, computes destination = ($8B) + offset, and continues +;;; interpreting from there. +SceneryOpJumpRelative: + jsr ReadRelativeAddr + +;;; Continue interpreting from L00A5/$A6. Used as the tail of +;;; opcodes like $0B and $0E (the 64K-only callback in chunk3). +SceneryJumpToFetched: + LDAX L00A5 + STAX $8B + jmp SceneryInterpreterStep + +;;; Read two bytes from ($8B),y as a signed 16-bit offset, add to +;;; ($8B), and leave the result in L00A5/$A6. Caller pre-set Y. +ReadRelativeAddr: + iny + lda ($8B),y + clc + adc $8B + sta L00A5 + iny + lda ($8B),y + adc $8C + sta $A6 + rts + +;;; Scenery opcode $1A: copy a 16-bit word. Reads dst pointer and +;;; src pointer (4 bytes total) and stores `*src` into `*dst`. +SceneryOpWriteWord: + iny + lda ($8B),y + sta L00A5 ; dst lo + iny + lda ($8B),y + sta $A6 ; dst hi + iny + lda ($8B),y + sta $A7 ; src lo + iny + lda ($8B),y + sty $B3 + sta $A8 ; src hi + ldy #$00 + lda ($A7),y + sta (L00A5),y + iny + lda ($A7),y + sta (L00A5),y + ldy $B3 + lda #$05 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $25: store a 16-bit immediate. Reads dst pointer +;;; and two literal bytes, then writes the literal pair into `*dst`. +SceneryOpStoreImmWord: + iny + lda ($8B),y + sta $A7 ; dst lo + iny + lda ($8B),y + sta $A8 ; dst hi + iny + lda ($8B),y ; imm lo + ldx #$00 + sta ($A7,x) + iny + lda ($8B),y ; imm hi + sty $B3 + ldy #$01 + sta ($A7),y + ldy $B3 + lda #$05 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $04: cull a polygon group by outcode. Reads a +;;; jump target, then walks a list of cached vertex indices (each +;;; followed by a high-bit terminator). Each cached vertex's outcode +;;; byte ($D7 in the cache row) is ANDed into $DC; if any bit is +;;; still set after processing all vertices, every vertex shares the +;;; same out-of-frustum half-plane and the whole record is skipped. +SceneryOpCullByOutcodeList: + jsr ReadRelativeAddr + lda #$02 + jsr AddTo8B + lda #$F8 ; initial outcode-AND mask +CullByOutcodeList_ResetAccum: + .refto CullByOutcodeList_ResetAccum + .refto L6DB9 +L6DB9: sta $DC +CullByOutcodeList_NextVertex: + .refto CullByOutcodeList_NextVertex + .refto L6DBB +L6DBB: ldy #$00 + iny + lda ($8B),y + bmi L6DD9 ; high bit = end of vertex list + jsr L698A ; point ($2D) at cached vertex N + ldy #$07 + lda ($2D),y ; outcode high byte + bmi L6DCF + lda #$00 ; vertex on-screen -> reset accumulator + beq L6DB9 +CullByOutcodeList_AndOutcode: + .refto CullByOutcodeList_AndOutcode + .refto L6DCF +L6DCF: dey + lda ($2D),y ; outcode bits + and $DC + sta $DC + jmp L6DBB + +CullByOutcodeList_End: + .refto CullByOutcodeList_End + .refto L6DD9 +L6DD9: lda #$02 + jsr AddTo8B + lda $DC + beq L6DF0 ; not all vertices culled -> continue + LDAX L00A5 ; jump to alternate target + STAX $8B + jmp SceneryInterpreterStep + +;;; Scenery opcode $13: jump-if-camera-outside-XY-box. The XY check +;;; lives in `TestSceneryRangePair` which pops our return address and redirects the +;;; interpreter on failure; on success we just continue. +SceneryOpJumpIfBeyondXY: + jsr TestSceneryRangePair +CullSucceedAndContinue: + .refto CullSucceedAndContinue + .refto L6DF0 +L6DF0: jmp SceneryInterpreterStep + +;;; Scenery opcode $14: jump-if-camera-outside-XYZ-box. After the +;;; XY check (`TestSceneryRangePair`), additionally compare the camera Z component +;;; (RadarView uses ZoomLevel+1/+2; otherwise $60/$61) against the +;;; record's Z bound at $98/$99. On failure redirect; otherwise step. +SceneryOpJumpIfBeyondXYZ: + jsr TestSceneryRangePair + sec + lda RadarView + beq L6E07 + lda ZoomLevel+1 + sbc $98 + lda ZoomLevel+2 + jmp L6E0D + +JumpIfBeyondXYZ_Z_NonRadar: + .refto JumpIfBeyondXYZ_Z_NonRadar + .refto L6E07 +L6E07: lda $60 + sbc $98 + lda $61 +JumpIfBeyondXYZ_Z_TestHi: + .refto JumpIfBeyondXYZ_Z_TestHi + .refto L6E0D +L6E0D: sbc $99 + bmi L6E14 + jmp L6EE6 + +JumpIfBeyondXYZ_Continue: + .refto JumpIfBeyondXYZ_Continue + .refto L6E14 +L6E14: jmp SceneryInterpreterStep + +TestSceneryRangePair: jsr ReadRelativeAddr + iny + lda ($8B),y + sta $98 + iny + lda ($8B),y + sta $99 + lda #$64 + sta L6E49 + lda #$65 + sta L6E50 + jsr TestSceneryRangeAxis + lda #$5C + sta L6E49 + lda #$5D + sta L6E50 + jsr TestSceneryRangeAxis + lda #$09 + jsr AddTo8B + rts + +;;; Tail of TestSceneryRangePair: read one 16-bit value from the +;;; record at ($8B),Y+1..Y+2, subtract the camera-position ZP slot +;;; selected by the L6E49/L6E50 self-mod (X = $5C/$5D or Y = $64/$65) +;;; to form a signed delta in $AB/$AC, then assert |delta| <= bound +;;; (= the 16-bit value at $98/$99 written by the caller). +TestSceneryRangeAxis: iny + lda ($8B),y + sec +L6E49 := *+1 + sbc $64 ; self-modified: camera-axis lo + sta $AB ; delta lo + iny + lda ($8B),y +L6E50 := *+1 + sbc $65 ; self-modified: camera-axis hi + sta $AC ; delta hi (signed) + bmi TestSceneryRangeAxis_NegDelta + ;; delta >= 0: assert bound - delta >= 0. + lda $98 + sec + sbc $AB + lda $99 + sbc $AC + bpl TestSceneryRangeOk + jmp TestSceneryRangeReject + + ;; delta < 0: assert bound + delta >= 0 (= bound - |delta|). +TestSceneryRangeAxis_NegDelta: + .refto TestSceneryRangeAxis_NegDelta + .refto L6E63 +L6E63: lda $98 + clc + adc $AB + lda $99 + adc $AC + bmi TestSceneryRangeReject +TestSceneryRangeOk: rts + +TestSceneryRangeReject: pla + pla + pla + pla + LDAX L00A5 + STAX $8B + jmp SceneryInterpreterStep + +;;; Scenery opcode $20: 1-axis bounding-box cull. After reading a +;;; jump target, runs `TestSceneryRange` once; if the test fails the +;;; helper redirects the interpreter to the alternate target instead +;;; of advancing past this record. +SceneryOpCullIfOutside1: + jsr ReadRelativeAddr + jsr TestSceneryRange + lda #$09 ; record length on the inside path + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $21: 2-axis bounding-box cull. +SceneryOpCullIfOutside2: + jsr ReadRelativeAddr + jsr TestSceneryRange + jsr TestSceneryRange + lda #$0F ; record length on the inside path + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $22: 3-axis bounding-box cull. +SceneryOpCullIfOutside3: + jsr ReadRelativeAddr + jsr TestSceneryRange + jsr TestSceneryRange + jsr TestSceneryRange + lda #$15 ; record length on the inside path + jmp SceneryOpAdvanceAndContinue + +;;; Range/visibility test helper. Reads a (pointer, lo, hi) triple +;;; out of the scenery record and compares the value at the pointer +;;; against the supplied bounds. On failure it pops the caller's +;;; return address and redirects the interpreter to the saved jump +;;; target (`L00A5/$A6`). +TestSceneryRange: + iny + lda ($8B),y + sta $8E + iny + lda ($8B),y + sta $8F + ldx #$00 + lda ($8E,x) + sta L6ED5 + inc $8E + bne L6EBF + inc $8F +TestSceneryRange_TestLow: + .refto TestSceneryRange_TestLow + .refto L6EBF +L6EBF: sec + iny + sbc ($8B),y + lda ($8E,x) + iny + sbc ($8B),y + bmi L6ECE + bvs L6EE4 + bvc L6ED0 +TestSceneryRange_LowOverflow: + .refto TestSceneryRange_LowOverflow + .refto L6ECE +L6ECE: bvc L6EE4 +TestSceneryRange_TestHigh: + .refto TestSceneryRange_TestHigh + .refto L6ED0 +L6ED0: iny + lda ($8B),y + sec +L6ED5 := *+1 + sbc #$00 ; self-modified + iny + lda ($8B),y + sbc ($8E,x) + bmi L6EE1 + bvs L6EE4 + bvc L6EE3 +TestSceneryRange_HighOverflow: + .refto TestSceneryRange_HighOverflow + .refto L6EE1 +L6EE1: bvc L6EE4 +TestSceneryRange_Pass: + .refto TestSceneryRange_Pass + .refto L6EE3 +L6EE3: rts + +;;; Strip caller's JSR return address from stack and take the alt-jump. +;;; Used by TestSceneryRange when the value is outside the range — the +;;; alt-jump aborts the parent CullIfOutsideN op chain. +TestSceneryRange_StripAndJump: + .refto TestSceneryRange_StripAndJump + .refto L6EE4 +L6EE4: pla + pla +;;; Take the alternate-target jump. Loads the previously-saved jump +;;; offset target from L00A5/$A6 (= written by ReadRelativeAddr) into +;;; the cursor $8B/$8C, then resumes the interpreter. Used by every +;;; conditional cull/jump op ($13/$14/$20/$21/$22/$23/$28/$04) when +;;; the predicate fires (= jump path). The L6EE4 entry above strips +;;; the calling JSR's return address first, so this can be reached +;;; from inside helper subroutines. +JumpToFetchedTarget: + .refto JumpToFetchedTarget + .refto L6EE6 +L6EE6: LDAX L00A5 + STAX $8B + jmp SceneryInterpreterStep + +;;; Scenery opcode $23: jump-if-bits-clear. Reads a target, a +;;; 16-bit pointer (into HiresRowPtr) and two mask bytes. If both +;;; (mask1 AND *(ptr+0)) and (mask2 AND *(ptr+1)) are zero, jumps to +;;; the alternate target. Used to gate scenery on aircraft state +;;; bits (e.g. landing-gear/lights flags). +SceneryOpJumpIfBitsClear: + jsr ReadRelativeAddr + iny + lda ($8B),y + sta $8E ; ptr lo (HiresRowPtr is $8E/$8F) + iny + lda ($8B),y + sta $8F + iny + lda ($8B),y + sta $A9 ; mask 1 + iny + lda ($8B),y ; mask 2 + sty $B3 + ldy #$01 + and (HiresRowPtr),y + bne L6F16 + lda $A9 + ldy #$00 + and (HiresRowPtr),y + beq L6EE6 +JumpIfBitsClear_NoJump: + .refto JumpIfBitsClear_NoJump + .refto L6F16 +L6F16: ldy $B3 + lda #$07 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $28: jump on 16-bit comparison between two memory +;;; locations. Mode byte ($A9) selects the test: +;;; 0 = equal, +;;; 1 = signed less-than, +;;; 2 = signed less-than (alternate path). +;;; Reads target, two 16-bit pointers, and on match jumps via L6EE6. +SceneryOpJumpIfWordCompare: + iny + lda ($8B),y + sta $A9 ; mode + jsr ReadRelativeAddr + iny + lda ($8B),y + sta $AB + iny + lda ($8B),y + sta $AC + iny + lda ($8B),y + sta $AD + iny + lda ($8B),y + sty $B3 + sta $AE + lda $A9 + beq L6F71 + cmp #$02 + beq L6F5A + ldy #$00 + sec + lda ($AB),y + sbc ($AD),y + iny + lda ($AB),y + sbc ($AD),y + bmi L6F55 + bvc L6F84 + bvs L6F57 +JumpIfWordCompare_Mode1Overflow: + .refto JumpIfWordCompare_Mode1Overflow + .refto L6F55 +L6F55: bvs L6F84 +JumpIfWordCompare_Mode1Jump: + .refto JumpIfWordCompare_Mode1Jump + .refto L6F57 +L6F57: jmp L6EE6 + +;;; Mode 2: signed less-than (alt path: ptr1 > ptr2 in source's +;;; reversed form). +JumpIfWordCompare_Mode2: + .refto JumpIfWordCompare_Mode2 + .refto L6F5A +L6F5A: ldy #$00 + sec + lda ($AB),y + sbc ($AD),y + iny + lda ($AB),y + sbc ($AD),y + bmi L6F6C + bvs L6F84 + bvc L6F6E +JumpIfWordCompare_Mode2Overflow: + .refto JumpIfWordCompare_Mode2Overflow + .refto L6F6C +L6F6C: bvc L6F84 +JumpIfWordCompare_Mode2Jump: + .refto JumpIfWordCompare_Mode2Jump + .refto L6F6E +L6F6E: jmp L6EE6 + +;;; Mode 0: equality test on 16-bit values *ptr1 vs *ptr2. +JumpIfWordCompare_Mode0: + .refto JumpIfWordCompare_Mode0 + .refto L6F71 +L6F71: ldy #$01 + lda ($AB),y + cmp ($AD),y + bne L6F84 + ldy #$00 + lda ($AB),y + cmp ($AD),y + bne L6F84 + jmp L6EE6 + +;;; $28 SceneryOpJumpIfWordCompare no-jump path. Restore Y from $B3 +;;; (= caller's saved Y) and advance the cursor by 8 bytes (= the +;;; full $28 record length). Reached by every "comparison did not +;;; match" branch in the three test modes (eq / signed-lt / alt-lt). +Op28NoJump: + .refto Op28NoJump + .refto L6F84 +L6F84: ldy $B3 + lda #$08 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $03: 64K-only - tail-jump into chunk3's `SceneryRotatedTransform`. +;;; In 48K mode it just advances past the 6-byte record. +SceneryOpCall64K_2: + lda Has64K + beq SceneryOpCall64K_2_Skip + jmp SceneryRotatedTransform + +SceneryOpCall64K_2_Skip: + .refto SceneryOpCall64K_2_Skip + .refto L6F93 +L6F93: lda #$06 + jmp SceneryOpAdvanceAndContinue + +;;; ============================================================ +;;; Polygon fill kernel entry. Called from $29 SceneryOpCopyToD2 when +;;; polygon-mode flag is set. Walks the PrimVerts/SecVerts vertex arrays +;;; through 4 clipping passes (Left/Top/Right/Bottom) then scan-line +;;; rasterizes the clipped polygon via PolygonScanFillRow ($7826) which +;;; emits horizontal spans through DrawColorSpan ($78E0). +;;; +;;; Sutherland-Hodgman style clipping. The 4 passes (Left, Top, Right, +;;; Bottom) all share the same structure -- only the plane equation +;;; and source/destination buffer alternate: +;;; +;;; Pass 1 (Left, L6F98): PrimVerts -> SecVerts, plane Z-X = 0 +;;; Pass 2 (Top, L7095): SecVerts -> PrimVerts, plane Z-Y = 0 +;;; Pass 3 (Right, L7190): PrimVerts -> SecVerts, plane X+Z = 0 +;;; Pass 4 (Bottom, L728B): SecVerts -> PrimVerts, plane Y+Z = 0 +;;; +;;; State across the pass: +;;; $B4 -- sticky outcode of the most recently classified vertex +;;; ($00 = inside the plane, $FF = outside) +;;; $B5 -- number of vertices in the input buffer; written back at +;;; end of pass as the new vertex count. +;;; X -- input vertex index, counts down from B5-1 to -1. +;;; Y -- output vertex index, counts up from 0. +;;; +;;; Per-pass control-flow targets (same shape in all 4 passes): +;;; +;;; _V0Outside \\__ V0 classified: branch picks $FF (outside) +;;; _V0StoreOC / or $00 (inside) and stores in $B4. +;;; _LoopTop Classify V[x]. If V[x] and V[x+1] are on +;;; the same side of the plane: skip clip; +;;; else clip the V[x]->V[x+1] edge at the +;;; plane via ClipVertex2ToX and emit the +;;; intersection vertex into output slot Y. +;;; _VNOutside \\__ Same outcode pick for V[x]. +;;; _VNStoreOC / +;;; _EmitInside Skip-clip-emit merge point. If $B4 == 0 +;;; (V[x] inside) copy V[x] to output slot Y; +;;; else skip. +;;; _NextVert dex (advance to prev input vertex); if +;;; more, jmp back to LoopTop; else fall to +;;; PassDone. +;;; _PassDone sty $B5 (new count). If 0 output verts +;;; the polygon is fully clipped -- rts. +;;; Otherwise fall through to the next pass. +;;; +;;; After all 4 passes complete, the bottom pass jumps to +;;; PolygonScanFill_ProjectVertices ($7684). +;;; +;;; PolygonScanFillSetup is the Left pass; its entry first bumps $B5 +;;; and copies V0 into the new top-of-buffer slot so the loop can +;;; treat the polygon as a closed ring (V[B5] == V[0]). +PolygonScanFillSetup: + .refto PolygonScanFillSetup + .refto L6F98 +L6F98: inc $B5 ; B5 = vertex count, ++ for ring-closing copy + ldx $B5 + lda PrimVertXLo + sta PrimVertXLo,x + lda PrimVertXHi + sta PrimVertXHi,x + lda PrimVertYLo + sta PrimVertYLo,x + lda PrimVertYHi + sta PrimVertYHi,x + lda PrimVertZLo + sta PrimVertZLo,x + lda PrimVertZHi + sta PrimVertZHi,x + ;; Classify V0 against the left plane (sign of Z-X). + lda PrimVertZLo + sec + sbc PrimVertXLo + lda PrimVertZHi + sbc PrimVertXHi + bmi L6FD3 ; Z-X < 0 -> outside left + lda #$00 ; inside + beq L6FD5 +L6FD3: lda #$FF ; outside +L6FD5: sta $B4 ; $B4 = sticky outcode of prev vertex (start = V0) + dex ; X = B5-1 = next input index (counts down to -1) + ldy #$00 ; Y = output index (counts up) + ;; Loop top: classify V[x] against the left plane. +L6FDA: lda PrimVertZLo,x + sec + sbc PrimVertXLo,x + lda PrimVertZHi,x + sbc PrimVertXHi,x + bmi L6FED ; outside left + lda #$00 ; inside + beq L6FEF +L6FED: lda #$FF ; outside +L6FEF: cmp $B4 ; same side as previous? + sta $B4 ; update sticky outcode (= V[x]'s) + beq L705F ; same side -> no edge crossing, skip clip-emit + ;; Edge V[x]->V[x+1] crosses the plane; clip and emit intersection. + lda PrimVertXLo,x + sta $D4 + lda PrimVertXHi,x + sta $D5 + lda PrimVertYLo,x + sta $D6 + lda PrimVertYHi,x + sta $D7 + lda PrimVertZLo,x + sta $D8 + lda PrimVertZHi,x + sta $D9 + lda PrimVertXLo+1,x + sta $CB + lda PrimVertXHi+1,x + sta $CC + lda PrimVertYLo+1,x + sta $CD + lda PrimVertYHi+1,x + sta $CE + lda PrimVertZLo+1,x + sta $CF + lda PrimVertZHi+1,x + sta $D0 + sty $B1 + stx $B0 + jsr ClipVertex2ToLeft ; intersection written back to $D4..$D9 + ldx $B0 + ldy $B1 + lda $D9 + sta SecVertZHi,y + ora $D8 + beq L705F ; clipped Z == 0 -> degenerate, skip emit + lda $D8 + sta SecVertZLo,y + lda $D4 + sta SecVertXLo,y + lda $D5 + sta SecVertXHi,y + lda $D6 + sta SecVertYLo,y + lda $D7 + sta SecVertYHi,y + iny ; advance output index past clipped vertex + ;; Skip-clip-emit merge: if V[x] (= prev) is inside, copy it to output. +L705F: lda $B4 + bne L7088 ; outside -> don't emit V[x] + lda PrimVertXLo,x + sta SecVertXLo,y + lda PrimVertXHi,x + sta SecVertXHi,y + lda PrimVertYLo,x + sta SecVertYLo,y + lda PrimVertYHi,x + sta SecVertYHi,y + lda PrimVertZLo,x + sta SecVertZLo,y + lda PrimVertZHi,x + sta SecVertZHi,y + iny + ;; Advance to prev input vertex; loop or fall through to pass-done. +L7088: dex + bmi L708E + jmp L6FDA + + ;; Left pass done. Write output count back, branch to Top pass + ;; if any vertices remain (else polygon is fully clipped away). +L708E: sty $B5 + lda $B5 + bne L7095 + rts + +;;; Polygon clipping pass 2: clip SecVerts→PrimVerts against the top +;;; frustum plane (= z = y). Each input edge that crosses the plane +;;; produces a new clipped intersection vertex. +PolygonClipTopPass: + .refto PolygonClipTopPass + .refto L7095 +L7095: ldx $B5 ; ring-close: copy V[0] to V[B5] (Left's count) + lda SecVertXLo + sta SecVertXLo,x + lda SecVertXHi + sta SecVertXHi,x + lda SecVertYLo + sta SecVertYLo,x + lda SecVertYHi + sta SecVertYHi,x + lda SecVertZLo + sta SecVertZLo,x + lda SecVertZHi + sta SecVertZHi,x + ;; Classify V0 against the top plane (sign of Z-Y). + lda SecVertZLo + sec + sbc SecVertYLo + lda SecVertZHi + sbc SecVertYHi + bmi L70CE ; Z-Y < 0 -> outside top + lda #$00 ; inside + beq L70D0 +L70CE: lda #$FF ; outside +L70D0: sta $B4 + dex + ldy #$00 + ;; Loop top: classify V[x] against the top plane. +L70D5: lda SecVertZLo,x + sec + sbc SecVertYLo,x + lda SecVertZHi,x + sbc SecVertYHi,x + bmi L70E8 + lda #$00 + beq L70EA +L70E8: lda #$FF +L70EA: cmp $B4 + sta $B4 + beq L715A ; same side -> skip clip-emit + lda SecVertXLo,x + sta $D4 + lda SecVertXHi,x + sta $D5 + lda SecVertYLo,x + sta $D6 + lda SecVertYHi,x + sta $D7 + lda SecVertZLo,x + sta $D8 + lda SecVertZHi,x + sta $D9 + lda SecVertXLo+1,x + sta $CB + lda SecVertXHi+1,x + sta $CC + lda SecVertYLo+1,x + sta $CD + lda SecVertYHi+1,x + sta $CE + lda SecVertZLo+1,x + sta $CF + lda SecVertZHi+1,x + sta $D0 + stx $B0 + sty $B1 + jsr ClipVertex2ToTop + ldx $B0 + ldy $B1 + lda $D9 + sta PrimVertZHi,y + ora $D8 + beq L715A ; clipped Z == 0 -> degenerate, skip emit + lda $D8 + sta PrimVertZLo,y + lda $D4 + sta PrimVertXLo,y + lda $D5 + sta PrimVertXHi,y + lda $D6 + sta PrimVertYLo,y + lda $D7 + sta PrimVertYHi,y + iny + ;; Skip-clip-emit merge: copy V[x] to output if inside. +L715A: lda $B4 + bne L7183 + lda SecVertXLo,x + sta PrimVertXLo,y + lda SecVertXHi,x + sta PrimVertXHi,y + lda SecVertYLo,x + sta PrimVertYLo,y + lda SecVertYHi,x + sta PrimVertYHi,y + lda SecVertZLo,x + sta PrimVertZLo,y + lda SecVertZHi,x + sta PrimVertZHi,y + iny + ;; Advance to prev input vertex; loop or fall through to pass-done. +L7183: dex + bmi L7189 + jmp L70D5 + + ;; Top pass done. Output count -> $B5; fall through to Right + ;; pass if anything remains. +L7189: sty $B5 + lda $B5 + bne L7190 + rts + +;;; Polygon clipping pass 3: clip PrimVerts→SecVerts against the right +;;; frustum plane (= z = -x). The half-plane test compares X+Z signs. +PolygonClipRightPass: + .refto PolygonClipRightPass + .refto L7190 +L7190: ldx $B5 ; ring-close: copy V[0] to V[B5] (Top's count) + lda PrimVertXLo + sta PrimVertXLo,x + lda PrimVertXHi + sta PrimVertXHi,x + lda PrimVertYLo + sta PrimVertYLo,x + lda PrimVertYHi + sta PrimVertYHi,x + lda PrimVertZLo + sta PrimVertZLo,x + lda PrimVertZHi + sta PrimVertZHi,x + ;; Classify V0 against the right plane (sign of X+Z). + lda PrimVertXLo + clc + adc PrimVertZLo + lda PrimVertXHi + adc PrimVertZHi + bpl L71C9 ; X+Z >= 0 -> inside + lda #$FF ; outside + bne L71CB +L71C9: lda #$00 ; inside +L71CB: sta $B4 + dex + ldy #$00 + ;; Loop top: classify V[x] against the right plane. +L71D0: lda PrimVertXLo,x + clc + adc PrimVertZLo,x + lda PrimVertXHi,x + adc PrimVertZHi,x + bpl L71E3 + lda #$FF + bne L71E5 +L71E3: lda #$00 +L71E5: cmp $B4 + sta $B4 + beq L7255 ; same side -> skip clip-emit + lda PrimVertXLo,x + sta $D4 + lda PrimVertXHi,x + sta $D5 + lda PrimVertYLo,x + sta $D6 + lda PrimVertYHi,x + sta $D7 + lda PrimVertZLo,x + sta $D8 + lda PrimVertZHi,x + sta $D9 + lda PrimVertXLo+1,x + sta $CB + lda PrimVertXHi+1,x + sta $CC + lda PrimVertYLo+1,x + sta $CD + lda PrimVertYHi+1,x + sta $CE + lda PrimVertZLo+1,x + sta $CF + lda PrimVertZHi+1,x + sta $D0 + stx $B0 + sty $B1 + jsr ClipVertex2ToRight + ldx $B0 + ldy $B1 + lda $D9 + sta SecVertZHi,y + ora $D8 + beq L7255 ; clipped Z == 0 -> degenerate, skip emit + lda $D8 + sta SecVertZLo,y + lda $D4 + sta SecVertXLo,y + lda $D5 + sta SecVertXHi,y + lda $D6 + sta SecVertYLo,y + lda $D7 + sta SecVertYHi,y + iny + ;; Skip-clip-emit merge: copy V[x] to output if inside. +L7255: lda $B4 + bne L727E + lda PrimVertXLo,x + sta SecVertXLo,y + lda PrimVertXHi,x + sta SecVertXHi,y + lda PrimVertYLo,x + sta SecVertYLo,y + lda PrimVertYHi,x + sta SecVertYHi,y + lda PrimVertZLo,x + sta SecVertZLo,y + lda PrimVertZHi,x + sta SecVertZHi,y + iny + ;; Advance to prev input vertex; loop or fall through to pass-done. +L727E: dex + bmi L7284 + jmp L71D0 + + ;; Right pass done. Fall through to Bottom pass if anything left. +L7284: sty $B5 + lda $B5 + bne L728B + rts + +;;; Polygon clipping pass 4: clip SecVerts→PrimVerts against the +;;; bottom frustum plane (= z = -y). After this pass the polygon is +;;; fully clipped to the viewport and ready for scan-line rasterizing. +PolygonClipBottomPass: + .refto PolygonClipBottomPass + .refto L728B +L728B: ldx $B5 ; ring-close: copy V[0] to V[B5] (Right's count) + lda SecVertXLo + sta SecVertXLo,x + lda SecVertXHi + sta SecVertXHi,x + lda SecVertYLo + sta SecVertYLo,x + lda SecVertYHi + sta SecVertYHi,x + lda SecVertZLo + sta SecVertZLo,x + lda SecVertZHi + sta SecVertZHi,x + ;; Classify V0 against the bottom plane (sign of Y+Z). + lda SecVertYLo + clc + adc SecVertZLo + lda SecVertYHi + adc SecVertZHi + bpl L72C4 ; Y+Z >= 0 -> inside + lda #$FF ; outside + bne L72C6 +L72C4: lda #$00 +L72C6: sta $B4 + dex + ldy #$00 + ;; Loop top: classify V[x] against the bottom plane. +L72CB: lda SecVertYLo,x + clc + adc SecVertZLo,x + lda SecVertYHi,x + adc SecVertZHi,x + bpl L72DE + lda #$FF + bne L72E0 +L72DE: lda #$00 +L72E0: cmp $B4 + sta $B4 + beq L7350 ; same side -> skip clip-emit + lda SecVertXLo,x + sta $D4 + lda SecVertXHi,x + sta $D5 + lda SecVertYLo,x + sta $D6 + lda SecVertYHi,x + sta $D7 + lda SecVertZLo,x + sta $D8 + lda SecVertZHi,x + sta $D9 + lda SecVertXLo+1,x + sta $CB + lda SecVertXHi+1,x + sta $CC + lda SecVertYLo+1,x + sta $CD + lda SecVertYHi+1,x + sta $CE + lda SecVertZLo+1,x + sta $CF + lda SecVertZHi+1,x + sta $D0 + stx $B0 + sty $B1 + jsr ClipVertex2ToBottom + ldx $B0 + ldy $B1 + lda $D9 + sta PrimVertZHi,y + ora $D8 + beq L7350 ; clipped Z == 0 -> degenerate, skip emit + lda $D8 + sta PrimVertZLo,y + lda $D4 + sta PrimVertXLo,y + lda $D5 + sta PrimVertXHi,y + lda $D6 + sta PrimVertYLo,y + lda $D7 + sta PrimVertYHi,y + iny + ;; Skip-clip-emit merge: copy V[x] to output if inside. +L7350: lda $B4 + bne L7379 + lda SecVertXLo,x + sta PrimVertXLo,y + lda SecVertXHi,x + sta PrimVertXHi,y + lda SecVertYLo,x + sta PrimVertYLo,y + lda SecVertYHi,x + sta PrimVertYHi,y + lda SecVertZLo,x + sta PrimVertZLo,y + lda SecVertZHi,x + sta PrimVertZHi,y + iny + ;; Advance to prev input vertex; loop or fall through to pass-done. +L7379: dex + bmi L737F + jmp L72CB + + ;; Bottom pass done. Polygon is now clipped against all 4 frustum + ;; sides; jump to projection / scan-fill setup. +L737F: jmp L7684 + +;;; ============================================================ +;;; Polygon Frustum Clipping +;;; +;;; The renderer uses 3D Cohen-Sutherland-style outcodes against six +;;; view-frustum half-spaces. Each vertex carries a 6-bit outcode +;;; byte: +;;; bit 7 ($80): z is negative - behind viewer +;;; bit 6 ($40): x + z < 0 - past right edge +;;; bit 5 ($20): z - x < 0 - past left edge +;;; bit 4 ($10): y + z < 0 - past bottom edge +;;; bit 3 ($08): z - y < 0 - past top edge +;;; +;;; Vertex 1 lives at $CB..$D0 (x lo/hi, y lo/hi, z lo/hi); its +;;; outcode is `$CA`. Vertex 2 lives at $D4..$D9 with outcode `$D3`. + +;;; Compute outcodes for vertex 1 (X1=$CB/$CC, Y1=$CD/$CE, Z1=$CF/$D0) +;;; into $CA. Behind-viewer (negative Z) sets bit 7. +ClassifyVertex1: + ldx #$00 + lda $D0 + bpl L738A + ldx #$80 +;;; Test V1 against right frustum plane (Z >= -X, equivalent to X+Z >= 0). +;;; Sets bit 6 of outcode if vertex is to the right. +ClassifyVertex1_TestRight: + .refto ClassifyVertex1_TestRight + .refto L738A +L738A: lda $CB + clc + adc $CF + lda $CC + adc $D0 + bpl L7399 + txa + ora #$40 + tax +;;; Test V1 against left frustum plane (Z >= X). Sets bit 5 if left. +ClassifyVertex1_TestLeft: + .refto ClassifyVertex1_TestLeft + .refto L7399 +L7399: lda $CF + sec + sbc $CB + lda $D0 + sbc $CC + bpl L73A8 + txa + ora #$20 + tax +;;; Test V1 against bottom frustum plane (Z >= -Y). Sets bit 4. +ClassifyVertex1_TestBottom: + .refto ClassifyVertex1_TestBottom + .refto L73A8 +L73A8: lda $CD + clc + adc $CF + lda $CE + adc $D0 + bpl L73B7 + txa + ora #$10 + tax +;;; Test V1 against top frustum plane (Z >= Y). Sets bit 3. +ClassifyVertex1_TestTop: + .refto ClassifyVertex1_TestTop + .refto L73B7 +L73B7: lda $CF + sec + sbc $CD + lda $D0 + sbc $CE + bpl L73C8 + txa + ora #$08 + sta $CA + rts + +ClassifyVertex1_StoreAndExit: + .refto ClassifyVertex1_StoreAndExit + .refto L73C8 +L73C8: stx $CA + rts + +;;; Compute outcodes for vertex 2 (X2=$D4/$D5, Y2=$D6/$D7, Z2=$D8/$D9) +;;; into $D3. Same six-half-space test as `ClassifyVertex1`. +ClassifyVertex2: + ldx #$00 + lda $D9 + bpl L73D3 + ldx #$80 +ClassifyVertex2_TestRight: + .refto ClassifyVertex2_TestRight + .refto L73D3 +L73D3: lda $D4 + clc + adc $D8 + lda $D5 + adc $D9 + bpl L73E2 + txa + ora #$40 + tax +ClassifyVertex2_TestLeft: + .refto ClassifyVertex2_TestLeft + .refto L73E2 +L73E2: lda $D8 + sec + sbc $D4 + lda $D9 + sbc $D5 + bpl L73F1 + txa + ora #$20 + tax +ClassifyVertex2_TestBottom: + .refto ClassifyVertex2_TestBottom + .refto L73F1 +L73F1: lda $D6 + clc + adc $D8 + lda $D7 + adc $D9 + bpl L7400 + txa + ora #$10 + tax +ClassifyVertex2_TestTop: + .refto ClassifyVertex2_TestTop + .refto L7400 +L7400: lda $D8 + sec + sbc $D6 + lda $D9 + sbc $D7 + bpl L7411 + txa + ora #$08 + sta $D3 + rts + +ClassifyVertex2_StoreAndExit: + .refto ClassifyVertex2_StoreAndExit + .refto L7411 +L7411: stx $D3 + rts + +;;; Swap both vertices, then run `ClipVertex2ToFrustum` against the +;;; swapped pair. Used so the clipper only needs to know how to +;;; clip vertex 2. +ClipBothVerticesToFrustum: + jsr SwapVertices + jsr ClipVertex2ToFrustum + +;;; Exchange the seven bytes (X, Y, Z, outcode) between vertex 1 +;;; ($CB..$D0/$CA) and vertex 2 ($D4..$D9/$D3). +SwapVertices: + ldx #$06 +SwapVertices_Loop: + .refto SwapVertices_Loop + .refto L741C +L741C: lda $CA,x + ldy $D3,x + sta $D3,x + sty $CA,x + dex + bpl L741C + rts + +;;; Clip vertex 2 in turn against the four lateral frustum planes +;;; (right, left, bottom, top), reclassifying after each clip. Bails +;;; out early if any clip leaves the segment wholly outside. +ClipVertex2ToFrustum: + lda $D3 + bne L742D +ClipVertex2ToFrustum_Exit: + .refto ClipVertex2ToFrustum_Exit + .refto L742C +L742C: rts + +ClipVertex2ToFrustum_TestRight: + .refto ClipVertex2ToFrustum_TestRight + .refto L742D +L742D: and #$40 ; right edge + beq L743D + jsr ClipVertex2ToRight + jsr ClassifyVertex2 + lda $D3 + and $CA + bne L742C +ClipVertex2ToFrustum_TestLeft: + .refto ClipVertex2ToFrustum_TestLeft + .refto L743D +L743D: lda $D3 + and #$20 ; left edge + beq L744F + jsr ClipVertex2ToLeft + jsr ClassifyVertex2 + lda $D3 + and $CA + bne L742C +ClipVertex2ToFrustum_TestBottom: + .refto ClipVertex2ToFrustum_TestBottom + .refto L744F +L744F: lda $D3 + and #$10 ; bottom edge + beq L7461 + jsr ClipVertex2ToBottom + jsr ClassifyVertex2 + lda $D3 + and $CA + bne L742C +ClipVertex2ToFrustum_TestTop: + .refto ClipVertex2ToFrustum_TestTop + .refto L7461 +L7461: lda $D3 + and #$08 ; top edge + beq L742C + jsr ClipVertex2ToTop + jmp ClassifyVertex2 + +;;; Clip vertex 2 against the top frustum plane (z = y). On overflow +;;; during the inner difference, halve both vertices via `HalveBothVertices` and +;;; retry from the entry below. +ClipVertex2ToTop_RetryAfterHalve: + .refto ClipVertex2ToTop_RetryAfterHalve + .refto L746D +L746D: jsr HalveBothVertices +ClipVertex2ToTop: + SUB16 $CF, $D8, $C4 + lda $CD + sec + sbc $D6 + tax + lda $CE + sbc $D7 + tay + txa + sec + sbc $C4 + sta $C4 + tya + sbc $C5 + bvs L746D + sta $C5 + lda $D8 + sec + sbc $D6 + tax + lda $D9 + sbc $D7 + jsr L16A2 + SUB16 $CB, $D4, $C4 + LDAX $C2 + sta $A9 + stx $AA + jsr ZPScale::ScaleC2ByC4 + clc + adc $D4 + sta $D4 + txa + adc $D5 + sta $D5 + SUB16 $CF, $D8, $C4 + LDAX $A9 + STAX $C2 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D8 + sta $D8 + sta $D6 + txa + adc $D9 + sta $D9 + sta $D7 + rts + +;;; Clip vertex 2 against the bottom frustum plane (z = -y). +ClipVertex2ToBottom_RetryAfterHalve: + .refto ClipVertex2ToBottom_RetryAfterHalve + .refto L74EB +L74EB: jsr HalveBothVertices +ClipVertex2ToBottom: + SUB16 $CF, $D8, $C4 + lda $D6 + sec + sbc $CD + tax + lda $D7 + sbc $CE + tay + txa + sec + sbc $C4 + sta $C4 + tya + sbc $C5 + bvs L74EB + sta $C5 + lda $D8 + clc + adc $D6 + tax + lda $D9 + adc $D7 + jsr L16A2 + SUB16 $CB, $D4, $C4 + LDAX $C2 + STAX $A9 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D4 + sta $D4 + txa + adc $D5 + sta $D5 + SUB16 $CF, $D8, $C4 + LDAX $A9 + STAX $C2 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D8 + sta $D8 + eor #$FF + sta $D6 + txa + adc $D9 + sta $D9 + eor #$FF + sta $D7 + inc $D6 + bne L7572 + inc $D7 +L7572: rts + +;;; Clip vertex 2 against the left frustum plane (z = x). +ClipVertex2ToLeft_RetryAfterHalve: + .refto ClipVertex2ToLeft_RetryAfterHalve + .refto L7573 +L7573: jsr HalveBothVertices +ClipVertex2ToLeft: + SUB16 $CF, $D8, $C4 + lda $CB + sec + sbc $D4 + tax + lda $CC + sbc $D5 + tay + txa + sec + sbc $C4 + sta $C4 + tya + sbc $C5 + bvs L7573 + sta $C5 + lda $D8 + sec + sbc $D4 + tax + lda $D9 + sbc $D5 + jsr L16A2 + SUB16 $CF, $D8, $C4 + LDAX $C2 + STAX $A9 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D8 + sta $D8 + sta $D4 + txa + adc $D9 + sta $D9 + sta $D5 + SUB16 $CD, $D6, $C4 + LDAX $A9 + STAX $C2 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D6 + sta $D6 + txa + adc $D7 + sta $D7 + rts + +;;; Clip vertex 2 against the right frustum plane (z = -x). +ClipVertex2ToRight_RetryAfterHalve: + .refto ClipVertex2ToRight_RetryAfterHalve + .refto L75F1 +L75F1: jsr HalveBothVertices +ClipVertex2ToRight: + SUB16 $CF, $D8, $C4 + lda $D4 + sec + sbc $CB + tax + lda $D5 + sbc $CC + tay + txa + sec + sbc $C4 + sta $C4 + tya + sbc $C5 + bvs L75F1 + sta $C5 + lda $D8 + clc + adc $D4 + tax + lda $D9 + adc $D5 + jsr L16A2 + SUB16 $CD, $D6, $C4 + LDAX $C2 + STAX $A9 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D6 + sta $D6 + txa + adc $D7 + sta $D7 + SUB16 $CF, $D8, $C4 + LDAX $A9 + STAX $C2 + jsr ZPScale::ScaleC2ByC4 + clc + adc $D8 + sta $D8 + eor #$FF + sta $D4 + txa + adc $D9 + sta $D9 + eor #$FF + sta $D5 + inc $D4 + bne L7678 + inc $D5 +L7678: rts + +;;; Halve every coordinate of both vertices. Used by the clipper to +;;; recover from intermediate-arithmetic overflow: after halving the +;;; line is mathematically unchanged in direction and the failing +;;; intersection retries cleanly. +HalveBothVertices: + ldx #$CB + jsr HalveTriple16 + ldx #$D4 + jsr HalveTriple16 + rts + +;;; Project all clipped polygon vertices to screen, find min/max +;;; row/column bounds, and add an estimated work cost to $32/$33. +;;; Walks PrimVerts ($B5 = vertex count) and writes screen coords back +;;; to SecVertXHi (= column) and SecVertYHi (= row) per vertex. +PolygonScanFill_ProjectVertices: + .refto PolygonScanFill_ProjectVertices + .refto L7684 +L7684: dey + bmi L7678 + sty $B5 + lda #$8B + sta $53 + lda #$63 + sta $EF + lda #$00 + sta $54 + sta $F0 + tya + tax +PolygonScanFill_ProjectVertexLoop: + .refto PolygonScanFill_ProjectVertexLoop + .refto L7699 +L7699: lda PrimVertXLo,x + sta $C2 + lda PrimVertXHi,x + sta $C3 + lda PrimVertZLo,x + sta $C4 + lda PrimVertZHi,x + sta $C5 + ldy PrimVertYLo,x + lda PrimVertYHi,x + stx $B2 + jsr ProjectVertex + ldx $B2 + sta SecVertXHi,x + cmp $53 + bcs L76C3 + sta $53 +PolygonScanFill_TestMaxX: + .refto PolygonScanFill_TestMaxX + .refto L76C3 +L76C3: cmp $54 + bcc L76C9 + sta $54 +PolygonScanFill_StoreYHi: + .refto PolygonScanFill_StoreYHi + .refto L76C9 +L76C9: tya + sta SecVertYHi,x + cmp $EF + bcs L76D3 + sta $EF +PolygonScanFill_TestMaxY: + .refto PolygonScanFill_TestMaxY + .refto L76D3 +L76D3: cmp $F0 + bcc L76D9 + sta $F0 +PolygonScanFill_NextVertex: + .refto PolygonScanFill_NextVertex + .refto L76D9 +L76D9: dex + bpl L7699 + lda $54 + sec + sbc $53 + lsr a + tax + lda $F0 + sec + sbc $EF + tay + jsr L180C + clc + adc $32 + sta $32 + tya + adc $33 + sta $33 + ldx $B5 + lda SecVertXHi,x + sta $0C77 + lda SecVertYHi,x + sta $0CF7 + inx + lda SecVertXHi + sta SecVertXHi,x + lda SecVertYHi + sta SecVertYHi,x + inx + lda SecVertXHi+1 + sta SecVertXHi,x + lda SecVertYHi+1 + sta SecVertYHi,x + inc $B5 + ldy #$00 + ldx #$00 +;;; Edge-list builder loop. For each consecutive vertex pair (V[y], +;;; V[y+1]), compute the screen-Y delta. Three cases: +;;; delta = 0 -> horizontal edge (PolygonScanFill_HorizEdge at L77EA) +;;; delta < 0 -> edge runs upward; encode top vertex first +;;; delta > 0 -> edge runs downward; encode top vertex first +;;; The per-edge state stored into PrimVerts[x]: +;;; PrimVertXLo = top screen row +;;; PrimVertXHi = row count - 1 (= remaining rows during scan) +;;; PrimVertYHi = top screen column +;;; PrimVertZLo = column delta per row (signed Q8.8 fraction) +;;; PrimVertZHi = "" +;;; PrimVertYLo = byte-bit half-row adjust flag +PolygonScanFill_BuildEdgeList: + .refto PolygonScanFill_BuildEdgeList + .refto L7724 +L7724: lda #$00 + sta PrimVertYLo,x + lda SecVertYHi+1,y + sec + sbc SecVertYHi,y + bne L7735 + jmp L77EA + +PSF_NonHoriz: + .refto PSF_NonHoriz + .refto L7735 +L7735: bcs PSF_EdgeDownward ; carry set -> V[y]->V[y+1] descends + ;; Edge goes UPWARD: encode -(delta) as row-count. + eor #$FF + adc #$01 + sta PrimVertXHi,x ; row count + lda SecVertYHi+1,y + sta PrimVertXLo,x ; top row + lda $0CF7,y + cmp SecVertYHi,y + bcc PSF_UpNoFraction + inc PrimVertYLo,x +PSF_UpNoFraction: + .refto PSF_UpNoFraction + .refto L774F +L774F: lda SecVertXHi+1,y + sta PrimVertYHi,x ; top column + lda SecVertXHi,y + sec + sbc SecVertXHi+1,y ; column delta + jmp PSF_AbsDelta + +;;; Edge goes DOWNWARD: row delta is positive. +PSF_EdgeDownward: + .refto PSF_EdgeDownward + .refto L775F +L775F: sta PrimVertXHi,x + lda SecVertYHi,y + sta PrimVertXLo,x + lda $0CFA,y + cmp SecVertYHi+1,y + bcc PSF_DownNoFraction + inc PrimVertYLo,x +PSF_DownNoFraction: + .refto PSF_DownNoFraction + .refto L7773 +L7773: lda SecVertXHi,y + sta PrimVertYHi,x + lda SecVertXHi+1,y + sec + sbc SecVertXHi,y +;;; Both up- and down-edge paths converge here. PHP saves sign of +;;; the column delta; we keep |delta| for the divide loop below. +PSF_AbsDelta: + .refto PSF_AbsDelta + .refto L7780 +L7780: php + bcs PSF_DeltaPositive + sta $AF + lda #$00 + sec + sbc $AF ; A = -A (= absolute value) +PSF_DeltaPositive: + .refto PSF_DeltaPositive + .refto L778A +L778A: sta $F5 + lda PrimVertXHi,x + sta $F6 + stx $B0 + sty $B1 + lda #$00 + sta $F4 + sta $F8 + sta $F9 + ldx #$10 ; 16-bit divide loop +PSF_DivLoop: + .refto PSF_DivLoop + .refto L779F +L779F: asl $F4 + rol $F5 + rol $F8 + rol $F9 + sec + lda $F8 + sbc $F6 + tay + lda $F9 + sbc $F7 + bcc PSF_NoQuotientBit + inc $F4 + sta $F9 + sty $F8 +PSF_NoQuotientBit: + .refto PSF_NoQuotientBit + .refto L77B9 +L77B9: dex + bne L779F + ldx $B0 + ldy $B1 + lda PrimVertYLo,x + beq PSF_AdjustHalfRow + dec PrimVertXHi,x ; consume one row of half-row adjust +PSF_AdjustHalfRow: + .refto PSF_AdjustHalfRow + .refto L77C8 +L77C8: plp + bcs PSF_PositiveSlope + ;; Slope is negative: store -F5:F4 as Z column-delta. + lda #$00 + sec + sbc $F4 + sta PrimVertZLo,x + lda #$00 + sbc $F5 + sta PrimVertZHi,x + jmp PSF_EdgeStored + +PSF_PositiveSlope: + .refto PSF_PositiveSlope + .refto L77DD +L77DD: lda $F4 + sta PrimVertZLo,x + lda $F5 + sta PrimVertZHi,x + jmp PSF_EdgeStored + +;;; Horizontal edge case: vertices V[y] and V[y+1] share the same +;;; screen row. Fold this edge plus any further consecutive horizontal +;;; edges into a single span (= encoded with PrimVertXHi = $FF +;;; sentinel and the min/max columns in PrimVertZHi/PrimVertYHi). +PolygonScanFill_HorizEdge: + .refto PolygonScanFill_HorizEdge + .refto L77EA +L77EA: lda #$FF + sta PrimVertXHi,x + lda SecVertYHi,y + sta PrimVertXLo,x + cmp $0CF7,y + php + lda SecVertXHi,y + sta PrimVertZHi,x + sta PrimVertYHi,x +PSF_HE_Next: + .refto PSF_HE_Next + .refto L7802 +L7802: iny + lda SecVertXHi,y + cmp PrimVertZHi,x + bcs PSF_HE_CheckMax + sta PrimVertZHi,x ; new minimum column +PSF_HE_CheckMax: + .refto PSF_HE_CheckMax + .refto L780E +L780E: cmp PrimVertYHi,x + bcc PSF_HE_MoreEdges + sta PrimVertYHi,x ; new maximum column +PSF_HE_MoreEdges: + .refto PSF_HE_MoreEdges + .refto L7816 +L7816: lda PrimVertXLo,x + cmp SecVertYHi+1,y + beq L7802 ; still on same row -> fold next edge + bcs PSF_HE_Reverse + plp + bcc PSF_NextEdgePair + bcs L7826 +PSF_HE_Reverse: + .refto PSF_HE_Reverse + .refto L7825 +L7825: plp +;;; Per-row scan-line emitter for the polygon fill kernel. Iterates +;;; rows from $B1 (= top row) to $F0 (= bottom row), maintaining edge +;;; X-intercepts in PrimVertY*/PrimVertZ* slots, sorting them, then +;;; calling DrawColorSpan for each pair of intercepts. +PolygonScanFillRow: + .refto PolygonScanFillRow +L7826: lda PrimVertYHi,x + sec + sbc PrimVertZHi,x + sta PrimVertZHi,x + dey +PSF_EdgeStored: + .refto PSF_EdgeStored + .refto L7831 +L7831: inx + iny +PSF_NextEdgePair: + .refto PSF_NextEdgePair + .refto L7833 +L7833: cpy $B5 + bpl L783A + jmp L7724 + +;;; Setup for the per-row scan emitter. $25 = max edge index, $C6/$C7 +;;; accumulates the work-cost into $32/$33. Sets $B1 = top row from +;;; $EF, then runs the row emitter loop. +PolygonScanFill_SetupRowEmit: + .refto PolygonScanFill_SetupRowEmit + .refto L783A +L783A: dex + stx $25 + jsr MapColorAndPrepRowRoutine + lda $EF + sta $B1 + lda #$00 + sta $C6 + sta $C7 +;;; Per-row scan-fill loop top: walk all edges (X = $25 down to 0), +;;; for each that crosses current row $B1 either emit a horizontal +;;; span (if PrimVertXHi has the $FF horizontal-edge sentinel) or +;;; capture the X intercept into the $02..$0F sort buffer for later +;;; pair-emit. After all edges, sort the captured X values and emit +;;; spans between consecutive pairs. +PolygonScanFill_RowTop: + .refto PolygonScanFill_RowTop + .refto L784A +L784A: ldx $25 + lda #$02 + sta L7889 + ldy $B1 + lda HiresTableLo,y + sta HiresRowPtr + lda HiresTableHi,y + sta HiresRowPtr+1 +PolygonScanFill_EdgeLoop: + .refto PolygonScanFill_EdgeLoop + .refto L785D +L785D: lda $B1 + cmp PrimVertXLo,x + bne L789B + lda PrimVertXHi,x + bpl L787B + lda PrimVertYHi,x + sta $27 + lda PrimVertZHi,x + stx $B0 + jsr DrawColorSpan + ldx $B0 + jmp L789B + +;;; Edge crosses current row but is not horizontal: interpolate the +;;; X intercept (PrimVertY += PrimVertZ Q8.8 step), capture the new +;;; X intercept into the sort buffer at L7889 (= self-modified target), +;;; then advance the buffer pointer. +PolygonScanFill_EdgeIntercept: + .refto PolygonScanFill_EdgeIntercept + .refto L787B +L787B: lda PrimVertYLo,x + clc + adc PrimVertZLo,x + sta PrimVertYLo,x + lda PrimVertYHi,x +L7889 := *+1 + sta $02 ; self-modified + inc L7889 + adc PrimVertZHi,x + sta PrimVertYHi,x + dec PrimVertXHi,x + bmi L789B + inc PrimVertXLo,x +PolygonScanFill_NextEdge: + .refto PolygonScanFill_NextEdge + .refto L789B +L789B: dex + bpl L785D + lda L7889 + sec + sbc #$03 + bmi L78E0 + sta $26 + tay + clc + adc $C6 + sta $C6 + bcc L78B2 + inc $C7 +;;; Bubble-sort the captured X intercepts in $02..$0F so the spans +;;; emit left-to-right. After sorting, emit a DrawColorLine span +;;; between each consecutive pair (= 1st+2nd, 3rd+4th, ...). +PolygonScanFill_SortIntercepts: + .refto PolygonScanFill_SortIntercepts + .refto L78B2 +L78B2: tya + tax + dex +PolygonScanFill_SortInner: + .refto PolygonScanFill_SortInner + .refto L78B5 +L78B5: lda $02,x + cmp $02,y + bcs L78C6 + pha + lda $02,y + sta $02,x + pla + sta $02,y +PolygonScanFill_SortInnerStep: + .refto PolygonScanFill_SortInnerStep + .refto L78C6 +L78C6: dex + bpl L78B5 + dey + bne L78B2 + ldx $26 +;;; Emit spans loop: pair up sorted intercepts and call DrawColorSpan +;;; for each pair. Walks X = $26 down to 0 in pairs of 2. +PolygonScanFill_EmitSpansLoop: + .refto PolygonScanFill_EmitSpansLoop + .refto L78CE +L78CE: lda $01,x + sta $27 + sec + sbc $02,x + stx $B2 + jsr DrawColorSpan + ldx $B2 + dex + dex + bpl L78CE +;;; Advance to next row. If $B1 < $F0 (= still within polygon), +;;; loop back to the row top. Else fall through to the work-cost +;;; final accumulation. +PolygonScanFill_NextRow: + .refto PolygonScanFill_NextRow + .refto L78E0 +L78E0: inc $B1 + ldy $F0 + cpy $B1 + bmi L78EB + jmp L784A + +PolygonScanFill_FinalCost: + .refto PolygonScanFill_FinalCost + .refto L78EB +L78EB: lda $33 + cmp #$20 + bcc L78F4 + jsr L8752 +L78F4: lda $C6 + asl a + rol $C7 + asl a + rol $C7 + asl a + rol $C7 + asl a + rol $C7 + clc + adc $32 + sta $32 + lda $C7 + adc $33 + sta $33 + rts + +;;; ============================================================ +;;; Draw horizontal span in color +;;; Inputs: +;;; A is width in color pixels (0...139) +;;; $27 is right edge in color pixels (0...139) +;;; + +DrawColorSpan: + ;; Edges of span (pixel-by-pixel) + sta $F1 + inc $F1 + ldx $27 + ldy AltColorPixelToByteTable,x + lda PixelToBitNumberTable,x + tax ; X is bit position + cmp #$06 + beq DCSByteCheck +DCSPixelLoop: + lda (HiresRowPtr),y +DCSPixelOraOp: + .refto DCSPixelOraOp + .refto L7921 +L7921: .byte OPC_ORA_abx ; self-modified opcode +DCSPixelOraOperand: + .refto DCSPixelOraOperand + .refto L7922 +L7922: .addr OrMaskTable2 ; self-modified operand - color table + cpx #$03 + bne L792D + sta (HiresRowPtr),y + dey + lda (HiresRowPtr),y +DCSPixelAndOp: + .refto DCSPixelAndOp + .refto L792D +L792D: .byte OPC_AND_abx ; self-modified opcode +DCSPixelAndOperand: + .refto DCSPixelAndOperand + .refto L792E +L792E: .addr AndMaskTable1 ; self-modified operand - color table + sta (HiresRowPtr),y + dex + bmi DCSEnterByteMode ; switch to whole-byte mode + dec $F1 + bne DCSPixelLoop +DCSDone: + rts + + ;; Middle of span (two bytes at a time) +DCSEnterByteMode: + ldx #$06 + dey + dec $F1 + beq DCSDone +DCSByteCheck: + lda $F1 + sec + sbc #$07 + bcc DCSPixelLoop ; back to pixel-by-pixel + beq DCSByteFinal ; early exit path + sta $F1 + lda ColorByteEven + sta (HiresRowPtr),y + dey + lda ColorByteOdd + sta (HiresRowPtr),y + dey + jmp DCSByteCheck + +DCSByteFinal: + lda ColorByteEven + sta (HiresRowPtr),y + dey + lda ColorByteOdd + sta (HiresRowPtr),y + rts + +;;; ============================================================ + +;;; Plot one "color pixel" at (X = column 0..139, Y = row). Uses the +;;; self-modified mask opcodes set up by `SetPixelDrawMode` to OR or +;;; AND the appropriate hi-res byte. +PlotColorPixel: +L7963: lda HiresTableHi,y + sta HiresRowPtr+1 + lda HiresTableLo,y + sta HiresRowPtr + ldy AltColorPixelToByteTable,x + lda PixelToBitNumberTable,x + tax + lda (HiresRowPtr),y +PlotColorPixel_OraOp: + .refto PlotColorPixel_OraOp + .refto L7976 +L7976: .byte OPC_ORA_abx ; self-modified opcode +PlotColorPixel_OraOperand: + .refto PlotColorPixel_OraOperand + .refto L7977 +L7977: .addr OrMaskTable2 ; self-modified operand + cpx #$03 + bne L7982 + sta (HiresRowPtr),y + dey + lda (HiresRowPtr),y +PlotColorPixel_AndOp: + .refto PlotColorPixel_AndOp + .refto L7982 +L7982: .byte OPC_AND_abx ; self-modified opcode +PlotColorPixel_AndOperand: + .refto PlotColorPixel_AndOperand + .refto L7983 +L7983: .addr AndMaskTable1 ; self-modified operand + sta (HiresRowPtr),y + rts + +;;; ============================================================ +;;; Line drawing +;;; +;;; Bresenham-style line draw between ($E9,$EA) and ($EB,$EC). +;;; ($E9,$EB) are color columns 0..139, ($EA,$EC) are rows 0..191. +;;; +;;; Major-axis selection: |dx|=$21 vs |dy|=$22. If |dx|>=|dy| the +;;; outer iteration runs along the column and the loop body steps +;;; the row conditionally (`DCLXMajor*`). If |dy|>|dx| the iteration +;;; runs along the row and the column is stepped conditionally +;;; (`DCLYMajor*`). $F1 holds the remaining-step counter (= the +;;; major-axis length) and $F2 holds the Bresenham error. +;;; +;;; The two row-stepping sites (`DCLYMajorRowStep`/`DCLXMajorRowStep`) +;;; are self-modified to either `inc $EA` or `dec $EA` depending on +;;; whether y2 > y1. +;;; +;;; Four mask sites (`L7982/L7A26/L7A2E/L7A9A`) and three OR sites +;;; (`L7976/L7A1A/L7A8E`) are self-modified by `SetPixelDrawMode` to +;;; switch the colour mix between ORA / AND / EOR plus the matching +;;; even/odd mask table. Two paint sites (`DCLYMajorSaveByte`, +;;; `DCLXMajorReadByte`) get patched to `BPL ` at night by +;;; `SceneryOpDayOnly` so day-only objects vanish, and restored to +;;; their normal STY/LDA forms by `SceneryOpModeWhite`. + +;;; Bresenham color-pixel line draw. Paints "color pixels" (= 1 bit +;;; in the hires bitplane; 7 color pixels per Apple II hires byte). +;;; Inputs: X1 = $E9, Y1 = $EA, X2 = $EB, Y2 = $EC. Color is implied +;;; by the self-modified mask opcodes at DCL{Y,X}Major{ORA,AND}* slots +;;; (= installed by SetPixelDrawMode). +;;; +;;; The algorithm is standard Bresenham: pick the major axis by +;;; comparing |dx| and |dy|, run a per-step loop incrementing the +;;; major axis and conditionally stepping the minor axis when the +;;; accumulated error overflows. +;;; +;;; Step 1: ensure X1 <= X2 by swapping endpoints if necessary. +DrawColorLine: + lda $E9 + sec + sbc $EB + bcs L79A3 + eor #$FF + adc #$01 + ldx $E9 + ldy $EB + stx $EB + sty $E9 + ldx $EA + ldy $EC + stx $EC + sty $EA +;;; Step 2: dx is now in A (= unsigned, since X1 <= X2). Save as $21. +;;; Compute initial pixel byte/bit position via PlotColorPixel ($7963) +;;; which sets up HiresRowPtr and returns X = bit-in-byte (0-6), +;;; Y = byte index in row. +DrawColorLine_PostSwap: + .refto DrawColorLine_PostSwap + .refto L79A3 +L79A3: sta $21 + ldx $E9 + ldy $EA + jsr L7963 + stx $B0 + sty $B1 + ldy #OPC_INC_zp + lda $EC + sec + sbc $EA + bcs L79BF + ldy #OPC_DEC_zp + eor #$FF + adc #$01 +;;; Step 3: dy is now in A (= unsigned). Save as $22, also pre-set the +;;; row-step opcode at L7A06/L7A74 to INC or DEC based on the original +;;; Y direction (bcs above means Y2 >= Y1 -> INC, otherwise DEC). +;;; If both dx and dy are zero, this is a single point — return. +DrawColorLine_TestZero: + .refto DrawColorLine_TestZero + .refto L79BF +L79BF: sta $22 + ora $21 + bne L79C6 + rts + +DCLChooseAxis: + .refto DCLChooseAxis +L79C6: lda $22 + sty L7A06 ; self-modify Y-major row step (inc/dec $EA) + sty L7A74 ; self-modify X-major row step (inc/dec $EA) + cmp $21 + ldx $B0 + ldy $B1 + bcc L7A43 + sta $F1 + asl a + clc + adc $0A + adc $32 + sta $32 + bcc L79E4 + inc $33 +;;; Y-major prep: F1 = dy (= total step count), F2 = dy - dx (= initial +;;; Bresenham error). If we're entering at byte-bit 3 (= the split-byte +;;; transition point for Apple II hires), increment Y so the first +;;; paint hits the correct byte. +DCLPrepYMajor: + .refto DCLPrepYMajor + .refto L79E4 +L79E4: lda $F1 + sec + sbc $21 + sta $F2 + cpx #$03 + bne L79F0 + iny +;;; Y-major inner loop top: error -= dx; on borrow step the column +;;; (and on a split-byte boundary, also step the column-byte). Then +;;; advance the row unconditionally and paint the pixel. +DCLYMajorTop: + .refto DCLYMajorTop +L79F0: lda $F2 + sec + sbc $21 ; e -= dx + bcs L7A04 ; if no borrow, skip column step + adc $22 ; e += dy (re-add to keep error bounded) + cpx #$03 ; at split-byte boundary? + bne L79FE + dey ; cross split: previous column-byte +DCLYMajor_DecPixelInByte: + .refto DCLYMajor_DecPixelInByte + .refto L79FE +L79FE: dex ; pixel-in-byte left + bpl L7A04 + ldx #$06 ; wrap to high pixel of previous byte + dey +DCLYMajorWriteRow: + .refto DCLYMajorWriteRow +L7A04: sta $F2 ; save updated error +DCLYMajorRowStep: + .refto DCLYMajorRowStep +L7A06: dec $EA ; self-modified opcode (inc/dec/BPL) +DCLYMajorSaveByte: + .byte OPC_STY_zp ; self-modified opcode (STY zp / BPL) + .byte $B1 ; self-modified operand +DCLYMajorPaintEntry: + .refto DCLYMajorPaintEntry +L7A0A: ldy $EA + lda HiresTableHi,y + sta HiresRowPtr+1 + lda HiresTableLo,y + sta HiresRowPtr + ldy $B1 + lda (HiresRowPtr),y +DCLYMajor_OraMaskOp: + .refto DCLYMajor_OraMaskOp + .refto L7A1A +L7A1A: .byte OPC_ORA_abx ; self-modified opcode +DCLYMajor_OraMaskOperand: + .refto DCLYMajor_OraMaskOperand + .refto L7A1B +L7A1B: .addr OrMaskTable2 ; self-modified operand + cpx #$03 + bne L7A2E + sta (HiresRowPtr),y + dey + lda (HiresRowPtr),y +DCLYMajor_AndMaskHiOp: + .refto DCLYMajor_AndMaskHiOp + .refto L7A26 +L7A26: .byte OPC_AND_abx ; self-modified opcode (split-byte hi half) +DCLYMajor_AndMaskHiOperand: + .refto DCLYMajor_AndMaskHiOperand + .refto L7A27 +L7A27: .addr AndMaskTable1 ; self-modified operand + sta (HiresRowPtr),y + iny + bne L7A33 +DCLYMajor_AndMaskLoOp: + .refto DCLYMajor_AndMaskLoOp + .refto L7A2E +L7A2E: .byte OPC_AND_abx ; self-modified opcode (non-split case) +DCLYMajor_AndMaskLoOperand: + .refto DCLYMajor_AndMaskLoOperand + .refto L7A2F +L7A2F: .addr AndMaskTable1 ; self-modified operand + sta (HiresRowPtr),y +DCLYMajorTickLoop: + .refto DCLYMajorTickLoop +L7A33: dec $F1 ; one fewer Y step remaining + bne L79F0 + rts + +;;; Dotted variant of the Y-major paint loop (no caller in the +;;; current binary). Paints every 4th pixel by skipping back to the +;;; tick decrement when the low 2 bits of $F1 are non-zero. + lda $F1 + and #$03 + bne L7A33 + sty $B1 + jmp L7A0A + +DCLPrepXMajor: + .refto DCLPrepXMajor +L7A43: lda $21 + sta $F1 + asl a + bcc L7A4D + inc $33 + clc +DCLPrepXMajor_AddCycles: + .refto DCLPrepXMajor_AddCycles + .refto L7A4D +L7A4D: adc $32 + sta $32 + bcc L7A55 + inc $33 +DCLPrepXMajor_AddOverhead: + .refto DCLPrepXMajor_AddOverhead + .refto L7A55 +L7A55: lda $32 + clc + adc #$0A + sta $32 + bcc L7A60 + inc $33 +DCLPrepXMajor_InitError: + .refto DCLPrepXMajor_InitError + .refto L7A60 +L7A60: lda $F1 + sec + sbc $22 + sta $F2 +;;; X-major inner loop top: error -= dy; on borrow step the row +;;; (then reload HiresRowPtr from the new row). Step the column +;;; unconditionally and paint the pixel. +DCLXMajorTop: + .refto DCLXMajorTop +L7A67: lda $F2 + sec + sbc $22 ; e -= dy + sta $F2 + bcs L7A86 ; if no borrow, skip row step + adc $21 ; e += dx (re-add to keep error bounded) + sta $F2 +DCLXMajorRowStep: + .refto DCLXMajorRowStep +L7A74: inc $EA ; self-modified opcode (inc/dec) + sty $B1 + ldy $EA + lda HiresTableHi,y + sta HiresRowPtr+1 + lda HiresTableLo,y + sta HiresRowPtr + ldy $B1 +DCLXMajorPaintEntry: + .refto DCLXMajorPaintEntry +L7A86: dex ; pixel-in-byte left + bpl DCLXMajorReadByte + ldx #$06 ; wrap to high pixel of previous byte + dey +DCLXMajorReadByte: + .byte OPC_LDA_izy ; self-modified opcode (LDA (HiresRowPtr),Y / BPL) + .byte $8E ; self-modified operand +DCLXMajor_OraMaskOp: + .refto DCLXMajor_OraMaskOp + .refto L7A8E +L7A8E: .byte OPC_ORA_abx ; self-modified opcode +DCLXMajor_OraMaskOperand: + .refto DCLXMajor_OraMaskOperand + .refto L7A8F +L7A8F: .addr OrMaskTable2 ; self-modified operand + cpx #$03 + bne L7A9A + sta (HiresRowPtr),y + dey + lda (HiresRowPtr),y +DCLXMajor_AndMaskOp: + .refto DCLXMajor_AndMaskOp + .refto L7A9A +L7A9A: .byte OPC_AND_abx ; self-modified opcode +DCLXMajor_AndMaskOperand: + .refto DCLXMajor_AndMaskOperand + .refto L7A9B +L7A9B: .addr AndMaskTable2 ; self-modified opcode + sta (HiresRowPtr),y +DCLXMajorTickLoop: + .refto DCLXMajorTickLoop +L7A9F: dec $F1 ; one fewer X step remaining + bne L7A67 + rts + +;;; Dotted variant of the X-major paint loop (no caller in the +;;; current binary). Paints every 4th pixel; otherwise skips ahead. + lda $F1 + and #$03 + bne L7AAF + lda (HiresRowPtr),y + jmp L7A8E + +L7AAF: cpx #$03 + bne L7A9F + dey + bpl L7A9F +;;; Scenery opcode $1B: restore the line drawer to its default +;;; opcodes (un-doing any prior `SceneryOpDayOnly` BPL skip), then +;;; set HIRES_WHITE1 as the active colour. The four `sta` writes +;;; here just rewrite the original bytes (STY $B1 / LDA ($8E),Y); +;;; the constants happen to alias OPC_LDA_izy = $B1 and the operand +;;; $8E = HiresRowPtr, so two of the four stores look like no-ops in +;;; the default state and only matter when night mode has patched +;;; them to BPL skips. +SceneryOpModeWhite: + lda #OPC_STY_zp + sta DCLYMajorSaveByte ; restore Y-major paint entry + lda #OPC_LDA_izy + sta DCLYMajorSaveByte+1 ; STY operand = $B1 + lda #OPC_LDA_izy + sta DCLXMajorReadByte ; restore X-major paint entry + lda #$8E ; LDA (HiresRowPtr),Y + sta DCLXMajorReadByte+1 + lda #$03 ; HIRES_WHITE1 + jsr SetPixelDrawMode + lda #$01 + jmp SceneryOpAdvanceAndContinue + +;;; Scenery opcode $1C: day/night-conditional draw mode. At night +;;; (`$083C` bit 0 = 0) patch the renderer kernels with BPL skips so +;;; ground objects disappear. During the day this is a no-op. +SceneryOpDayOnly: + lda $083C + and #$01 + bne L7AF2 ; day: leave drawer alone + lda #OPC_BPL + sta DCLYMajorSaveByte ; night: patch Y-major to BPL +$2E + sta DCLXMajorReadByte ; night: patch X-major to BPL +$16 + lda #$2E + sta DCLYMajorSaveByte+1 ; jumps past the paint code + lda #$16 + sta DCLXMajorReadByte+1 + lda #$01 ; HIRES_VIOLET (so any later + jsr SetPixelDrawMode ; un-patched draws stay sane) +SceneryOpDayOnly_Exit: + .refto SceneryOpDayOnly_Exit + .refto L7AF2 +L7AF2: lda #$01 + jmp SceneryOpAdvanceAndContinue + +;;; ??? to hires color mapping + +;;; NOTE: These differ from the Applesoft Hires colors +HIRES_BLACK1 = 0 +HIRES_VIOLET = 1 +HIRES_GREEN = 2 +HIRES_WHITE1 = 3 +;;; HIRES_BLACK2 = 4 +HIRES_BLUE = 5 +HIRES_ORANGE = 6 +;;; HIRES_WHITE2 = 7 + +;;; Notes: +;;; * land rendered as green (day)/black (night) separately +;;; * radar and 3D view use different entries ??? +;;; * day/night use different entries??? +;;; * palette bit not set when rendering lines +;;; 3D View: +;;; * 1st entry used for "black" buildings (day/night) / water color (night) +;;; * 3rd entry is used for water color (day) +;;; * 10th entry used for airplane wing/tail (day/night) +;;; * 16th entry used for "city" terrain (day/night) +;;; Radar: +;;; * third entry is used for water color +;;; * last entry used for runways +ToHiresColorTable: + .byte HIRES_BLACK1, HIRES_GREEN, HIRES_VIOLET, HIRES_GREEN + .byte HIRES_VIOLET, HIRES_BLACK1, HIRES_VIOLET, HIRES_VIOLET + .byte HIRES_BLACK1, HIRES_WHITE1, HIRES_BLACK1, HIRES_GREEN + .byte HIRES_VIOLET, HIRES_WHITE1, HIRES_VIOLET, HIRES_WHITE1 + +;;; Scenery opcode $12: set the active drawing colour. The next byte +;;; is an index into `ToHiresColorTable` (0..15) which maps onto a +;;; hires-colour ID for `SetPixelDrawMode`. +SceneryOpSetColor: + iny + lda ($8B),y + tay + lda ToHiresColorTable,y + jsr SetPixelDrawMode + lda #$02 + jmp SceneryOpAdvanceAndContinue + +;;; Configure the four self-modified mask opcodes used by +;;; `PlotColorPixel` and `DrawColorLine`. Selects ORA / AND / EOR +;;; (depending on A's sign and the parity in the mask tables) so the +;;; same pixel-plot inner loop can paint different colours. +SetPixelDrawMode: + tax + bpl L7B1D + ldx #$5D + bne L7B26 ; always + +;;; Color is non-negative: ORA mode (= paint over). Pick ORA or AND +;;; opcode based on whether OrMaskTable1's first byte has its high bit +;;; set (= the color affects the high-bit palette select). +SetPixelDrawMode_PosColor: + .refto SetPixelDrawMode_PosColor + .refto L7B1D +L7B1D: ldx #OPC_ORA_abx + bit OrMaskTable1 + bne L7B26 + ldx #OPC_AND_abx +;;; Patch the four AND-mask opcode sites in DrawColorLine ($7982, +;;; $7A2E, $7A9A, $7A26) and DrawColorSpan ($792D) with the chosen +;;; opcode (ORA or AND depending on which mask table was selected). +SetPixelDrawMode_PatchAndOps: + .refto SetPixelDrawMode_PatchAndOps + .refto L7B26 +L7B26: stx L7982 + stx L7A2E + stx L7A9A + stx L7A26 + LDXY #OrMaskTable1 + bit OrMaskTable1 + bne L7B3F + LDXY #AndMaskTable1 +;;; Patch the four AND-mask operand sites with OrMaskTable1 or +;;; AndMaskTable1 address. +SetPixelDrawMode_PatchAndOperands: + .refto SetPixelDrawMode_PatchAndOperands + .refto L7B3F +L7B3F: STXY L7983 + STXY L7A2F + STXY L7A9B + STXY L7A27 + tax + bpl L7B5E + ldx #OPC_EOR_abx + bne L7B67 +;;; Color is non-negative: ORA mode for the second mask set too. +SetPixelDrawMode_PosColor2: + .refto SetPixelDrawMode_PosColor2 + .refto L7B5E +L7B5E: ldx #OPC_ORA_abx + bit OrMaskTable2 + bne L7B67 + ldx #OPC_AND_abx +;;; Patch the three ORA-mask opcode sites in PlotColorPixel ($7976), +;;; DrawColorLine Y-major ($7A1A), and X-major ($7A8E). +SetPixelDrawMode_PatchOraOps: + .refto SetPixelDrawMode_PatchOraOps + .refto L7B67 +L7B67: stx L7976 + stx L7A1A + stx L7A8E + LDXY #OrMaskTable2 + bit OrMaskTable2 + bne L7B7D + LDXY #AndMaskTable2 +SetPixelDrawMode_PatchOraOperands: + .refto SetPixelDrawMode_PatchOraOperands + .refto L7B7D +L7B7D: STXY L7977 + STXY L7A1B + STXY L7A8F + rts + +ColorTableEven: + .byte $00, $2A, $55, $7F, $80, $AA, $D5, $FF + +ColorTableOdd: + .byte $00, $55, $2A, $7F, $80, $D5, $AA, $FF + +MapColorAndPrepRowRoutine: + lda $0876 + and #$0F + tay + ldx ToHiresColorTable,y + +SetEvenAndOddColorsAndPrepRowRoutine: + lda ColorTableEven,x + sta ColorByteEven + lda ColorTableOdd,x + sta ColorByteOdd + txa + ldx #OPC_ORA_abx + bit OrMaskTable1 + bne :+ + ldx #OPC_AND_abx +: stx L792D + LDXY #OrMaskTable1 + bit OrMaskTable1 + bne :+ + LDXY #AndMaskTable1 +: STXY L792E + ldx #OPC_ORA_abx + bit OrMaskTable2 + bne :+ + ldx #OPC_AND_abx +: stx L7921 + LDXY #OrMaskTable2 + bit OrMaskTable2 + bne :+ + LDXY #AndMaskTable2 +: STXY L7922 + rts + +;;; Project vertex 1 ($CB..$D0) onto the screen, leaving the screen +;;; coordinates at $D1 (column) / $D2 (row). +ProjectV1ToScreen: + lda $CB + sta $C2 + lda $CC + sta $C3 + lda $CF + sta $C4 + lda $D0 + sta $C5 + ldy $CD + lda $CE + jsr ProjectVertex + sta $D1 + sty $D2 + rts + +;;; Project vertex 2 ($D4..$D9) onto the screen, leaving the screen +;;; coordinates at $DA (column) / $DB (row). +ProjectV2ToScreen: + lda $D4 + sta $C2 + lda $D5 + sta $C3 + lda $D8 + sta $C4 + lda $D9 + sta $C5 + ldy $D6 + lda $D7 + jsr ProjectVertex + sta $DA + sty $DB + rts + +;;; Perspective divide for a single component (Y on entry from +;;; the Y branch, X on entry from the X branch). The L7D76 site +;;; (`lda L7Dxx,x`) is rewritten by `ProjectVertex` to point at +;;; either the Y or X LUT before calling. +;;; Perspective divide: 7-bit signed quotient = num / denom. +;;; Inputs: +;;; AY = numerator (A = high byte / sign, Y = low byte) +;;; $C4/$C5 = denominator +;;; Output: +;;; A = signed result (= input table[idx], possibly EOR'd if negative) +;;; +;;; The L7D76 site is a self-modified `LDA L7D77,X` instruction; the +;;; operand (L7D77/L7D78) is rewritten by ProjectVertex per axis to +;;; point at either kPerspXTable ($7D80, X projection) or kPerspYTable +;;; ($7E00, Y projection). The table converts the 7-bit fraction +;;; (= L00A5 after the divide) into a signed screen-pixel offset. +;;; +;;; Algorithm: shift-subtract, 7 iterations unrolled (bits 6 down to 0 +;;; of quotient). Two paths through the loop: +;;; * Positive: trial-subtract denominator. If no borrow, bit=1 and +;;; stay positive. If borrow, bit=0 and switch to negative path. +;;; * Negative: trial-add denominator. If no carry, bit=0 and stay +;;; negative. If carry, bit=1 and switch back to positive. +;;; +;;; Two early-out paths above handle |num| == |denom| (= unity, idx=$7F) +;;; and num+denom == 0 (= -unity, idx=$7F with sign). +PerspectiveDivide: + tax + cmp $C5 + bne L7C3D + cpy $C4 + bne L7C3D + ldx #$7F + lda #$00 + sta $A6 + jmp L7D76 + +;;; Numerator != denominator. Test for unity-negative case (= num+denom = 0). +PerspectiveDivide_TestNegUnity: + .refto PerspectiveDivide_TestNegUnity + .refto L7C3D +L7C3D: tya + clc + adc $C4 + bne L7C51 + txa + adc $C5 + bne L7C51 + ldx #$7F + lda #$FF + sta $A6 + jmp L7D76 + +;;; Main entry: AX has been classified, store sign-byte to $A6, branch +;;; into either the positive or negative iteration cascade. +;;; +;;; The 8 iterations are unrolled across two interleaved chains: +;;; * Positive path (trial-subtract): L7C51 -> L7C68 -> L7C79 -> L7C8A +;;; -> L7D29 -> L7D3A -> L7D4B -> L7D5C -> L7D6D (post-loop) +;;; * Negative path (trial-add): L7C9E -> L7CB0 -> L7CC1 -> L7CD2 +;;; -> L7CE3 -> L7CF4 -> L7D05 -> L7D16 -> L7D6D +;;; Each iteration's branch (bmi on pos / bpl on neg) crosses to the +;;; opposite path at the SAME iteration index, so the quotient ends +;;; up encoded as a sequence of "stayed on this path" bits in L00A5. +PerspectiveDivide_StartIter: + .refto PerspectiveDivide_StartIter + .refto L7C51 +L7C51: txa + stx $A6 + bmi L7C9E ; iter 1: numerator negative -> NegPath + clc + rol L00A5 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7CB0 ; iter 1 borrowed -> NegPath iter 2 +L7C68: rol L00A5 ; PosPath iter 2 (= no borrow last iter) + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7CC1 ; borrowed -> NegPath iter 3 +L7C79: rol L00A5 ; PosPath iter 3 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7CD2 ; borrowed -> NegPath iter 4 +L7C8A: rol L00A5 ; PosPath iter 4 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7CE3 ; borrowed -> NegPath iter 5 + jmp L7D29 ; -> PosPath iter 5 + +;;; Negative-path entry (= numerator was negative). Same shift-add +;;; pattern but with carry-set ROL and trial-add instead of trial-sub. +PerspectiveDivide_NegPath: + .refto PerspectiveDivide_NegPath + .refto L7C9E +L7C9E: sec + rol L00A5 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7C68 ; iter 1 carried -> PosPath iter 2 +L7CB0: rol L00A5 ; NegPath iter 2 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7C79 ; carried -> PosPath iter 3 +L7CC1: rol L00A5 ; NegPath iter 3 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7C8A ; carried -> PosPath iter 4 +L7CD2: rol L00A5 ; NegPath iter 4 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7D29 ; carried -> PosPath iter 5 +L7CE3: rol L00A5 ; NegPath iter 5 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7D3A ; carried -> PosPath iter 6 +L7CF4: rol L00A5 ; NegPath iter 6 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7D4B ; carried -> PosPath iter 7 +L7D05: rol L00A5 ; NegPath iter 7 + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7D5C ; carried -> PosPath iter 8 +L7D16: rol L00A5 ; NegPath iter 8 (last) + cpy #$80 + rol a + tax + tya + asl a + clc + adc $C4 + tay + txa + adc $C5 + bpl L7D6D ; done + bcc L7D6D +L7D29: rol L00A5 ; PosPath iter 5 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7CF4 ; borrowed -> NegPath iter 6 +L7D3A: rol L00A5 ; PosPath iter 6 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7D05 ; borrowed -> NegPath iter 7 +L7D4B: rol L00A5 ; PosPath iter 7 + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7D16 ; borrowed -> NegPath iter 8 +L7D5C: rol L00A5 ; PosPath iter 8 (last) + cpy #$80 + rol a + tax + tya + asl a + sec + sbc $C4 + tay + txa + sbc $C5 + bmi L7D6D ; done +;;; Done with iterations. L00A5 holds the 7-bit quotient. Rotate the +;;; final bit in, then EOR if the divide ended on a borrow (= last +;;; trial-subtract failed) so the quotient encodes one's-complement. +;;; Shift right by 1 to drop the carry sentinel, transfer to X = idx. +PerspectiveDivide_PostLoop: + .refto PerspectiveDivide_PostLoop + .refto L7D6D +L7D6D: lda L00A5 + rol a + bcc L7D74 + eor #$FF +PerspectiveDivide_FinalIdx: + .refto PerspectiveDivide_FinalIdx + .refto L7D74 +L7D74: lsr a + tax +;;; Self-modified `LDA $7Dxx,X` (= $BD opcode + 16-bit operand). The +;;; operand at L7D77/L7D78 is rewritten by ProjectVertex to point at +;;; either the X projection table ($7D80) or Y table ($7E00). +PerspectiveDivide_TableLookup: + .refto PerspectiveDivide_TableLookup + .refto L7D76 +L7D76: .byte $BD +PerspectiveDivide_TableLo: + .refto PerspectiveDivide_TableLo + .refto L7D77 +L7D77: .byte $FF +PerspectiveDivide_TableHi: + .refto PerspectiveDivide_TableHi + .refto L7D78 +L7D78: .byte $FF + ldy $A6 + bpl L7D7F + eor #$FF +PerspectiveDivide_Exit: + .refto PerspectiveDivide_Exit + .refto L7D7F +L7D7F: rts + +;;; 7D80 + + .byte $00, $00, $01, $01, $02, $02, $03, $03 + .byte $04, $04, $05, $05, $06, $07, $07, $08 + .byte $08, $09, $09, $0A, $0A, $0B, $0B, $0C + .byte $0D, $0D, $0E, $0E, $0F, $0F, $10, $10 + .byte $11, $11, $12, $13, $13, $14, $14, $15 + .byte $15, $16, $16, $17, $17, $18, $18, $19 + .byte $1A, $1A, $1B, $1B, $1C, $1C, $1D, $1D + .byte $1E, $1E, $1F, $20, $20, $21, $21, $22 + .byte $22, $23, $23, $24, $24, $25, $26, $26 + .byte $27, $27, $28, $28, $29, $29, $2A, $2A + .byte $2B, $2C, $2C, $2D, $2D, $2E, $2E, $2F + .byte $2F, $30, $30, $31, $31, $32, $33, $33 + .byte $34, $34, $35, $35, $36, $36, $37, $37 + .byte $38, $39, $39, $3A, $3A, $3B, $3B, $3C + .byte $3C, $3D, $3D, $3E, $3F, $3F, $40, $40 + .byte $41, $41, $42, $42, $43, $43, $44, $45 + .byte $00, $00, $00, $01, $01, $01, $02, $02 + .byte $03, $03, $03, $04, $04, $05, $05, $05 + .byte $06, $06, $07, $07, $07, $08, $08, $08 + .byte $09, $09, $0A, $0A, $0A, $0B, $0B, $0C + .byte $0C, $0C, $0D, $0D, $0E, $0E, $0E, $0F + .byte $0F, $10, $10, $10, $11, $11, $11, $12 + .byte $12, $13, $13, $13, $14, $14, $15, $15 + .byte $15, $16, $16, $17, $17, $17, $18, $18 + .byte $19, $19, $19, $1A, $1A, $1A, $1B, $1B + .byte $1C, $1C, $1C, $1D, $1D, $1E, $1E, $1E + .byte $1F, $1F, $20, $20, $20, $21, $21, $21 + .byte $22, $22, $23, $23, $23, $24, $24, $25 + .byte $25, $25, $26, $26, $27, $27, $27, $28 + .byte $28, $29, $29, $29, $2A, $2A, $2A, $2B + .byte $2B, $2C, $2C, $2C, $2D, $2D, $2E, $2E + .byte $2E, $2F, $2F, $30, $30, $30, $31, $31 + +;;; ============================================================ + +;;; ============================================================ +;;; Perspective Projection +;;; +;;; Project a 3D vertex (x = $C2/$C3, y = $C4/$C5, z = Y/A) to 2D +;;; screen coordinates. The Y projection table lives at $7E00, the +;;; X projection table at $7D80; both are 16-row LUTs indexed by +;;; z-bucket. The L7D76 instruction is the actual `lda abs,x` that +;;; gets re-pointed to the right table by the two `stx L7D77/L7D78` +;;; pairs below. +;;; +;;; Result: +;;; A = screen X (column 0..139, biased by $46 to centre) +;;; Y = screen Y (row 0..98, biased by $31) +ProjectVertex: + ldx #$00 ; -> $7E00 (Y projection table) + stx L7D77 + ldx #$7E + stx L7D78 + jsr PerspectiveDivide + sta $AF + lda #$31 + sec + sbc $AF + sta $A7 + + lda #$80 ; -> $7D80 (X projection table) + sta L7D77 + lda #$7D + sta L7D78 + lda $C3 ; X high + ldy $C2 ; X low + jsr PerspectiveDivide + clc + adc #$46 ; centre on column $46 + ldy $A7 ; row from earlier + rts + +;;; xform-B overflow recovery: BOTH X and Z deltas overflowed signed-16 +;;; during the SBC chain. Halve Z (already in A from caller's bvs), +;;; then sign-extend-halve X delta. Both axes scaled down together so +;;; they remain proportional. Falls into XformBHalveAccumulators +;;; epilogue at L7F7F. +XformBOverflowRecoverXZ: + .refto XformBOverflowRecoverXZ + .refto L7EAD +L7EAD: ror a + sta $A3 + ror $A2 + lda $9F + rol a + ror $9F + ror $9E + jmp L7F7F + +;;; Vertex transform "B" (Object-coordinate matrix-multiply rotate +;;; and translate). Reads 6 bytes from the scenery stream at ($8B)+1 +;;; (xLo, xHi, yLo, yHi, zLo, zHi), subtracts the camera position at +;;; $66/$67/$68/$69/$6A/$6B, applies the 3x3 rotation matrix at +;;; $4A..$53 (built each frame from the aircraft attitude), and +;;; writes the transformed result into the caller's vertex slot +;;; ($CB..$D0 or $D4..$D9, indexed by `$E5`). Companion routine at +;;; $80C5 ("transform A") uses the same matrix but a different bias +;;; for the $32/$33 work counter (#$C7 vs #$51 here). +TransformVertex7EBC: +L7EBC: sty $E5 + lda $32 + clc + adc #$51 ; transform-B work-counter bias + sta $32 + bcc L7EC9 + inc $33 +L7EC9: LDAX $4A + STAX $18 + lda $4C + sta $1A + LDAX $4D + STAX $1B + lda $4F + sta $1D + LDAX $50 + STAX $1E + lda $52 + sta $20 + ldy #$01 + lda $2F + sta $08EE + lda #$40 + sta $2F + lda ($8B),y + iny + sec + sbc $66 + sta $9E + lda ($8B),y + iny + sbc $67 + bvs L7F64 + sta $9F + lda ($8B),y + iny + sec + sbc $6A + sta $A2 + lda ($8B),y + iny + sbc $6B + bvs L7EAD + sta $A3 +;;; xform-B auto-scale loop. Iteratively halves the deltas + base +;;; accumulators while keeping the running absolute value of any of +;;; ($9F, $A3, $19, $1C, $1F) above the $40 threshold (= bit 6 set +;;; AFTER adc #$40). Each iteration increments $2F (= scale exponent) +;;; and shifts all 5 16-bit values left by 1. +XformBAutoScaleLoop: + .refto XformBAutoScaleLoop + .refto L7F1A +L7F1A: lda $9F + clc + adc #$40 + bmi L7F96 + lda $A3 + clc + adc #$40 + bmi L7F96 + lda $19 + clc + adc #$40 + bmi L7F96 + lda $1C + clc + adc #$40 + bmi L7F96 + lda $1F + clc + adc #$40 + bmi L7F96 + inc $2F + asl $9E + rol $9F + asl $A2 + rol $A3 + asl $1A + rol $18 + rol $19 + asl $1D + rol $1B + rol $1C + asl $20 + rol $1E + rol $1F + jmp L7F1A + +;;; xform-B overflow recovery: Z delta overflowed signed-16 during +;;; SBC chain (X delta was OK). Halve Z and fall into shared +;;; XformBHalveAccumulators epilogue at L7F7F. +XformBOverflowRecoverZ: + .refto XformBOverflowRecoverZ + .refto L7F5C +L7F5C: ror a + sta $A3 + ror $A2 + jmp L7F7F + +;;; xform-B overflow recovery: X delta overflowed signed-16 during +;;; SBC chain. Halve X (sign-extending) then continue reading Z delta +;;; from the stream. If Z also overflows we drop into the XZ recovery +;;; path; otherwise this falls into the same halve-and-loop epilogue. +XformBOverflowRecoverX: + .refto XformBOverflowRecoverX + .refto L7F64 +L7F64: ror a + sta $9F + ror $9E + lda ($8B),y + iny + sec + sbc $6A + sta $A2 + lda ($8B),y + iny + sbc $6B + bvs L7F5C + sta $A3 + rol a + ror $A3 + ror $A2 +;;; xform-B accumulator halve epilogue (= post-overflow). Sign-extending +;;; halve of all three 24-bit base accumulators by 1 (= maintains scale +;;; alignment with the down-shifted deltas). Decrements $2F. +XformBHalveBaseAccumulators: + .refto XformBHalveBaseAccumulators + .refto L7F7F +L7F7F: lda $19 + rol a + ror $19 + ror $18 + lda $1C + rol a + ror $1C + ror $1B + lda $1F + rol a + ror $1F + ror $1E + dec $2F +;;; xform-B matrix-multiply tail. Auto-scale loop has converged (= one +;;; of the high bytes is in [$40..$BF] band). Begin the 9 MultiplyByC2 +;;; calls that compute X/Y/Z output = base + sum-of-(delta*matrix). +XformBMatrixMultiply: + .refto XformBMatrixMultiply + .refto L7F96 +L7F96: ldx $9F + ldy $79 + jsr L1818 + clc + adc $18 + sta $18 + tya + adc $19 + sta $19 + bpl L7FAD + lda #$FF + bmi L7FAF +L7FAD: lda #$00 +L7FAF: bvc L7FB3 + eor #$FF +L7FB3: sta $1A + ldx $A3 + ldy $85 + jsr L1818 + clc + adc $18 + sta $18 + tya + adc $19 + sta $19 + tya + bpl L7FCD + lda #$FF + bmi L7FCF +L7FCD: lda #$00 +L7FCF: adc $1A + sta $1A + ldx $9F + ldy $7B + jsr L1818 + clc + adc $1B + sta $1B + tya + adc $1C + sta $1C + bpl L7FEA + lda #$FF + bmi L7FEC +L7FEA: lda #$00 +L7FEC: bvc L7FF0 + eor #$FF +L7FF0: sta $1D + ldx $A3 + ldy $87 + jsr L1818 + clc + adc $1B + sta $1B + tya + adc $1C + sta $1C + tya + bpl L800A + lda #$FF + bmi L800C +L800A: lda #$00 +L800C: adc $1D + sta $1D + ldx $9F + ldy $7D + jsr L1818 + clc + adc $1E + sta $1E + tya + adc $1F + sta $1F + bpl L8027 + lda #$FF + bmi L8029 +L8027: lda #$00 +L8029: bvc L802D + eor #$FF +L802D: sta $20 + ldx $A3 + ldy $89 + jsr L1818 + clc + adc $1E + sta $1E + tya + adc $1F + sta $1F + tya + bpl L8047 + lda #$FF + bmi L8049 +L8047: lda #$00 +L8049: adc $20 + sta $20 + lda $1A + eor $1D + eor $20 + beq L8059 + cmp #$FF + bne L806D +;;; Post-multiply consistency check: all three sign-extension bytes +;;; ($1A, $1D, $20) should agree with their respective hi-byte signs +;;; ($19, $1C, $1F). If any disagree, the multiply produced an +;;; out-of-range 24-bit result; halve all accumulators (= XformBHalveAcc) +;;; until the high byte fits the sign byte. +XformB_PostMultiplyCheckSigns: + .refto XformB_PostMultiplyCheckSigns + .refto L8059 +L8059: lda $1A + eor $19 + bmi L8070 + lda $1D + eor $1C + bmi L8070 + lda $20 + eor $1F + bpl L8079 + bmi L8070 +;;; All-different sign bytes: 3 halves needed (= aggressive overflow). +XformB_TripleHalve: + .refto XformB_TripleHalve + .refto L806D +L806D: jsr L80B0 +;;; One sign mismatch: 2 halves needed. +XformB_DoubleHalve: + .refto XformB_DoubleHalve + .refto L8070 +L8070: jsr L80B0 + jsr L80B0 + jmp L8091 + +;;; Sign bytes consistent. Final test: check whether high bytes +;;; ($19, $1C, $1F) sit in the [-$40..+$3F] band. If any falls +;;; outside, halve once more. +XformB_PostMultiplyTestRange: + .refto XformB_PostMultiplyTestRange + .refto L8079 +L8079: lda $19 + clc + adc #$40 + bmi L808E + lda $1C + clc + adc #$40 + bmi L808E + lda $1F + clc + adc #$40 + bpl L8091 +;;; Single halve (= 1 axis was just out of range). +XformB_SingleHalve: + .refto XformB_SingleHalve + .refto L808E +L808E: jsr XformBHalveAccumulators +;;; xform-B output writeback. Stores the 16-bit results from the +;;; accumulator at $18/$19 (X), $1B/$1C (Y), $1E/$1F (Z) into the +;;; caller's vertex slot at $E5..$E5+5 (= $CB.. for V1, $D4.. for V2). +;;; Then advances $8B by 5 (= opcode + 4 stream bytes for xform-B). +XformBStoreOutput: + .refto XformBStoreOutput + .refto L8091 +L8091: ldx $E5 + lda $18 + sta $00,x + lda $19 + sta $01,x + lda $1B + sta $02,x + lda $1C + sta $03,x + lda $1E + sta $04,x + lda $1F + sta $05,x + lda #$05 + jmp AddTo8B + +;;; Halve all three 24-bit accumulators ($18/$19/$1A, $1B/$1C/$1D, +;;; $1E/$1F/$20) by one. Decrements $2F (= scale exponent) so the +;;; auto-scale loop can continue. Called by the post-multiply overflow +;;; recovery branches L8059, L8070, L808E. +XformBHalveAccumulators: + .refto XformBHalveAccumulators + .refto L80B0 +L80B0: lsr $1A + ror $19 + ror $18 + lsr $1D + ror $1C + ror $1B + lsr $20 + ror $1F + ror $1E + dec $2F + rts + +;;; Vertex transform "A" at $80C5 — companion to `TransformVertex7EBC`. +;;; Same scenery-stream/camera/matrix inputs, different work-counter +;;; bias (#$C7 instead of #$51) and a slightly different overflow +;;; recovery path (jumps to L81D9/L820F/L821E rather than the +;;; in-line shift at L7EAD/L7F5C/L7F64). +TransformVertex80C5: + sty $E5 + lda $32 + clc + adc #$C7 ; transform-A work-counter bias + sta $32 + bcc L80D2 + inc $33 +;;; xform-A SBC chain entry. Reads X delta from stream relative to +;;; camera $66/$67. On overflow jumps to the L81D9 X recovery path. +XformA_SBCChain: + .refto XformA_SBCChain + .refto L80D2 +L80D2: ldy #$01 + lda ($8B),y + iny + sec + sbc $66 + sta $9E + lda ($8B),y + iny + sbc $67 + bvc L80E6 + jmp L81D9 + +L80E6: sta $9F + lda ($8B),y + iny + sec + sbc $68 + sta $A0 + lda ($8B),y + iny + sbc $69 + bvc L80FA + jmp L820F + +L80FA: sta $A1 + lda ($8B),y + iny + sec + sbc $6A + sta $A2 + lda ($8B),y + iny + sbc $6B + bvc L810E + jmp L821E + +XformA_SBCChain_StoreZHi: + .refto XformA_SBCChain_StoreZHi + .refto L810E +L810E: sta $A3 +;;; xform-A auto-scale loop. Mirrors XformBAutoScaleLoop but checks +;;; only the 3 stream-delta high bytes ($9F, $A1, $A3) — xform-A has +;;; full 3-axis stream so no base-accumulator slots in the test set. +XformA_AutoScaleLoop: + .refto XformA_AutoScaleLoop + .refto L8110 +L8110: lda $9F + clc + adc #$40 + bmi L8125 + lda $A1 + clc + adc #$40 + bmi L8125 + lda $A3 + clc + adc #$40 + bpl L813A +L8125: lda $9F + rol a + ror $9F + ror $9E + lda $A1 + rol a + ror $A1 + ror $A0 + lda $A3 + rol a + ror $A3 + ror $A2 +L813A: lda #$00 + sta $2F + sta $30 + lda #$07 + jsr AddTo8B + ldx #$9E + ldy #$78 + lda #$A9 + jsr ZPScale + ldx #$A0 + ldy #$7E + lda #$AB + jsr ZPScale + LDAX $A2 + STAX $C2 + LDAX $84 + STAX $C4 + jsr ZPScale::ScaleC2ByC4 + jsr L8234 + ldx #$9E + ldy #$7A + lda #$A9 + jsr ZPScale + ldx #$A0 + ldy #$80 + lda #$AB + jsr ZPScale + LDAX $A2 + STAX $C2 + LDAX $86 + STAX $C4 + jsr ZPScale::ScaleC2ByC4 + jsr L8234 + ldx #$9E + ldy #$7C + lda #$A9 + jsr ZPScale + ldx #$A0 + ldy #$82 + lda #$AB + jsr ZPScale + LDAX $A2 + STAX $C2 + LDAX $88 + STAX $C4 + jsr ScaleC2ByC4 + jsr L8234 + ldx $E5 + lda $30 + beq L81D8 + lda $FF,x + rol a + ror $FF,x + ror $FE,x + lda $FD,x + rol a + ror $FD,x + ror $FC,x + lda $FB,x + rol a + ror $FB,x + ror $FA,x +L81D8: rts + +;;; xform-A overflow recovery: X delta overflowed signed-16 in SBC. +;;; Halve X (sign-extending), then continue reading Y delta from +;;; stream. If Y also overflows we go to L820F; else we proceed to +;;; read Z and complete the chain. +XformA_OverflowRecoverX: + .refto XformA_OverflowRecoverX + .refto L81D9 +L81D9: ror a + sta $9F + ror $9E + lda ($8B),y + iny + sec + sbc $68 + sta $A0 + lda ($8B),y + iny + sbc $69 + bvs L81F1 + sta $A1 + rol $A1 +L81F1: ror a + sta $A1 + ror $A0 +L81F6: lda ($8B),y + iny + sec + sbc $6A + sta $A2 + lda ($8B),y + iny + sbc $6B + bvs L8207 + cmp #$80 +L8207: ror a + sta $A3 + ror $A2 + jmp L8110 + +;;; xform-A overflow recovery: Y delta overflowed (X already OK or +;;; recovered). Halve Y (sign-extending), shift X to match scale, +;;; continue with Z read. +XformA_OverflowRecoverY: + .refto XformA_OverflowRecoverY + .refto L820F +L820F: ror a + sta $A1 + ror $A0 + lda $9F + rol a + ror $9F + ror $9E + jmp L81F6 + +;;; xform-A overflow recovery: Z delta overflowed (X and Y already +;;; OK or recovered). Halve all three (sign-extending) so they share +;;; the same scale. +XformA_OverflowRecoverZ: + .refto XformA_OverflowRecoverZ + .refto L821E +L821E: ror a + sta $A3 + ror $A2 + lda $A1 + rol a + ror $A1 + ror $A0 + lda $9F + rol a + ror $9F + ror $9E + jmp L8110 + +L8234: clc + adc $A9 + tay + txa + adc $AA + tax + tya + clc + adc $AB + tay + txa + adc $AC + ldx $E5 + sta $01,x + sty $00,x + inc $E5 + inc $E5 + clc + adc #$40 + bpl L8255 + inc $30 +L8255: rts + + + +;;; ============================================================ +;;; Draw Artificial Horizon + +;;; 34-byte signed slope table indexed by `(pitch + $20) / 2` in +;;; `UpdateArtificialHorizon`. Each entry encodes the projected horizon +;;; line's column offset at the corresponding pitch (the routine +;;; multiplies it by bank/sin-table values to get the final tilt). +;;; The da65 output here was disassembled as nonsense instructions; the +;;; equivalent ca65 source below produces byte-identical output. +ArtHorizonSlopeTable: + .refto ArtHorizonSlopeTable + .refto L8256 +L8256: cmp ($CF,x) ; $D1 $CF + .byte $D4 + cmp $E1DD,y ; $D9 $DD $E1 + cpx $E7 ; $E4 $E7 + nop ; $EA + sbc $F2EF ; $ED $EF $F2 + .byte $F4 + .byte $F7 + sbc $FEFC,y ; $F9 $FC $FE + brk ; $00 + .byte $02 + ora $07 ; $05 $07 + asl a ; $0A + .byte $0C + .byte $0F + .byte $12 + ora $18,x ; $15 $18 + .byte $1B + asl $2622,x ; $1E $22 $26 + .byte $2B + and ($40),y ; $31 $40 + +;;; ============================================================ + +;;; Repaint the artificial-horizon area in the instrument panel from +;;; the current bank ($6F) and pitch ($6D). Roughly: +;;; +;;; 1. Fill the upper $1F rows of the AH area with the "top half" +;;; colour (orange when nominally upright, blue when inverted). +;;; 2. If |pitch| >= $20 the horizon line is off-screen -- +;;; just apply the round-bezel masks and return. +;;; 3. Look up the per-pitch slope offset in ArtHorizonSlopeTable, +;;; run it through the sin/multiply helpers (L1763/L180C/L1768) +;;; twice -- once for $6F + $AD, once for $6F - $AD -- to get +;;; the two horizon endpoints' (column,row) in $E9/$EA and +;;; $EB/$EC. (E* = right endpoint, EA = its row, etc.) +;;; 4. Select the "below-horizon" colour from sign(pitch) ^ +;;; sign(bank) and self-modify DrawSkyGroundRow to jump to +;;; AltDrawSkyGroundRow before invoking FillMixedViewportRows +;;; to rasterise the slanted divider. +;;; 5. Restore the patched opcodes. +;;; 6. If both endpoints are on the same side of viewport row $27, +;;; the rest of the area is already the right colour -- jump to +;;; the bezel masks. Otherwise fill the remaining strip via +;;; `L8387 FillHorizonRowSpan`. +;;; 7. Apply bezel AND/OR masks. +.proc UpdateArtificialHorizon + ;; Pick top-half colour from sign(pitch). bpl -> upright (top = + ;; sky orange); bmi -> inverted (top = ground blue). + ldx #$06 + lda $6D + bpl L8280 + ldx #HIRES_BLUE +L8280: jsr SetEvenAndOddColorsAndPrepRowRoutine + lda #$8D + sta $E7 + lda #$1F + jsr FillHorizonRowSpan ; top $1F rows + + ;; Range-check pitch in [-$20, $20). If outside, the horizon + ;; line is off-screen -- just apply the bezel masks. + lda $6D + clc + adc #$20 + cmp #$40 + bcc L8298 + jmp ApplyArtificialHorizonMasks + +L8298: lsr a + tax + lda ArtHorizonSlopeTable,x + sta $AD + lda $6F + sec + sbc $AD + sta $98 + jsr L1763 + ldy #$12 + jsr L180C + tya + clc + adc #$27 + sta $E9 + lda $98 + jsr L1768 + txa + rol a + php + txa + ror a + plp + php + ror a + plp + ror a + clc + adc #$7E + sta $EA + lda $6F + eor #$80 + clc + adc $AD + sta $98 + jsr L1763 + ldy #$12 + jsr L180C + tya + clc + adc #$27 + sta $EB + lda $98 + jsr L1768 + txa + rol a + php + txa + ror a + plp + php + ror a + plp + ror a + clc + adc #$7E + sta $EC + lda #$00 + sta $8A + lda $6F + bpl L82FD + dec $8A +L82FD: ldx #HIRES_BLUE + lda $6D + bpl L830B + lda $8A + eor #$FF + sta $8A + ldx #HIRES_ORANGE +L830B: jsr SetEvenAndOddColorsAndPrepRowRoutine + + ;; Modify routines + lda #OPC_RTS + sta TidySkyGroundEdgeInRow + + lda #OPC_JMP_abs + sta DrawSkyGroundRow + lda #AltDrawSkyGroundRow + sta DrawSkyGroundRow+2 + + jsr FillMixedViewportRows + + ;; Restore routines + lda #OPC_STX_zp + sta TidySkyGroundEdgeInRow + + lda #OPC_LDY_zp + sta DrawSkyGroundRow + lda #$E7 + sta DrawSkyGroundRow+1 + lda #OPC_LDA_aby + sta DrawSkyGroundRow+2 + + ;; Compare (E9-$27) vs (EB-$27): if same sign (= EOR's MSB clear) + ;; both endpoints are on the same side of the center column $27 + ;; and we don't need a second fill below the horizon -- just + ;; apply the bezel masks. + lda $E9 + clc + sbc #$27 + sta $98 + lda $EB + clc + sbc #$27 + eor $98 + bpl UAH_ApplyMasks + ;; Endpoints straddle center. Quadrant (= sign of bank+$40, + ;; sign of pitch) chooses which side gets the bottom fill. + lda $6F + clc + adc #$40 + bmi UAH_PitchCheck + lda $6D + bmi UAH_BelowFill + ;; Bottom-fill the rows BETWEEN the horizon endpoints and row $6F. +UAH_BetweenFill: + .refto UAH_BetweenFill + .refto L8354 +L8354: lda $EA + cmp $EC + bcc UAH_StoreFillStart + lda $EC ; pick max(EA, EC) as fill start row +UAH_StoreFillStart: + .refto UAH_StoreFillStart + .refto L835C +L835C: sta $E7 + dec $E7 + sec + sbc #$6F + bpl UAH_DoFill ; positive count -> fill it +UAH_ApplyMasks: + .refto UAH_ApplyMasks + .refto L8365 +L8365: jmp ApplyArtificialHorizonMasks + +UAH_PitchCheck: + .refto UAH_PitchCheck + .refto L8368 +L8368: lda $6D + bmi UAH_BetweenFill + ;; Bottom-fill from row $8D (= panel bottom) UP to min(EA, EC). +UAH_BelowFill: + .refto UAH_BelowFill + .refto L836C +L836C: lda $EA + cmp $EC + bcs UAH_BelowStore + lda $EC ; pick min(EA, EC) +UAH_BelowStore: + .refto UAH_BelowStore + .refto L8374 +L8374: sta $B5 + inc $B5 + lda #$8D + sta $E7 + sec + sbc $B5 + bmi UAH_SkipFill +UAH_DoFill: + .refto UAH_DoFill + .refto L8381 +L8381: jsr FillHorizonRowSpan +UAH_SkipFill: + .refto UAH_SkipFill + .refto L8384 +L8384: jmp ApplyArtificialHorizonMasks + +;;; FillHorizonRowSpan -- paint the 6-byte-wide artificial-horizon +;;; column (hires bytes 8..13 of each row) with the currently selected +;;; ColorByteOdd/ColorByteEven, walking UP from $E7 for A rows. +;;; In: A = row count - 1 +;;; $E7 = bottom row to paint +;;; ColorByteOdd / ColorByteEven = pattern bytes +FillHorizonRowSpan: + .refto FillHorizonRowSpan + .refto L8387 +L8387: sta $B5 ; B5 = remaining rows (counts down past 0) + ldx $E7 +L838B: lda HiresTableLo,x + sta HiresRowPtr + lda HiresTableHi,x + sta HiresRowPtr+1 + ldy #$08 + lda ColorByteOdd + sta (HiresRowPtr),y ; col 8 + iny + iny + sta (HiresRowPtr),y ; col 10 + iny + iny + sta (HiresRowPtr),y ; col 12 + ldy #$09 + lda ColorByteEven + sta (HiresRowPtr),y ; col 9 + iny + iny + sta (HiresRowPtr),y ; col 11 + iny + iny + sta (HiresRowPtr),y ; col 13 + dex + dec $B5 + bpl L838B + rts +.endproc + +;;; ============================================================ + +;;; Apply AND / OR mask to artificial horizon + +.proc ApplyArtificialHorizonMask + ldx #$6E +row: + lda HiresTableLo,x + sta HiresRowPtr + lda HiresTableHi,x + sta HiresRowPtr+1 + stx $B0 + ldx #$00 + ldy #$08 +col: + lda ($2D,x) +MaskOpCode: + ora (HiresRowPtr),y ; self-modified (AND/ORA) + sta (HiresRowPtr),y + inc $2D + bne :+ + inc $2E +: iny + cpy #$0E + bne col + ldx $B0 + inx + cpx #$8E + bne row + rts +.endproc + +;;; ============================================================ + +.proc ApplyArtificialHorizonMasks + lda #>HorizonANDMask + sta $2E + lda #HorizonORAMask + sta $2E + lda # |dy|, row-major otherwise). +FillMixedViewportRows_StoreDx: + .refto FillMixedViewportRows_StoreDx + .refto L843A +L843A: sta $21 + stx L8483 + stx L849A + ldy #$E6 + lda $EC + sec + sbc $EA + bcs L8451 + ldy #$C6 + eor #$FF + adc #$01 +FillMixedViewportRows_StoreDy: + .refto FillMixedViewportRows_StoreDy + .refto L8451 +L8451: sta $22 + sty L8484 + sty L84A8 + ldx $E9 + ldy $EA + sty $E7 + jsr DrawSkyGroundRow + jsr TidySkyGroundEdgeInRow + lda $22 + ora $21 + bne L846C + rts + +;;; Pick major axis and run inner loop. +FillMixedViewportRows_PickAxis: + .refto FillMixedViewportRows_PickAxis + .refto L846C +L846C: lda $22 + cmp $21 + bcc L8491 + sta $93 + sta $F2 +;;; Y-major Bresenham: rows step every iteration, columns conditionally. +FillMixedViewportRows_YMajorTop: + .refto FillMixedViewportRows_YMajorTop + .refto L8476 +L8476: lda $F2 + sec + sbc $21 + sta $F2 + bcs L8484 + adc $22 + sta $F2 +FillMixedViewportRows_YMajor_ColStep: + .refto FillMixedViewportRows_YMajor_ColStep + .refto L8483 +L8483: dex ; self-modified (inc/dec) +FillMixedViewportRows_YMajor_RowStep: + .refto FillMixedViewportRows_YMajor_RowStep + .refto L8484 +L8484: dec $E7 ; self-modified (inc/dec) + jsr DrawSkyGroundRow + jsr TidySkyGroundEdgeInRow + dec $93 + bne L8476 + rts + +;;; X-major Bresenham: columns step every iteration, rows conditionally. +FillMixedViewportRows_XMajorTop: + .refto FillMixedViewportRows_XMajorTop + .refto L8491 +L8491: lda $21 + sta $93 + sec + sbc $22 + sta $F2 +FillMixedViewportRows_XMajor_ColStep: + .refto FillMixedViewportRows_XMajor_ColStep + .refto L849A +L849A: inx ; self-modified (inc/dec) + lda $F2 + sec + sbc $22 + sta $F2 + bcs L84B0 + adc $21 + sta $F2 +FillMixedViewportRows_XMajor_RowStep: + .refto FillMixedViewportRows_XMajor_RowStep + .refto L84A8 +L84A8: inc $E7 ; self-modified (inc/dec) + jsr DrawSkyGroundRow + jsr TidySkyGroundEdgeInRow +FillMixedViewportRows_XMajor_TickLoop: + .refto FillMixedViewportRows_XMajor_TickLoop + .refto L84B0 +L84B0: dec $93 + bne L849A + rts +.endproc + +;;; ============================================================ + +;;; Update the turn-coordinator gauge. Runs when bit 1 of `$FB` +;;; (turn-coordinator operational) is set. Computes a "tilt" index +;;; from $09CD/$09CE, clamps it to 0..15, and redraws via the shared +;;; painter at `L8505` if it changed since the last frame. Two +;;; parallel "previous angle" cells are kept ($0A40 / $0A41) so each +;;; hires page can be repainted independently. +.proc UpdateTurnCoordinator + lda $FB + and #$02 + beq L8504 + lda $09CD + asl a + tax + lda $09CE + rol a + tay + txa + adc $09CD + tax + tya + adc $09CE + cpx #$80 + adc #$08 + bmi UTC_Clamp0 + cmp #$10 + bmi UTC_StoreTilt + lda #$0F ; clamp to maximum tilt + bne UTC_StoreTilt +UTC_Clamp0: + .refto UTC_Clamp0 + .refto L84DC +L84DC: lda #$00 +UTC_StoreTilt: + .refto UTC_StoreTilt + .refto L84DE +L84DE: sta $AF + lda $0A40 + ldx HiresPageDelta + bpl UTC_ComparePrev + lda $0A41 +UTC_ComparePrev: + .refto UTC_ComparePrev + .refto L84EA +L84EA: cmp $AF + bne UTC_AngleChanged + rts + +UTC_AngleChanged: + .refto UTC_AngleChanged + .refto L84EF +L84EF: jsr DrawTurnCoordinatorAtAngle ; XOR-undraw old + lda $AF + +;;; External entry: draw the turn-coordinator at angle A and snapshot +;;; the new angle into the current page's slot ($0A40 or $0A41). +;;; Called via $6015 trampoline. +DrawTurnCoordinatorAndSnapshot: +::L84F4 := * + .refto DrawTurnCoordinatorAndSnapshot + ldx HiresPageDelta + bpl UTC_StoreOnPage1 + sta $0A41 + jmp UTC_FinalDraw + +UTC_StoreOnPage1: + .refto UTC_StoreOnPage1 + .refto L84FE +L84FE: sta $0A40 +UTC_FinalDraw: + .refto UTC_FinalDraw + .refto L8501 +L8501: jsr DrawTurnCoordinatorAtAngle ; XOR-draw new +UTC_Exit: + .refto UTC_Exit + .refto L8504 +L8504: rts + +;;; Draw the turn-coordinator's wing-bar at angle index A. Looks up +;;; four delta bytes (dx, dy, vx, vy) at $0DE0,X (X = A*4) and emits +;;; two `DrawColorLine` strokes mirrored around the centre (12, 166). +DrawTurnCoordinatorAtAngle: + .refto DrawTurnCoordinatorAtAngle + .refto L8505 +L8505: pha + lda #$83 + jsr SetPixelDrawMode + pla + asl a + asl a + pha + tax + lda $0DE0,x + clc + adc #$0C + sta $E9 + lda $0DE1,x + clc + adc #$A6 + sta $EA + lda #$0C + sec + sbc $0DE0,x + sta $EB + lda #$A6 + sec + sbc $0DE1,x + sta $EC + jsr DrawColorLine + pla + tax + lda #$0C + sta $E9 + clc + adc $0DE2,x + sta $EB + lda #$A6 + sta $EA + clc + adc $0DE3,x + sta $EC + jsr DrawColorLine + rts +.endproc + + ;; Bitmap for artificial horizon + + ;; stride is 6 bytes + ;; height is 32 rows + + ;; $854D - ORA mask +HorizonORAMask: + PIXELS "...#..........##.....#.....##..........#.." + PIXELS "..#.........##......###......##.........#." + PIXELS ".##.......##.......#####.......##.......##" + PIXELS "##......##.......................##......#" + PIXELS "#......#..........................##......" + PIXELS "......#.............................#....." + PIXELS ".....#...............................#...." + PIXELS "....#.................................#..." + PIXELS "...##.................................##.." + PIXELS "...#...................................#.." + PIXELS "...#...............#####...............#.." + PIXELS "..#.....................................#." + PIXELS "..#.....................................#." + PIXELS "..#................#####................#." + PIXELS "..#.....................................#." + PIXELS "..#.....................................#." + PIXELS "..#.......#########..#..#########.......#." + PIXELS "..#.....................................#." + PIXELS "..#.....................................#." + PIXELS "..#................#####................#." + PIXELS "..#....................................#.." + PIXELS "...#...................................#.." + PIXELS "...#...............#####...............#.." + PIXELS "....#.................................#..." + PIXELS "....#.................................#..." + PIXELS ".....#...............................#...." + PIXELS ".....##.............................##...." + PIXELS "......#.............................#....." + PIXELS "#......#...........................#......" + PIXELS ".#......##.........####..........##......#" + PIXELS "..#.......##.....##....##......##.......##" + PIXELS "..##.......######........######........##." + +HorizonANDMask: + PIXELS "...#..........###############..........#.." + PIXELS "..#.........###################.........#." + PIXELS ".##.......#######################.......##" + PIXELS "##......###########################......#" + PIXELS "#......#############################......" + PIXELS "......###############################....." + PIXELS ".....#################################...." + PIXELS "....###################################..." + PIXELS "...#####################################.." + PIXELS "...#####################################.." + PIXELS "...#####################################.." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..#######################################." + PIXELS "..######################################.." + PIXELS "...#####################################.." + PIXELS "...#####################################.." + PIXELS "....###################################..." + PIXELS "....###################################..." + PIXELS ".....#################################...." + PIXELS ".....#################################...." + PIXELS "......###############################....." + PIXELS "#......#############################......" + PIXELS ".#......###########################......#" + PIXELS "..#.......#########....##########.......##" + PIXELS "..##.......######........######........##." + +;;; ============================================================ + + ;; Dead pad (115 bytes, $86CD..$873F). Same $FF $FF $00 $00 + ;; pattern as Chunk2TrailingData and the chunk3 head -- 1984-era + ;; assembler residue. Verified unused: no immediate, indirect, + ;; patch-table, or scenery-dispatch reach. Sits between the + ;; HorizonANDMask bitmap above and TogglePauseRelay below; + ;; appears to be padding aligning the next code section. + .byte $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00, $FF, $FF, $00, $00, $FF, $FF + .byte $00, $00 + +;;; ============================================================ + + jmp UpdateMinutesHoursDisplay + + ;; Called from chunk3 +TogglePauseRelay: + jmp TogglePause + + jmp LA524 + + jmp ApplyMagnetoState + +L874C: jmp SyncPositionFromLive + + jmp ResetAircraftSystems + +L8752: jmp ProcessInputTick + + jmp LoadSceneryFile0 + + jmp LoadSceneryFile1 + + jmp LoadSceneryFile2 + + jmp LoadSceneryFile3 + + jmp LoadSceneryFile4 + + jmp RefreshElevatorIndicator + + jmp L90E8 + + jmp RefreshRudderIndicator + + jmp RefreshAileronAndRoll + +L8770: jmp LA4D2 + +Set3DigitStringRelay: + jmp Set3DigitString + +;;; Trampoline jumped to from SceneryOpHeader after the 5-byte +;;; section-header copy. Demand-loads the scenery section bytecode +;;; from disk via SceneryHeaderLoadIfMiss (which compares $08E5 to +;;; the cached slot at $08EA,x and runs SceneryHeaderRunSection on +;;; a miss). Despite the 3-byte JMP layout that would allow runtime +;;; patching, the 64K patch table does NOT rewrite this slot in the +;;; shipped binary -- it remains a fixed `jmp SceneryHeaderLoadIfMiss` +;;; in both 48K and 64K modes. +SceneryHeaderLoadTrampoline: + .refto SceneryHeaderLoadTrampoline + .refto L8776 +L8776: jmp SceneryHeaderLoadIfMiss + + jmp LA6F9 + + jmp L915D + +;;; ============================================================ +;;; Main Loop +;;; +;;; Driven by `MainGameEntry` after setup -- enters at `L877F` so the +;;; first iteration syncs the patch-table frame slots before the +;;; normal loop body. Each iteration advances `UpdateCounter` by 1; +;;; the four low-bit combinations rotate which "slow" jobs are run so +;;; they amortise across four frames (= ~6 Hz instead of ~24 Hz). +;;; +;;; UpdateCounter phase map: +;;; .....00 L8863 tail: ComputeDayPhase + MaybeBootDOS + Fuel-L + +;;; (when bit 2 set: DME / NAV / ADF requests) + flight env +;;; .....10 L882D: Oil-Temp + AutoTrim+Yaw -> L8863 tail +;;; .....01 L8848 tail: Fuel-R + Alt-10K + VOR1/2 + Heading + ADF +;;; indicator + timer rotate +;;; .....11 L8842 + L8848: Oil-Press + RealityMode + L8848 tail + +;;; First-time main-loop entry (used by MainGameEntry). Runs +;;; PatchSlot_FrameSync once before falling into the regular loop. +MainLoopEntry: + .refto MainLoopEntry + .refto L877F +L877F: jsr PatchSlot_FrameSync +MainLoop: lda $33 + clc + adc #$0D + sta $33 + lda EditModeFlag + ora ModeLibraryAction + beq :+ + jsr PatchSlot_PreMode +: + jsr PatchSlot_FrameSync + lda SlewMode + beq L87A2 + jsr ApplySlewDeltas + jmp L87A8 + +L87A2: jsr IntegratePhysicsStep +L87A5: jsr NoOp ; 64k: Patched to JSR `ApplyWind` +L87A8: jsr LA60C + jsr LoadDispatcherPointer + jsr L6000 + ldx #$00 + lsr $089A + bcc L87BE + lda $0899 + bne L87BE + inx +L87BE: jsr ShowSimpleCrashMessage ; 64k: Patched to JSR `HandleCrashOrSplash` + jsr FlipPagesFillViewportRelay + jsr L6006 +P64K_9: jsr NoOp ; 64k: Patched to JSR `UpdateCoursePlotter` + jsr AnimateVOR2Needle + jsr DrawRPM + jsr DrawVOR1IndicatorChanges + jsr DrawVOR2IndicatorChanges + jsr IntegrateClimbRate + jsr MaybeDrawSlewOverlays + jsr L6012 + jsr DrawHeading + jsr DrawMagCompass +P64K_6: jsr NoOp ; 64K: Patched to JSR `UpdateInstrumentLights` + lda WW1AceMode + beq :+ + jsr PatchSlot_Gunsight ; Draw gunsight +: +P64K_3: jsr NoOp ; 64K: Patched to JSR `DrawViewOverlays` + jsr UpdateAltimeterPose + jsr UpdateAirspeedDerivedValue +P64K_8: jsr NoOp ; 64k: Patched to JSR `HideOrShowInstruments` + lda DemoMode + beq :+ +P64K_C: jsr DemoMode48K ; 64k: Patched to JSR `DemoMode64K` + ;; UpdateCounter dispatch (low 2 bits select 1-of-4 slow jobs). +: lda UpdateCounter + lsr a + bcs L8836 ; low bit set -> phase 01 or 11 + lsr a + bcs L882D ; phase 10 + + ;; ----- Phase 00: day phase, boot DOS, fuel-left, wind ---- + jsr LA5E1 + jsr MaybeBootDOS + jsr UpdateFuelTankGauges::Left +P64K_B: jsr NoOp ; 64k: Patched to JSR `ComputeWindComponents` + lda UpdateCounter + and #$04 ; ...every 8th iteration also do radio scans + beq L8863 + + jsr DrawDME + jsr RequestNAV1Lookup + jsr RequestNAV2Lookup +P64K_1: jsr NoOp ; 64k: Patched to JSR `RequestADFStationLookup` + jmp L8863 + + ;; ----- Phase 10: oil temp + auto-trim -------------------- +L882D: jsr UpdateOilTempAndPressureGauges::Temp + jsr UpdateAutoTrimAndYaw + jmp L8863 + + ;; ----- Phases 01 / 11: VOR + heading + radio updates ----- +L8836: lsr a ; A = `UpdateCounter` >> 1 + bcs L8842 + ;; Phase 01 + jsr UpdateFuelTankGauges::Right +P64K_4: jsr NoOp ; 64k: Patched to JSR `UpdateAltimeter10K` + jmp L8848 + + ;; Phase 11 +L8842: jsr UpdateOilTempAndPressureGauges::Pressure +P64K_5: jsr NoOp ; 64k: Patched to JSR `RealityModeHook` + ;; Common tail for phases 01/11. +L8848: jsr UpdateVOR1Bearing + jsr UpdateVOR2Bearing + jsr UpdateMagneticHeading +P64K_2: jsr NoOp ; 64k: Patched to JSR `UpdateADFIndicator` + lda $0832 + asl a + rol $0833 + adc #$00 + sta $0832 + jmp FinishMainLoop + + ;; Common tail for phases 00 / 10. Runs the flight-envelope + ;; check, slip-skid update, and (if joystick mode active) a + ;; per-frame calibration poll. +L8863: jsr CheckFlightEnvelope + jsr UpdateSlipSkid + lda JoystickMode + beq FinishMainLoop + jsr CalibrateJoystickIfButtonDown + +;;; End-of-frame: advance UpdateCounter (rotates the slow-jobs +;;; quarter-frame phase) and re-enter the loop body at MainLoop. +FinishMainLoop: + inc UpdateCounter + jmp MainLoop + +;;; ============================================================ + +;;; Keyboard input ring buffer + +KeyBufWritePos: .byte 0 ; write position +KeyBufReadPos: .byte 0 ; read position +KeyBuffer: .byte $FF, $FF, $00, $00, $BF, $FF, $00, $00 + + + ;; "Last drawn" / OBS-knob byte for the four cockpit + ;; instruments. Stored separately so each redraw routine can + ;; XOR-erase the previous needle/digit position. +VOR1ObsCourse: brk ; VOR1 course knob (0..$B3 wraps) +LastDrawnHeading: .byte $FF +LastDrawnRPM: brk +VOR2ObsCourse: brk ; VOR2 course knob (0..$B3 wraps) + brk + brk + +;;; Instrument panel displays + +msg_heading: MESSAGE $A1, $20, "000", str_heading +msg_recip: MESSAGE $B1, $20, "000", str_recip +msg_magcompass: MESSAGE $5F, $65, "000", str_magcompass +msg_com1: MESSAGE $6C, $69, "2485", str_com1 +msg_nav1: MESSAGE $7A, $69, "1000", str_nav1 +msg_nav2: MESSAGE $88, $69, "1135", str_nav2 +msg_vor1_course: MESSAGE $6C, $54, "000", str_vor1_course +msg_vor1_recip: MESSAGE $8C, $54, "000", str_vor1_recip +msg_vor2_course: MESSAGE $97, $54, "000", str_vor2_course +msg_vor2_recip: MESSAGE $B7, $54, "000", str_vor2_recip +msg_xpndr: MESSAGE $88, $7B, "1200", str_xpndr +msg_rpm: MESSAGE $B3, $7B, "2370", str_rpm +msg_clock_hh: MESSAGE $91, $72, "00", str_clock_hh +msg_clock_mm: MESSAGE $91, $7B, "00", str_clock_mm +msg_clock_ss: MESSAGE $91, $84, "00", str_clock_ss +msg_dme: MESSAGE $7A, $7D, "000", str_dme +msg_lights_on: MESSAGE $9A, $87, "1" +msg_lights_off: MESSAGE $9A, $87, "O" +msg_carbheat_on: MESSAGE $BB, $6E, "HEAT" +msg_carbheat_off: MESSAGE $BB, $6E, "C.H." + + ;; Indexed, 8 bytes apart +msg_vor_flags: + ;; VOR1 flag + MESSAGE $82, $54, "OFF" + .byte $07, $08 + MESSAGE $82, $54, "TO " + .byte $07, $08 + MESSAGE $82, $54, "FR " + .byte $07, $08 + + ;; VOR2 flag + MESSAGE $AD, $54, "OFF" + .byte $07, $08 + MESSAGE $AD, $54, "TO " + .byte $07, $08 + MESSAGE $AD, $54, "FR " + .byte $07, $08 + +msg_omi: MESSAGE $6C, $7D, "XX", str_omi + +;;; Redraw all the radio panels in one shot (NAV1, NAV2, COM1, +;;; Transponder, Clock). Called from InitInstruments after the gauges. +DrawAllRadios: + .refto DrawAllRadios + .refto L8933 +L8933: jsr DrawNav1 + jsr DrawNav2 + jsr DrawCom1 + jsr DrawXPNDR + jsr UpdateMinutesHoursDisplay + rts + +;;; Integrate climb rate ($0843, signed 16-bit) into altitude +;;; ($5F/$60) and the climb-rate indicator state ($0A36/$0A37). +;;; Clamps altitude to a minimum of $0003 (treated as ground level). +;;; +;;; The inner step (L8959..rts at L89A9) is called twice -- once via +;;; `jsr L8959` at L8956 and once by falling through -- so each call +;;; to IntegrateClimbRate advances the indicator and altitude by 0, +;;; +1, +2, -1 or -2 units per frame. +;;; +;;; Each invocation of the inner step: +;;; * Picks a +1/-1/0 nudge by comparing climb rate to the current +;;; needle position ($0A36/$0A37); 0 short-circuits directly to +;;; the RTS via the `beq L89A9` middle of the cpx/cmp chain. +;;; * Applies the nudge to the indicator ($0A36/$0A37). +;;; * Applies the OPPOSITE nudge to altitude ($5F/$60). (The +;;; `tya / eor #$FF / tay` flips the sign for the altitude step.) +;;; * Clamps altitude to a minimum of $0003 -- below that the +;;; aircraft is on the ground. +IntegrateClimbRate: + LDAX $0843 + ldy L0A11+1 + cpy #$0A + bcs ICR_RunTwice + sta $0A36 + stx $0A37 +ICR_RunTwice: + .refto ICR_RunTwice + .refto L8956 +L8956: jsr L8959 +;;; Inner step: nudge needle and altitude by +/- 1 toward the current +;;; climb-rate target. +ICR_Step: + .refto ICR_Step + .refto L8959 +L8959: ldy #$00 + LDAX $0843 + cpx $0A37 ; cmp climb_hi vs needle_hi + bcc ICR_DecNeedle ; climb < needle -> dey (Y = -1) + bne ICR_Continue ; climb > needle (= hi differs) -> Y = 0 + cmp $0A36 ; equal hi; cmp climb_lo vs needle_lo + bcc ICR_DecNeedle + beq ICR_Done ; exactly equal -> no nudge this tick, rts + bne ICR_Continue +ICR_DecNeedle: + .refto ICR_DecNeedle + .refto L8971 +L8971: dey +ICR_Continue: + .refto ICR_Continue + .refto L8972 +L8972: tya + bmi ICR_StoreNudge + lda #$01 +ICR_StoreNudge: + .refto ICR_StoreNudge + .refto L8977 +L8977: clc + adc $0A36 ; needle += nudge (1 if Y=0, 0 if Y=-1) + sta $0A36 + tya + adc $0A37 + sta $0A37 + tya + eor #$FF + tay + bmi ICR_StoreAlt + lda #$01 +ICR_StoreAlt: + .refto ICR_StoreAlt + .refto L898D +L898D: clc + adc $5F ; altitude += inverse-nudge + sta $5F + tya + adc $60 + sta $60 + bmi ICR_ClampGround + bne ICR_Done + lda $5F + cmp #$03 + bcs ICR_Done +ICR_ClampGround: + .refto ICR_ClampGround + .refto L89A1 +L89A1: lda #$03 ; altitude < $0003: clamp to ground + sta $5F + lda #$00 + sta $60 +ICR_Done: + .refto ICR_Done + .refto L89A9 +L89A9: rts + +msg_crash2: MESSAGE $32, $14, "CRASH !!!" + +;;; 48K crash handler - shows "CRASH !!!" and aborts the simulation. +;;; The 64K patch (`P64K_8B6` slot via the patch table) redirects +;;; this to chunk3's `HandleCrashOrSplash`, which differentiates +;;; between water splash, mountain hit, building hit, and aircraft +;;; problem. +ShowSimpleCrashMessage: + lda $0834 + bne :+ + rts + +: lda #$00 + sta $0834 + jsr ClearViewportsToBlack + CALLAX DrawMessageWhite, msg_crash2 + ldx #$01 + jmp L9093 + +;;; Ctrl+P / P key +TogglePause: + lda EditModeFlag + pha + inc $08BB + lda #$01 + sta EditModeFlag +L89DC: lda $32 + clc + adc #$0A + sta $32 + bcc L89E7 + inc $33 +L89E7: lda $33 + cmp #$20 + bcc L89F0 + jsr ProcessInputTick +L89F0: lda $08B1 + bpl L89DC + ldx #$00 + stx $08B1 + tax + pla + sta EditModeFlag + txa + and #$7F + lsr $08BB +L8A05: rts + +;;; ============================================================ + +;;; Course Plotter replaces this with a NOP... +MaybeBootDOS: + rts +;;; Which falls into this + ror RebootFlag + bcs L8A05 ; ignore! + lda RdROMWrRAM1 + lda RdROMWrRAM1 + jmp (RebootVector) + +;;; ============================================================ + +;;; Periodic input/sound tick. Called from the scenery interpreter +;;; (and other long-running loops) when the frame counter `$33` +;;; reaches `$20`, so even slow operations remain responsive to +;;; keyboard, joystick and the engine "tick" sound effect. +ProcessInputTick: + sbc #$11 + sta $33 + + ;; Make sound if needed + lda SoundMode + beq :+ + lda EngineOnFlag + beq :+ + lda SPKR ; click on each tick +: + inc $34 + + ;; Enqueue key if needed + lda KBD + bpl :+ + and #$7F + jsr WriteKeyBuffer + sta KBDSTRB +: + lda KeyBufReadPos + cmp KeyBufWritePos + beq :+ + + lda LCBank1Flag + bne :+ + jsr MaybeProcessKey +: + lda JoystickMode + beq :+ + jsr ReadPaddleDeltas +: + inc InputTickCounter + lda InputTickCounter + lsr a + bcc :+ +L8A54: rts + +: + lda $08BB + bne L8A54 + lda InputCounter + beq L8A62 + dec InputCounter +L8A62: jsr ComputeFlightDerivedValues + jsr UpdateAirspeedIndicator + jsr UpdateVerticalSpeedIndicator + lda InputTickCounter + and #$03 + bne L8A54 + lda InputTickCounter + and #$07 + bne L8A54 + jsr LA55A + + ;; Increment Seconds/Minutes/Hours +.scope + ldx Seconds + cpx #59 + bne IncSeconds + + ldy Minutes + cpy #59 + bne IncMinutes + + inc Hours + lda Hours + cmp #24 + bne :+ + lda #0 + sta Hours +: ldy #$FF +IncMinutes: + iny + sty Minutes + ldx #$FF +IncSeconds: + inx + stx Seconds +.endscope + + ;; Update clock seconds display + jsr CheckForAbort + txa + ldx #'0'-1 +: inx + sec + sbc #10 + bcs :- + clc + adc #'0'+10 + stx str_clock_ss + sta str_clock_ss+1 + CALLAX DrawMessageOrange, msg_clock_ss + + lda Seconds + bne NoOp ; skip unless we just rolled over +UpdateMinutesHoursDisplay: + + ;; Update clock minutes display + jsr CheckForAbort + lda Minutes + ldx #'0'-1 +: inx + sec + sbc #10 + bcs :- + clc + adc #'0'+10 + stx str_clock_mm + sta str_clock_mm+1 + CALLAX DrawMessageOrange, msg_clock_mm + + ;; Update clock hours display + lda Hours + ldx #'0'-1 +: inx + sec + sbc #10 + bcs :- + clc + adc #'0'+10 + stx str_clock_hh + sta str_clock_hh+1 + CALLAX DrawMessageOrange, msg_clock_hh + +NoOp: rts + +LastElevatorPosition: .byte 0 +LastAileronPosition: .byte 0 + + +;;; Indexed by ASCII code, $00 through $5F +KeyTable: + + .addr Ignore ; Ctrl+@ + .addr ADF ; Ctrl+A + .addr AltimeterAdjust ; Ctrl+B + .addr ComRadio ; Ctrl+C + .addr HeadingAdjust ; Ctrl+D + .addr LogSceneryDisk ; Ctrl+E + .addr FuelTankSelect ; Ctrl+F + .addr Ignore ; Ctrl+G + .addr LessThrottle ; Ctrl+H Left Arrow + .addr CarbHeat ; Ctrl+I + .addr Ignore ; Ctrl+J Down Arrow + .addr Ignore ; Ctrl+K Up Arrow + .addr ToggleLights ; Ctrl+L + .addr MagsAndMixture ; Ctrl+M Return + .addr NavRadio ; Ctrl+N + .addr Ignore ; Ctrl+O + .addr TogglePause ; Ctrl+P + .addr Ignore ; Ctrl+Q + .addr Ignore ; Ctrl+R + .addr Ignore ; Ctrl+S + .addr TransponderOrTransferParams ; Ctrl+T + .addr MoreThrottle ; Ctrl+U Right Arrow + .addr VORS ; Ctrl+V + .addr Ignore ; Ctrl+W + .addr Transponder ; Ctrl+X + .addr Ignore ; Ctrl+Y + .addr Ignore ; Ctrl+Z + .addr EditMode ; Ctrl+[ Escape + .addr Ignore ; Ctrl+\ + .addr Ignore ; Ctrl+] + .addr Ignore ; Ctrl+^ + .addr Ignore ; Ctrl+_ + .addr BrakesOrGuns ; Space + .addr Ignore ; ! + .addr Ignore ; " + .addr Ignore ; # + .addr Ignore ; $ + .addr Ignore ; % + .addr Ignore ; & + .addr Ignore ; ' + .addr Ignore ; ( + .addr Ignore ; ) + .addr Ignore ; * + .addr ReadModeFromLibrary ; + + .addr KeyDecrease ; , (a.k.a. unshifted <) + .addr Ignore ; - + .addr KeyIncrease ; . (a.k.a. unshifted >) + .addr ToggleThrottle ; / + .addr Ignore ; 0 + .addr Select1 ; 1 + .addr Select2 ; 2 + .addr MagnetosLeft ; 3 + .addr SelectRadarView ; 4 + .addr Select3DView ; 5 + .addr Ignore ; 6 + .addr Ignore ; 7 + .addr SlewPitchUp ; 8 + .addr SlewPitchDown ; 9 + .addr Ignore ; : + .addr Ignore ; ; + .addr Ignore ; < + .addr Ignore ; = + .addr Ignore ; > + .addr Ignore ; ? + .addr Ignore ; @ + .addr ADF ; A + .addr YokeUp ; B + .addr RudderLeft ; C + .addr SlewToggleDigits ; D + .addr Ignore ; E + .addr YokeLeft ; F + .addr YokeCenter ; G + .addr YokeRight ; H + .addr Ignore ; I + .addr Ignore ; J + .addr ExitDemoMode ; K + .addr ToggleLights ; L + .addr RudderRight ; M + .addr FlapsDown ; N + .addr Ignore ; O + .addr TogglePause ; P + .addr CoursePlotting ; Q + .addr TrimDown ; R + .addr SaveModeToLibrary ; S + .addr YokeDown ; T + .addr Ignore ; U + .addr TrimUp ; V + .addr DeclareWar ; W + .addr DropBomb ; X + .addr FlapsUp ; Y + .addr SlewResetAngles ; Z + .addr Ignore ; [ + .addr Ignore ; \ + .addr Ignore ; ] + .addr Ignore ; ^ + .addr Ignore ; _ + +;;; ============================================================ + +;;; $8BC0 + + pha + txa + pha + lda KBD + bpl :+ + and #$7F + jsr WriteKeyBuffer + sta KBDSTRB +: + pla + tax + pla + rts + +;;; ============================================================ + +;;; Output: C=0 if buffer empty, otherwise C=1, A=key +.proc ReadKeyBuffer + lda KeyBufReadPos + cmp KeyBufWritePos + bne :+ + clc + rts + +: inc KeyBufReadPos + lda KeyBufReadPos + and #$07 + tax + lda KeyBuffer,x + sec + rts +.endproc + +;;; ============================================================ + +;;; Input: A = key (high bit stripped) + +.proc WriteKeyBuffer + pha + inc KeyBufWritePos + lda KeyBufWritePos + and #$07 + tax + pla + sta KeyBuffer,x + rts +.endproc + +;;; ============================================================ + +;;; Center position, after calibrating with button down. +Paddle1Center: .byte $40 +Paddle0Center: .byte $40 + +.proc ReadPaddleDeltas + jsr ReadPaddles + tya + sec + sbc Paddle1Center + jsr ScalePaddleValue + pha + txa + sec + sbc Paddle0Center + jsr ScalePaddleValue + cmp #$80 + ror a + jsr L9259 + pla + cmp #$80 + ror a + jmp L91D6 +.endproc + +;;; ============================================================ + +.proc CalibrateJoystickIfButtonDown + lda BUTN0 + ora BUTN1 + bpl :+ + jsr ReadPaddles + sty Paddle1Center + stx Paddle0Center +: rts +.endproc + +;;; ============================================================ + +;;; Read Apple II paddles 0 and 1 by triggering PTRIG and polling +;;; PADDL0/PADDL1 in lockstep until each one's discharge timer expires. +;;; X = paddle0 count, Y = paddle1 count. If paddle0 hits 256 first +;;; (X wraps to 0), the routine pops its return address and exits via +;;; `L8C53 -> rts` so the caller's caller gets control -- a "paddle +;;; saturated, abort the calling context" signal. +.proc ReadPaddles + ldy #$00 + ldx #$00 + sta PTRIG +loop: + lda PADDL0 + bpl Paddle0Done ; paddle0 done -> poll only paddle1 + inx + bne Paddle1Poll + beq Saturated ; paddle0 overflowed (X wrapped) -> pop & exit +Paddle1Poll: + .refto Paddle1Poll + .refto L8C41 +L8C41: lda PADDL1 ; paddle0 still discharging; check paddle1 + bpl Paddle1DoneFirst + iny + bne loop +Paddle0Done: + .refto Paddle0Done + .refto L8C49 +L8C49: nop ; paddle0 finished early; keep timing parity + nop + lda PADDL1 + bpl BothDone + iny + bne loop +Saturated: + .refto Saturated + .refto L8C53 +L8C53: pla ; saturation path: drop caller's return addr + pla + rts + +Paddle1DoneFirst: + .refto Paddle1DoneFirst + .refto L8C56 +L8C56: nop ; paddle1 finished early; back to main loop + jmp loop + +BothDone: + .refto BothDone + .refto L8C5A +L8C5A: rts ; both paddles done + +.endproc + +;;; ============================================================ + +;;; Scale and clamp value to -127...127 +;;; Uses: $C6 + +.proc ScalePaddleValue + sta $C6 + clc + adc $C6 ; *= 2 (signed); overflow -> clamp + bvc Done + bpl ClampNeg + lda #127 + rts + +ClampNeg: + .refto ClampNeg + .refto L8C67 +L8C67: lda #AS_BYTE(-127) +Done: + .refto Done + .refto L8C69 +L8C69: rts +.endproc + +;;; ============================================================ + +.proc DemoMode48K + lda YokeVertPos + cmp #$18 + bcs :+ + jsr YokeUp +: jsr MoreThrottle + rts +.endproc + +;;; ============================================================ + +.proc MaybeProcessKey + jsr ReadKeyBuffer + bcc :+ + ldx EditModeFlag + beq L8C88 + ora #$80 + sta $08B1 +: rts + +L8C88: cmp #'`' ; ignore lower-case range + bcc :+ + rts + +: asl a ; *= 2 + clc + adc #KeyTable + adc #$00 + sta @mod+2 +@mod: jmp ($0000) ; self-modified +.endproc + +;;; ============================================================ + +.proc EditMode + lda #$01 + sta EditModeFlag + rts +.endproc + +;;; 1 key +;;; Magnetos: off +;;; Nav Radio / VORS: Select 1 +.proc Select1 + ldx #$01 + lda InputMode + cmp #$03 ; Magnetos ? + bne S1_NotMagneto + lda #$00 + ldx #$00 + beq ApplyMagnetoState ; always +S1_NotMagneto: + .refto S1_NotMagneto + .refto L8CB2 +L8CB2: cmp #$06 ; Nav Radio (upper) ? + beq S1_StoreNAV + cmp #$07 ; Nav Radio (lower) ? + bne S1_TestVOR +S1_StoreNAV: + .refto S1_StoreNAV + .refto L8CBA +L8CBA: stx $0A63 + rts + +S1_TestVOR: + .refto S1_TestVOR + .refto L8CBE +L8CBE: cmp #$0C ; VORS ? + bne S1_Exit + stx $0A71 +S1_Exit: + .refto S1_Exit + .refto L8CC5 +L8CC5: rts +.endproc + +;;; Apply a magneto-state selection (called from the 1/2/3 key +;;; handlers when in magneto-input mode). On 48K this just exits +;;; input mode; in 64K the slot is patched to JMP `SetMagnetoFromA`, +;;; which takes A = state, X = encoded bit pattern and updates the +;;; per-magneto flags before returning control to the simulator. +ApplyMagnetoState: + ldy #$00 ; 64k: Patched to JMP `SetMagnetoFromA` + sty InputMode + rts + +;;; 2 key +;;; Magnetos: Right +;;; Nav Radio / VORS: Select 2 +.proc Select2 + lda InputMode + ldx #$02 + cmp #$03 ; Magnetos + bne S2_NotMagneto + lda #$01 + ldx #$01 + bne ApplyMagnetoState ; always + +S2_NotMagneto: + .refto S2_NotMagneto + .refto L8CD9 +L8CD9: cmp #$06 ; Nav Radio (upper digits) + beq S2_StoreNAV + cmp #$07 ; Nav Radio (lower digits) + bne S2_TestVOR +S2_StoreNAV: + .refto S2_StoreNAV + .refto L8CE1 +L8CE1: stx $0A63 + rts + +S2_TestVOR: + .refto S2_TestVOR + .refto L8CE5 +L8CE5: cmp #$0C + bne S2_Exit + stx $0A71 +S2_Exit: + .refto S2_Exit + .refto L8CEC +L8CEC: rts +.endproc + +;;; 3 key +MagnetosLeft: + lda #$01 + ldx #$02 + bne ApplyMagnetoState ; always + +;;; 8 key +SlewPitchUp: + dec SlewPitchRate + rts + +;;; 9 key +SlewPitchDown: + inc SlewPitchRate + rts + +;;; ??? + lda #$00 + ldx #$05 + bne ApplyMagnetoState + +;;; InputMode values (= $FA), used by KeyDecrease / KeyIncrease / +;;; Select1 / Select2 etc. to route the input keys to whichever +;;; instrument is currently being edited: +;;; +;;; $02 Radar View (`,` zoom out / `.` zoom in) +;;; $03 Magnetos / Mixture (1/2/3 keys, chunk3 mixture patch) +;;; $04 Com Radio upper digits +;;; $05 Com Radio lower digits +;;; $06 NAV Radio upper digits (page $0A63 picks NAV1/NAV2) +;;; $07 NAV Radio lower digits +;;; $08..$0B Transponder digit 0..3 +;;; $0C VOR OBS course (page $0A71 picks VOR1/VOR2) +;;; $0D..$0F ADF frequency digit 0..2 (chunk3 patch path) +;;; $10 Fuel Tank Select +;;; +;;; KeyDecrease branches via a chain of `sec sbc #2 / dex / cpx` to +;;; route the keypress to the right "decrement" action and then to +;;; the matching redraw routine. The handler that calls it always +;;; resets `InputCounter` first so the next key starts a fresh timeout. + +;;; , key +.proc KeyDecrease + lda InputMode ; 64k: patched to JSR `KeyDecreasePatch` + nop + ldx #$00 + stx InputCounter + sec + sbc #$02 ; A = mode - 2; A == 0 -> Radar View + bne L8D20 + + ;; ----- Mode $02: Radar View - Zoom Out + sec + rol ZoomLevel + rol ZoomLevel+1 + rol ZoomLevel+2 + rol ZoomLevel+2 + clc + ror ZoomLevel+2 + rts + +KD_NotRadar: + .refto KD_NotRadar + .refto L8D20 +L8D20: ldx WW1AceMode + beq KD_NotWW1 + jmp KD_FuelGate ; in WW1 ace mode -> bottom-tier dispatch + + ;; ----- Mode $04: Com Radio - upper digits ---------------- +KD_NotWW1: + .refto KD_NotWW1 + .refto L8D28 +L8D28: sec + sbc #$02 + tax + bne KD_NotComUpper ; not Com upper -> next case + + ;; Com Radio (upper digits) + lda str_com1 + ldx str_com1+1 + cmp #'1' + bne L8D42 + cpx #'8' + bne L8D42 + lda #'3' + ldx #'5' + bne L8D4C +L8D42: dex + cpx #'0'-1 + bne L8D4C + ldx #'9' + sec + sbc #1 +L8D4C: jmp L8E73 + + ;; ----- Mode $05: Com Radio - lower digits ---------------- +KD_NotComUpper: + .refto KD_NotComUpper + .refto L8D4F +L8D4F: dex + bne KD_NotComLower + lda str_com1+2 + ldx str_com1+3 + jsr DecComOrNavLowerDigits + jmp L8E88 + + ;; ----- Mode $06: NAV Radio - upper digits ---------------- +KD_NotComLower: + .refto KD_NotComLower + .refto L8D5E +L8D5E: dex + bne KD_NotNAVUpper + lda $0A63 ; $0A63 = currently-selected NAV (2 = NAV2) + cmp #$02 + beq KD_NAV2Upper + lda str_nav1 + ldx str_nav1+1 + jsr DecNavDigits + jmp L8EA4 + +KD_NAV2Upper: + .refto KD_NAV2Upper + .refto L8D74 +L8D74: lda str_nav2 + ldx str_nav2+1 + jsr DecNavDigits + jmp L8EB6 + + ;; ----- Mode $07: NAV Radio - lower digits ---------------- +KD_NotNAVUpper: + .refto KD_NotNAVUpper + .refto L8D80 +L8D80: dex + bne KD_NotNAVLower + lda $0A63 + cmp #$02 + beq KD_NAV2Lower + lda str_nav1+2 + ldx str_nav1+3 + jsr DecComOrNavLowerDigits + jmp L8ED2 + +KD_NAV2Lower: + .refto KD_NAV2Lower + .refto L8D96 +L8D96: lda str_nav2+2 + ldx str_nav2+3 + jsr DecComOrNavLowerDigits + jmp L8EE4 + + ;; ----- Modes $08..$0B: Transponder digits 0..3 ----------- + ;; X = mode - 8 = digit slot. +KD_NotNAVLower: + .refto KD_NotNAVLower + .refto L8DA2 +L8DA2: dex + cpx #$04 + bcs KD_NotXpndr ; X >= 4 -> not transponder + lda str_xpndr,x + cmp #'0' + bne KD_XpndrSub + lda #'8' ; wrap '0' -> '7' (after sbc 1 below) +KD_XpndrSub: + .refto KD_XpndrSub + .refto L8DB0 +L8DB0: sec + sbc #1 + sta str_xpndr,x + jmp DrawXPNDR + + ;; ----- Mode $0C: VOR1/VOR2 OBS course -------------------- +KD_NotXpndr: + .refto KD_NotXpndr + .refto L8DB9 +L8DB9: dex + dex + dex + dex ; X = mode - 12 + bne KD_FuelGate ; X != 0 -> Fuel Tank or other + lda $0A71 ; $0A71 = currently-selected VOR (2 = VOR2) + cmp #$02 + beq KD_DecVOR2 + dec VOR1ObsCourse + lda VOR1ObsCourse + cmp #$FF + bne KD_DrawVOR1 + lda #$B3 ; wrap to $B3 (= 180 deg in compass units?) + sta VOR1ObsCourse +KD_DrawVOR1: + .refto KD_DrawVOR1 + .refto L8DD5 +L8DD5: jmp DrawVOR1 + +KD_DecVOR2: + .refto KD_DecVOR2 + .refto L8DD8 +L8DD8: dec VOR2ObsCourse + lda VOR2ObsCourse + cmp #$FF + bne KD_DrawVOR2 + lda #$B3 + sta VOR2ObsCourse +KD_DrawVOR2: + .refto KD_DrawVOR2 + .refto L8DE7 +L8DE7: jmp DrawVOR2 + + ;; ----- Mode $10: Fuel Tank Select ------------------------ +KD_FuelGate: + .refto KD_FuelGate + .refto L8DEA +L8DEA: lda InputMode + cmp #$10 + bne KD_Exit + + + lda $0998 + beq KD_FuelDone + dec $0998 +KD_FuelDone: + .refto KD_FuelDone + .refto L8DF8 +L8DF8: jmp UpdateFuelTankIndicator + +KD_Exit: + .refto KD_Exit + .refto L8DFB +L8DFB: rts +.endproc + +.proc DecComOrNavLowerDigits + cpx #'0' + bne Decrement1s + ldx #'5' ; '5' is the bottom of the .25 cycle + cmp #'0' + bne Decrement10s + lda #'9' + rts + +Decrement1s: + .refto Decrement1s + .refto L8E09 +L8E09: ldx #'0' + rts + +Decrement10s: + .refto Decrement10s + .refto L8E0C +L8E0C: sec + sbc #1 + rts +.endproc + +.proc DecNavDigits + cmp #'0' + bne :+ + cpx #'8' + bne :+ + lda #'1' + ldx #'7' + rts +: + dex + cpx #'0'-1 + bne :+ + ldx #'9' + lda #'0' +: + rts +.endproc + +;;; . key +KeyIncrease: + lda InputMode ; 64k: Patched to JSR `KeyIncreasePatch` + nop + ldx #$00 + stx InputCounter + sec + sbc #$02 + bne L8E47 ; not Radar View + + ;; Radar View - Zoom In + clc + ror ZoomLevel+2 + ror ZoomLevel+1 + ror ZoomLevel + + lda #$1F + ora ZoomLevel + sta ZoomLevel + rts + +KI_NotRadar: + .refto KI_NotRadar + .refto L8E47 +L8E47: ldx WW1AceMode + beq KI_NotWW1 + jmp KI_FuelGate ; in WW1 ace mode -> bottom-tier dispatch + + ;; ----- Mode $04: Com Radio - upper digits ---------------- +KI_NotWW1: + .refto KI_NotWW1 + .refto L8E4F +L8E4F: sec + sbc #$02 + tax + bne KI_NotComUpper + lda str_com1 + ldx str_com1+1 + cmp #'3' + bne KI_ComIncrement + cpx #'5' + bne KI_ComIncrement + lda #'1' ; wrap '35' (= 135.x) back to '18' (= 118.x) + ldx #'8' + bne KI_StoreCom +KI_ComIncrement: + .refto KI_ComIncrement + .refto L8E69 +L8E69: inx + cpx #'9'+1 + bne KI_StoreCom + ldx #'0' + clc + adc #1 +KI_StoreCom: + .refto KI_StoreCom + .refto L8E73 +L8E73: sta str_com1 + stx str_com1+1 + jmp KI_DrawCom + + ;; ----- Mode $05: Com Radio - lower digits ---------------- +KI_NotComUpper: + .refto KI_NotComUpper + .refto L8E7C +L8E7C: dex + bne KI_NotComLower + lda str_com1+2 + ldx str_com1+3 + jsr IncComOrNavLowerDigits +L8E88: sta str_com1+2 + stx str_com1+3 +KI_DrawCom: + .refto KI_DrawCom + .refto L8E8E +L8E8E: jmp DrawCom1 + + ;; ----- Mode $06: NAV Radio - upper digits ---------------- +KI_NotComLower: + .refto KI_NotComLower + .refto L8E91 +L8E91: dex + bne KI_NotNAVUpper + lda $0A63 + cmp #$02 + beq KI_NAV2Upper + lda str_nav1 + ldx str_nav1+1 + jsr IncNavDigits +L8EA4: sta str_nav1 + stx str_nav1+1 + jmp DrawNav1 + +KI_NAV2Upper: + .refto KI_NAV2Upper + .refto L8EAD +L8EAD: lda str_nav2 + ldx str_nav2+1 + jsr IncNavDigits +L8EB6: sta str_nav2 + stx str_nav2+1 + jmp DrawNav2 + + ;; ----- Mode $07: NAV Radio - lower digits ---------------- +KI_NotNAVUpper: + .refto KI_NotNAVUpper + .refto L8EBF +L8EBF: dex + bne KI_NotNAVLower + lda $0A63 + cmp #$02 + beq KI_NAV2Lower + lda str_nav1+2 + ldx str_nav1+3 + jsr IncComOrNavLowerDigits +L8ED2: sta str_nav1+2 + stx str_nav1+3 + jmp DrawNav1 + +KI_NAV2Lower: + .refto KI_NAV2Lower + .refto L8EDB +L8EDB: lda str_nav2+2 + ldx str_nav2+3 + jsr IncComOrNavLowerDigits +L8EE4: sta str_nav2+2 + stx str_nav2+3 + jmp DrawNav2 + + ;; ----- Modes $08..$0B: Transponder digits 0..3 ----------- +KI_NotNAVLower: + .refto KI_NotNAVLower + .refto L8EED +L8EED: dex + cpx #$04 + bcs KI_NotXpndr ; X >= 4 -> not transponder + lda str_xpndr,x + cmp #'7' + bne KI_XpndrAdd + lda #'0'-1 ; wrap '7' -> '0' (after adc 1 below) +KI_XpndrAdd: + .refto KI_XpndrAdd + .refto L8EFB +L8EFB: clc + adc #$01 + sta str_xpndr,x + jmp DrawXPNDR + + ;; ----- Mode $0C: VOR1/VOR2 OBS course -------------------- +KI_NotXpndr: + .refto KI_NotXpndr + .refto L8F04 +L8F04: dex + dex + dex + dex ; X = mode - 12 + bne KI_FuelGate + lda $0A71 + cmp #$02 + beq KI_IncVOR2 + inc VOR1ObsCourse + lda VOR1ObsCourse + cmp #$B4 ; wrap back to 0 at $B4 (= 180 deg) + bne KI_DrawVOR1 + lda #$00 + sta VOR1ObsCourse +KI_DrawVOR1: + .refto KI_DrawVOR1 + .refto L8F20 +L8F20: jmp DrawVOR1 + +KI_IncVOR2: + .refto KI_IncVOR2 + .refto L8F23 +L8F23: inc VOR2ObsCourse + lda VOR2ObsCourse + cmp #$B4 + bne KI_DrawVOR2 + lda #$00 + sta VOR2ObsCourse +KI_DrawVOR2: + .refto KI_DrawVOR2 + .refto L8F32 +L8F32: jmp DrawVOR2 + + ;; ----- Mode $10: Fuel Tank Select ------------------------ +KI_FuelGate: + .refto KI_FuelGate + .refto L8F35 +L8F35: lda InputMode + cmp #$10 + bne KI_Exit + lda $0998 + bne KI_FuelDone + inc $0998 +KI_FuelDone: + .refto KI_FuelDone + .refto L8F43 +L8F43: jsr UpdateFuelTankIndicator +KI_Exit: + .refto KI_Exit + .refto L8F46 +L8F46: rts + +;;; A = 10s digit, X = 1s digit +.proc IncComOrNavLowerDigits + cpx #'5' + bne SwapTo5 + ldx #'0' + cmp #'9' + bne Increment10s + txa + rts + +SwapTo5: + .refto SwapTo5 + .refto L8F53 +L8F53: ldx #'5' + rts + +Increment10s: + .refto Increment10s + .refto L8F56 +L8F56: ldx #'0' + clc + adc #1 + rts +.endproc + +;;; A = 10s digit, X = 1s digit +.proc IncNavDigits + cmp #'1' + bne :+ + cpx #'7' + bne :+ + lda #'0' + inx + rts +: + inx + cpx #'9'+1 + bne :+ + ldx #'0' + clc + adc #$01 +: rts +.endproc + +;;; W key +.proc DeclareWar + lda #$01 + ora WarDeclared + sta WarDeclared + rts +.endproc + + rts ; ??? + +;;; Ctrl+T +.proc TransponderOrTransferParams + lda SlewMode + beq Transponder + + ;; Transfer bank, pitch, heading to current flight parameters. + SUB16C #$00, $6C, $09AD + SUB16C #$00, $6E, $09AF + + LDAX $70 + STAX $09E4 + + rts +.endproc + +;;; Ctrl+X +.proc Transponder + lda InputCounter + beq L8FBE + lda InputMode + cmp #$08 + bcc L8FBE + cmp #$0C + bcs L8FBE + adc #$01 + bne L8FC0 +L8FBE: lda #$08 ; Transponder +L8FC0: jmp SetInputModeAndCounter +.endproc + +;;; Ctrl+A / A key +ADF: nop ; 64k: Patched to JMP `ADFKeyboardHook` + nop + nop + +;;; Ctrl+B +AltimeterAdjust: + lda #$00 + sta $099E + sta $099F + sta $09A0 + sta $09A1 + rts + +;;; Ctrl+D +HeadingAdjust: + LDAX $08B2 + sec + sbc $70 + tay + txa + sbc $71 + tax + tya + clc + adc $0848 + tay + txa + adc $0849 + tax + tya + clc + adc $0846 + tay + txa + adc $0847 + tax + tya + sta $08AB + stx $08AC + rts + +;;; Ctrl+I +CarbHeat: + lda $0A58 + eor #$FF + sta $0A58 + jmp DrawCarbHeatAndLights + +;;; K key +ExitDemoMode: + lsr DemoMode + rts + +;;; Ctrl+L / L key +ToggleLights: + lda PanelLights + eor #$FF + sta PanelLights + jmp DrawCarbHeatAndLights + +;;; X key +DropBomb: + lda WW1AceMode + cmp #$01 + bne :+ + lda WW1AceBombsStr + cmp #'0' + beq :+ + lda $0A56 + ora $0A57 + bne :+ + dec WW1AceBombsStr + LDAX $0A39 + STAX $0A56 +: rts + + +;;; Ctrl+C +ComRadio: + lda #$01 ; ??? + lda InputCounter + beq :+ + lda InputMode + cmp #$04 ; Com Radio (upper digits) + bne :+ + inc InputMode ; set to lower digits + rts +: + cmp #$05 ; lower digits? + bne :+ + inc $0909 +: lda #$04 ; upper digits + bne SetInputModeAndCounter + +;;; Ctrl+V +VORS: + lda #$0C ; VORS + sta InputMode + rts + +;;; Ctrl+N +NavRadio: + lda InputCounter + beq L906F + lda InputMode + cmp #$06 ; Nav Radio (upper digits) + bne L906F + inc InputMode ; set to lower digits + rts + +L906F: lda #$06 ; upper digits + +SetInputModeAndCounter: + sta InputMode + lda #$03 + sta InputCounter + rts + +;;; Ctrl+M +MagsAndMixture: + lda #$03 ; Magnetos / Fuel Mixture + sta InputMode + rts + +;;; Space - dual function: brake (on ground) / fire guns (in air). +;;; When on ground: decrement the brake counter L0A11+1, clamping to 0. +;;; When in air: jump to FireGuns to start a gun burst. +BrakesOrGuns: + lda OnGroundFlag + beq FireGuns + dec L0A11+1 + bpl L9090 + lda #$00 + sta L0A11 + sta L0A11+1 +L9090: rts + +;;; + key (and S key) - select a saved aircraft-mode preset. +;;; A=1 reads from library; A=2 saves to library. Sets the action +;;; flag for the next main-loop iteration to pick up. +ReadModeFromLibrary: + ldx #$01 +L9093: stx ModeLibraryAction + rts + +;;; S key +SaveModeToLibrary: + ldx #$02 + bne L9093 ; always + +;;; Q key +CoursePlotting: + jmp NoOp ; 64k: Patched to JMP `CoursePlottingSystem` + +;;; Z key +SlewResetAngles: + lda SlewMode + beq :+ + lda #$00 + sta $6E + sta $6F + sta $70 + sta $71 + sta $6C + sta $6D +: rts + +;;; In-air branch of BrakesOrGuns. If WW1 ace mode has bullets loaded +;;; ($0A54/$0A55 non-zero), starts a 3-tick gun burst by setting the +;;; firing counter at $0A60. Reused as a no-op when no bullets. +FireGuns: + .refto FireGuns + .refto L90B2 +L90B2: lda $0A54 + ora $0A55 + beq L90BF + lda #$03 + sta $0A60 +L90BF: rts + +;;; Ctrl+U +MoreThrottle: + inc SlewAltRate + lda #$78 + lsr $08C5 + bcs L90E5 + lda $0A6F + clc + adc #$08 + bpl L90E5 +L90D2: rts + +;;; Ctrl+H +LessThrottle: + dec SlewAltRate + lda #$00 + lsr $08C5 + bcs L90E5 + lda $0A6F + sec + sbc #$08 + bmi L90D2 +L90E5: sta $0A6F +L90E8: lda $0A6F + lsr a + lsr a + lsr a + jmp UpdateThrottleIndicator + +;;; / key +ToggleThrottle: + lda #$01 + sta $08C5 + rts + +;;; Ctrl+F +FuelTankSelect: + lda #$10 ; Fuel Tank Select + sta InputMode + rts + +;;; 4 key +;;; Magnetos: Both +;;; Otherwise: Radar View +SelectRadarView: ; 64k: Patched to JMP `SelectRadarViewPatch` + ldx #$02 ; Radar View + stx InputMode +L9100: dex + stx RadarView + rts + +;;; 5 key +;;; Magnetos: Start +;;; Otherwise: 3D View +Select3DView: ; 64k: Patched to JMP `Select3DViewPatch` + ldx #$01 ; 3D View + lda RadarView + bne L9100 + stx InputMode + rts + +;;; V key +TrimUp: + ldx #$0A ; back left + jsr MaybeSetViewDirectionAndAbort + + inc SlewYawRate + lda ElevatorTrim + clc + adc #4 + cmp #84 + jmp L9135 + +;;; R key -- trim down (and view-set: front left). +;;; Side effect: `inc $08C8` is the ONLY in-binary write to the WW1 +;;; war-report trigger flag. Pressing R in WW1 Ace mode therefore +;;; force-fires `ShowWarReport`. Likely a developer test-bind held +;;; over from in-house play; all "real" war-report triggers come from +;;; scenery bytecode via $1A / $25 opcodes. +TrimDown: + ldx #$0E ; front left + jsr MaybeSetViewDirectionAndAbort + + inc $08C8 ; <-- WW1 war-report trigger + inc SlewRollRate + lda ElevatorTrim + sec + sbc #4 + cmp #AS_BYTE(-84) +L9135: beq L913A + sta ElevatorTrim +L913A: lda #$50 + sec + sbc ElevatorTrim + lsr a + lsr a + lsr a + lsr a + jsr UpdateTrimIndicator + +Ignore: rts ; Used as no-op in `KeyTable` + +;;; Y key +FlapsUp: + ldx #$02 ; front right + jsr MaybeSetViewDirectionAndAbort + + dec SlewRollRate + lda $0A5F + sec + sbc #$20 + bpl L915A + lda #$00 +L915A: sta $0A5F +L915D: lsr a + lsr a + lsr a + lsr a + jsr UpdateFlapsIndicator + LDAX $0A5E + STAX $C2 + LDAX $0A31 + jsr ScaleC2ByAX + STAX $09F0 + jsr L9386 + LDAX $0A5E + STAX $C2 + LDAX $09DA + jsr ScaleC2ByAX + stx $09D9 + rts + +;;; N key +FlapsDown: + ldx #$06 ; back right + jsr MaybeSetViewDirectionAndAbort + + dec SlewYawRate + lda SlewMode + bne L91B0 + lda $0A5F + clc + adc #$20 + bpl L915A + lda #$60 + bpl L915A +L91B0: rts + +;;; T key +YokeDown: + ldx #$00 ; front + jsr MaybeSetViewDirectionAndAbort + + lda YokeVertPos + sec + sbc #4 + ldx SlewMode + bne L91C9 + ldx InputCounter + beq L91C9 + sec + sbc #$0C +L91C9: sta YokeVertPos + ora #$00 + bpl L91D6 + cmp #$B0 + bcs L91D6 + lda #$B0 +L91D6: sta YokeVertPos + lda #$03 + sta InputCounter +;;; Refresh the elevator-position indicator on the panel from the +;;; current `YokeVertPos`. Skips the redraw when the indicator is +;;; already at the right position. +RefreshElevatorIndicator: + lda #$50 + sec + sbc YokeVertPos + lsr a + lsr a + lsr a + cmp LastElevatorPosition + beq L91F2 + sta LastElevatorPosition + jsr UpdateElevatorPositionIndicator +L91F2: rts + +;;; D key +SlewToggleDigits: + inc ShowSlewDigits + rts + +;;; F key +YokeLeft: + ldx #$0C ; left + jsr MaybeSetViewDirectionAndAbort + + lda YokeHorizPos + sec + sbc #4 + cmp #AS_BYTE(-128) + beq L91F2 + sta YokeHorizPos + jmp ApplyAutoCoordination + +;;; G key +YokeCenter: + ldx #$FF ; down + jsr MaybeSetViewDirectionAndAbort + +;;; Reset yoke / rudder to centered, and (in slew mode) zero out the +;;; slew rates as well. Also tweaks $097C / $08B9 (probably engine +;;; mass-flow / inertia smoothing). +ResetYokeAndSlew: + lda #$00 + ldx SlewMode + beq :+ + sta YokeVertPos + sta SlewPitchRate + sta SlewRollRate + sta SlewYawRate + sta SlewAltRate +: + sta YokeHorizPos + sta RudderPos + lda $097C + lsr a + clc + adc $09CC + cmp $097C + bcc :+ + LDAX $09CB + STAX $08B9 +: jsr ApplyAutoCoordination + jmp ApplyRudderWithCoordination + +;;; H key +YokeRight: + ldx #$04 ; right + jsr MaybeSetViewDirectionAndAbort + + lda YokeHorizPos + clc + adc #$04 + bvs L92A0 +L9259: sta YokeHorizPos +;;; In auto-coordination mode, copy the horizontal yoke position +;;; into a paired rudder offset at $0A52->$0A64 so banking +;;; automatically applies a matching rudder kick. +ApplyAutoCoordination: + lda AutoCoordinationMode + beq RefreshAileronAndRoll + LDAX $0A52 ; includes `YokeHorizPos` + STAX $0A64 ; includes `RudderPos` + jsr RefreshRudderIndicator +;;; Refresh the aileron-position indicator from the yoke's horizontal +;;; axis (skipping the redraw if the indicator is already there) and +;;; update the roll-rate scaling at $09E8. +RefreshAileronAndRoll: + lda YokeHorizPos + clc + adc #$7F ; map -$80...$7F to 0...255 + lsr a + lsr a + lsr a + cmp LastAileronPosition + sta LastAileronPosition + beq L9284 + jsr UpdateAileronPositionIndicator +L9284: LDAX $0A52 ; includes `YokeHorizPos` + STAX $C2 + LDAX $0A2F + jsr ScaleC2ByAX + STAX $09E8 + jsr ComputeSideslipDerived +L92A0: rts + +;;; C key +RudderLeft: + lda RudderPos + sec + sbc #4 + cmp #AS_BYTE(-128) + bne RudderCommon + rts + +;;; M key +RudderRight: + lda RudderPos + clc + adc #4 + bvs L92DA + +RudderCommon: + sta RudderPos +;;; Push rudder changes through to the gauge (and the linked yoke +;;; position when auto-coordination is on). +ApplyRudderWithCoordination: + lda AutoCoordinationMode + beq RefreshRudderIndicator + LDAX $0A64 ; includes `RudderPos` + STAX $0A52 ; includes `YokeHorizPos` + jsr RefreshAileronAndRoll +RefreshRudderIndicator: + lda RudderPos + clc + adc #$7F ; map -$80...$7F to 0...255 + lsr a + lsr a + lsr a + jsr UpdateRudderPositionIndicator + jsr ComputeSideslipDerived +L92DA: rts + +;;; B key +.proc YokeUp + ldx #$08 ; back + jsr MaybeSetViewDirectionAndAbort + + lda YokeVertPos + clc + adc #$04 + ldx SlewMode + bne L92F3 + ldx InputCounter + beq L92F3 + clc + adc #$0C +L92F3: sta YokeVertPos + ora #$00 + bmi L9300 + cmp #$50 + bcc L9300 + lda #$50 +L9300: jmp L91D6 +.endproc + +;;; ============================================================ + +;;; Inputs: X = view direction +;;; If `InputMode` is 3D View, this aborts caller steps. + +.proc MaybeSetViewDirectionAndAbort + lda InputMode + cmp #$01 ; 3D View? + bne :+ + + dec InputMode ; return to Normal Flight mode + stx ViewDirection + pla ; abort caller steps + pla +: + rts +.endproc + +;;; ============================================================ + +;;; Recompute sideslip-derived state whenever the yoke or rudder +;;; changes. Stores the rudder-vs-aileron differential at $09C2 +;;; (drives the slip/skid ball) and updates derived scaling factors +;;; at $09E6, $09C3 and $0A0B. +ComputeSideslipDerived: + ldx RudderPos + jsr AXDiv2 + stx $B7 + ldx YokeHorizPos + jsr AXDiv2 + txa + sec + sbc $B7 + sta $09C2 + sta $C3 + lda #$00 + sta $C2 + LDAX $0A29 + jsr ScaleC2ByAX + STAX $09E6 + LDAX $09C1 + STAX $C2 + LDAX $0A25 + jsr ScaleC2ByAX + STAX $09C3 + LDAX $09C1 + STAX $C2 + LDAX $0A27 + jsr ScaleC2ByAX + STAX $0A0B + LDAX $09C1 + STAX $C2 + LDAX $0A23 + jsr ScaleC2ByAX + STAX $09C5 + +L9386: lda $09F0 + clc + adc $09B1 + tax + lda $09F1 + adc $09B2 + tay + txa + sec + sbc $09E8 + sta $09EE + tya + sbc $09E9 + sta $09EF + txa + clc + adc $09E8 + tax + tya + adc $09E9 + tay + txa + clc + adc $09C5 + sta $09EC + tya + adc $09C6 + sta $09ED + rts + + .byte $64 + .byte $64 + .byte $64 + .byte $5A + pha + .byte $37 + ora $1919,y + .byte $19 + +L93C9: rts + +;;; ============================================================ + +;;; Per-tick: derive secondary flight values from the primary state. +;;; Computes sin/cos of the pitch and bank angles ($09AE / $09B0), +;;; converts them into position-component multipliers, and runs a +;;; cascade of `ScaleC2ByAX` / table-lookup steps to update derived +;;; quantities like climb rate, rudder authority and airspeed +;;; modifiers used by the gauges. Skipped entirely while in slew mode. +;;; +;;; Outline of the cascade (each step writes into one or more of the +;;; per-frame derived 16-bit cells at $09BF..$0A1F): +;;; +;;; 1. ($0A03,$0A04) = sin(pitch), ($09B7,$09B8) = cos(pitch) +;;; 2. ($0A05,$0A06) = sin(bank), ($09BB,$09BC) = cos(bank) +;;; 3. Rudder authority gauge update via L180C/UpdateAltimeterIndicator. +;;; 4. ($09AB,$09AC) = airspeed-derived term used by the slip/skid +;;; ball and the stall logic at L94BF/L94D5/L94E1. +;;; 5. ($0A19,$0A1A) / ($0A1B,$0A1C) = body-frame X/Z velocity +;;; contributions; cleared while $08C3 (gear-warning?) is set. +;;; 6. Stall/spin trigger: when the airspeed-derived term wraps +;;; below $D1 or above $29, light the L94D5 fault state ($08B6/ +;;; $0899) -- otherwise jump past the trigger. +;;; 7. Final scale-and-add chain at L9544..L9779 producing climb +;;; rate ($0A0F/$0A10) and the auto-coordination value $09CF/$09D0. +;;; 8. Sideslip clamp at L978B: if |$09AD| crosses 90 deg, flip the +;;; signs of $09AE/$09B0/$09E5 to keep the side-slip indicator +;;; symmetric. +;;; +;;; All internal L-labels in this proc are branch targets inside the +;;; cascade; the order is sequential so it reads as one long block of +;;; multiply-add steps. +.proc ComputeFlightDerivedValues + lda SlewMode + bne L93C9 + ldx $09AD + lda $09AE + jsr L1778 + STAX $0A03 + lda $09AE + jsr L1763 + STAX $09B7 + ldx $09AF + lda $09B0 + jsr L1778 + STAX $0A05 + lda $09B0 + jsr L1763 + STAX $09BB + STAX $09C7 + lda #$D6 + sta $B8 + lda #$0D + sta $B9 + jsr UpdateAltimeterIndicator + tax + ldy RudderPos + jsr L180C + sty $09D4 + sta $09D3 + lda #$B8 + sta $B8 + lda #$0D + sta $B9 + jsr UpdateAltimeterIndicator + LDAX $0A5A + jsr ScaleC2ByAX + sta $09AB + sta $C2 + txa + clc + adc #$07 + sta $09AC + sta $C3 + LDAX $0A2D + jsr ScaleC2ByAX + STAX $09EA + lda #$BF + sta $B8 + lda #$93 + sta $B9 + jsr UpdateAltimeterIndicator + LDAX $C2 + sta L00A5 + stx $A6 + LDAX L00A5 + STAX $C2 + LDAX $09EC + jsr ScaleC2ByAX + clc + adc $09EA + sta $0A19 + txa + adc $09EB + sta $0A1A + lda $08C3 ; gear-warning flag + beq CFDV_ComputeZContribution + lda #$00 + sta $0A19 + sta $0A1A +CFDV_ComputeZContribution: + .refto CFDV_ComputeZContribution + .refto L9493 +L9493: LDAX L00A5 + STAX $C2 + LDAX $09EE + jsr ScaleC2ByAX + clc + adc $09EA + sta $0A1B + txa + adc $09EB + sta $0A1C + ;; Stall test: if the airspeed-derived term $09AC is in a + ;; danger band (= |.| >= $D1 or in [+$29, +$2B]), set the + ;; "engine fault" cells via L94D5; otherwise clear them. + lda $09AC + bmi CFDV_NegativeBand + ldx #$80 + cmp #$29 + bcc CFDV_StoreSpinSeed + ldx #$AA +CFDV_StoreSpinSeed: + .refto CFDV_StoreSpinSeed + .refto L94BF +L94BF: stx $3A36 + stx $3E36 + stx $22B6 + stx $5A36 + stx $5E36 + stx $42B6 + cmp #$2C + bcc CFDV_NoFault +CFDV_SetFault: + .refto CFDV_SetFault + .refto L94D5 +L94D5: ldx #$0F + stx $08B6 + ldx #$06 + stx $0899 + bne CFDV_StoreFault +CFDV_NegativeBand: + .refto CFDV_NegativeBand + .refto L94E1 +L94E1: cmp #$D1 + bcc CFDV_SetFault +CFDV_NoFault: + .refto CFDV_NoFault + .refto L94E5 +L94E5: ldx #$00 +CFDV_StoreFault: + .refto CFDV_StoreFault + .refto L94E7 +L94E7: LDAX L0A11 + STAX $C2 + jsr ScaleC2ByAX + STAX $0A13 + STAX $C2 + LDAX $0A19 + jsr ScaleC2ByAX + stx $09F7 + asl a + rol $09F7 + asl a + rol $09F7 + sta $09F6 + LDAX $0A13 + STAX $C2 + LDAX $0A1B + jsr ScaleC2ByAX + stx $09F9 + asl a + rol $09F9 + asl a + rol $09F9 + sta $09F8 + ldx #$00 + lda $5F + cmp #$14 + lda $60 + sbc #$00 + bpl CFDV_HavePitchFactor + ldx #$0B +CFDV_HavePitchFactor: + .refto CFDV_HavePitchFactor + .refto L9544 +L9544: txa + pha + ADD16C $09F8, $09F6, $C2 + LDAX $09A9 + jsr ScaleC2ByAXIntoC2 + pla + clc + adc $09F3 + tax + lda $09F2 + jsr ScaleC2ByAX + STAX $09F4 + lda $08B6 ; spin-decay counter + beq CFDV_AfterSpinDecay + dec $08B6 + lda #$00 + sta $09F4 + sta $09F5 +CFDV_AfterSpinDecay: + .refto CFDV_AfterSpinDecay + .refto L9582 +L9582: LDAX $0A03 + STAX $C2 + LDAX $0A17 + jsr ScaleC2ByAX + STAX $0A09 + LDAX $09B7 + STAX $C2 + LDAX $0A17 + jsr ScaleC2ByAX + STAX $09BF + LDAX $09F4 + STAX $C2 + LDAX $09C7 + jsr ScaleC2ByAX + sec + sbc $09BF + sta $0A0F + txa + sbc $09C0 + sta $0A10 + LDAX $0A19 + STAX $C2 + jsr ScaleC2ByAX + STAX $0A1D + LDAX $0A1B + STAX $C2 + jsr ScaleC2ByAX + STAX $0A1F + clc + adc $0A1D + bcc CFDV_AfterCarry + inx + clc +CFDV_AfterCarry: + .refto CFDV_AfterCarry + .refto L9603 +L9603: adc $09E6 + sta $C2 + txa + adc $0A1E + clc + adc $09E7 + clc + adc $09D9 + sta $C3 + LDAX $09B5 + jsr ScaleC2ByAX + clc + adc $09B3 + sta $C2 + txa + adc $09B4 + sta $C3 + LDAX $0A13 + jsr ScaleC2ByAXIntoC2 + LDAX $09A9 + jsr ScaleC2ByAX + clc + adc $0A09 + sta $09CF + txa + adc $0A0A + sta $09D0 + lda L0A11 + ora L0A11+1 + beq CFDV_ClimbRate + lda $09CF + clc + adc $0A2B + sta $09CF + bcc CFDV_ClimbRate + inc $09D0 +CFDV_ClimbRate: + .refto CFDV_ClimbRate + .refto L9662 +L9662: lda $0A0D + sec + sbc $09CF + sta $09D7 + sta $C2 + lda $0A0E + sbc $09D0 + sta $09D8 + sta $C3 + LDAX $09FB + jsr ScaleC2ByAX + STAX $09D1 + lda #$CC + sta $B8 + lda #$0D + sta $B9 + jsr UpdateAltimeterIndicator + LDAX $0A05 + jsr ScaleC2ByAXIntoC2 + LDAX $09F4 + jsr L97B0 + ldy OnGroundFlag + beq CFDV_OffGround + tay + txa + jmp CFDV_StoreTurnRate + +CFDV_OffGround: + .refto CFDV_OffGround + .refto L96AF +L96AF: clc + adc $09C3 + tay + txa + adc $09C4 +CFDV_StoreTurnRate: + .refto CFDV_StoreTurnRate + .refto L96B8 +L96B8: sty $09CD + sta $09CE + lda OnGroundFlag + beq CFDV_TurnAfterGround + lda L0A11 + ora L0A11+1 + beq CFDV_TurnAfterGround + ;; On ground with airspeed: divide turn rate by 8. + LDAX $0A64 + jsr AXDiv2 + jsr AXDiv2 + jsr AXDiv2 + sta $09CD + stx $09CE +CFDV_TurnAfterGround: + .refto CFDV_TurnAfterGround + .refto L96E0 +L96E0: lda #$C2 + sta $B8 + lda #$0D + sta $B9 + jsr UpdateAltimeterIndicator + lda $0899 ; AC-problem severity + beq CFDV_ClimbScale + LDAX $0A0F + jmp CFDV_DoClimbScale + +CFDV_ClimbScale: + .refto CFDV_ClimbScale + .refto L96F9 +L96F9: lda $0A10 + cmp #$80 + ror a + tax + lda $0A0F + ror a +CFDV_DoClimbScale: + .refto CFDV_DoClimbScale + .refto L9704 +L9704: jsr ScaleC2ByAX + sta $09C9 + txa + sta $09CA + ldx OnGroundFlag + beq CFDV_AfterGroundZero + tax + bpl CFDV_AfterGroundZero + lda #$00 + sta $09C9 + sta $09CA +CFDV_AfterGroundZero: + .refto CFDV_AfterGroundZero + .refto L971E +L971E: lda #$76 + sta $C2 + lda #$FD + sta $C3 + LDAX $09C9 + jsr ScaleC2ByAX + clc + adc $09D1 + tay + txa + adc $09D2 + tax + tya + ldy $0899 + beq CFDV_AccelOK + LDAX $09D1 +CFDV_AccelOK: + .refto CFDV_AccelOK + .refto L9745 +L9745: clc + adc L0A11 + sta L0A11 + txa + adc L0A11+1 + bvc CFDV_AccelClamp + lda #$64 ; overflow -> cap airspeed at $64 +CFDV_AccelClamp: + .refto CFDV_AccelClamp + .refto L9754 +L9754: bpl CFDV_StoreAirspeed + lda #$00 + sta L0A11 +CFDV_StoreAirspeed: + .refto CFDV_StoreAirspeed + .refto L975B +L975B: sta L0A11+1 + ;; Sideslip wrap-around: if accumulated |sideslip| >= +/- 90 deg + ;; (= |.| crosses $40 in the high byte), flip the signs of the + ;; related state cells to keep the indicator within range. + ADD16C $09AD, $09C9, $09AD + bpl CFDV_SideslipFlip + jsr CFDV_NegateSideslip + jsr CFDV_SideslipFlip +CFDV_NegateSideslip: + .refto CFDV_NegateSideslip + .refto L9779 +L9779: SUB16C #$0000, $09AD, $09AD + rts + +CFDV_SideslipFlip: + .refto CFDV_SideslipFlip + .refto L978B +L978B: tax + lda $09AD + cpx #$40 + bcc CFDV_NoFlip + jsr L1880 + sta $09AD + txa + eor #$80 + sta $09AE + lda $09B0 + eor #$80 + sta $09B0 + lda $09E5 + eor #$80 + sta $09E5 +CFDV_NoFlip: + .refto CFDV_NoFlip + .refto L97AF +L97AF: rts + +L97B0: jsr ScaleC2ByAX + asl a + tay + txa + rol a + asl a + tax + tya + asl a + bcc L97BE + inx +L97BE: rts +.endproc + +;;; ============================================================ + +;;; Per-frame physics integration. Computes the delta-time bucket +;;; at $09DE/$09DF (saturated to $7F when the frame counter has +;;; rolled), scales the body-frame velocity by it, then resolves the +;;; result into world-frame deltas via the attitude sin/cos cells +;;; populated by `ComputeFlightDerivedValues`. Skipped on the ground +;;; (the `OnGroundFlag` branch zeroes the stride). Called from the +;;; non-slew path of the main loop, just before `ApplyWind`. +.proc IntegratePhysicsStep + lda #$01 + sta $08C2 + lda $32 + tax + sec + sbc $08B7 + stx $08B7 + sta $09DE + lda $33 + tax + sbc $08B8 + stx $08B8 +L97DA: dec $34 + bmi L97E5 + clc + adc #$10 + bvc L97DA + lda #$7F +L97E5: inc $34 + sta $09DF + sta $C3 + lda #$00 + sta $C2 + LDAX $09CD + jsr ScaleC2ByAX + clc + adc $09E4 + sta $09E4 + txa + adc $09E5 + sta $09E5 + LDAX $09DE + STAX $C2 + LDAX L0A11 + jsr ScaleC2ByAX + STAX $09E0 + LDAX $0A0B + ldy OnGroundFlag + beq L982F + lda #$00 + ldx #$00 +L982F: clc + adc $09E4 + txa + adc $09E5 + sta $B7 + jsr L1768 + sta $0A07 + stx $0A08 + lda $B7 + jsr L1763 + STAX $09BD + LDAX $09B7 + STAX $C2 + LDAX $09E0 + jsr ScaleC2ByAX + STAX $09B9 + STAX $C2 + LDAX $0A07 + jsr ScaleC2ByAX + STAX $09D5 + LDAX $0A03 + STAX $C2 + LDAX $09E0 + jsr ScaleC2ByAX + clc + adc $08A1 + tay + txa + adc $08A2 + tax + tya + STAX $09A7 + LDAX $09BD + STAX $C2 + LDAX $09B9 + jsr ScaleC2ByAX + STAX $0A01 + lda $09F6 + sec + sbc $09F8 + sta $09FF + lda $09F7 + sbc $09F9 + asl $09FF + rol a + asl $09FF + rol a + sta $0A00 + sta $C3 + lda $09FF + sta $C2 + LDAX $09FD + jsr ScaleC2ByAX + STAX $09CB + SUB16C $09CB, $08B9, $09CB + lda $08B9 + ora $08BA + beq L990E + ldx #$00 + stx $08B9 + stx $08BA + inx + stx $089A +L990E: LDAX $09E0 + STAX $C2 + LDAX $09CB + jsr ScaleC2ByAX + asl a + tay + txa + rol a + tax + tya + clc + adc $09AF + sta $09AF + txa + adc $09B0 + sta $09B0 + ldx #$00 + lda $0A01 + clc + adc $62 + sta $62 + lda $0A02 + bpl L9945 + dex +L9945: adc $63 + sta $63 + txa + adc $64 + sta $64 + txa + adc $65 + sta $65 + ldx #$00 + lda $09D5 + clc + adc $5A + sta $5A + lda $09D6 + bpl IPS_NoBorrowX + dex +IPS_NoBorrowX: + .refto IPS_NoBorrowX + .refto L9963 +L9963: adc $5B + sta $5B + txa + adc $5C + sta $5C + txa + adc $5D + sta $5D + ldx #$00 + lda $09A7 + clc + adc $5E + sta $5E + lda $09A8 + bpl IPS_NoBorrowZ + dex +IPS_NoBorrowZ: + .refto IPS_NoBorrowZ + .refto L9981 +L9981: adc $5F + sta $5F + txa + adc $60 + sta $60 + bpl IPS_NotBelowGround + ;; Altitude went below ground -> clamp to (3, 0, 0). + lda #$00 + sta $5E + sta $60 + lda #$03 + sta $5F +IPS_NotBelowGround: + .refto IPS_NotBelowGround + .refto L9996 +L9996: lda $089E + beq IPS_ApplyAttitude + jsr LA524 ; reset altitude / climb on touchdown + dec $089E +IPS_ApplyAttitude: + .refto IPS_ApplyAttitude + .refto L99A1 +L99A1: LDAX $09AB + STAX $C2 + LDAX $0A21 + jsr ScaleC2ByAX + sec + sbc $09AD + pha + txa + sbc $09AE + ldy ViewDirection + bne IPS_AfterPitchAdj + clc + adc #$00 ; ViewDirection == 0 -> add nothing (placeholder) +IPS_AfterPitchAdj: + .refto IPS_AfterPitchAdj + .refto L99C5 +L99C5: tax + pla + ldy OnGroundFlag + bne IPS_StorePitchYaw + ;; In air: add aero-pitch contribution from $09DC/$09DD. + clc + adc $09DC + tay + txa + adc $09DD + tax + tya +IPS_StorePitchYaw: + .refto IPS_StorePitchYaw + .refto L99D7 +L99D7: sta $6C + txa + clc + adc $09FA + sta $6D + lda #$00 + sta $09FA + sec + sbc $09AF + sta $6E + lda #$00 + sbc $09B0 + sta $6F + LDAX $09E4 + STAX $70 + ;; OnGroundFlag = (altitude $5E:$5F:$60 < $00:$03:$01) ? -1 : 0. + ldx #$00 + lda $5E + cmp #$01 + lda $5F + sbc #$03 + lda $60 + sbc #$00 + bpl IPS_StoreGroundFlag + dex +IPS_StoreGroundFlag: + .refto IPS_StoreGroundFlag + .refto L9A0D +L9A0D: stx OnGroundFlag + txa + beq IPS_AirborneTail ; OnGroundFlag = 0 -> in air + ;; ----- On-ground branch: zero yaw/bank, take off-roll path. + lda #$00 + sta $09AF + sta $09B0 + sta $09CB + sta $09CC + ldx $09AE + bpl IPS_GroundContinue + sta $09AD + sta $09AE +IPS_GroundContinue: + .refto IPS_GroundContinue + .refto L9A2C +L9A2C: lda $089E + beq IPS_NotJustLanded + jsr BrakesOrGuns ; just landed -> brake + jmp IPS_TouchdownDone + +IPS_NotJustLanded: + .refto IPS_NotJustLanded + .refto L9A37 + ;; Check for crash: if pitch ($0A16) is heavily negative or + ;; the bank angle exceeds $14 (= ~28 deg), flag crash code 4 + ;; in $0834. +L9A37: ldx #$04 + lda $0A16 + bpl IPS_TouchdownDone + clc + adc #$08 + bpl IPS_CheckBank + stx $0834 ; pitch crash + lda #$00 + sta L0A11+1 ; cut airspeed +IPS_CheckBank: + .refto IPS_CheckBank + .refto L9A4B +L9A4B: lda $6F + clc + adc #$0A + cmp #$14 + bcc IPS_TouchdownDone + stx $0834 ; bank crash +IPS_TouchdownDone: + .refto IPS_TouchdownDone + .refto L9A57 +L9A57: ldx #$00 + lda $09E2 + bne IPS_StoreClimbDelay + dec $09E2 + ldx $0A16 +IPS_StoreClimbDelay: + .refto IPS_StoreClimbDelay + .refto L9A64 +L9A64: stx $09FA + jmp IPS_Airspeed + + ;; ----- Airborne branch: maintain $09E2 ground delay. +IPS_AirborneTail: + .refto IPS_AirborneTail + .refto L9A6A +L9A6A: cmp $09E2 + beq IPS_Airspeed + sta $09E2 + lda $63 + and #$03 + bne IPS_Airspeed + ldx #$00 + adc UpdateCounter + bpl IPS_StoreDamage + dex +IPS_StoreDamage: + .refto IPS_StoreDamage + .refto L9A7F +L9A7F: sta $09A0 + stx $09A1 +IPS_Airspeed: + .refto IPS_Airspeed + .refto L9A85 +L9A85: LDAX $09DE + STAX $C4 + lda $09A8 + ldx $09A7 + jsr L16A2 + LDAX $C2 + STAX $0A15 +.endproc + +UpdateAutoTrimAndYaw_Exit: + .refto UpdateAutoTrimAndYaw_Exit + .refto L9AA2 +L9AA2: rts + +;;; ============================================================ + +;;; Per-frame auto-rudder trim, yaw decay, and bomb-drop housekeeping. +;;; * Reads the heading-error fixed-point at $0830/$0831 and scales +;;; it into $09A9/$09AA (auto-trim correction). +;;; * Decays $09AF/$09B0 (yaw integrator) toward zero by 5 per frame +;;; when the rudder is centred, clamping at zero. +;;; * Triggers bomb/respawn behaviour via $0838 (and WW1Ace bomb count). +;;; Skipped in slew mode. +.proc UpdateAutoTrimAndYaw + lda SlewMode + bne L9AA2 + lda $0830 + tax + asl a + sta $C2 + lda $0831 + rol a + sta $C3 + ror a + lsr a + tay + txa + ror a + adc $C2 + sta $C2 + tya + adc $C3 + sta $C3 + lda #$FE + sbc $C2 + sta $09A9 + lda #$7F + sbc $C3 + sta $09AA + ;; Yaw integrator decay: pull $09AF/$09B0 toward zero by $05. + ;; Only runs when rudder is centered (RudderPos == 0). + lda RudderPos + bne UATY_BombCheck + LDAX $09AF + bmi UATY_NegativeYaw + sec + sbc #$05 + bcc UATY_PosCarry + inx +UATY_PosCarry: + .refto UATY_PosCarry + .refto L9AE4 +L9AE4: dex + bmi UATY_ClampZero + bpl UATY_StoreYaw +UATY_NegativeYaw: + .refto UATY_NegativeYaw + .refto L9AE9 +L9AE9: clc + adc #$05 + bcs UATY_NegCarry + dex +UATY_NegCarry: + .refto UATY_NegCarry + .refto L9AEF +L9AEF: inx + bmi UATY_StoreYaw +UATY_ClampZero: + .refto UATY_ClampZero + .refto L9AF2 +L9AF2: lda #$00 + tax +UATY_StoreYaw: + .refto UATY_StoreYaw + .refto L9AF5 +L9AF5: sta $09AF + stx $09B0 +UATY_BombCheck: + .refto UATY_BombCheck + .refto L9AFB + ;; Bomb-drop / respawn handler: $0838 = 1 means respawn, + ;; $0838 = 2 means respawn with WW1Ace bombs reloaded. +L9AFB: lda $0838 + beq UATY_Done + lda L0A11+1 + bne UATY_NotIdle + jsr ResetAircraftSystems + lda $0838 + cmp #$02 + bne UATY_NotIdle + lda #$64 + sta $0A54 ; bomb count = 100 + lda #$35 + sta WW1AceBombsStr ; "5" digit +UATY_NotIdle: + .refto UATY_NotIdle + .refto L9B19 +L9B19: lda #$00 + sta $0838 +UATY_Done: + .refto UATY_Done + .refto L9B1E +L9B1E: rts +.endproc + +;;; ============================================================ + +;;; Reset all instrument-operational and engine-fault flags back to +;;; their "all systems nominal" state, used when the aircraft is +;;; respawned after a crash or rebooted via the war-game flow. +.proc ResetAircraftSystems + lda #$19 + sta $0994 + sta $0997 + ldx #$FF + stx InstrumentOperationalFlags + stx $0915 + stx $089B + stx $089C + inx + stx $08A4 + stx $0991 + inx + stx $099A + stx $0999 + inc $08A5 + rts +.endproc + +;;; ============================================================ + +ApplySlewDeltas: + lsr $08C2 + bcc :+ + jsr ResetYokeAndSlew +: + lda YokeHorizPos + jsr MapYokePosToSlewDelta + clc + adc $5B + sta $5B + txa + adc $5C + sta $5C + lda $BC ; hi byte + adc $5D + sta $5D + + lda #$00 + sec + sbc YokeVertPos + jsr MapYokePosToSlewDelta + clc + adc $63 + sta $63 + txa + adc $64 + sta $64 + lda $BC ; hi byte + adc $65 + sta $65 + + ldx #$FF ; sign extend + lda SlewAltRate + bmi :+ + inx +: clc + adc $5F + sta $5F + txa + adc $60 + sta $60 + txa + adc $61 + sta $61 + bpl :+ + lda #$00 + sta $5F + sta $60 + sta $61 +: lda $60 + sta $60 ; ??? + + lda SlewPitchRate + asl a + tay + lda $6F + clc + adc #$40 + bpl :+ + tya + eor #$FF + clc + adc #$01 + tay +: tya + clc + adc $6D + sta $6D + bmi L9BC5 + cmp #$41 + bcc L9BE8 + jmp L9BC9 + +L9BC5: cmp #$C0 + bcs L9BE8 +L9BC9: SUB16C #$00, $6C, $6C + lda $6D + eor #$80 + sta $6D + lda $6F + eor #$80 + sta $6F + lda $71 + eor #$80 + sta $71 + +L9BE8: lda SlewRollRate + clc + adc $6F + sta $6F + lda $71 + sec + sbc SlewYawRate + sta $71 +;;; Copy the live north/east position vectors ($64-$65 / $5C-$5D) +;;; into the persistent `NorthPosition` / `EastPosition` slots, +;;; biasing the high bytes by $40. Used after slew or edit-mode +;;; updates so the new aircraft position is saved. +SyncPositionFromLive: + lda $65 + clc + adc #$40 + sta NorthPosition+1 + lda $64 + sta NorthPosition + lda $5D + clc + adc #$40 + sta EastPosition+1 + lda $5C + sta EastPosition + rts + +;;; Returns low byte in A, high byte in $BC +.proc MapYokePosToSlewDelta + tax + bpl L9C2B + lda #$FF + sta $BC + sta $B7 +L9C1C: asl $B7 + rol a + rol $BC + inx + inx + inx + inx + bmi L9C1C +L9C27: tax + lda $B7 + rts + +L9C2B: lda #$00 + sta $BC + sta $B7 +L9C31: dex + dex + dex + dex + bmi L9C27 + sec + rol $B7 + rol a + rol $BC + jmp L9C31 +.endproc + +;;; ============================================================ + +;;; Periodically (every 6 frames after the previous trigger) check +;;; whether the aircraft is in an unusual flight envelope -- pitch +;;; out of [-15, +15], bank past +-44 degrees, or airspeed above +;;; ~155 knots -- and refresh the warning timer at $0899. +CheckFlightEnvelope: + ldx $0899 + dex + bmi :+ + stx $0899 + rts + +: lda $09AE + clc + adc #$0F + cmp #$1E + bcs L9C68 + lda $09B0 + clc + adc #$16 + cmp #$2C + bcs L9C68 + lda L0A11+1 + clc + adc #$53 + cmp #$A6 + bcc L9C6D +L9C68: lda #$06 + sta $0899 +L9C6D: rts + +;;; In slew mode, draw the slew direction overlays. The 64K patch +;;; replaces the three-NOP slot with `JSR DrawSlewOverlays`. +;;; Both `MaybeDrawSlewOverlays` and `DrawHeading` end at the shared +;;; RTS at `L9C76`. +MaybeDrawSlewOverlays: + lda SlewMode + beq L9C76 +P64K_A: nop ; 64k: Patched to JSR `DrawSlewOverlays` + nop + nop +L9C76: rts + +.proc DrawHeading + lda $FB + and #$04 + beq L9C76 + LDAX $70 + clc + adc $08AB + tay + txa + adc $08AC + tax + tya + sec + sbc $0848 + tay + txa + sbc $0849 + tax + tya + sec + sbc $0846 + tay + txa + sbc $0847 + tax + tya + jsr L9CE6 + cmp LastDrawnHeading + beq L9C76 + sta LastDrawnHeading + pha + stx $3E + CALLAX Set3DigitString, str_heading + CALLAX DrawMessageOrange, msg_heading + ldx $3E + pla + sec + sbc #$B4 + tay + txa + sbc #$00 + tax + tya + cpx #$80 + bcc L9CD4 + adc #$67 + bcc L9CD3 + inx +L9CD3: inx +L9CD4: STAX ValueForString + CALLAX Set3DigitString, str_recip + JUMPAX DrawMessageOrange, msg_recip +.endproc + +L9CE6: tay + txa + lsr a + tax + tya + ror a + sta $C2 + stx $C3 + LDAX #$168 + jsr ScaleC2ByAX + STAX ValueForString + rts + +;;; Inputs: A,X = output string addr +;;; $B6-B7 = value +;;; Output: string set to 3-digit number +.proc Set3DigitString + ptr := $B8 + + sta ptr + stx ptr+1 + + LDAX #100 + ldy #0 + jsr DivideByAXAndSetDigitY + LDAX #10 + ldy #1 + jsr DivideByAXAndSetDigitY + lda ValueForString + ora #'0' + ldy #2 + sta (ptr),y + rts +.endproc + +.proc DrawMagCompass + lda $FC + and #$10 + beq L9D6A + lda ViewDirection + bne L9D6A ; skip unless forward + LDAX $08B2 + sec + sbc $70 + tay + txa + sbc $71 + tax + tya + clc + adc $0848 + tay + txa + adc $0849 + tax + tya + clc + adc $0846 + tay + txa + adc $0847 + tax + ;; X = signed heading-error; slew $08B2/$08B3 (= magnetic- + ;; compass display angle) toward 0 with rate that depends on + ;; the magnitude bucket: small (-10..+10) = no step; medium + ;; (within +/- $F6 / $0A) = step by $50; large = step by $B5 + ;; (positive) / $4B (negative). The sign bias bytes ($00, + ;; $FB, $FF, $04) on each path balance the slew into a + ;; 16-bit two's complement result. + tya + inx + dex + bmi DH_NegativeError + bne DH_PositiveError + lda #$00 ; error == 0 -> no step + ldx #$00 + jmp DH_ApplyStep + +DH_PositiveError: + .refto DH_PositiveError + .refto L9D58 +L9D58: cpx #$0A + bpl DH_PosLarge + lda #$B5 + ldx #$FF + jmp DH_ApplyStep + +DH_PosLarge: + .refto DH_PosLarge + .refto L9D63 +L9D63: lda #$50 + ldx #$FB + jmp DH_ApplyStep + +DH_RTS: + .refto DH_RTS + .refto L9D6A +L9D6A: rts + +DH_NegativeError: + .refto DH_NegativeError + .refto L9D6B +L9D6B: cpx #$F6 + bmi DH_NegLarge + lda #$4B + ldx #$00 + jmp DH_ApplyStep + +DH_NegLarge: + .refto DH_NegLarge + .refto L9D76 +L9D76: lda #$B0 + ldx #$04 +DH_ApplyStep: + .refto DH_ApplyStep + .refto L9D7A +L9D7A: clc + adc $08B2 + tay + txa + adc $08B3 + tax + tya + sta $08B2 + stx $08B3 + jsr L9CE6 + CALLAX Set3DigitString, str_magcompass + JUMPAX DrawMessageWhite, msg_magcompass +.endproc + +;;; Decode a 4-character ASCII frequency string at AX into a packed +;;; BCD pair (A = lower digit pair, X = upper digit pair). +DecodeBCDFreqString: + sta $BE + stx $BF + ldy #$00 + lda ($BE),y + asl a + asl a + asl a + asl a + sta $B7 + iny + lda ($BE),y + and #$0F + ora $B7 + tax + iny + lda ($BE),y + asl a + asl a + asl a + asl a + sta $B6 + iny + lda ($BE),y + and #$0F + ora $B6 + rts + +DrawNav1: + jsr CheckForAbort + CALLAX DrawMessageOrange, msg_nav1 + LDAX #str_nav1 + jsr DecodeBCDFreqString + sta $08F7 + stx $08F8 +;;; Clear the NAV1-active bit and arm a lookup pass against the +;;; scenery for the freshly typed frequency. +RequestNAV1Lookup: + lda #$FE + jmp RequestNAVLookupCommon + +DrawNav2: + jsr CheckForAbort + lda ADFMode + bne L9E08 + CALLAX DrawMessageOrange, msg_nav2 + LDAX #str_nav2 + jsr DecodeBCDFreqString + sta $08F5 + stx $08F6 +;;; Clear the NAV2-active bit and arm a lookup pass. +RequestNAV2Lookup: + lda #$FD +;;; Common tail: AND `$08F4` with the supplied mask, set the lookup +;;; gate at `$08F2`. +RequestNAVLookupCommon: + and $08F4 + sta $08F4 + lda #$01 + sta $08F2 +L9E08: rts + +.proc DrawCom1 + jsr CheckForAbort + CALLAX DrawMessageOrange, msg_com1 + LDAX #str_com1 + jsr DecodeBCDFreqString + sta $089F + stx $08A0 + lda #$01 + sta $0913 + sta $0909 + rts +.endproc + +.proc DrawVOR1 + lda $FB + and #$20 + beq L9E63 + lda VOR1ObsCourse + jsr ATo3Digits + sty str_vor1_course + stx str_vor1_course+1 + sta str_vor1_course+2 + lda VOR1ObsCourse + sec + sbc #$5A + bcs L9E49 + clc + adc #$B4 +L9E49: jsr ATo3Digits + sty str_vor1_recip + stx str_vor1_recip+1 + sta str_vor1_recip+2 + CALLAX DrawMessageWhite, msg_vor1_course + CALLAX DrawMessageWhite, msg_vor1_recip +L9E63: rts +.endproc + +;;; A to 3-digit string in A,X,Y +;;; Input: A +;;; Output: "YXA" (Y is most significant digit, A is least) +.proc ATo3Digits + ldy #'0'-1 +: iny + sec + sbc #50 ; not 100 ??? + bcs :- + clc + adc #50 + + asl a ; *= 2 (because 50 I guess) + + ldx #'0'-1 +: inx + sec + sbc #10 + bcs :- + clc + adc #'0'+10 + rts +.endproc + +.proc DrawVOR2 + lda ADFMode + bne L9EBB + lda $FD + and #$08 + beq L9EBB + lda VOR2ObsCourse + jsr ATo3Digits + sty str_vor2_course + stx str_vor2_course+1 + sta str_vor2_course+2 + lda VOR2ObsCourse + sec + sbc #$5A + bcs L9EA1 + clc + adc #$B4 +L9EA1: jsr ATo3Digits + sty str_vor2_recip + stx str_vor2_recip+1 + sta str_vor2_recip+2 + CALLAX DrawMessageWhite, msg_vor2_course + CALLAX DrawMessageWhite, msg_vor2_recip +L9EBB: rts +.endproc + +.proc DrawDME + jsr CheckForAbort + lsr $097E + bcc Done ; nothing requested + lda $097D + bne OutOfRange ; signal too weak / no station tuned + ;; Format distance: $097F = 1s digit, $0980 = 10s/100s digits. + lda $097F + clc + adc #'0' + sta str_dme+2 + lda $0980 + ldx #$2F ; '0'-1 +DivideLoop: + .refto DivideLoop + .refto L9ED7 +L9ED7: inx ; bump 10s digit + sec + sbc #$0A + bcs L9ED7 + clc + adc #$3A ; recover 1s digit (= remainder + ':') + stx str_dme + sta str_dme+1 +DrawIt: + .refto DrawIt + .refto L9EE6 +L9EE6: + CALLAX DrawMessageOrange, msg_dme +Done: + .refto Done + .refto L9EED +L9EED: rts + +OutOfRange: + .refto OutOfRange + .refto L9EEE +L9EEE: lda #' ' + sta str_dme + sta str_dme+1 + sta str_dme+2 + jmp L9EE6 +.endproc + +.proc CheckForAbort + lda $FC + and #$80 + bne :+ + pla + pla +: rts +.endproc + +.proc DrawXPNDR + jsr CheckForAbort + CALLAX DrawMessageOrange, msg_xpndr + rts +.endproc + +DrawCarbHeatAndLights: + LDAX #msg_carbheat_on + ldy $0A58 + bne :+ + CALLAX DrawMessageWhite, msg_carbheat_off + jmp DrawLights + +: jsr DrawMessageOrange + +DrawLights: + jsr CheckForAbort + LDAX #msg_lights_on + ldy PanelLights + bne :+ + LDAX #msg_lights_off +: +P64K_7: jmp DrawMessageWhite ; 64k: Patched to JMP `DrawMagnetoStateHook` + +.proc DrawRPM + lda $0990 + tax + cmp LastDrawnRPM + beq L9F6A + stx LastDrawnRPM + txa + lsr a + tax + lda #'0' ; even RPM bucket -> '0' + bcc DrawRPM_StoreOnes + lda #'5' ; odd RPM bucket -> '5' +DrawRPM_StoreOnes: + .refto DrawRPM_StoreOnes + .refto L9F4E +L9F4E: sta str_rpm+2 + txa + ldx #'0'-1 +DrawRPM_DivLoop: + .refto DrawRPM_DivLoop + .refto L9F54 +L9F54: inx + sec + sbc #10 + bcs L9F54 + clc + adc #'0' + 10 + stx str_rpm ; 10s digit + sta str_rpm+1 ; 1s digit + CALLAX DrawMessageOrange, msg_rpm +DrawRPM_Done: + .refto DrawRPM_Done + .refto L9F6A +L9F6A: rts +.endproc + +;;; ============================================================ + +;;; Recompute the live altimeter pose. Skips when bit 3 of $FB +;;; (altimeter operational) is clear. +UpdateAltimeterPose: + lda $FB + and #$08 + bne :+ + rts +: + lda $5F + sta $0A39 + clc + adc $0A36 + sta $0830 + lda $60 + sta $0A3A + adc $0A37 + sta $0831 + ADD16 $0830, $099E, $C2 + LDAX #$24F4 + jsr ScaleC2ByAX + ;; Modulo-$0370 reduce by repeated subtract. +UAP_ModLoop: + .refto UAP_ModLoop + .refto L9FA1 +L9FA1: sta L00A5 + stx $A6 + sec + sbc #$70 + tay + txa + sbc #$03 + bmi UAP_ModDone + tax + tya + jmp L9FA1 + +UAP_ModDone: + .refto UAP_ModDone + .refto L9FB3 +L9FB3: LDAX L00A5 + STAX $C2 + LDAX #$CCC + jsr ScaleC2ByAX + eor #$FF + clc + adc #$16 + bpl UAP_Store10K + clc + adc #$58 +UAP_Store10K: + .refto UAP_Store10K + .refto L9FCC +L9FCC: sta $29 ; 10000s-ft hand angle byte + LDAX L00A5 + ;; Modulo-$58 reduce for the 1000s-ft hand angle. +UAP_Mod58Loop: + .refto UAP_Mod58Loop + .refto L9FD2 +L9FD2: sec + sbc #$58 + bcs L9FD2 + dex + bpl L9FD2 + eor #$FF + clc + adc #$BE + bpl UAP_Store1K + clc + adc #$58 +UAP_Store1K: + .refto UAP_Store1K + .refto L9FE4 +L9FE4: sta $28 ; 1000s-ft hand angle byte + jmp UpdateAltimeterIndicator::Init2 + +;;; Recompute an airspeed-derived value (likely the engine pitch / +;;; sound bucket) from `L0A11+1`. Skips when bit 0 of $FB (airspeed +;;; instrument operational) is clear. Distinct from chunk4's +;;; `UpdateAirspeedIndicator`, which animates the gauge needle. +UpdateAirspeedDerivedValue: + lda $FB + and #$01 + bne UADV_Body + rts + +UADV_Body: + .refto UADV_Body + .refto L9FF0 +L9FF0: lda L0A11+1 + cmp #$5A ; cap airspeed hi byte at $5A + bcc UADV_HavePitch + lda #$5A +UADV_HavePitch: + .refto UADV_HavePitch + .refto L9FF9 +L9FF9: pha + lsr a + lsr a + tax + lda $07DF,x + sta $B6 + lda $07E0,x + sec + sbc $B6 ; interpolation delta + tay + lda L0A11 + rol a + pla + rol a + and #$07 ; 3-bit fraction of airspeed + asl a + asl a + asl a + asl a + tax + jsr L180C ; interp delta * fraction / 16 + tya + clc + adc $B6 + sta $0A33 + rts + +;;; Recompute the magnetic-heading byte at $2A from the raw heading +;;; value at $0A15/$0A16. Skips entirely if instrument bit 7 of $FB +;;; (heading indicator) is off. +UpdateMagneticHeading: + lda $FB + and #$80 + bne :+ + rts + +: lda $0A15 + rol a + tay + lda $0A16 + bmi UMH_NegClamp + cmp #$09 ; clamp positive turn rate to +$09 + bcc UMH_HavePrimary + lda #$09 + bne UMH_HavePrimary +UMH_NegClamp: + .refto UMH_NegClamp + .refto LA03A +LA03A: cmp #$F7 ; clamp negative turn rate to -$09 + bcs UMH_HavePrimary + lda #$F7 +UMH_HavePrimary: + .refto UMH_HavePrimary + .refto LA040 +LA040: rol a + cpy #$80 + rol a + eor #$FF + clc + adc #$84 +UMH_ModLoop: + .refto UMH_ModLoop + .refto LA049 +LA049: cmp #$58 + bcc UMH_Store + sbc #$58 + bne LA049 +UMH_Store: + .refto UMH_Store + .refto LA051 +LA051: sta $2A + rts + +;;; Update the slip/skid ball indicator. On the ground the lateral +;;; offset comes straight from $09C2; in the air it's derived from +;;; the rudder position. Falls back to a clamped 0..16-ish range +;;; before handing the index to `UpdateSlipSkidIndicator`. +UpdateSlipSkid: + lda $FB + and #$02 + beq USS_Exit + lda OnGroundFlag + bne USS_FromRudder + lda $09C2 + jmp USS_MapByte + +USS_FromRudder: + .refto USS_FromRudder + .refto LA065 +LA065: lda #$00 + ldy L0A11+1 + beq USS_MapByte + sec + sbc RudderPos +USS_MapByte: + .refto USS_MapByte + .refto LA070 +LA070: clc + adc #$7F ; map -$80...$7F to 0...255 + lsr a + lsr a + sec + sbc #$1F + bmi USS_NegClamp + cmp #$09 + bcc USS_Final + lda #$08 + bne USS_Final +USS_NegClamp: + .refto USS_NegClamp + .refto LA082 +LA082: cmp #$F8 + bcs USS_Final + lda #$F8 +USS_Final: + .refto USS_Final + .refto LA088 +LA088: clc + adc #$09 + jmp UpdateSlipSkidIndicator + +USS_Exit: + .refto USS_Exit + .refto LA08E +LA08E: rts + +LA08F: brk +LA090: brk +LA091: .byte $8C +LA092: .byte $02 + ora ($05),y + stx $F807 + ora #$57 + .byte $0C + .byte $9F + asl $10CD + cpx $12 + .byte $DF + .byte $14 + cmp ($16,x) + .byte $89 + clc + and $CF1A,y + .byte $1B + lsr a + ora $1EAE,x + brk + .byte $20 + +PLVORNeedle: + .byte 14 + .byte 0,1, 0,2, 0,3, 0,4, 0,5, 0,6, 0,7 + .byte 1,1, 1,2, 1,3, 1,4, 1,5, 1,6, 1,7 + + .byte $00, $00 + +;;; Recompute the VOR1 navigation bearing. The VOR1 station's tuning +;;; offset (`VOR1ObsCourse`) is shifted by an aircraft-velocity term whenever +;;; `$0905` says the station is active, then handed to +;;; `ComputeVORBearing` with instrument bit $08. +UpdateVOR1Bearing: + lda #$01 + ldy VOR1ObsCourse + ldx $0905 + beq LA0EE + pha + LDAX $0852 + STAX $C2 + LDAX #$1555 + jsr ScaleC2ByAX + tay + pla +LA0EE: ldx #$08 + jsr ComputeVORBearing + sta $0987 + jsr LA32D + sta $0982 + jmp LA34B + +;;; Compute the bearing from the aircraft to a VOR station. Inputs: +;;; A = instrument index ($01 for VOR1, $02 for VOR2) +;;; X = mask byte testing `$08F4` (instrument operational flags) +;;; Y = station tuning offset +;;; Returns the bearing in A and the slope in $B6/$B7. +ComputeVORBearing: + and $08F4 + bne CVB_Active + sta $B7 + sta $B6 + rts + +CVB_Active: + .refto CVB_Active + .refto LA109 +LA109: sty $3F + jsr ComputeStationDelta + lda $3F + sta $C2 + lda #$00 + sta $C3 + lda #$6C + ldx #$01 + jsr MultiplyAXByC2 + ldx $C6 + tay + txa + sec + sbc $B6 + sta $C2 + tya + sbc $B7 + sta $C3 + LDAX #$21C + jsr ScaleC2ByAX + STAX $B6 + jmp ClampVORDeflection + +;;; Recompute the VOR2 navigation bearing. +UpdateVOR2Bearing: + lda #$02 + ldy VOR2ObsCourse + ldx #$00 + jsr ComputeVORBearing + sta $0988 + jsr LA32D + sta $0984 + rts + +;;; If VOR1 indicator is enabled and the bearing has changed, redraw +;;; the to/from flag and animate the OMI/needle one step toward the +;;; new value. Falls through to track $0981 -> $0982 too. +DrawVOR1IndicatorChanges: + lda $FB + and #$20 + beq LA1B7 + lda $0987 + cmp $0989 + sta $0989 + beq LA17A + lda $0981 + pha + jsr DrawVOR1CourseDeviationIndicatorNeedle + lda $0985 + pha + jsr LA205 + lda $0987 + jsr DrawVORFlag + pla + jsr LA205 + pla + jsr DrawVOR1CourseDeviationIndicatorNeedle +LA17A: lda $0981 + cmp $0982 + beq LA197 + php + pha + jsr DrawVOR1CourseDeviationIndicatorNeedle + pla + plp + bpl DV1IC_AfterAdjust + clc + adc #$02 +DV1IC_AfterAdjust: + .refto DV1IC_AfterAdjust + .refto LA18E +LA18E: sec + sbc #$01 + sta $0981 + jsr DrawVOR1CourseDeviationIndicatorNeedle +AnimateVOR1Glideslope: + .refto AnimateVOR1Glideslope + .refto LA197 +LA197: lda $0985 + cmp $0986 + beq DV1IC_DrawOMI + php + pha + jsr ToggleGlideslopePixel + pla + plp + bpl DV1IC_GSAfterAdjust + clc + adc #$02 +DV1IC_GSAfterAdjust: + .refto DV1IC_GSAfterAdjust + .refto LA1AB +LA1AB: sec + sbc #$01 + sta $0985 + jsr ToggleGlideslopePixel +DV1IC_DrawOMI: + .refto DV1IC_DrawOMI + .refto LA1B4 +LA1B4: jsr DrawOMI +DV1IC_Exit: + .refto DV1IC_Exit + .refto LA1B7 +LA1B7: rts + +;;; Like `DrawVOR1IndicatorChanges` but for VOR2. Skips when the ADF +;;; is enabled (it shares the gauge). Falls through to +;;; `AnimateVOR2Needle`. +DrawVOR2IndicatorChanges: + lda $FD + and #$08 + beq DV2IC_Exit + lda ADFMode + bne DV2IC_Exit + lda $0988 + cmp $098A + sta $098A + beq AnimateVOR2Needle + lda $0983 + pha + jsr DrawVOR2CourseDeviationIndicatorNeedle + lda $0988 + clc + adc #$03 + jsr DrawVORFlag + pla + jsr DrawVOR2CourseDeviationIndicatorNeedle +AnimateVOR2Needle: + lda ADFMode + bne DV2IC_Exit + lda $0983 + cmp $0984 + beq DV2IC_Exit + php + pha + jsr DrawVOR2CourseDeviationIndicatorNeedle + pla + plp + bpl AV2N_AfterAdjust + clc + adc #$02 +AV2N_AfterAdjust: + .refto AV2N_AfterAdjust + .refto LA1FB +LA1FB: sec + sbc #$01 + sta $0983 + jsr DrawVOR2CourseDeviationIndicatorNeedle +DV2IC_Exit: + .refto DV2IC_Exit + .refto LA204 +LA204: rts + +;;; Toggle a single pixel of the glideslope-needle indicator. X = signed +;;; offset (-127..127) from the glideslope origin; A is overwritten. +ToggleGlideslopePixel: + .refto ToggleGlideslopePixel + .refto LA205 +LA205: tax + bmi TGP_Exit + clc + adc #$76 + tax + lda HiresTableHi,x + sta $B7 + clc + adc HiresPageDelta + sta $3F + lda HiresTableLo,x + sta $B6 + sta $3E + ldy #$17 + jsr TGP_TogglePair + jsr TGP_TogglePair +TGP_TogglePair: + .refto TGP_TogglePair + .refto LA225 +LA225: iny + lda #$7F + eor ($B6),y + sta ($B6),y + sta ($3E),y +TGP_Exit: + .refto TGP_Exit + .refto LA22E +LA22E: rts + +.proc DrawVORFlag + asl a ; *= 8 + asl a + asl a + ldx #>msg_vor_flags + clc + adc # just inside positive limit + bcc LA30F + bcs LA2F5 ; always + +;;; Negative branch entry; overlapping with LA2EA's `inc $0E90,x`. +;;; When entered at LA2E9 the byte stream reads "cpx #$FE / bcc LA2FB" +;;; -- so X >= -1 jumps to the positive-clamp path, otherwise falls +;;; through to the negative-clamp test below. +LA2E9: .byte $E0 +;;; Alternate entry from the 64K NAV scanner (jsr LA2EA). Increments +;;; a per-station counter at $0E90,x, then falls into the same range +;;; tests as the LA2D9 path. +LA2EA: inc $0E90,x + bne LA30C + cmp #$D4 ; check A >= $D4 (= -44 signed) for "in range" + bcc LA2FB + bcs LA30F + ;; Positive overflow: clamp B6/B7 by subtracting $021C (= +540). +LA2F5: lda #$1C + ldx #$02 + bne LA2FF ; always + + ;; Negative overflow: clamp B6/B7 by subtracting -$021C. +LA2FB: lda #$E4 + ldx #$FD +LA2FF: sec + sbc $B6 + sta $B6 + txa + sbc $B7 + sta $B7 + lda #$01 + rts + +LA30C: lda #$02 ; on-radial + rts + +LA30F: lda #$00 ; dead zone -- zero deflection + sta $B7 + sta $B6 + rts + +;;; ============================================================ + +;;; Draw VOR1/2 course deviation indicator needle +;;; Inputs: A = needle position + +;;; VOR2 +DrawVOR2CourseDeviationIndicatorNeedle: + ldy #$A1 ; y pos + bne LA31C ; always + +;;; VOR1 +DrawVOR1CourseDeviationIndicatorNeedle: + ldy #$76 ; y pos + +LA31C: tax + bmi LA32C + clc + adc #$A5 ; x base pos + tax + lda #PLVORNeedle + jmp DrawPixelListHelper + +LA32C: rts + +;;; ============================================================ + + +LA32D: LDAX $B6 + bmi LA33D + bne LA339 + cmp #$1E + bcc LA346 +LA339: lda #$1E + bne LA346 +LA33D: inx + bne LA344 + cmp #$E2 + bcs LA346 +LA344: lda #$E2 +LA346: clc + adc #$1E + lsr a + rts + +LA34B: lda $0850 + beq LA3A5 + ldx $0851 + sta $9E + stx $9F + lsr $9F + ror $9E + jsr LA4D2 + lda $0A3A + lsr a + tax + lda $0A39 + ror a + cpx $BF + bcc LA375 + bne LA371 + cmp $BE + bcc LA375 +LA371: ldy #$12 + bne LA3A2 +LA375: sta $98 + lda $BE + sta $C4 + lda $BF + sta $C5 + txa + ldx $98 + jsr L16A2 + lda $C2 + sec + sbc $9E + tax + lda $C3 + sbc $9F + clc + adc #$12 + cmp #$24 + bcc LA39E + ldy #$24 + and #$FF + bpl LA39F + lda #$00 +LA39E: tay +LA39F: tya + lsr a + tay +LA3A2: sty $0986 +LA3A5: ldy #$00 + sty $0850 + sty $0851 + lda $08F4 + ror a + bcs LA3BA + lda #$01 + sta $097D + bne LA3E9 +LA3BA: lda #$00 + sta $097D + jsr LA4D2 + LDAX $BE + STAX $C2 + lda #$1B + ldx #$01 + jsr MultiplyAXByC2 + asl a + tay + txa + rol a + sta $0980 + tya + lsr a + tay + ldx #$14 + jsr L180C + tya + cmp $097F + sta $097F + beq LA3EE +LA3E9: lda #$01 + sta $097E +LA3EE: rts + +LA3EF: lda $B9 + bpl LA3FE + ldx #$B8 + jsr LA497 + jsr LA3FE + jmp L1880 + +LA3FE: lda $BB + bpl LA414 + ldx #$BA + jsr LA497 + jsr LA414 + jsr L1880 + tay + txa + eor #$80 + tax + tya + rts + +LA414: LDAX $B8 + cpx $BB + bcc LA448 + bne LA42A + cmp $BA + bcc LA448 + bne LA42A + inc $B8 + bne LA42A + inc $B9 +LA42A: lda $B8 + ldx $BA + sta $BA + stx $B8 + lda $B9 + ldx $BB + sta $BB + stx $B9 + jsr LA448 + jsr L1880 + tay + txa + clc + adc #$40 + tax + tya + rts + +LA448: LDAX $BA + STAX $C4 + lda $B9 + ldx $B8 + jsr L16A2 + lda $C3 + lsr a + lsr a + and #$FE + tay + lda $C2 + lsr a + lsr a + lsr a + sta $C2 + lda $C3 + asl a + asl a + asl a + asl a + and #$60 + clc + adc $C2 + sta $C3 + lda LA08F,y + sta $BE + sec + sbc LA091,y + pha + lda LA090,y + sta $BF + sbc LA092,y + tax + pla + jsr ScaleC2ByAX + sec + sbc $BE + eor #$FF + tay + txa + sbc $BF + eor #$FF + tax + tya +;;; Shared early-exit RTS reachable from DrawOMI's "no change" path +;;; and as the natural end of the routine above it. +DrawOMI_NoChange: + .refto DrawOMI_NoChange + .refto LA496 +LA496: rts + +;;; Negate the 16-bit value at ($00,X) in place (= 16-bit two's +;;; complement). Used by the L1880 / abs-value helper chain. +Negate16AtZX: + .refto Negate16AtZX + .refto LA497 +LA497: lda #$00 + sec + sbc $00,x + sta $00,x + lda #$00 + sbc $01,x + sta $01,x + rts + +;;; Update the OMI (Outer/Middle marker) indicator lights. Reads the +;;; two marker-active flags from $084E (outer) and $084C (middle), +;;; packs them into a 2-bit code, and (if changed) writes "X" or "0" +;;; into the two-character `str_omi` then redraws the marker message. +.proc DrawOMI + lda #$00 + lsr $084E + rol a + lsr $084C + rol a + cmp $098B + sta $098B + beq DrawOMI_NoChange + ldx #'0' + lsr a + bcc OMI_StoreFirst + ldx #'X' +OMI_StoreFirst: + .refto OMI_StoreFirst + .refto LA4BE +LA4BE: stx str_omi + ldx #'0' + lsr a + bcc OMI_StoreSecond + ldx #'X' +OMI_StoreSecond: + .refto OMI_StoreSecond + .refto LA4C8 +LA4C8: stx str_omi+1 + JUMPAX DrawMessageWhite, msg_omi +.endproc + +;;; Compute the 16-bit "great-circle distance" magnitude from the +;;; aircraft-to-station X delta ($098C/$098D) and Z delta ($098E/$098F) +;;; using a fast |dx| + max(|dx|,|dz|) approximation: orders the two +;;; magnitudes, then computes max + (min * something via ScaleC2ByAX +;;; and L16A2). Result in $BE/$BF. +ComputeStationDistance: + .refto ComputeStationDistance + .refto LA4D2 +LA4D2: LDAX $098C + bpl CSD_HaveDX ; abs(dx) -> $BE/$BF + jsr L1880 +CSD_HaveDX: + .refto CSD_HaveDX + .refto LA4DD +LA4DD: sta $BE + stx $BF + LDAX $098E + bpl CSD_HaveDZ ; abs(dz) -> AX + jsr L1880 +CSD_HaveDZ: + .refto CSD_HaveDZ + .refto LA4EC +LA4EC: cpx $BF + bcc CSD_Swap ; dz < dx -> swap so $BE/$BF holds the smaller + bne CSD_Order + cmp $BE + bcs CSD_Order ; dz >= dx -> already ordered +CSD_Swap: + .refto CSD_Swap + .refto LA4F6 +LA4F6: tay + lda $BF + stx $BF + tax + lda $BE + sty $BE +CSD_Order: + .refto CSD_Order + .refto LA500 +LA500: sta $C0 ; larger magnitude lo + stx $C1 + asl a + sta $C4 ; 2 * larger.lo + txa + rol a + sta $C5 + ldx $BE + lda $BF + jsr L16A2 + LDAX $BE + jsr ScaleC2ByAX + clc + adc $C0 + sta $BE ; result lo + txa + adc $C1 + sta $BF ; result hi + rts + +;;; On-touchdown altitude clamp. Computes (Altitude + 9) * $4E0C and +;;; compares it against the climb-rate state at $0A36/$0A37. If the +;;; result is negative or too small (< $0003), clamps altitude to +;;; (3, 0, 0). Otherwise keeps the (clamped) computed altitude. +ClampAltitudeOnTouchdown: + .refto ClampAltitudeOnTouchdown + .refto LA524 +LA524: ADD16C Altitude, #$09, $C2 + LDAX #$4E0C + jsr MultiplyAXByC2 + txa + sec + sbc $0A36 + tay + lda $C9 + sbc $0A37 + bmi ClampAlt_Below + bne ClampAlt_Store + cpy #$03 + bcs ClampAlt_Store +ClampAlt_Below: + .refto ClampAlt_Below + .refto LA54D +LA54D: lda #$00 + ldy #$03 +ClampAlt_Store: + .refto ClampAlt_Store + .refto LA551 +LA551: sty $5F + sta $60 + lda #$00 + sta $61 + rts + +;;; 48K engine model. Computes target RPM from throttle ($0A6F), +;;; clamps to fuel-tank-available state ($0994 for left, $0997 for +;;; right), applies a Reality-Mode idle floor of $0D, and updates the +;;; fuel-consumption counters every 32 ticks. +;;; +;;; In 64K mode this entire routine is patched out via PatchTable +;;; to JMP `UpdateEngineWithMagneto` (chunk3). +UpdateSimpleEngine: + .refto UpdateSimpleEngine + .refto LA55A +LA55A: lda $0A6F ; 64k: Patched to JMP `UpdateEngineWithMagneto` + lsr a + tay + ldx $0997 + lda $0998 + bne USE_PickTank + ldx $0994 +USE_PickTank: + .refto USE_PickTank + .refto LA56A +LA56A: dex + bpl USE_StoreRPM + ldy #$00 ; no fuel -> RPM = 0 +USE_StoreRPM: + .refto USE_StoreRPM + .refto LA56F +LA56F: sty $0990 + tya + asl a + sta $C3 + lda #$00 + sta $C2 + LDAX #$320 + jsr ScaleC2ByAXIntoC2 + LDAX $09A9 + jsr ScaleC2ByAX + STAX $0A0D + lda RealityMode + bne USE_StoreFloor + lda $0990 + cmp #$0D + bcs USE_StoreFloor + lda #$0D +USE_StoreFloor: + .refto USE_StoreFloor + .refto LA59E +LA59E: sta $0990 + lda InputTickCounter + and #$1F + bne USE_GaugeFloors ; only burn fuel every 32 ticks + ldx #$F2 + ldy $0A6F + jsr L180C + sta L00A5 + sty $A6 + ldx #$00 + lda $0998 ; tank select + beq USE_BurnLeft + ldx #$03 ; right-tank slot offset +USE_BurnLeft: + .refto USE_BurnLeft + .refto LA5BC +LA5BC: lda L00A5 + clc + adc $0992,x + sta $0992,x + tya + bpl USE_GaugeFloors + adc $0993,x + sta $0993,x + lda #$FF + adc $0994,x + bmi USE_GaugeFloors ; underflow -> tank empty, leave alone + sta $0994,x +USE_GaugeFloors: + .refto USE_GaugeFloors + .refto LA5D8 +LA5D8: lda #$05 + sta $099C + sta $099D + rts + +;;; 48K day-phase classifier. Maps the Hours clock to one of four +;;; phases stored at $083C ($01 = morning, $02 = daylight, $04 = +;;; dawn/dusk). Also derives a Season-dependent multiplier into +;;; $0884 as 2^(Season-1). Patched out in 64K mode. +ComputeDayPhase48K: + .refto ComputeDayPhase48K + .refto LA5E1 +LA5E1: ldx #$01 ; 64k: Patched to JMP `ComputeDayPhase` + lda Hours + cmp #$05 + bcc CDP_DarkOrDawn ; 0..4 + cmp #$06 + bcc CDP_Twilight ; 5 + cmp #$13 + bcc CDP_Store ; 6..18 = day, X stays $01 + cmp #$14 + bcc CDP_Twilight ; 19 +CDP_DarkOrDawn: + .refto CDP_DarkOrDawn + .refto LA5F6 +LA5F6: ldx #$04 + bne CDP_Store +CDP_Twilight: + .refto CDP_Twilight + .refto LA5FA +LA5FA: ldx #$02 +CDP_Store: + .refto CDP_Store + .refto LA5FC +LA5FC: stx $083C + ldx Season + lda #$01 +CDP_SeasonMul: + .refto CDP_SeasonMul + .refto LA604 +LA604: sta $0884 + asl a + dex + bne LA604 + rts + +LA60C: nop ; 64k: patched to JMP `DrawATISMessage` + nop + rts + + ;; Block descriptors used by `LoadSceneryFile0..4` and the + ;; common bootstrap path. Each is a 16-bit "block + page" + ;; value handed to the scenery reader so it knows which disk + ;; block to fetch and where it ought to land in memory. +LA60F: .word $0322 ; LoadSceneryFile0 descriptor +LA611: .word $0625 ; LoadSceneryFile1 descriptor +LA613: .word $032D ; LoadSceneryFile2 descriptor +LA615: .word $012B ; LoadSceneryFile3 descriptor +LA617: .word $0142 ; LoadSceneryFile4 descriptor +LA619: .word $012C ; bootstrap descriptor (LoadSceneryFileCommon) + +;;; Reload the dispatcher cursor ($8B/$8C) from the scenery file's +;;; head address (LA7E0) and, if the "load error" flag at $1E0E is +;;; set, hang in a black-screen spin-loop until the user reboots -- +;;; the disk load failed catastrophically. +LoadDispatcherPointer: LDAX LA7E0 + STAX $8B + lda $1E0E + beq LDP_Ok + lda $5F + bmi LDP_Hang + lda UpdateCounter + bpl LDP_Ok +LDP_Hang: + .refto LDP_Hang + .refto LA632 +LA632: nop + jsr ClearViewportsToBlack + jmp LDP_Hang + +LDP_Ok: + .refto LDP_Ok + .refto LA639 +LA639: rts + +SceneryHeaderLoadIfMiss: ldx $08E9 + lda $08E5 + cmp $08EA,x + beq LA66A + sta $08EA,x + sta $9E + lda #$00 +SceneryHeaderLoadIfMiss_InvalidateNewerSlots: + .refto SceneryHeaderLoadIfMiss_InvalidateNewerSlots + .refto LA64C +LA64C: inx + cpx #$04 + bcs LA656 + sta $08EA,x + bcc LA64C +SceneryHeaderLoadIfMiss_RunLoad: + .refto SceneryHeaderLoadIfMiss_RunLoad + .refto LA656 +LA656: lda $08E6 + sta $9F + LDAX $08E7 + STAX L1E03 + jsr SceneryHeaderRunSection +SceneryHeaderLoadIfMiss_Hit: + .refto SceneryHeaderLoadIfMiss_Hit + .refto LA66A +LA66A: rts + +;;; Per-frame scenery loaders. Each entry selects a different file +;;; descriptor (`LA60F`..`LA617`, paired entries below). The bodies +;;; converge at `LoadSceneryFileCommon`, which seeds the scenery +;;; loader's state from the descriptor and pulls in the next chunk. +LoadSceneryFile0: + LDAX LA60F + jmp LoadSceneryFileCommon + +LoadSceneryFile1: + LDAX LA611 + jmp LoadSceneryFileCommon + +LoadSceneryFile2: + LDAX LA613 + jmp LoadSceneryFileCommon + +LoadSceneryFile3: + LDAX LA615 + jmp LoadSceneryFileCommon + +LoadSceneryFile4: + LDAX LA617 + jmp LoadSceneryFileCommon + +LoadSceneryFileCommon: + STAX $9E + + lda #LA7E0 + sta L1E03+1 + + jsr SceneryHeaderRunSection + lda #$00 + sta $08EA + sta $08EB + sta $08EC + sta $08ED + LDAX LA619 + STAX $9E + LDAX LA7E0 + STAX L1E03 +SceneryHeaderRunSection: lda HISCR +SceneryHeaderRunSectionInner: lda $9E + sta L1E01 + jsr LoaderNoOpSuccess + bcs SceneryHeaderRunSectionFail + lda #$01 + sta $08A3 +SceneryHeaderRunSectionLoop: jsr L1EAD + bcs SceneryHeaderRunSectionFail + dec $9F + bne SceneryHeaderRunSectionLoop + jsr LA6F9 + bcs SceneryHeaderRunSectionFail + lda #$00 + sta $08A3 + rts + +SceneryHeaderRunSectionFail: lda L1E01 + jmp L1F89 + +;;; Section-load tail: copy 8KB from $4000-$5FFF (= disk-load buffer) +;;; into $2000-$3FFF (= primary scenery sector). The copy uses +;;; ($A5)=src and ($A7)=dst pointers, advancing 256 bytes at a time +;;; via the inner loop, for 32 pages total. +SceneryCopyLoadedSection: + .refto SceneryCopyLoadedSection + .refto LA6F9 +LA6F9: jsr L1EBC + bcs SceneryHeaderRunSectionFail + lda #$00 + sta L00A5 + lda #$40 + sta $A6 + lda #$00 + sta $A7 + lda #$20 + sta $A8 + ldx #$20 + ldy #$00 +SceneryCopyLoadedSection_PageLoop: + .refto SceneryCopyLoadedSection_PageLoop + .refto LA712 +LA712: lda (L00A5),y + sta ($A7),y + iny + bne LA712 + inc $A6 + inc $A8 + dex + bne LA712 + lda $0A41 + sta $0A40 + clc + rts + + tax + sta $B5C1 + lda $9D06 + sta $B5C3 + lda $9D07 + sta $B5C4 + lda $40 + sta LAA4F + lda $41 + sta LAA50 + rts + +;;; Initialize a scenery-section record at ($40) by copying the 30-byte +;;; default template from LAA75. +SceneryInitSectionRecord: + .refto SceneryInitSectionRecord + ldy #$1D +LA745: lda LAA75,y + sta ($40),y + dey + bpl LA745 + rts + +;;; Snapshot 8 bytes (offset $1E..$25) of the current section record +;;; into the global state buffer at LB5A9. +SceneryStashSectionTail: + .refto SceneryStashSectionTail + ldy #$1E +LA750: lda ($40),y + sta $B5A9,y + iny + cpy #$26 + bne LA750 + rts + +;;; Clear LAA51/LAA52 (= the current section-record forward chain). +SceneryClearChain: + .refto SceneryClearChain + ldy #$00 + sty LAA51 + sty LAA52 + rts + + ;; Section-search entry: byte-overlap trick. The .byte $85 + ;; opcode (= sta zp) absorbs the next 2 bytes as operand, so + ;; entering at the `lda #$00` line reads as "lda #$00 / sta + ;; $20 / .byte $92 / .byte $A7 / jmp LA773" -- effectively + ;; pre-zeroing LAA52 before falling into the search loop. +SceneryFindSectionByDefault: + .refto SceneryFindSectionByDefault + lda #$00 + .byte $85 ; lda #$00 / sta $20 ... see comment + eor $20 + .byte $92 + .byte $A7 + jmp SceneryFindLoop + +;;; Section-search loop. Walks the chain via SceneryReadNextSectionPtr, +;;; comparing each record's 30-byte template against LAA75; on match +;;; returns CC, else CS at end-of-chain. +SceneryFindSection: + .refto SceneryFindSection + .refto LA76E +LA76E: jsr SceneryReadNextSectionPtr + beq SceneryFindFail +SceneryFindLoop: + .refto SceneryFindLoop + .refto LA773 +LA773: jsr SceneryReadRecordType + bne SceneryFindCompare + lda $40 + sta $44 + lda $41 + sta $45 + bne LA76E +SceneryFindCompare: + .refto SceneryFindCompare + .refto LA782 +LA782: ldy #$1D +SceneryFindCompareLoop: + .refto SceneryFindCompareLoop + .refto LA784 +LA784: lda ($40),y + cmp LAA75,y + bne LA76E ; mismatch -> next record + dey + bpl LA784 + clc + rts ; matched + +SceneryFindFail: + .refto SceneryFindFail + .refto LA790 +LA790: sec + rts + +;;; Read the "next record" pointer from offsets $24/$25 of the current +;;; record (zero = end of chain). +SceneryReadNextSectionPtrDefault: + .refto SceneryReadNextSectionPtrDefault + lda $9D00 + ldx $9D01 + bne SceneryAdvancePtr +SceneryReadNextSectionPtr: + .refto SceneryReadNextSectionPtr + .refto LA79A +LA79A: ldy #$25 + lda ($40),y + beq SceneryReadDone + tax + dey + lda ($40),y +SceneryAdvancePtr: + .refto SceneryAdvancePtr + .refto LA7A4 +LA7A4: stx $41 + sta $40 + txa +SceneryReadDone: + .refto SceneryReadDone + .refto LA7A9 +LA7A9: rts + +;;; Read byte 0 (= record type) of the current section record. +SceneryReadRecordType: + .refto SceneryReadRecordType + .refto LA7AA +LA7AA: ldy #$00 + lda ($40),y + rts + +;;; Address-equality test: if LAAB3 is set and ($40)/($41) matches +;;; LAAB4/LAAB5, leave X alone; otherwise decrement X. +SceneryCheckCurrentMatchesLAAB3: + .refto SceneryCheckCurrentMatchesLAAB3 + lda LAAB3 + beq SceneryCheckFail + lda LAAB4 + cmp $40 + bne SceneryCheckExit + lda LAAB5 + cmp $41 + beq SceneryCheckExit +SceneryCheckFail: + .refto SceneryCheckFail + .refto LA7C2 +LA7C2: dex +SceneryCheckExit: + .refto SceneryCheckExit + .refto LA7C3 +LA7C3: rts + +;;; ATIS preamble-clear hook: if the current record's record-type +;;; matches $B5C2 (= the right type) and isn't tagged with the high +;;; bit, run the ATIS-counter-increment via LA2EA (= ClampVORDeflection +;;; alt entry) and re-enter the section dispatcher. +SceneryRunATISPreamble: + .refto SceneryRunATISPreamble + eor $B5C2 + beq SceneryATISExit + and #$7F + beq SceneryATISExit + jsr LA2EA + jmp SceneryHeaderRunSectionInner + +SceneryATISExit: + .refto SceneryATISExit + .refto LA7D3 +LA7D3: rts + + sec + lda $9D00 + sta $40 + lda $9D01 + .byte $DF + .byte $A7 + .byte $41 + + ;; The next $20 bytes here get stashed to LABCC +LA7E0: .word LB000 + + ;; Reset/Interrupt handler +ResetInterruptHandler: + jmp InitFromReset + jmp InitFromReset + + ;; Three dispatch slots intended as runtime-patchable hooks. + ;; As shipped each is `JSR LoadSceneryFileN` followed by 3 BRK + ;; bytes (= padding so a JSR can be widened to a JMP-then-imm + ;; sequence). In the disassembled binary nothing currently + ;; rewrites these slots -- the BRK pads are reserved space for + ;; possible mode transitions but the shipped chunks call them + ;; only as fixed `jsr LoadSceneryFileN`. Mode-specific behaviour + ;; comes from the scenery file each `LoadSceneryFile*` pulls, + ;; not from rewriting the slot itself. + +PatchSlot_PreMode: + jsr LoadSceneryFile1 + brk + brk + brk + +PatchSlot_Gunsight: + jsr LoadSceneryFile2 + brk + brk + brk + +PatchSlot_FrameSync: + jsr LoadSceneryFile3 + brk + brk + brk + +;;; Ctrl+E - dump scenery disk contents (debug aid) +LogSceneryDisk: + jsr LoadSceneryFile4 + brk + brk + brk + + ;; This memory is re-used after the simulation starts +LA800 := $A800 +LA802 := $A802 +LA803 := $A803 +LA804 := $A804 +LA805 := $A805 +LA806 := $A806 +LA807 := $A807 +LA808 := $A808 +LA809 := $A809 +LA80A := $A80A +LA80B := $A80B +LA80D := $A80D + + ;; Also this, but less certain +LA95B := $A95B +LA95C := $A95C +LA95D := $A95D +LA95E := $A95E +LA95F := $A95F +LA960 := $A960 +LA961 := $A961 +LA962 := $A962 +LA963 := $A963 + +LAA4F := $AA4F +LAA50 := $AA50 +LAA51 := $AA51 +LAA52 := $AA52 + +LAA75 := $AA75 + +LAAB3 := $AAB3 +LAAB4 := $AAB4 +LAAB5 := $AAB5 + + .assert * = $A800, error, .sprintf("location mismatch, expected %04x, was %04x", $A800, *) + +msg_intro: + MESSAGE $0C, $0D, "SUBLOGIC FLIGHT SIMULATOR II" + MESSAGE $14, $2D, "VERSION 2.0" + MESSAGE $1C, $08, "COPYRIGHT 1984 BY BRUCE ARTWICK" + MESSAGE $24, $07, "PRODUCED BY SUBLOGIC CORPORATION" + MESSAGE $32, $0A, "WHAT DISPLAY ARE YOU USING?" + MESSAGE $3C, $0A, "A. COLOR TV OR COMPOSITE MONITOR" + MESSAGE $44, $0A, "B. BLACK AND WHITE TV OR MONITOR" + MESSAGE $58, $0A, "(TYPE A OR B)" + .byte 0, 0 + +msg_select_mode: + MESSAGE $28, $0A, "SELECT OPERATING MODE" + MESSAGE $30, $0A, "A. DEMO MODE" + MESSAGE $38, $0A, "B. REGULAR FLIGHT MODE" + MESSAGE $4C, $0A, "(TYPE A OR B)" + .byte 0, 0 + +msg_lowercase: + MESSAGE $0C, $0A, "A LOWER CASE CHARACTER WAS TYPED." + MESSAGE $14, $0A, "IF YOU ARE USING AN APPLE IIE" + MESSAGE $1C, $0A, "MAKE SURE THE CAPS LOCK KEY" + MESSAGE $24, $0A, "IS PRESSED." + + MESSAGE $32, $0A, "WHAT DISPLAY ARE YOU USING?" + MESSAGE $3C, $0A, "A. COLOR TV OR COMPOSITE MONITOR" + MESSAGE $44, $0A, "B. BLACK AND WHITE TV OR MONITOR" + MESSAGE $58, $0A, "(TYPE A OR B)" + .byte 0, 0 + +msg_demo: + MESSAGE $0C, $0A, "DEMO RUNS CONTINUOUSLY. PRESS" + MESSAGE $14, $0A, "THE K KEY TO BREAK OUT OF DEMO" + MESSAGE $1C, $0A, "AND RESUME NORMAL FLIGHT." + MESSAGE $32, $0A, "PRESS ANY KEY TO CONTINUE....." + .byte 0, 0 + +msg_48k_demo: + MESSAGE $0C, $0A, "SIMPLE 48K DEMO ACTIVATED. THE" + MESSAGE $14, $0A, "AIRCRAFT WILL TAKE OFF AND FLY" + MESSAGE $1C, $0A, "STRAIGHT ONLY. A BETTER DEMO IS" + MESSAGE $24, $0A, "AVAILABLE WITH 64K. PRESS" + MESSAGE $2C, $0A, "THE K KEY TO BREAK OUT OF DEMO." + MESSAGE $3C, $0A, "PRESS ANY KEY TO CONTINUE....." + .byte 0, 0 + +;;; ============================================================ + +ColorModePatch: + .byte $BB + .byte $BB + .byte $77 + .byte $77 + .byte $11 + .byte $11 + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $00 + .byte $00 + .byte $FF + .byte $FF + .byte $22 + .byte $22 + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $22 + .byte $22 + +BWModePatch: + .byte $11 + .byte $11 + .byte $FF + .byte $FF + .byte $11 + .byte $11 + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $11 + .byte $11 + .byte $FF + .byte $FF + .byte $FF + .byte $FF + .byte $00 + .byte $00 + +;;; ============================================================ + +;;; Reset handler. Re-runs the multi-stage loader: open the disk, +;;; pull the chunks (via `ReloadGameChunks` / `LoadIntroPanel`), probe for 64K, fetch the +;;; first scenery file. Any failure jumps out via `InitFailureExit` +;;; which surfaces an error code. +InitFromReset: + nop + ldx #$3F + txs + jsr LoaderNoOpSuccess + bcs InitFailureExit + jsr ReloadGameChunks + bcs InitFailureExit + jsr LoadIntroPanel + bcs InitFailureExit + jsr ProbeLCMemory + bcs InitFailureExit + jsr LA6F9 + bcs InitFailureExit + +;;; ============================================================ + +.assert * = $ABAE, error, "mismatch" + +;;; Per @qkumba, this is entry point after all chunks are loaded + jsr InitZeroPage + jmp MainGameEntry + +InitFailureExit: + lda L1E01 + jmp L1F89 + +MainGameEntry: + jsr Apply64KPatchTable + jsr InitGraphicsScreens + jsr PromptColorOrBW + jsr InitInstruments + jsr InitInstrumentSaveBuffers + jmp MainLoopEntry + + ;; Used to stash $20 bytes from `LA7E0` +LABCC: + brk + bcs $AC1B + sta ($AB),y + jmp InitFromReset + + jsr LoadSceneryFile1 + brk + brk + brk + jsr LoadSceneryFile2 + brk + brk + brk + jsr LoadSceneryFile3 + brk + brk + brk + jsr LoadSceneryFile4 + brk + brk + brk + +;;; Reload chunk4 ($0200..) and chunk5 ($6000..) from disk after a +;;; reset. Saves the $20 bytes at `LA7E0` first (since chunk5 will +;;; overwrite them), restores them once both reads complete. +ReloadGameChunks: + ;; Save `LA7E0` + ldx #$1F +: lda LA7E0,x + sta LABCC,x + dex + bpl :- + + lda #<$0200 + sta L1E03 + lda #>$0200 + sta L1E03+1 + lda #$00 + sta L1E01 + +: jsr L1EAD + bcs LAC39 + lda L1E01 + cmp #$07 + bcc :- + lda #$00 + sta L1E03 + lda #$60 + sta L1E03+1 + lda #$08 + sta L1E01 +: jsr L1EAD + bcs LAC39 + lda L1E01 + cmp #$1A + bcc :- + + ;; Restore `LA7E0` + ldx #$1F +: lda LABCC,x + sta LA7E0,x + dex + bpl :- + + clc +LAC39: rts + +;;; ============================================================ + +.proc PromptColorOrBW + jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_intro + + ;; Color/B&W prompt + +: jsr TogglePause + cmp #'a' + beq lower + cmp #'b' + beq lower + cmp #'A' + beq PatchInColor + cmp #'B' + beq PatchInBW + bne :- +lower: jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_lowercase + jmp :- + + ;; Color +PatchInColor: + LDAX #ColorModePatch + jmp Common + + ;; B&W +PatchInBW: + LDAX #BWModePatch + + ;; Common +Common: + ldy #$00 + STAX $BE +: lda ($BE),y + sta ColorOrBWModePatch,y + iny + cpy #$16 + bne :- + + ;; fall through to Demo/Regular prompt +.endproc + + ;; Demo/Regular Flight Mode prompt + + jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_select_mode +: jsr TogglePause + cmp #'A' + beq LAC98 + cmp #'B' + beq LACB4 + bne :- +LAC98: inc DemoMode + + ;; Demo mode + + jsr ClearViewportsToBlack + CALLAX DrawMultiMessage, msg_demo + lda Has64K + bne :+ + CALLAX DrawMultiMessage, msg_48k_demo +: jsr TogglePause +LACB4: lda #$00 + sta EditModeFlag + rts + +;;; ============================================================ + +.proc InitInstruments + lda #$01 + sta $08BE + lda $0A33 + jsr UpdateAltimeterIndicator::Init + lda $2A + jsr UpdateAirspeedIndicator::Init + lda #$00 + jsr UpdateVerticalSpeedIndicator::Init + lda #$0A + jsr UpdateElevatorPositionIndicator::Init + lda #$0F + jsr UpdateAileronPositionIndicator::Init + lda #$0F + jsr UpdateRudderPositionIndicator::Init + lda #$00 + jsr UpdateThrottleIndicator::Init + lda #$00 + jsr UpdateFlapsIndicator::Init + lda #$05 + jsr UpdateTrimIndicator::Init + lda #$00 + jsr UpdateMixtureControlIndicator::Init + lda #$00 + jsr UpdateSlipSkidIndicator::Init + lda #$00 + jsr UpdateFuelTankGauges::InitLeft + lda #$00 + jsr UpdateFuelTankGauges::InitRight + lda #$00 + jsr UpdateOilTempAndPressureGauges::InitTemp + lda #$00 + jsr UpdateOilTempAndPressureGauges::InitPressure + jsr DrawVOR1 + jsr DrawVOR2 + jsr DrawAllRadios + rts +.endproc + +;;; ============================================================ + +;;; Reload chunk1 (the loading-panel hi-res page) from disk into +;;; $4000. Pulls sectors $1A..$21 inclusive (8 sectors = 8 KB). +.proc LoadIntroPanel + lda #$00 + sta L1E03 + lda #$40 + sta L1E03+1 + lda #$1A + sta L1E01 +LAD24: jsr L1EAD + bcs LAD31 + lda L1E01 + cmp #$22 + bcc LAD24 + clc +LAD31: rts +.endproc + +;;; ============================================================ + +.proc InitGraphicsScreens + ;; Show graphics page 1 + lda TXTCLR + lda MIXCLR + lda HIRES + lda LOWSCR + + page1_ptr := $B8 + page2_ptr := $C0 + + ;; Copy instrument panel from page1 to page2 + ldy #0 + ldx #191 ; bottom of screen +Loop: lda HiresTableLo,x + sta page1_ptr + sta page2_ptr + lda HiresTableHi,x + sta page1_ptr+1 + clc ; TODO: Just EOR with %01100000 + adc #$20 + cmp #$60 + bcc :+ + sec + sbc #$40 +: sta page2_ptr+1 + + lda (page1_ptr),y + and #$7F ; clear high bit + sta (page1_ptr),y + sta (page2_ptr),y + + dex + cpx #99 ; bottom of viewport + bne Loop + rts +.endproc + +;;; ============================================================ + +;;; 64k Patch Table - each entry is 5 bytes +;;; .addr address-to-patch +;;; jsr (or jmp) ... + + ;; Patch table +PatchTable: + .addr ADF + jmp ADFKeyboardHook + + .addr P64K_1 + jsr RequestADFStationLookup + + .addr SceneryOpADFRecord + jmp LookupADFStation + + .addr P64K_2 + jsr UpdateADFIndicator + + .addr KeyDecrease + jsr KeyDecreasePatch + + .addr KeyIncrease + jsr KeyIncreasePatch + + .addr P64K_3 + jsr DrawViewOverlays + + .addr P64K_4 + jsr UpdateAltimeter10K + + .addr P64K_5 + jsr RealityModeHook + + .addr P64K_6 + jsr UpdateInstrumentLights + + .addr LA55A + jmp UpdateEngineWithMagneto + + .addr P64K_7 + jmp DrawMagnetoStateHook + + .addr ApplyMagnetoState + jmp SetMagnetoFromA + + .addr SelectRadarView + jmp SelectRadarViewPatch + + .addr Select3DView + jmp Select3DViewPatch + + .addr P64K_8 + jsr HideOrShowInstruments + + ;; 64K scenery loader patches: rewrite each chunk4 thunk + ;; (`L1EAD`..`L1EC1`) so it jumps into the chunk3 + ;; implementation instead of the original 48K $20xx target. + .addr L1EAD + jmp SceneryLoaderEntry1 + .addr L1EB0 + jmp SceneryLoaderEntry2 + .addr L1EB3 + jmp SceneryLoaderEntry3 + .addr L1EB6 + jmp SceneryLoaderEntry4 + .addr L1EB9 + jmp SceneryLoaderEntry5 + .addr L1EBC + jmp SceneryLoaderEntry6 + + .addr L1EC1 + jmp SceneryLoaderEntry7 + + .addr CoursePlotting + jmp CoursePlottingMenu + + .addr P64K_9 + jsr UpdateCoursePlotter + + .addr P64K_A + jsr DrawSlewOverlays + + .addr LA60C + jmp DrawATISMessage + + .addr L6026 + jmp UpdateCOMMessageChunks + + .addr LA5E1 + jmp ComputeDayPhase + + .addr L87A5 + jsr ApplyWind + + .addr P64K_B + jsr ComputeWindComponents + + .addr P64K_C + jsr DemoMode64K + + .addr L87BE + jsr HandleCrashOrSplash + + .word $0000 ; sentinel + +;;; ============================================================ + +;;; Detect 64K + +ProbeLCMemory: + lda LCBANK2 ; turn on LCBANK2 for read/write + lda LCBANK2 + + ldx #$00 ; try writing/reading all 256 values +: stx $D000 + cpx $D000 + bne LAE9E ; fail, just RTS + dex + bne :- + + lda #<$D000 + sta L1E03 + lda #>$D000 + sta L1E03+1 + lda #$00 + sta L1E01 + + jsr L1EB3 + + bcs LAE43 + + nop + nop + nop + nop + nop + nop + nop + + lda #$01 + sta Has64K + clc +LAE43: rts + +;;; ============================================================ + +.proc Apply64KPatchTable + lda Has64K + beq LAE9E + ldx #$00 +Loop: ldy #$00 + lda PatchTable,x + inx + sta L00A5 + lda PatchTable,x + inx + sta $A6 + ora L00A5 + beq LAE74 + lda PatchTable,x + inx + sta (L00A5),y + iny + lda PatchTable,x + inx + sta (L00A5),y + iny + lda PatchTable,x + inx + sta (L00A5),y + jmp Loop + +LAE74: LDAX #$D000 + STAX $0932 + lda #$19 + sta $0931 + lda #$01 + sta L1E07 + + ;; Patch interrupt vectors in LC bank + LDAX #ResetInterruptHandler + STAX $FFFA ; NMI + STAX $FFFC ; Reset + STAX $FFFE ; IRQ +.endproc + +LAE9E: rts + +;;; ============================================================ + +.proc InitZeroPage + ldx #$00 +: lda InitialZeroPageData,x + sta $00,x + inx + bne :- + rts +.endproc + +;;; ============================================================ + +;;; Initialize a series of 64-byte buffers pointed to by $0932, +;;; with each buffer's first byte stamped with an increasing tag +;;; starting at $0A. Loop runs until the tag matches the count at +;;; $0931. Used at startup to prep the instrument-save scratch area. +.proc InitInstrumentSaveBuffers + LDAX $0932 + STAX $B8 + + lda #$0A +Loop: + ldy #$40 +: sta ($B8),y + dey + bpl :- + clc + adc #$01 + pha + + lda $B8 + clc + adc #$41 + sta $B8 + bcc LAECC + inc $B9 + +LAECC: pla + cmp $0931 + bne Loop + rts +.endproc + +;;; ============================================================ + +;;; Unused??? + tay + jsr DrawMultiMessage + lda WarDeclared + cmp #$01 + bne LAEF4 + lda #$03 + sta WarDeclared + CALLAX DrawMultiMessage, $A83B ; In middle of another MESSAGE ??? + ldx #$00 + ldy #$00 +: inx + bne :- + iny + bne :- +LAEF4: rts + +;;; Unused??? + lda $0A60 + beq $AF39 + dec $0A60 + jsr TickBombTimer + + + +;;; Intial data for zero page + +InitialZeroPageData: + .byte $4C, $3C, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $28, $00, $55, $2A, $00, $00, $00 + .byte $11, $22, $4F, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $9E + .byte $81, $9E, $AA, $49, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $2D, $00, $60, $00 + .byte $36, $2E, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $06, $56, $00 + .byte $58, $AF, $00, $00, $1F, $01, $00, $03 + .byte $00, $00, $00, $00, $24, $03, $00, $FE + .byte $00, $10, $00, $FE, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $FF, $FE, $08, $00, $FF, $00, $08, $00 + .byte $FF, $FF, $00, $00, $FF, $FF, $00, $00 + .byte $FF, $00, $00, $00, $FF, $20, $00, $03 + .byte $4C, $46, $E6, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $09 + .byte $01, $00, $00, $00, $00, $02, $AB, $20 + .byte $2C, $E2, $31, $55, $35, $11, $09, $15 + .byte $00, $00, $00, $00, $00, $00, $02, $00 + .byte $55, $FC, $C9, $3A, $B0, $0A, $C9, $20 + .byte $F0, $EF, $38, $E9, $30, $38, $E9, $D0 + .byte $60, $80, $00, $C7, $52, $FF, $00, $00 + .byte $FF, $00, $00, $00, $FF, $FF, $00, $00 + .byte $00, $FF, $00, $00, $00, $FF, $00, $00 + .byte $FF, $FF, $00, $00, $00, $FF, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $00, $00, $00, $00, $00 + .byte $00, $00, $00, $FF, $FF, $0F, $00, $00 + +;;; ============================================================ + +LB000: + adc $C908,y + inx + + + lda WW1AceScore+1 + sbc #3 + bcc :+ + lda #$00 + sta WW1AceScore + sta WW1AceScore+1 +: LDAX WW1AceScore + STAX ValueForString + CALLAX Set3DigitStringRelay, WW1AceScoreStr + rts + +;;; Tick the WW1 bomb-fall timer at $0A54/$0A55 down by one (clamped +;;; at zero) and redraw the live "Time on target" digits. +TickBombTimer: + lda $0A54 + sec + sbc #$01 + sta $0A54 + bcs :+ + lda $0A55 + sbc #$00 + sta $0A55 + bcs :+ + lda #$00 + sta $0A54 + sta $0A55 +: LDAX $0A54 + STAX ValueForString + CALLAX Set3DigitStringRelay, $A87B + rts + + LDAX LA802 + STAX $098C + LDAX LA805 + STAX $098E + jsr L8770 + LDAX $BE + STAX $098C + LDAX LA808 + STAX $098E + jsr L8770 + lda LA804 + eor LA807 + eor LA80A + beq LB09A + tay + iny + tya + beq LB09A + lda #$7F +LB09A: sta LA80D + LDAX $BE + STAX LA80B + ldx LA800 + rts + + ldy #$6B + sty $B6 +LB0AF: lda HiresTableHi,y + sta $A6 + lda HiresTableLo,y + sta L00A5 + ldy #$1D + lda #$82 + sta (L00A5),y + lda #$80 +LB0C1: iny + cpy #$28 + bcs LB0CA + sta (L00A5),y + bcc LB0C1 +LB0CA: inc $B6 + ldy $B6 + cpy #$9B + bcc LB0AF + lda #$BE + sta $3472 + sta $5472 + lda #$88 + sta $3072 + sta $3872 + sta $3C72 + sta $5072 + sta $5872 + sta $5C72 + rts + + ldy #$01 + sec + lda (L00A5),y + sbc LA95B,x + sta LA802 + iny + lda (L00A5),y + sbc LA95C,x + sta LA803 + iny + lda (L00A5),y + sbc LA95D,x + sta LA804 + iny + iny + sec + lda (L00A5),y + sbc LA95E,x + sta LA805 + iny + lda (L00A5),y + sbc LA95F,x + sta LA806 + iny + lda (L00A5),y +LB123: sbc LA960,x + sta LA807 + iny + iny + sec + lda (L00A5),y + sbc LA961,x + sta LA808 + iny + lda (L00A5),y + sbc LA962,x + sta LA809 + iny + lda (L00A5),y + sbc LA963,x + sta LA80A + iny + rts + +;;; Clear the ATIS-message scroll region of both hires screens and +;;; reset the soft-switch byte at $0978 + $FC's "ATIS in progress" bit. +;;; +;;; This routine has TWO entry shapes encoded into the same byte stream: +;;; +;;; * Fall-through entry (= via `jsr LB148`, no label): +;;; lda $FC / and #$7F / sta $FC ; clear ATIS-busy +;;; lda $0978 / and #$7F / sta $0978 ; clear scroll dirty bit +;;; ldy #$65 / sty $B6 ; start row $65 +;;; ... +;;; reads the bytes as a 16-byte instruction stream. +;;; +;;; * `LB150` entry (= jumped to from `LB1F4`): +;;; ora #$29 / .byte $7F / sta $0978 ; just smash $0978 = $29 +;;; ldy #$65 / sty $B6 +;;; ... +;;; starts 2 bytes earlier inside what the fall-through entry +;;; reads as the lda-and-sta of $0978. The .byte $AD at the head +;;; is the `lda abs` opcode that absorbs `$78 $09` (= `$0978`) as +;;; its operand when entered before LB150. +ClearATISScrollRegion: + .refto ClearATISScrollRegion + lda $FC + and #$7F + sta $FC + .byte $AD ; lda $0978 when entered from above + sei ; (also valid 1-byte SEI = byte $78) + ;; Alternate entry: just smash $0978 to $29 with no clearing. +LB150: + .refto LB150 + ora #$29 + .byte $7F + sta $0978 + ldy #$65 + sty $B6 +ClearATIS_RowLoop: + .refto ClearATIS_RowLoop + .refto LB15A +LB15A: lda HiresTableHi,y + sta $A6 + clc + adc HiresPageDelta + sta $A8 + lda HiresTableLo,y + sta L00A5 + sta $A7 + ldy #$1D + lda #$83 + and (L00A5),y ; preserve only top-3 bits of byte $1D + sta (L00A5),y + sta ($A7),y + lda #$80 + ;; Fill columns $1E..$27 of the row with $80 (= black). +ClearATIS_FillByte: + .refto ClearATIS_FillByte + .refto LB177 +LB177: iny + cpy #$28 + bcs ClearATIS_NextRow + sta (L00A5),y + sta ($A7),y + bcc LB177 +ClearATIS_NextRow: + .refto ClearATIS_NextRow + .refto LB182 +LB182: inc $B6 + ldy $B6 + cpy #$A1 + bcc LB15A + rts + +;;; Compose the per-frame "operational instruments" mask in $0991 +;;; from the damage counters $08A4 (= time-on-ground) and $0898 (= +;;; AC-problem severity). Each progressively-broken instrument gets +;;; an OR-bit added once its damage threshold is crossed. +ComputeInstrumentDamageMask: + .refto ComputeInstrumentDamageMask + ldy $08A4 + lda $0898 + cmp #$04 + lda #$00 + bcs CIDM_SevereDispatch + ;; Mild ($0898 < $04) thresholds: $07, $14, $23, $28, $3C. + cpy #$3C + bcs CIDM_BreakAttitude + cpy #$28 + bcs CIDM_BreakCompass + cpy #$23 + bcs CIDM_BreakHeading + cpy #$14 + bcs CIDM_BreakRPM + cpy #$07 + bcs CIDM_BreakVOR + bcc CIDM_Store + ;; Severe ($0898 >= $04) thresholds: tighter cutoffs. +CIDM_SevereDispatch: + .refto CIDM_SevereDispatch + .refto LB1AD +LB1AD: cpy #$14 + bcs CIDM_BreakAttitude + cpy #$07 + bcs CIDM_BreakCompass + cpy #$01 + bcs CIDM_BreakRPM + bcc CIDM_Store +CIDM_BreakAttitude: + .refto CIDM_BreakAttitude + .refto LB1BB +LB1BB: inc $08C3 +CIDM_BreakCompass: + .refto CIDM_BreakCompass + .refto LB1BE +LB1BE: ora #$01 +CIDM_BreakHeading: + .refto CIDM_BreakHeading + .refto LB1C0 +LB1C0: ora #$02 +CIDM_BreakRPM: + .refto CIDM_BreakRPM + .refto LB1C2 +LB1C2: ldy UpdateCounter + cpy #$3F + bcs CIDM_BreakVOR + ora #$08 +CIDM_BreakVOR: + .refto CIDM_BreakVOR + .refto LB1CA +LB1CA: ldy UpdateCounter +LB1CB := *-1 + cpy #$3F + ora #$04 +CIDM_Store: + .refto CIDM_Store + .refto LB1D0 +LB1D0: ora $0991 + sta $0991 + rts + + ;; ============================================================ + ;; The block from here through ~$B340 is the in-place ATIS + ;; message-scroll digit pipeline. Original disassembly notes + ;; suggested this region was DMA-relocated elsewhere at runtime + ;; (the comment "$D1B7: Overwritten - via code at LD402"), but + ;; cross-referencing confirms LD402 is just the inner + ;; sector-copy loop of `SceneryReadFixed` -- a generic loader + ;; byte-copy, not a block-relocate. The routines below run + ;; exactly where they're assembled. They implement: + ;; * input-digit ingestion (LB1F4 / LB1F7..LB236) + ;; * BCD-to-decimal accumulator (LB238 / LB255..LB283) + ;; * decimal-digit emit + display (LB285..LB2F4) + ;; * full screen-row clear loop (LB303..LB337) + ;; + ;; The 3 bytes immediately below are stale "header" data -- not + ;; reached by any code path; the routines start at LB1F4 below. + adc $B920,y + asl $EEB0,x + jsr LB238 + jsr L1EB0 + bcs LB1CB ; ??? + bit $1FBD + lda #$00 + sta $1E0E + jmp LB1F4 + + cmp #$30 + bcs LB1F7 +;;; Digit-ingest re-entry: return to ClearATIS region setup, then +;;; loop back for the next digit. +DigitIngest_NotDigit: + .refto DigitIngest_NotDigit + .refto LB1F4 +LB1F4: jmp LB150 + +;;; Digit-ingest body: A holds the latest character. Reject anything +;;; non-numeric, shift the 5-digit accumulator $0924..$0928 up, store +;;; A as the new low digit, increment the input-position counter. +DigitIngest_Numeric: + .refto DigitIngest_Numeric + .refto LB1F7 +LB1F7: cmp #$3A + bcs LB1F4 + ldy $092A + cpy #$0B + beq LB1F4 + inc $0921 + sta $AF + lda $0923 + bne DigitIngest_ShiftAccum + ;; First digit -- clear the on-screen positions $05..$0B with $A0. + ldy #$0C + lda #$A0 +DigitIngest_ClearLoop: + .refto DigitIngest_ClearLoop + .refto LB210 +LB210: sta ($3E),y + dey + cpy #$04 + bne LB210 +DigitIngest_ShiftAccum: + .refto DigitIngest_ShiftAccum + .refto LB217 +LB217: ldx #$04 +DigitIngest_ShiftLoop: + .refto DigitIngest_ShiftLoop + .refto LB219 +LB219: lda $0924,x + sta $0925,x + dex + bpl LB219 + lda $AF + sta $0924 + inc $0923 + ldy $092A + ora #$80 + sta ($3E),y + iny + sty $092A + jmp LB1F4 + +;;; Loader-pointer helper: copy $0932/$0933 into the scenery-loader +;;; cursor L1E03 and reset its sector counter L1E01 to $40. +LoaderSetCursorAt0932: + .refto LoaderSetCursorAt0932 + .refto LB238 +LB238: LDAX $0932 + STAX L1E03 + lda #$40 + sta L1E01 + rts + +;;; Accumulate $0924..$0929 (ASCII digits '0'..'9') into the 24-bit +;;; total $091C/$091D/$091E using per-digit multipliers from +;;; InitialZeroPageData+$B4/$BA/$C0. +AccumulateDigitsToTotal: + .refto AccumulateDigitsToTotal + ldx #$00 + stx $091C + stx $091D + stx $091E +AccDigits_PerDigit: + .refto AccDigits_PerDigit + .refto LB255 +LB255: lda $0924,x + and #$7F + sec + sbc #$30 ; ASCII -> binary digit 0..9 + tay + beq AccDigits_Next + clc +AccDigits_AddOnce: + .refto AccDigits_AddOnce + .refto LB261 +LB261: lda InitialZeroPageData + $B4,x + adc $091C + sta $091C + lda InitialZeroPageData + $BA,x + adc $091D + sta $091D + lda InitialZeroPageData + $C0,x + adc $091E + sta $091E + dey + bne LB261 +AccDigits_Next: + .refto AccDigits_Next + .refto LB27F +LB27F: inx + cpx #$06 + bne LB255 + rts + +;;; Display the accumulated total as decimal digits at row $3E (5 + 6 +;;; chars at offsets 5..A), most-significant first. Leading zeros +;;; are suppressed via the $0922 sentinel. +DisplayTotalAsDecimal: + .refto DisplayTotalAsDecimal + .refto LB285 +LB285: jsr LB123 + lda #$00 + sta $0922 + lda #$05 + sta $092A + ldy #$05 + lda #$A0 ; blank padding for positions $05..$0A +DT_ClearLoop: + .refto DT_ClearLoop + .refto LB296 +LB296: sta ($3E),y + iny + cpy #$0B + bne LB296 + ldx #$05 +DT_NextDigit: + .refto DT_NextDigit + .refto LB29F +LB29F: lda #$30 ; '0' + sta $092D +DT_DivLoop: + .refto DT_DivLoop + .refto LB2A4 + ;; Repeatedly subtract InitialZeroPageData+$B4/$BA/$C0,x from + ;; the running total; each successful subtract bumps the digit. +LB2A4: lda $091C + sec + sbc InitialZeroPageData + $B4,x + sta $092E + lda $091D + sbc InitialZeroPageData + $BA,x + sta $092F + lda $091E + sbc InitialZeroPageData + $C0,x + bmi DT_DigitDone + sta $091E + lda $092F + sta $091D + lda $092E + sta $091C + inc $092D + bne LB2A4 +DT_DigitDone: + .refto DT_DigitDone + .refto LB2D3 +LB2D3: lda $092D + cpx #$00 + beq DT_EmitNonZero + cmp #$30 + bne DT_EmitNonZero + lda $0922 + beq DT_SkipLeadingZero +DT_EmitNonZero: + .refto DT_EmitNonZero + .refto LB2E3 +LB2E3: inc $0922 + ldy $092A + lda $092D + ora #$80 + sta ($3E),y + inc $092A +DT_SkipLeadingZero: + .refto DT_SkipLeadingZero + .refto LB2F3 +LB2F3: dex + bpl LB29F + rts + +;;; Full screen-rows clear: copy the message at L003C into a 5x40 +;;; viewport region starting at $3E:$3F = $0400 / $0480 / etc, with +;;; the high bit set on each byte so it renders in inverse video. +ClearMessageRows: + .refto ClearMessageRows + sta L003C + stx $3D + lda #$00 + ldx #$04 + sta $3E + stx $3F +CMR_RowTop: + .refto CMR_RowTop + .refto LB303 +LB303: ldy #$00 +CMR_ByteLoop: + .refto CMR_ByteLoop + .refto LB305 +LB305: lda (L003C),y + ora #$80 + sta ($3E),y + iny + cpy #$28 + bne LB305 + tya + clc + adc L003C + sta L003C + bcc CMR_AdvanceDst + inc $3D +CMR_AdvanceDst: + .refto CMR_AdvanceDst + .refto LB31A +LB31A: ADD16C $3E, #$80, $3E + cmp #$08 + bmi LB303 + lda $3E + sec + sbc #$D8 + cmp #$78 + bne CMR_NextStripe + rts + +CMR_NextStripe: + .refto CMR_NextStripe + .refto LB335 +LB335: sta $3E + lda $3F + sbc #$03 + sta $3F + bne LB303 + lda $091F + sta $A9 + jsr LB849 + lda $0976 + cmp #$D2 + bcs ATIS_NoChange + cmp $0934 + beq ATIS_NoChange + cmp #$6E + bcs ATIS_NegPath + cmp $0931 + bcs ATIS_NoChange + sta $0934 + jsr LB9CE + lda $0934 + sta $0976 + jmp ATIS_AfterUpdate + +ATIS_NegPath: + .refto ATIS_NegPath + .refto LB36B +LB36B: sec + sbc #$64 + cmp $0931 + bcs ATIS_NoChange + sta $0934 + sta $0976 + jsr LB9C3 +ATIS_AfterUpdate: + .refto ATIS_AfterUpdate + .refto LB37C +LB37C: jsr ATIS_Process + lda $A9 + sta $091F + rts + +ATIS_NoChange: + .refto ATIS_NoChange + .refto LB385 +LB385: lda $0934 + sta $0976 + jmp LB37C + +;;; ATIS process driver: read each variable-length record from the +;;; pre-fetched message table (pointed at by InitialZeroPageData+$CA), +;;; copy its data words to $091C/$091D/$091E, and run DisplayTotalAsDecimal +;;; per record. +ATIS_Process: + .refto ATIS_Process + .refto LB38E +LB38E: ldx $0920 + lda InitialZeroPageData + $CA,x + sta L00A5 + lda InitialZeroPageData + $CB,x + sta $A6 +ATIS_RecordTop: + .refto ATIS_RecordTop + .refto LB39B +LB39B: ldy #$00 + lda (L00A5),y + sta $092E + bne ATIS_HaveRecord + rts + +ATIS_HaveRecord: + .refto ATIS_HaveRecord + .refto LB3A5 +LB3A5: jsr LB808 + ldy #$00 + sty $091C + sty $091D + sty $091E +ATIS_CopyData: + .refto ATIS_CopyData + .refto LB3B3 +LB3B3: lda ($3E),y + sta $091C,y + iny + cpy $092E + bne LB3B3 + jsr LB285 + jmp LB39B + + jsr L874C + ADD16 $5F, $0A36, $C2 + lda #$EB + ldx #$D1 + jsr MultiplyAXByC2 + asl a + rol $C8 + + .assert * = $B3E0, error, .sprintf("EOF mismatch, %04X", *) diff --git a/src/complete.s b/src/complete.s new file mode 100644 index 0000000..ba044a6 --- /dev/null +++ b/src/complete.s @@ -0,0 +1,136 @@ + + .setcpu "6502" + .refto __APPLE2__ + + ;; ca65 pre-defines these target-related symbols; .refto keeps + ;; them from tripping --warnings-as-errors at -W2. + .refto CPU_NONE + .refto CPU_6502 + .refto CPU_6502X + .refto CPU_6502DTV + .refto CPU_65SC02 + .refto CPU_65C02 + .refto CPU_65816 + .refto CPU_SWEET16 + .refto CPU_HUC6280 + .refto CPU_M740 + .refto CPU_4510 + .refto CPU_45GS02 + .refto CPU_W65C02 + .refto CPU_65CE02 + .refto CPU_ISET_NONE + .refto CPU_ISET_6502 + .refto CPU_ISET_6502X + .refto CPU_ISET_6502DTV + .refto CPU_ISET_65SC02 + .refto CPU_ISET_65C02 + .refto CPU_ISET_65816 + .refto CPU_ISET_SWEET16 + .refto CPU_ISET_HUC6280 + .refto CPU_ISET_M740 + .refto CPU_ISET_4510 + .refto CPU_ISET_45GS02 + .refto CPU_ISET_W65C02 + .refto CPU_ISET_65CE02 + +.include "macros.inc" + +;;; ============================================================ +;;; Subset of ca65's opcodes.inc + +OPC_BPL = $10 +OPC_ORA_izy = $11 +OPC_CLC = $18 +OPC_ORA_abx = $1D +OPC_AND_izy = $31 +OPC_SEC = $38 +OPC_AND_abx = $3D +OPC_EOR_imm = $49 +OPC_JMP_abs = $4C +OPC_EOR_abx = $5D +OPC_RTS = $60 +OPC_ADC_izy = $71 +OPC_STY_zp = $84 +OPC_STX_zp = $86 +OPC_LDY_zp = $A4 +OPC_LDA_imm = $A9 +OPC_LDA_izy = $B1 +OPC_LDA_aby = $B9 +OPC_DEC_zp = $C6 +OPC_DEX = $CA +OPC_INC_zp = $E6 +OPC_INX = $E8 +OPC_NOP = $EA +OPC_SBC_izy = $F1 + +;;; ============================================================ +;;; Apple II I/O locations + +KBD := $C000 +KBDSTRB := $C010 +SPKR := $C030 +TXTCLR := $C050 +MIXCLR := $C052 +LOWSCR := $C054 +HISCR := $C055 +HIRES := $C057 +BUTN0 := $C061 +BUTN1 := $C062 +PADDL0 := $C064 +PADDL1 := $C065 +PTRIG := $C070 +LCBANK2 := $C083 +RdROMWrRAM1 := $C089 +LCBANK1 := $C08B + +;;; ============================================================ +;;; Zero Page + +ColorByteEven := $23 +ColorByteOdd := $24 + +UpdateCounter := $2B + +InputTickCounter := $31 + +HiresPageDelta := $8D ; Either +$20 or -$20 + +HiresRowPtr := $8E ; $8E-$8F + +PixelListData := $9C ; For `DrawPixelList` + +ValueForString := $B6 ; $B6-$B7 +;;; Used by `Set3DigitString` and `DivideByAXAndSetDigitY` + +FillColor := $ED ; e.g. ground color +AltFillColor := $EE ; e.g. sky color + +TmpStash := $F3 + +InputMode := $FA +;;; $00 = Normal Flight +;;; $01 = 3D View +;;; $02 = Radar View +;;; $03 = Magnetos / Fuel Mixture +;;; $04 = Com Radio (upper digits) +;;; $05 = Com Radio (lower digits) +;;; $06 = Nav Radio (upper digits) +;;; $07 = Nav Radio (lower digits) +;;; $08 = Transponder +;;; $0C = VORS +;;; $0D = ADF (high digit) +;;; $0E = ADF (mid digit) +;;; $0F = ADF (low digit) +;;; $10 = Fuel Tank Select + +;;; ============================================================ +;;; Other locations + +ZPSwapStorage := $FF40 + +;;; ============================================================ + +.include "chunk2.s" +.include "chunk3.s" +.include "chunk4.s" +.include "chunk5.s" diff --git a/src/macros.inc b/src/macros.inc new file mode 100644 index 0000000..5d2b435 --- /dev/null +++ b/src/macros.inc @@ -0,0 +1,156 @@ + +;;; Generic helpers +.define _is_immediate(arg) (.match (.mid (0, 1, {arg}), #)) +.define _immediate_value(arg) (.right (.tcount ({arg})-1, {arg})) +.macro _op_lo op, arg + .if _is_immediate {arg} + op #<_immediate_value {arg} + .else + op arg + .endif +.endmacro +.macro _op_hi op, arg + .if _is_immediate {arg} + op #>_immediate_value {arg} + .else + op arg+1 + .endif +.endmacro + +;;; Allow specifying negative constants + +.define AS_BYTE(arg) (arg) & $FF +.define AS_WORD(arg) (arg) & $FFFF + +;;; Emit a null-terminated string, prepended by `row`/`col` position +;;; An optional `label` parameter can be supplied, which will point +;;; at the start of the string data. + +.macro MESSAGE row, col, str, label + .byte row, col ; position + +.if .paramcount > 3 +label: ; optional label +.endif + + .byte str ; string data + .byte 0 ; null terminator +.endmacro + +;;; Load A,X with an address, and JSR a target routine +.macro CALLAX target, addr + lda #addr + jsr target +.endmacro + +;;; Load A,X with an address, and JMP a target routine +.macro JUMPAX target, addr + lda #addr + jmp target +.endmacro + +;;; Load A,X +;;; LDAX #$1234 ; immediate +;;; LDAX $1234 ; absolute +;;; LDAX $1234,y ; absolute,y +.macro LDAX arg, reg + .if .paramcount > 1 + lda arg, reg + ldx arg+1, reg + .else + _op_lo lda, {arg} + _op_hi ldx, {arg} + .endif +.endmacro + +;;; Store A,X +;;; STAX $1234 ; absolute +.macro STAX arg + sta arg + stx arg+1 +.endmacro + +;;; Load X,Y +;;; LDXY #$1234 ; immediate +;;; LDXY $1234 ; absolute +;;; LDXY $1234,y ; absolute,y +.macro LDXY arg, reg + .if .paramcount > 1 + ldx arg, reg + ldy arg+1, reg + .else + _op_lo ldx, {arg} + _op_hi ldy, {arg} + .endif +.endmacro + +;;; Store X,Y +;;; STXY $1234 ; absolute +.macro STXY arg + stx arg + sty arg+1 +.endmacro + +.macro ADD16 arg1, arg2, result + clc + _op_lo lda, {arg1} + _op_lo adc, {arg2} + sta result + _op_hi lda, {arg1} + _op_hi adc, {arg2} + sta result+1 +.endmacro + +.macro ADD16C arg1, arg2, result + _op_lo lda, {arg1} + clc + _op_lo adc, {arg2} + sta result + _op_hi lda, {arg1} + _op_hi adc, {arg2} + sta result+1 +.endmacro + +.macro SUB16 arg1, arg2, result + sec + _op_lo lda, {arg1} + _op_lo sbc, {arg2} + sta result + _op_hi lda, {arg1} + _op_hi sbc, {arg2} + sta result+1 +.endmacro + +.macro SUB16C arg1, arg2, result + _op_lo lda, {arg1} + sec + _op_lo sbc, {arg2} + sta result + _op_hi lda, {arg1} + _op_hi sbc, {arg2} + sta result+1 +.endmacro + +;;; Define bitmap pattern as a string +;;; * '#' is a 1 bit, anything else is a 0 +;;; * must be a multiple of 7 pixels wide. +;;; * high bit is always set +;;; e.g. PIXELS "..##############....." +.macro PIXELS str, dummy + .if .paramcount < 2 + + ;; Pad out with 0 bits + PIXELS .concat(str, "......"), 1234 + + .else + + ;; Process each 7 characters into a byte. + .repeat .strlen(str) / 7, idx + __pixel__ .set idx * 7 + .byte ((.strat(str, __pixel__+0) = '#') << 0) | ((.strat(str, __pixel__+1) = '#') << 1) | ((.strat(str, __pixel__+2) = '#') << 2) | ((.strat(str, __pixel__+3) = '#') << 3) | ((.strat(str, __pixel__+4) = '#') << 4) | ((.strat(str, __pixel__+5) = '#') << 5) | ((.strat(str, __pixel__+6) = '#') << 6) | $80 + .endrepeat + + .endif +.endmacro diff --git a/src/sceneryVm.c b/src/sceneryVm.c new file mode 100644 index 0000000..e69de29