Finally reasonable rendering.
This commit is contained in:
commit
78936ffc22
127 changed files with 46326 additions and 0 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
|
@ -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
|
||||
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.claude/
|
||||
loader/
|
||||
out/
|
||||
port/bin/
|
||||
port/obj/
|
||||
port/screenshots/
|
||||
tmp/
|
||||
downloads/
|
||||
port/tools/__pycache__/
|
||||
extracted_db/
|
||||
|
||||
*.bin.*
|
||||
*~
|
||||
1450
ARCHITECTURE.md
Normal file
1450
ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load diff
77
Makefile
Normal file
77
Makefile
Normal file
|
|
@ -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 $@ $<
|
||||
22
README.md
Normal file
22
README.md
Normal file
|
|
@ -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.
|
||||
666
SESSION_RECOVERY.md
Normal file
666
SESSION_RECOVERY.md
Normal file
|
|
@ -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 <yaw_byte> <pitch_byte> <bank_byte> [wx wy wz]`
|
||||
runs the port's `sceneryAttachCamera` and dumps `$78..$89`. Build
|
||||
with `make -C port bin/matrixProbe`.
|
||||
- `port/bin/fs2trace --matrix <yaw_i16> <pitch_i16> <bank_i16> <vd_byte>`
|
||||
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 <stream_addr> [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 <addr> [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.
|
||||
BIN
orig/1_4000-5fff
(Stored with Git LFS)
Normal file
BIN
orig/1_4000-5fff
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/2_f600-fbff
(Stored with Git LFS)
Normal file
BIN
orig/2_f600-fbff
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/3_d300-f3ff
(Stored with Git LFS)
Normal file
BIN
orig/3_d300-f3ff
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/4_0200-25ff
(Stored with Git LFS)
Normal file
BIN
orig/4_0200-25ff
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/5_6000-b3df
(Stored with Git LFS)
Normal file
BIN
orig/5_6000-b3df
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/FS2#062499
(Stored with Git LFS)
Normal file
BIN
orig/FS2#062499
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
orig/flight simulator 2 with scenery PRODOS (san inc pack).po
(Stored with Git LFS)
Normal file
BIN
orig/flight simulator 2 with scenery PRODOS (san inc pack).po
(Stored with Git LFS)
Normal file
Binary file not shown.
73
port/Makefile
Normal file
73
port/Makefile
Normal file
|
|
@ -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)
|
||||
255
port/PORT_64K_AUDIT.md
Normal file
255
port/PORT_64K_AUDIT.md
Normal file
|
|
@ -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.
|
||||
338
port/PORT_STATUS.md
Normal file
338
port/PORT_STATUS.md
Normal file
|
|
@ -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).
|
||||
1006
port/docs/scenery_opcodes.md
Normal file
1006
port/docs/scenery_opcodes.md
Normal file
File diff suppressed because it is too large
Load diff
238
port/include/aircraft.h
Normal file
238
port/include/aircraft.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
38
port/include/apple2hires.h
Normal file
38
port/include/apple2hires.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
24
port/include/audio.h
Normal file
24
port/include/audio.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#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
|
||||
81
port/include/camera.h
Normal file
81
port/include/camera.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#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
|
||||
59
port/include/chunk5Setup.h
Normal file
59
port/include/chunk5Setup.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
|
||||
// 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
|
||||
33
port/include/chunk5Transform.h
Normal file
33
port/include/chunk5Transform.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
// 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
|
||||
68
port/include/coursePlotter.h
Normal file
68
port/include/coursePlotter.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
94
port/include/cpu6502.h
Normal file
94
port/include/cpu6502.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
11
port/include/fixture.h
Normal file
11
port/include/fixture.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Hardcoded scenery byte streams used until the .po reader exists.
|
||||
|
||||
#ifndef FIXTURE_H
|
||||
#define FIXTURE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern const uint8_t fixtureSceneryDemo[];
|
||||
extern const uint32_t fixtureSceneryDemoLength;
|
||||
|
||||
#endif
|
||||
21
port/include/font.h
Normal file
21
port/include/font.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#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
|
||||
30
port/include/framebuffer.h
Normal file
30
port/include/framebuffer.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#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
|
||||
64
port/include/fs2math.h
Normal file
64
port/include/fs2math.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
// `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
|
||||
92
port/include/hires.h
Normal file
92
port/include/hires.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
// 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
|
||||
13
port/include/hud.h
Normal file
13
port/include/hud.h
Normal file
|
|
@ -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
|
||||
14
port/include/instruments.h
Normal file
14
port/include/instruments.h
Normal file
|
|
@ -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
|
||||
28
port/include/math6502.h
Normal file
28
port/include/math6502.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
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
|
||||
22
port/include/needleData.h
Normal file
22
port/include/needleData.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
39
port/include/palette.h
Normal file
39
port/include/palette.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
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
|
||||
14
port/include/panelDigits.h
Normal file
14
port/include/panelDigits.h
Normal file
|
|
@ -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
|
||||
39
port/include/projection.h
Normal file
39
port/include/projection.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
119
port/include/radios.h
Normal file
119
port/include/radios.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
57
port/include/renderer.h
Normal file
57
port/include/renderer.h
Normal file
|
|
@ -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
|
||||
68
port/include/sceneryData.h
Normal file
68
port/include/sceneryData.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
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<region>
|
||||
// - ../downloads/scenery/extracted/A2.SD<region>
|
||||
// - ../../downloads/scenery/extracted/A2.SD<region>
|
||||
// - /home/scott/claude/flight/downloads/scenery/extracted/A2.SD<region>
|
||||
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
|
||||
172
port/include/sceneryProjection.h
Normal file
172
port/include/sceneryProjection.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#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
|
||||
719
port/include/sceneryStationsData.h
Normal file
719
port/include/sceneryStationsData.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
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
|
||||
147
port/include/sceneryVm.h
Normal file
147
port/include/sceneryVm.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#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
|
||||
53
port/include/timeOfDay.h
Normal file
53
port/include/timeOfDay.h
Normal file
|
|
@ -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 <stdint.h>
|
||||
|
||||
|
||||
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
|
||||
43
port/include/title.h
Normal file
43
port/include/title.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <SDL.h>
|
||||
#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
|
||||
46
port/include/types.h
Normal file
46
port/include/types.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// Shared types and constants for the FS2 modernized port.
|
||||
|
||||
#ifndef TYPES_H
|
||||
#define TYPES_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// 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
|
||||
75
port/include/wind.h
Normal file
75
port/include/wind.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
29
port/include/world.h
Normal file
29
port/include/world.h
Normal file
|
|
@ -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
|
||||
141
port/include/ww1ace.h
Normal file
141
port/include/ww1ace.h
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#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
|
||||
BIN
port/sceneryRam_FS2.1.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_FS2.1_chicago.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1_chicago.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_FS2.1_frozen.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1_frozen.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_FS2.1_la.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1_la.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_FS2.1_ny.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1_ny.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_FS2.1_seattle.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_FS2.1_seattle.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD1.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD1.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD11.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD11.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD13.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD13.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD14A.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD14A.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD14B.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD14B.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD2.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD2.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD3.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD3.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD4.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD4.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD5.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD5.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD6.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD6.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD7A.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD7A.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SD7B.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SD7B.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
BIN
port/sceneryRam_SDS1.bin
(Stored with Git LFS)
Normal file
BIN
port/sceneryRam_SDS1.bin
(Stored with Git LFS)
Normal file
Binary file not shown.
681
port/src/aircraft.c
Normal file
681
port/src/aircraft.c
Normal file
|
|
@ -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 <stddef.h>
|
||||
#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;
|
||||
}
|
||||
83
port/src/apple2hires.c
Normal file
83
port/src/apple2hires.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
258
port/src/audio.c
Normal file
258
port/src/audio.c
Normal file
|
|
@ -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 <math.h>
|
||||
#include <SDL.h>
|
||||
#include <stdint.h>
|
||||
#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);
|
||||
}
|
||||
176
port/src/camera.c
Normal file
176
port/src/camera.c
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Camera state and world-to-camera transform.
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#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);
|
||||
}
|
||||
556
port/src/chunk5Setup.c
Normal file
556
port/src/chunk5Setup.c
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
785
port/src/chunk5Transform.c
Normal file
785
port/src/chunk5Transform.c
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#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 <skip>; 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
|
||||
}
|
||||
161
port/src/coursePlotter.c
Normal file
161
port/src/coursePlotter.c
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// Course Plotter implementation. See coursePlotter.h.
|
||||
|
||||
#include <string.h>
|
||||
#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);
|
||||
}
|
||||
408
port/src/cpu6502.c
Normal file
408
port/src/cpu6502.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
63
port/src/fixture.c
Normal file
63
port/src/fixture.c
Normal file
|
|
@ -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);
|
||||
108
port/src/font.c
Normal file
108
port/src/font.c
Normal file
|
|
@ -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 <ctype.h>
|
||||
#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;
|
||||
}
|
||||
107
port/src/framebuffer.c
Normal file
107
port/src/framebuffer.c
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
// Software framebuffer implementation.
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
238
port/src/fs2math.c
Normal file
238
port/src/fs2math.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
348
port/src/hires.c
Normal file
348
port/src/hires.c
Normal file
|
|
@ -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 <string.h>
|
||||
#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;
|
||||
}
|
||||
137
port/src/hud.c
Normal file
137
port/src/hud.c
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
// HUD strip drawn into the instrument-panel area beneath the
|
||||
// viewport. Five digital readouts plus a small attitude indicator.
|
||||
|
||||
#include <stdio.h>
|
||||
#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);
|
||||
}
|
||||
622
port/src/instruments.c
Normal file
622
port/src/instruments.c
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
// Live instrument needles. Coordinates pinned by visual inspection of
|
||||
// the FS2 panel bitmap; refine if the gauges appear misaligned.
|
||||
|
||||
#include <stdio.h>
|
||||
#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;
|
||||
}
|
||||
1200
port/src/main.c
Normal file
1200
port/src/main.c
Normal file
File diff suppressed because it is too large
Load diff
63
port/src/math6502.c
Normal file
63
port/src/math6502.c
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Fixed-point sin/cos and signed multiply.
|
||||
|
||||
#include <math.h>
|
||||
#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;
|
||||
}
|
||||
131
port/src/needleData.c
Normal file
131
port/src/needleData.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
55
port/src/palette.c
Normal file
55
port/src/palette.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
174
port/src/panelDigits.c
Normal file
174
port/src/panelDigits.c
Normal file
|
|
@ -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 <stdio.h>
|
||||
#include <string.h>
|
||||
#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);
|
||||
}
|
||||
157
port/src/projection.c
Normal file
157
port/src/projection.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
397
port/src/radios.c
Normal file
397
port/src/radios.c
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
// NAV / COM / ADF radio state and lookups. See radios.h.
|
||||
|
||||
#include <stddef.h>
|
||||
#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
393
port/src/renderer.c
Normal file
393
port/src/renderer.c
Normal file
|
|
@ -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 <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
342
port/src/sceneryData.c
Normal file
342
port/src/sceneryData.c
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
// Scenery loader implementation. See sceneryData.h.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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_<region>.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.<region>` (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;
|
||||
}
|
||||
441
port/src/sceneryProjection.c
Normal file
441
port/src/sceneryProjection.c
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
// 3D vertex pipeline -- ports the math from chunk5 polygon code.
|
||||
//
|
||||
// L7EBC, ClassifyVertex1/2, ProjectV1ToScreen, ProjectV2ToScreen,
|
||||
// PerspectiveDivide, EmitPrimaryVertex.
|
||||
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
2837
port/src/sceneryVm.c
Normal file
2837
port/src/sceneryVm.c
Normal file
File diff suppressed because it is too large
Load diff
103
port/src/timeOfDay.c
Normal file
103
port/src/timeOfDay.c
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
86
port/src/title.c
Normal file
86
port/src/title.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
155
port/src/wind.c
Normal file
155
port/src/wind.c
Normal file
|
|
@ -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;
|
||||
}
|
||||
241
port/src/world.c
Normal file
241
port/src/world.c
Normal file
|
|
@ -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 <stddef.h>
|
||||
#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);
|
||||
}
|
||||
}
|
||||
712
port/src/ww1ace.c
Normal file
712
port/src/ww1ace.c
Normal file
|
|
@ -0,0 +1,712 @@
|
|||
// FS2 World War 1 Ace combat mode. See ww1ace.h for the data flow.
|
||||
|
||||
#include <stdio.h>
|
||||
#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));
|
||||
}
|
||||
}
|
||||
209
port/tools/chunk5SetupTest.c
Normal file
209
port/tools/chunk5SetupTest.c
Normal file
|
|
@ -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 <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue