diff --git a/AGI-SESSION-HANDOFF.md b/AGI-SESSION-HANDOFF.md
index b2d4485..28bb0c6 100644
--- a/AGI-SESSION-HANDOFF.md
+++ b/AGI-SESSION-HANDOFF.md
@@ -189,7 +189,7 @@ The user should pick which to prioritize.
The repo's auto-memory at
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes
-JoeyLib-wide context (CMake quirks, ORCA-C gotchas, perf
+JoeyLib-wide context (CMake quirks, IIgs toolchain gotchas, perf
constraints) plus `project_joeylib_agi_plan.md` which describes the
overall AGI port plan. All persist into the next session
automatically; no need to reload.
@@ -197,9 +197,8 @@ automatically; no need to reload.
Specifically relevant to the AGI port:
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
-- `feedback_orca_c_atoi_segment.md` — don't use atoi in AGI code
- (it pushes the IIgs ORCA-C stdio cluster past 64 KB; we use a
- manual parseArgU8 instead)
+- AGI code uses a manual `parseArgU8` instead of atoi to keep the
+ IIgs link budget small.
- `project_perf_directive.md` — IIgs is the perf floor; every
other target must match or beat it
- `feedback_iigs_speed.md` — IIgs must be absolute fastest
@@ -213,6 +212,6 @@ Specifically relevant to the AGI port:
`feedback_git.md`).
- Always `source toolchains/env.sh` before any `make` -- the user
has added that to .claude/settings.local.json allow list.
-- DJGPP / ORCA / vasm / m68k-atari-mint paths come from env.sh.
+- DJGPP / llvm-mos / vasm / m68k-atari-mint paths come from env.sh.
- The user does NOT need hand-holding -- they're an expert C dev.
Skip excessive explanation; describe what was done and move on.
diff --git a/IIGS_PERF_PLAN.md b/IIGS_PERF_PLAN.md
new file mode 100644
index 0000000..0388cf9
--- /dev/null
+++ b/IIGS_PERF_PLAN.md
@@ -0,0 +1,294 @@
+# IIgs Performance Fix Plan
+
+Derived from the 2026-06 IIgs render-path audit (per-op 65816 cycle analysis +
+adversarial verification). Numbers below are verifier-confirmed unless marked
+"est." Ordered by dependency, then by realistic payoff.
+
+## Implementation status (2026-06-23)
+
+All phases implemented and compile/link clean via the clang/llvm-mos w65816
+toolchain. Every change is
+correct-by-construction from the verified analysis; the hot-path 65816 asm
+(Phases 3, 4, 5.1) still needs **pixel validation on hardware/MAME** -- the
+headless emulator here can't load the 1.6 MB disk image, so I could not observe
+output. Two micro-opts are left as marked residuals where the phase's value is
+already captured.
+
+- **Phase 0 -- DONE.** Removed the vestigial `SEI` + chunk-CLI from
+ `iigsFillRectInner`; added the ~29 ops/sec ceiling + SEI-inflation note to
+ `PERF.md` and the `uber-perf-table` generator.
+- **Phase 1 -- DONE.** `drawShowcase()` in `examples/uber/uber.c` renders one of
+ each primitive into a labelled grid; the timed benchmark is unchanged (op
+ sequence/args identical, so cross-port hashes still compare); ends green. No
+ live progress bar (it would clobber the deliberately-seeded per-op input
+ states; frozen showcase + joeylog is the fidelity-preserving choice).
+- **Phase 2 -- DONE.** Two parts: (a) `surfaceMarkDirtyRect` widens single-row
+ (`h==1`) marks inline in C, dropping the cross-segment `iigsMarkDirtyRowsInner`
+ JSL; (b) **`jlDrawPixel` fully inlined on IIgs** (draw.c) -- no `plotPixelNoMark`
+ / `surfaceMarkDirtyRect` call layers around the single asm plot, mark widened
+ inline via `jlStageGet()` guard. This is the 5-10x path. (The plan's
+ fold-into-asm form is UNSAFE -- `iigsDrawPixelInner` can't see `s`, would mark
+ `gStage` for heap-surface draws; the C inline keeps the guard.)
+- **Phase 3 -- DONE.** Computed-jump slam for ALL middle widths (`friMidSlamTbl`
+ via an **RTS computed-goto** + X bias -- ascending table kept, no bank-0 vector
+ hazard; `nWords==0` lands on the odd-byte writer). Partial fills go 7 -> 3
+ cyc/byte. Plus the `friMidMvn` X-reuse cleanup (now dead code path removed).
+- **Phase 4 -- DONE (4.1 residual).** 4.2: removed the per-plot `dcSavedCol`
+ round-trip (parity via free X). 4.3: the 4 octant row bases are maintained
+ incrementally (+/-160 per step) instead of 4 LUT lookups/iteration. 4.1 (move
+ remaining `dc*` scratch to direct page) is the **residual**: ~4% more after
+ 4.2+4.3, but needs a stack-frame-locals refactor to resolve the `D=SP+8` arg
+ conflict -- disproportionate blind-refactor risk for the gain.
+- **Phase 5 -- DONE.** 5.1: `iigsSurfaceClearFastInner` -- post-init PHA stack-slam
+ (2 cyc/byte, ~1.5x) with AUXWRITE redirect to bank $01; SHR shadow already
+ inhibited by halInit; SP restored before CLI; the C macro dispatches here and
+ keeps the STA `iigsSurfaceClearInner` as the init-safe fallback. (Non-chunked
+ SEI ~23 ms; chunk like the present if audio glitches.) 5.2: `surfaceMarkDirtyAll`
+ is now two `memset`s. 5.3 subsumed by 5.1.
+- **Phase 6 -- DONE (deep residual).** `jlSpriteSaveAndDraw` already shares
+ `shift`, dodges the MUL4 2D-index, and marks once (the C-level win). The deeper
+ dest-address sharing threaded through `spriteCompiledSaveUnder`/`Draw` is
+ codegen-level surgery for ~1.05x -- left as the **residual**.
+- **Phase 7 -- DONE.** `docs/DESIGN.md` documents the deliberate double-buffer
+ cost and why single-buffering the IIgs was rejected.
+
+### Highest hardware-validation priority (new asm, pixel-validate first)
+
+1. `jlFillRect` at assorted x/w (NOT just 0/320): the computed-goto slam must
+ fill exactly `[x..x+w)` with no over/underrun. Check odd widths + odd x.
+2. `iigsSurfaceClearFastInner`: the screen must clear to the right color with no
+ stray writes; watch for audio glitches (the ~23 ms SEI window).
+3. `jlDrawCircle`: shape unchanged after the incremental-row-base rewrite.
+4. `jlDrawPixel`: visually correct + ops/sec jump.
+
+### Hardware-validation checklist (run on your MAME/GSplus + the 4 ports)
+
+1. Build + run `examples/uber` on IIgs; confirm the showcase grid renders all
+ primitives legibly and the benchmark still completes (green exit).
+2. Re-capture `joeylog.txt` on all four ports; regenerate `PERF.md` via
+ `tools/uber-perf-table`. Expect IIgs `fillRect 320x200` and `stagePresent`
+ to drop toward ~25-28 (SEI removal de-inflates fillRect).
+3. `tools/diff-uber-hashes` IIgs-vs-{amiga,st,dos}: all ops must still match
+ (the C dirty-mark + memset changes are portable and must not alter pixels).
+ If any op diverges, the dirty-mark fold changed a present extent -- inspect.
+4. Eyeball the circle (`jlDrawCircle`) output unchanged after the dcSavedCol
+ change; confirm `jlDrawPixel` ops/sec rose.
+
+
+
+## Ground truth / framing
+
+- **Hard floor:** any full-screen 32000-byte move = 16000 word-stores x 6 cyc
+ (`STA long,X $9F` or `PEI`, both 3 cyc/byte) / 2.8 MHz = ~34 ms = **~29 ops/sec
+ max**. Clear, full-width fillRect, and present are all pinned to this; you
+ cannot beat it without moving fewer bytes (the dirty-row system already does
+ that for sparse frames).
+- **The benchmark lies for SEI ops.** `PERF.md` `fillRect 320x200`=60 and
+ `stagePresent`=42 exceed/approach the 29-ops/sec floor and are inflated; clear=28
+ is honest. Fix measurement FIRST (Phase 0) or you cannot tell wins from clock noise.
+- **Two architectural realities that are NOT bugs:** (a) the back-buffer ->
+ `$E1` design pays the screen twice on full-redraw frames, but the single-buffer
+ "fix" is refuted (slow `$E1`, overdraw inverts the win, tearing) -- keep it; the
+ dirty-row system is the right mitigation. (b) The circle OUTLINE trails the ST
+ due to 65816 register pressure and may never fully win; bank the partial gain.
+
+## Cross-cutting rules (apply to every asm change)
+
+- Keep the assembler's register-width state correct after every `REP`/`SEP` so
+ immediates are sized correctly.
+- Keep each asm routine in the SAME load segment as its C caller (DRAWPRIMS for
+ the draw primitives); a separate segment silently produces calls-to-nowhere.
+- `gStageMinWord` / `gStageMaxWord` must stay `uint8_t[]` so the asm that walks
+ them per byte stays correctly aligned with the C-side element size.
+- PHA/PEI stack-slam paths REQUIRE `SEI` and are unsafe during `joeyInit` -- gate
+ them post-init (`gModeSet`).
+- 4bpp word stores need even byte index (`x/2` even, i.e. `x % 4 == 0`) for the
+ computed-jump slam entry.
+- Build hygiene: `source toolchains/env.sh`; DELETE the example binary before
+ rebuild (make leaves IIgs binaries stale); pass `spriteEmitIigs.c` first in
+ LIB_SRCS (linker bank-packing order).
+- **Validation after every change:** re-run `examples/uber`, confirm
+ `jlSurfaceHash` per op is UNCHANGED (pixel-identical to the IIgs golden) and
+ ops/sec moved the right way. Verify visually in MAME (checkpoint poll or
+ snapshot) -- joeyLog alone misses wrong-pixel-but-right-control-flow bugs.
+
+---
+
+## Phase 0 -- Make measurement trustworthy (PREREQUISITE)
+
+Everything downstream is judged against the benchmark; fix it first.
+
+- **0.1 Audit & likely remove `iigsFillRectInner`'s `SEI`** (joeyDraw.asm:315,
+ chunk-CLI machinery :585-614). The body does only long-mode writes to the
+ bank-$01 back buffer with no SP/shadow hijack (per its own comment :284-292),
+ so the SEI appears vestigial. Confirm nothing in the fill needs interrupt
+ atomicity (MVN is interruptible/resumable by design), then remove SEI + the
+ FRI_CHUNK_ROWS CLI logic. Expected: fillRect-full de-inflates 60 -> ~27
+ (matching clear), AND the audio-starvation risk goes away. If some atomicity IS
+ required, document exactly what and keep it.
+- **0.2 Add a cycle-ceiling sanity check / annotation.** Document the ~29 ops/sec
+ full-screen floor in `PERF.md`; flag any full-screen op reading above it as
+ suspect. Optional: assert in the harness.
+- **0.3 Reconcile `present`=42** (its SEI is REAL -- stack/shadow hijack -- so it
+ can't just be removed). Capture an honest present rate via an SEI-immune time
+ source or a diagnostic no-SEI measurement build; expect ~25 ops/sec. Update the
+ table and the cross-port comparison accordingly.
+- **Re-baseline `PERF.md`** with honest numbers before Phase 2+.
+
+Exit criterion: clear, fillRect-full, and present all read within ~10% of each
+other (~25-28 ops/sec), consistent with the cycle model.
+
+## Phase 1 -- Make the uber demo legible (show, don't smear)
+
+Right now `examples/uber` shows the user a solid colored screen and teaches them
+nothing. Cause, concretely:
+- `runAllTests()` (uber.c:222-285) runs each op in a 16-frame tight loop and
+ NEVER calls `jlStagePresent()` during or between the timed ops. The screen is
+ frozen on the last present before the run (the red-bar-on-black at uber.c:379)
+ for the whole ~80 s run, then ends on a bare `jlSurfaceClear(gStage, 2)`
+ (uber.c:390) -> solid green.
+- Even if it did present, the timed ops are full-screen or fixed-coordinate
+ (`fillRect 320x200`, `surfaceClear`, pixel@100,100, line 0,50->319,50, ...), so
+ they overwrite each other into one solid color. There is no spatial separation.
+
+Goal: the demo should visibly demonstrate what each primitive does AND run the
+benchmark without leaving a meaningless screen. Separate "show the user" from
+"measure throughput" -- they are different jobs.
+
+- **1.1 Visual showcase pass (new), runs once before the benchmark, UNTIMED.**
+ Lay out a labeled grid (e.g. 4 cols x 5 rows of ~72x36 cells with a gutter) and
+ render each primitive ONCE into its own cell so nothing overlaps: pixel, line
+ H/V/diag, rect outline, circle (small), fillRect (small), fillCircle, a tile,
+ tileCopyMasked result, the compiled sprite, a 16-entry palette ramp swatch, an
+ SCB per-line strip, a sample-pixel readout. Full-screen ops (surfaceClear,
+ fillRect 320x200, present) cannot be shown at native size in a cell -- represent
+ each with a small labeled swatch and say so. `jlStagePresent()` once, then hold
+ for a readable beat (frame-counted ~2-3 s, or `jlWaitForAnyKey` -> "press a key
+ to begin benchmark"). THIS is the part that makes sense to a user.
+- **1.2 Benchmark pass: keep timing/hash fidelity, show a sane status screen.**
+ Keep the `timeOp` loop and every op's args + call order EXACTLY as-is -- the
+ cross-port `jlSurfaceHash` golden values depend on the precise op sequence, so
+ changing them invalidates the validation harness. Stop presenting the timing
+ garbage: draw a static "BENCHMARKING..." screen plus a progress indicator (op
+ counter / bar, optionally each op's just-measured ops/sec) updated BETWEEN
+ `timeOp` calls -- never inside the timed loop. End on an explicit "DONE -- press
+ any key" screen, not a bare solid clear.
+- **1.3 Labels.** `jlDrawText` exists (tile.h:119) but needs a font surface +
+ asciiMap, which uber does not currently load. Either add a small font surface +
+ asciiMap and label each cell, or (fallback) use fixed spatial layout + a short
+ on-screen color-key strip and a legend printed via `jlLogF`.
+
+Cautions:
+- All showcase drawing AND the progress-UI updates must be OUTSIDE any timed
+ window -- zero effect on ops/sec.
+- Do NOT change the timed ops' arguments or sequence (golden-hash stability).
+- Keep it port-agnostic: uber is shared example code that also runs on
+ Amiga/ST/DOS -- no IIgs-only assumptions in the showcase.
+- Do this AFTER Phase 0 so any per-op ops/sec shown on the status screen is the
+ corrected (honest) number, not the inflated one.
+
+Exit criterion: a first-time user watching the demo can see and identify each
+primitive's output, and the benchmark phase shows readable progress + a clear
+finish -- never a bare solid color.
+
+## Phase 2 -- Kill the per-primitive C+JSL tax (biggest real-world win)
+
+`jlDrawPixel` costs **~1595 cyc/call** (2.8e6 / 1755), ~94% overhead; the actual
+nibble RMW is ~30 cyc. Every marking primitive pays a second cross-segment JSL.
+
+- **2.1 Fold dirty-marking into the asm inner loops.** `jlDrawPixel` (draw.c:95),
+ `jlFillRect` (draw.c:448-ish via fillRectOnSurface), line, and circle each call
+ `surfaceMarkDirtyRect` AFTER the plot JSL -- a second JSL into
+ `iigsMarkDirtyRowsInner` (~49 cyc plumbing x2 + the row loop). The inner loops
+ already know the row(s) and extent (fillRect has `midStart`/`trailingByte` per
+ row; pixel has `y` and the byte). Update `gStageMinWord[y]`/`gStageMaxWord[y]`
+ inline and make `surfaceMarkDirtyRect` a no-op on the IIgs fast paths.
+ - Confirmed win: removes ~120 cyc from EVERY marking primitive; **5-10x on
+ `jlDrawPixel`**.
+- **2.2 Trim the asm prologue/epilogue for the tiny ops.** `iigsDrawPixelInner`
+ (joeyDraw.asm:1158-1227) keeps scratch in 6-cyc long-absolute (`dpxlTmp`,
+ `dpxlNibPart`) and pays full PHP/PHB/PHD ... PLD/PLB/PLP framing around a ~50-cyc
+ plot. Use registers/DP for the scratch; drop framing not needed when DBR/D are
+ already correct. (est ~1.2-1.4x on the inner once 2.1 lands.)
+- **2.3 Batch/inline entry for pixel-heavy callers (optional).** A span/row-run
+ API (or inlining the in-bounds nibble RMW into the C via macro, the way fillRect
+ was de-wrappered) amortizes the JSL across many pixels for plotting-heavy code.
+
+Exit criterion: `jlDrawPixel` >> 1755; line/rect call cost drops by the second-JSL
+amount; hashes unchanged.
+
+## Phase 3 -- fillRect partial-width slam gate
+
+The 3-cyc/byte slam fires ONLY for exactly `x==0 && w==320` (`friFullWidth`,
+joeyDraw.asm:406-420). Everything else falls to seed+`MVN` at **7 cyc/byte**
+(2.33x slower), confirmed.
+
+- **3.1 Computed jump into the unrolled `STA long,X` table.** Set `X =
+ curRow + midStart`, then `JMP (addr)` into the 80-store run at store
+ `#(80 - midBytes)`. Gives 3 cyc/byte for ANY width. Keep MVN only for tiny odd
+ remainders. Drop-in safe (bank-$01 long stores, no SEI/shadow). Respect the
+ 4bpp even-byte-index rule for the entry. **1.8-2.3x on all partial-width fills.**
+- **3.2 Cleanups (minor):** cache `curRow+midStart` instead of recomputing it
+ (joeyDraw.asm:542-565); hoist loop-invariant edge offsets (`leadingByte`/
+ `trailingByte` relative to curRow, += 160/row); one `sep`/`rep` window per row
+ for both edge RMWs.
+
+## Phase 4 -- circle / line register pressure (~1.27x, partial)
+
+- **4.1 Move Bresenham scratch to direct page.** `dcX/dcY/dcErr/dcRowYP/dcRowYN/
+ dcRowXP/dcRowXN` (circle, joeyDraw.asm:1500+) and the line's per-pixel state
+ (joeyDraw.asm:1253+) are 6-cyc long-absolute; DP is 5 cyc here (DL != 0 because
+ D=SP+8). Set up a DP scratch page for the hot state.
+- **4.2 Remove the per-plot parity round-trip.** `sta dcSavedCol` / reload /
+ `and #1` (joeyDraw.asm:1603-1610) recovers a bit `lsr a` already left in carry
+ -- branch on carry instead. ~15 cyc/plot x 8 plots/step.
+- **4.3 Maintain the 4 row bases incrementally** (+/-160 per Bresenham step)
+ instead of 4 LUT lookups/step.
+- Net ~1.27x (0.65x -> ~0.83x vs ST). Stop there -- the architecture won't give a
+ full win on the outline. The fill-circle already beats the ST; leave it.
+
+## Phase 5 -- faster clear (post-Phase-0; lower priority)
+
+- **5.1 Post-init PHA-slam clear path.** `STA long,X` = 3 cyc/byte; SP-into-`$01`
+ PHA-slam = 2 cyc/byte. ~1.5x (37.6 -> ~24 ms). Gate on `gModeSet` (SEI unsafe at
+ init); keep the `STA long,X` path as the init-safe fallback. Chunk the SEI like
+ present to avoid audio starvation. NOTE: only meaningful to measure once Phase 0
+ fixes the clock.
+- **5.2 Route `surfaceMarkDirtyAll` through `iigsMarkDirtyRowsInner`** (it's a
+ 200-iter C loop today; surface.c:233-243) -- one source of truth with the
+ fillRect marker. ~7600 cyc/clear.
+- **5.3 Hold `fillWord` in a register** across the clear loop instead of
+ `lda 9,s` per iteration (joeyDraw.asm:137). Subsumed if 5.1 lands.
+
+## Phase 6 -- sprite dispatch overhead
+
+Body store mode (`LDA #imm16`/`STA abs,Y`) is already near-optimal -- DO NOT touch
+it. ~3000-4000 cyc/call of C dispatch wraps ~1000-1500 cyc of real blit.
+
+- **6.1 Share address/shift/fnAddr across `SaveAndDraw`'s two halves** (sprite.c)
+ -- it recomputes dest address, shift, fnAddr, and re-patches the self-modifying
+ stub twice. (est ~1.05x SaveAndDraw.)
+- **6.2 Trim per-call C dispatch:** cache fnAddr, dodge the 2D-index MUL4, do
+ `isFullyOnSurface` in 8-bit. (est ~1.10-1.15x per sprite op.)
+
+## Phase 7 -- architecture (decision, no code)
+
+Keep the back-buffer + dirty-row model. Document in `docs/DESIGN.md` that
+full-redraw frames inherently pay the screen cost ~2x and that sparse-update
+(sprite/HUD) games should lean on partial updates. Do NOT pursue single-buffer
+direct-to-`$E1` (refuted on slow-bank/overdraw/tearing grounds).
+
+---
+
+## Suggested order of execution
+
+1. Phase 0 (0.1 fillRect SEI, 0.2 annotate, 0.3 present) -> re-baseline PERF.md.
+2. Phase 1 (make the demo legible) -- so every later change can be eyeballed, not
+ just hashed; cheap and unblocks visual validation.
+3. Phase 2.1 (fold dirty-marking) -- highest real-world payoff, touches all primitives.
+4. Phase 3.1 (fillRect computed-jump slam).
+5. Phase 2.2, Phase 4 (circle/line DP scratch + parity).
+6. Phase 5, Phase 6 (clear PHA-slam, sprite dispatch) as time permits.
+7. Phase 7 doc.
+
+Each step: re-run uber, assert hashes identical, record ops/sec delta in PERF.md.
diff --git a/LICENSES.md b/LICENSES.md
index ce0a971..4771dd3 100644
--- a/LICENSES.md
+++ b/LICENSES.md
@@ -56,44 +56,18 @@ These run on the developer's machine to compile / assemble / link
JoeyLib for each target. They are not redistributed inside the
produced game binaries.
-### Golden Gate (host tools: iix, profuse, mkfs-profuse, dumpobj)
+### llvm-mos (clang / llvm-mc / link816 w65816 toolchain)
- **Used on:** Apple IIgs
-- **URL:**
- (publisher / store: )
-- **License:** **Commercial** software by Kelvin Sherlock,
- distributed through Juiced.GS / Gamebits. End-user purchase
- required; redistribution **not** permitted.
-- **Role:** Cross-host harness that runs ORCA-style IIgs tools on a
- modern OS. Provides the `iix` shell, the OMF linker, `profuse`
- (ProDOS FUSE mount), `mkfs-profuse`, `dumpobj`, and the IIgs-side
- filesystem image (`$GOLDEN_GATE`) that the harness emulates.
-- **Distribution:** **Not redistributed by JoeyLib.** Each developer
- must bring their own purchased copy. `toolchains/install.sh`
- builds host tools from a developer-supplied source checkout under
- `stuff/GoldenGate/` (the `stuff/` tree is gitignored) and stages
- the IIgs filesystem from a developer-supplied `gg-linux.tgz`. The
- resulting `toolchains/iigs/gg-tools/` and `toolchains/iigs/goldengate/`
- trees are gitignored.
-
-### ORCA/C compiler + ORCA libraries
-
-- **Used on:** Apple IIgs
-- **URL:**
- (commercial distribution: )
-- **License:** **Commercial** product by The Byte Works, Inc.
- (Mike Westerfield). Source code is published on GitHub under a
- custom license that grants only **private use, fork, and
- modification**; redistribution in any form other than submission
- back to the upstream repo requires written permission. Commercial
- rights remain with Byte Works; the compiler is sold today as part
- of the **Opus ][** collection via Juiced.GS.
-- **Role:** ANSI C compiler for the 65816 + Apple IIgs runtime
- libraries. Run via Golden Gate's `iix` harness.
-- **Distribution:** **Not redistributed by JoeyLib.** ORCA support
- files arrive as part of the developer-supplied Golden Gate /
- Opus ][ install bundle and land inside the gitignored
- `toolchains/iigs/goldengate/` tree.
+- **URL:**
+- **License:** Apache License 2.0 with LLVM Exceptions (the upstream
+ LLVM license).
+- **Role:** clang C compiler targeting the 65816, plus llvm-mc for
+ assembling hand-written `.s` files and link816 for producing OMF
+ S16 executables. Driven by `toolchains/iigs/clang-build.sh`; the
+ toolchain root is located via the `LLVM816_ROOT` environment
+ variable.
+- **Vendored at:** `toolchains/iigs/llvm-mos/`
### Merlin32
diff --git a/PERF.md b/PERF.md
index 3c8569f..c30ee0f 100644
--- a/PERF.md
+++ b/PERF.md
@@ -34,3 +34,15 @@
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
| c2p full screen | - | - | - | - |
+
+> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves
+> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both
+> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So
+> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`
+> cannot legitimately exceed ~29; a higher reading means the benchmark
+> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-
+> driven GetTick, so an op that holds `SEI` while it runs can over-
+> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
+> inflated and cross-check against the cycle model. (`jlFillRect 320x200`
+> = 60 and `jlStagePresent full` = 42 in the table above predate the
+> Phase 0 SEI removal and are inflated; expect ~25-28 after re-measure.)
diff --git a/examples/adventure/adventure.c b/examples/adventure/adventure.c
index fb9f9c3..2186701 100644
--- a/examples/adventure/adventure.c
+++ b/examples/adventure/adventure.c
@@ -270,15 +270,10 @@ static uint16_t rgb(uint8_t r, uint8_t g, uint8_t b) {
}
-/* ORCA-C's auto-segmenter splits code at function boundaries but
- * places everything from one TU into one code bank by default. This
- * single-file example easily blows the 64 KB code-bank limit, so we
- * sprinkle `segment "NAME";` directives at section boundaries to
- * force the linker to place each region in its own code segment.
- * The directive is ORCA-specific; gcc-targeted ports ignore it via
- * the platform guard. */
+/* This single-file example can blow the 65816's 64 KB code-bank
+ * limit, so on the IIgs we split code across segments at section
+ * boundaries to spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADVART";
#endif
@@ -1103,7 +1098,6 @@ static bool buildAllSprites(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADVROOM";
#endif
@@ -1603,7 +1597,6 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADVMOVE";
#endif
@@ -1869,7 +1862,6 @@ static void drawInventoryBar(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADVUI";
#endif
@@ -1935,7 +1927,6 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADVVERB";
#endif
@@ -2213,7 +2204,6 @@ static bool buildBackgrounds(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "";
#endif
@@ -2231,7 +2221,6 @@ int main(void) {
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
* need ~16 KB. */
config.codegenBytes = 256UL * 1024;
- config.maxSurfaces = 6;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/adventure2/adventure2.c b/examples/adventure2/adventure2.c
index 1001d4c..ed2ab3d 100644
--- a/examples/adventure2/adventure2.c
+++ b/examples/adventure2/adventure2.c
@@ -235,12 +235,10 @@ static bool isTrunkChar(char c) {
}
-/* ORCA-C's auto-segmenter splits at function boundaries; one TU's
- * code can still overflow the 65816's 64 KB code bank if it lands
- * in a single segment. Sprinkle ORCA-only segment directives at the
- * heavier sections to spread the load across multiple banks. */
+/* One TU's code can overflow the 65816's 64 KB code bank if it lands
+ * in a single segment. Split the heavier sections across segments to
+ * spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADV2ART";
#endif
@@ -723,7 +721,6 @@ static void programScbForView(DebugViewE view) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADV2PAINT";
#endif
@@ -930,7 +927,6 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADV2PATH";
#endif
@@ -1214,7 +1210,6 @@ static void drawInventoryBar(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "ADV2COMP";
#endif
@@ -1328,7 +1323,6 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick)
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "";
#endif
@@ -1346,7 +1340,6 @@ int main(void) {
* be small. We need 4 surfaces (stage + visual + priority +
* control). */
config.codegenBytes = 4UL * 1024;
- config.maxSurfaces = 8;
config.audioBytes = 0;
if (!jlInit(&config)) {
diff --git a/examples/agi/agi.c b/examples/agi/agi.c
index ebd1300..51ec225 100644
--- a/examples/agi/agi.c
+++ b/examples/agi/agi.c
@@ -307,7 +307,6 @@ static uint32_t gAgiV11SubTickAccum; // counts AGI ticks toward next
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIMAIN";
#endif
// ----- Internal helpers (alphabetical) -----
@@ -1687,7 +1686,6 @@ static void updateEgoFromInput(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "";
#endif
// ----- Entry point -----
@@ -1703,7 +1701,6 @@ int main(int argc, char **argv) {
bool vmReady;
cfg.codegenBytes = 0u; // no sprites needed -- backdrop-restore path is direct memcpy
- cfg.maxSurfaces = 4u; // stage + backdrop (2 used)
// AGI sound is software-synthesized PSG squarewaves pushed
// through the Sound Blaster auto-init DMA path (see agiSynthFill
// and src/port/dos/audio.c). The audio pool covers the 4 KB
diff --git a/examples/agi/agiObj.c b/examples/agi/agiObj.c
index 2401df9..747ac3b 100644
--- a/examples/agi/agiObj.c
+++ b/examples/agi/agiObj.c
@@ -24,7 +24,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIOBJ";
#endif
diff --git a/examples/agi/agiPic.c b/examples/agi/agiPic.c
index 518ca3c..9da1796 100644
--- a/examples/agi/agiPic.c
+++ b/examples/agi/agiPic.c
@@ -81,7 +81,6 @@ static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y)
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIPIC";
#endif
// ----- Internal helpers (alphabetical) -----
@@ -122,7 +121,7 @@ static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data
// the minor axis is linearly interpolated with round-half-away-from-
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
-// 65816 build avoids ORCA-C's long-arithmetic helpers entirely.
+// 65816 build avoids long-arithmetic helpers entirely.
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
int16_t dx;
int16_t dy;
diff --git a/examples/agi/agiRes.c b/examples/agi/agiRes.c
index 78ac34b..ab9fe32 100644
--- a/examples/agi/agiRes.c
+++ b/examples/agi/agiRes.c
@@ -48,9 +48,8 @@
// IIgs 64 KB code-bank rule: park this TU in its own load segment
// so it doesn't pile onto _ROOT (which still holds main() and the
-// ORCA-C startup). Cross-platform builds ignore this pragma.
+// runtime startup). Cross-platform builds ignore this pragma.
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIRES";
#endif
@@ -154,7 +153,7 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
}
volNibble = (uint8_t)(buf[0] >> 4);
- // 20-bit offset, cast to uint32_t before shift so ORCA-C's
+ // 20-bit offset, cast to uint32_t before shift so a
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];
diff --git a/examples/agi/agiText.c b/examples/agi/agiText.c
index 8621cc5..80bdc97 100644
--- a/examples/agi/agiText.c
+++ b/examples/agi/agiText.c
@@ -23,7 +23,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGITXT";
#endif
diff --git a/examples/agi/agiView.c b/examples/agi/agiView.c
index 31cee78..f81513f 100644
--- a/examples/agi/agiView.c
+++ b/examples/agi/agiView.c
@@ -40,7 +40,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIVIEW";
#endif
@@ -143,7 +142,6 @@ uint8_t agiActorPriorityForY(int16_t y) {
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIDRAW";
#endif
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,
diff --git a/examples/agi/agiVm.c b/examples/agi/agiVm.c
index 5a4fe1e..c51cf19 100644
--- a/examples/agi/agiVm.c
+++ b/examples/agi/agiVm.c
@@ -273,7 +273,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS
-segment "AGIVM";
#endif
diff --git a/examples/audio/audio.c b/examples/audio/audio.c
index 246527a..b2692a8 100644
--- a/examples/audio/audio.c
+++ b/examples/audio/audio.c
@@ -118,7 +118,6 @@ int main(void) {
uint32_t sfxLen;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/draw/draw.c b/examples/draw/draw.c
index c636e92..5174bda 100644
--- a/examples/draw/draw.c
+++ b/examples/draw/draw.c
@@ -243,7 +243,6 @@ int main(void) {
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/hello/hello.c b/examples/hello/hello.c
index 57348ff..a47f82c 100644
--- a/examples/hello/hello.c
+++ b/examples/hello/hello.c
@@ -10,7 +10,6 @@ int main(void) {
jlConfigT config;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/joy/joy.c b/examples/joy/joy.c
index bfdfd72..975cfe8 100644
--- a/examples/joy/joy.c
+++ b/examples/joy/joy.c
@@ -218,7 +218,6 @@ int main(void) {
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/keys/keys.c b/examples/keys/keys.c
index 9dd57a6..0846b15 100644
--- a/examples/keys/keys.c
+++ b/examples/keys/keys.c
@@ -218,7 +218,6 @@ int main(void) {
int16_t cursorRow;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/pattern/pattern.c b/examples/pattern/pattern.c
index 28c88e5..a64de86 100644
--- a/examples/pattern/pattern.c
+++ b/examples/pattern/pattern.c
@@ -106,7 +106,6 @@ int main(void) {
jlSurfaceT *screen;
config.codegenBytes = 8 * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/spacetaxi/spacetaxi.c b/examples/spacetaxi/spacetaxi.c
index 732b534..f4ce554 100644
--- a/examples/spacetaxi/spacetaxi.c
+++ b/examples/spacetaxi/spacetaxi.c
@@ -111,7 +111,6 @@ int main(void) {
// halSpriteSavePlanes / halSpriteRestorePlanes as static lib code.
// 32 KB is plenty for whatever other codegen needs are around.
config.codegenBytes = 32UL * 1024;
- config.maxSurfaces = 4; // stage + work
config.audioBytes = 32UL * 1024; // music + SFX
if (!jlInit(&config)) {
diff --git a/examples/spacetaxi/stAudio.c b/examples/spacetaxi/stAudio.c
index c4e718a..00cdfe4 100644
--- a/examples/spacetaxi/stAudio.c
+++ b/examples/spacetaxi/stAudio.c
@@ -24,7 +24,6 @@
#include "spacetaxi.h"
-JOEYLIB_SEGMENT("STAXI")
#define ST_SFX_VOICE 2u
#define ST_THRUST_VOICE 1u
diff --git a/examples/spacetaxi/stEngine.c b/examples/spacetaxi/stEngine.c
index 33a1ea4..56273a6 100644
--- a/examples/spacetaxi/stEngine.c
+++ b/examples/spacetaxi/stEngine.c
@@ -27,7 +27,6 @@
#include "spacetaxi.h"
-JOEYLIB_SEGMENT("STAXI")
// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps
// the cab at ~2 px/frame which is in line with the C64 max effective
diff --git a/examples/spacetaxi/stHud.c b/examples/spacetaxi/stHud.c
index b235d01..b47259c 100644
--- a/examples/spacetaxi/stHud.c
+++ b/examples/spacetaxi/stHud.c
@@ -11,7 +11,6 @@
#include "spacetaxi.h"
-JOEYLIB_SEGMENT("STAXI")
void stHudDraw(jlSurfaceT *stage, const StGameT *game) {
diff --git a/examples/spacetaxi/stLevel.c b/examples/spacetaxi/stLevel.c
index be604a6..c496ad6 100644
--- a/examples/spacetaxi/stLevel.c
+++ b/examples/spacetaxi/stLevel.c
@@ -13,7 +13,6 @@
#include "spacetaxi.h"
-JOEYLIB_SEGMENT("STAXI")
// STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav)
// and the full VIC color block ($7D00-$7D08). Drops the patience byte
diff --git a/examples/spacetaxi/stPassenger.c b/examples/spacetaxi/stPassenger.c
index e3df99d..e6f167d 100644
--- a/examples/spacetaxi/stPassenger.c
+++ b/examples/spacetaxi/stPassenger.c
@@ -20,7 +20,6 @@
#include "spacetaxi.h"
-JOEYLIB_SEGMENT("STAXI")
#define ST_PASSENGER_W_PX 16
#define ST_PASSENGER_H_PX 16
diff --git a/examples/spacetaxi/stRender.c b/examples/spacetaxi/stRender.c
index d89b3e9..8bad0c0 100644
--- a/examples/spacetaxi/stRender.c
+++ b/examples/spacetaxi/stRender.c
@@ -22,7 +22,6 @@
// All STAXI example sources share the STAXI load segment so the
// IIgs binary's _ROOT bank stays under 64 KB. No-op on other ports.
-JOEYLIB_SEGMENT("STAXI")
// Tile bank files are per-level: each level's .dat references a
// numeric bank id, and the host loads tiles/tbankN.tbk on demand
diff --git a/examples/sprite/sprite.c b/examples/sprite/sprite.c
index 1395968..a1dada9 100644
--- a/examples/sprite/sprite.c
+++ b/examples/sprite/sprite.c
@@ -107,7 +107,6 @@ int main(void) {
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball
* with all variants. */
config.codegenBytes = 32UL * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
diff --git a/examples/uber/uber.c b/examples/uber/uber.c
index 04660d6..d78ac75 100644
--- a/examples/uber/uber.c
+++ b/examples/uber/uber.c
@@ -217,6 +217,195 @@ static void buildBallSprite(void) {
}
+// ----- Visual showcase -----
+//
+// Before the (visually meaningless) timed benchmark, draw one example
+// of each primitive into its own grid cell so a viewer can actually SEE
+// what the library renders instead of a single solid color. The
+// benchmark that follows never presents, so this showcase stays on
+// screen for the whole timed run; results go to joeylog.txt. A legend
+// mapping cell index -> primitive is also logged.
+
+#define SC_SCREEN_W 320
+#define SC_SCREEN_H 200
+#define SC_PAL_COUNT 16
+#define SC_PAL_SWATCH_W (SC_SCREEN_W / SC_PAL_COUNT)
+#define SC_PAL_STRIP_H 12
+#define SC_GRID_TOP 16
+#define SC_COLS 4
+#define SC_ROWS 3
+#define SC_CELLS (SC_COLS * SC_ROWS)
+#define SC_GUTTER 4
+#define SC_INSET 5
+#define SC_BG_COLOR 10
+#define SC_BORDER_COLOR 1
+#define SC_HOLD_FRAMES 210
+
+
+static const uint16_t gShowcasePal[SC_PAL_COUNT] = {
+ 0x000, 0xFFF, 0xF00, 0x0F0, 0x00F, 0xFF0, 0x0FF, 0xF0F,
+ 0xF80, 0xAAA, 0x555, 0x8AF, 0x8F8, 0xF8C, 0x840, 0xACE
+};
+
+
+static void showcaseCellRect(uint16_t index, int16_t *outX, int16_t *outY, int16_t *outW, int16_t *outH) {
+ uint16_t col;
+ uint16_t row;
+ int16_t cellW;
+ int16_t cellH;
+
+ col = (uint16_t)(index % SC_COLS);
+ row = (uint16_t)(index / SC_COLS);
+ cellW = (int16_t)((SC_SCREEN_W - (SC_COLS + 1) * SC_GUTTER) / SC_COLS);
+ cellH = (int16_t)((SC_SCREEN_H - SC_GRID_TOP - (SC_ROWS + 1) * SC_GUTTER) / SC_ROWS);
+ *outX = (int16_t)(SC_GUTTER + (int16_t)col * (cellW + SC_GUTTER));
+ *outY = (int16_t)(SC_GRID_TOP + SC_GUTTER + (int16_t)row * (cellH + SC_GUTTER));
+ *outW = cellW;
+ *outH = cellH;
+}
+
+
+static void drawShowcase(void) {
+ uint16_t cell;
+ int16_t x;
+ int16_t y;
+ int16_t w;
+ int16_t h;
+ int16_t ix;
+ int16_t iy;
+ int16_t iw;
+ int16_t ih;
+ int16_t cx;
+ int16_t cy;
+ int16_t r;
+ int16_t k;
+ int16_t px;
+ int16_t py;
+ int16_t minDim;
+ uint8_t bx;
+ uint8_t by;
+ uint8_t bxStart;
+ uint8_t bxEnd;
+ uint8_t byStart;
+ uint8_t byEnd;
+ uint16_t held;
+
+ jlPaletteSet(gStage, 0, gShowcasePal);
+ jlScbSetRange(gStage, 0, 199, 0);
+ jlSurfaceClear(gStage, SC_BG_COLOR);
+
+ // Top strip: all 16 palette entries as swatches.
+ for (k = 0; k < SC_PAL_COUNT; k++) {
+ jlFillRect(gStage, (int16_t)(k * SC_PAL_SWATCH_W), 0, SC_PAL_SWATCH_W, SC_PAL_STRIP_H, (uint8_t)k);
+ }
+
+ for (cell = 0; cell < SC_CELLS; cell++) {
+ showcaseCellRect(cell, &x, &y, &w, &h);
+ jlDrawRect(gStage, x, y, (uint16_t)w, (uint16_t)h, SC_BORDER_COLOR);
+ ix = (int16_t)(x + SC_INSET);
+ iy = (int16_t)(y + SC_INSET);
+ iw = (int16_t)(w - 2 * SC_INSET);
+ ih = (int16_t)(h - 2 * SC_INSET);
+ cx = (int16_t)(ix + iw / 2);
+ cy = (int16_t)(iy + ih / 2);
+ minDim = (iw < ih) ? iw : ih;
+
+ switch (cell) {
+ case 0:
+ // Pixels: a scatter of single plotted pixels.
+ for (py = iy; py < iy + ih; py += 3) {
+ for (px = ix; px < ix + iw; px += 3) {
+ jlDrawPixel(gStage, px, py, (uint8_t)(2 + ((px + py) % 14)));
+ }
+ }
+ break;
+ case 1:
+ // Horizontal lines.
+ for (k = 0; k < ih; k += 4) {
+ jlDrawLine(gStage, ix, (int16_t)(iy + k), (int16_t)(ix + iw - 1), (int16_t)(iy + k), (uint8_t)(2 + (k / 4) % 14));
+ }
+ break;
+ case 2:
+ // Vertical lines.
+ for (k = 0; k < iw; k += 4) {
+ jlDrawLine(gStage, (int16_t)(ix + k), iy, (int16_t)(ix + k), (int16_t)(iy + ih - 1), (uint8_t)(2 + (k / 4) % 14));
+ }
+ break;
+ case 3:
+ // Diagonals: an X plus a fan from the center.
+ jlDrawLine(gStage, ix, iy, (int16_t)(ix + iw - 1), (int16_t)(iy + ih - 1), 5);
+ jlDrawLine(gStage, ix, (int16_t)(iy + ih - 1), (int16_t)(ix + iw - 1), iy, 6);
+ for (k = 0; k < iw; k += 8) {
+ jlDrawLine(gStage, cx, cy, (int16_t)(ix + k), iy, (uint8_t)(8 + (k / 8) % 8));
+ }
+ break;
+ case 4:
+ // Rectangle outlines, concentric.
+ for (k = 0; 2 * k < minDim - 4; k += 5) {
+ jlDrawRect(gStage, (int16_t)(ix + k), (int16_t)(iy + k), (uint16_t)(iw - 2 * k), (uint16_t)(ih - 2 * k), (uint8_t)(2 + (k / 5) % 14));
+ }
+ break;
+ case 5:
+ // Filled rectangles, overlapping.
+ jlFillRect(gStage, ix, iy, (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 2);
+ jlFillRect(gStage, (int16_t)(ix + iw / 3), (int16_t)(iy + ih / 3), (uint16_t)(iw * 2 / 3), (uint16_t)(ih * 2 / 3), 4);
+ break;
+ case 6:
+ // Circle outlines, concentric.
+ for (r = (int16_t)(minDim / 2); r > 2; r -= 4) {
+ jlDrawCircle(gStage, cx, cy, (uint16_t)r, (uint8_t)(2 + (r / 4) % 14));
+ }
+ break;
+ case 7:
+ // Filled circles.
+ jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 1), 8);
+ jlFillCircle(gStage, cx, cy, (uint16_t)(minDim / 4), 5);
+ break;
+ case 8:
+ // Tiles: an 8x8-block checkerboard inside the cell.
+ bxStart = (uint8_t)((ix + 7) / 8);
+ bxEnd = (uint8_t)((ix + iw) / 8);
+ byStart = (uint8_t)((iy + 7) / 8);
+ byEnd = (uint8_t)((iy + ih) / 8);
+ for (by = byStart; by < byEnd; by++) {
+ for (bx = bxStart; bx < bxEnd; bx++) {
+ jlTileFill(gStage, bx, by, (uint8_t)(((bx + by) & 1) ? 6 : 8));
+ }
+ }
+ break;
+ case 9:
+ // Sprite: the compiled ball at a few positions.
+ jlSpriteDraw(gStage, gSprite, ix, iy);
+ jlSpriteDraw(gStage, gSprite, (int16_t)(ix + iw - 16), (int16_t)(iy + ih - 16));
+ jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(cy - 8));
+ break;
+ case 10:
+ // Flood fill: outline a circle, then flood its interior.
+ jlDrawCircle(gStage, cx, cy, (uint16_t)(minDim / 2 - 2), 1);
+ jlFloodFill(gStage, cx, cy, 12);
+ break;
+ default:
+ // Mini scene: ground, sun, horizon, ball.
+ jlFillRect(gStage, ix, (int16_t)(iy + ih / 2), (uint16_t)iw, (uint16_t)(ih - ih / 2), 4);
+ jlFillCircle(gStage, cx, (int16_t)(iy + ih / 3), (uint16_t)(ih / 4), 5);
+ jlDrawLine(gStage, ix, (int16_t)(iy + ih / 2), (int16_t)(ix + iw - 1), (int16_t)(iy + ih / 2), 1);
+ jlSpriteDraw(gStage, gSprite, (int16_t)(cx - 8), (int16_t)(iy + ih / 2 - 16));
+ break;
+ }
+ }
+
+ jlLogF("UBER: showcase cells: 0=pixels 1=lineH 2=lineV 3=diag 4=rect 5=fillRect 6=circle 7=fillCircle 8=tiles 9=sprite 10=flood 11=scene\n");
+ jlStagePresent();
+
+ // Hold the showcase on screen, then auto-advance to the benchmark so
+ // the headless perf-capture run still completes without a keypress.
+ held = jlFrameCount();
+ while ((uint16_t)(jlFrameCount() - held) < SC_HOLD_FRAMES) {
+ /* hold */
+ }
+}
+
+
// ----- Main -----
static void runAllTests(void) {
@@ -296,9 +485,8 @@ int main(void) {
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
* compiled sprite emitter generates. UL on the multiply because
- * ORCA-C's 16-bit int overflows on 32 * 1024. */
+ * a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024;
- config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
@@ -373,10 +561,11 @@ int main(void) {
jlLogF("UBER: audioInit failed (skipping audio)\n");
}
- // Reset stage + run all per-frame timed tests.
- jlSurfaceClear(gStage, 0);
- jlFillRect(gStage, 0, 0, 320, 8, 1);
- jlStagePresent();
+ // Visual showcase: render one of each primitive into its own grid
+ // cell so the run shows something legible (the timed benchmark below
+ // never presents, so this stays on screen throughout it). The first
+ // timed op is jlSurfaceClear, so this leaves the benchmark untouched.
+ drawShowcase();
runAllTests();
@@ -391,6 +580,10 @@ int main(void) {
jlStagePresent();
jlLogF("UBER: press any key to exit\n");
+ // Flush the log to disk BEFORE the blocking key wait, so an automated
+ // (headless) run that kills the process at this prompt still captures
+ // the results -- joeyLog otherwise only flushes at the atexit fclose.
+ jlLogFlush();
jlWaitForAnyKey();
jlSpriteDestroy(gSprite);
diff --git a/include/joey/audio.h b/include/joey/audio.h
index 3d11aa6..c95445b 100644
--- a/include/joey/audio.h
+++ b/include/joey/audio.h
@@ -24,6 +24,11 @@
#define JOEY_AUDIO_SFX_SLOTS 4
+// Number of PSG-style polyphonic tone voices (jlAudioVoice selects
+// voice 0/1/2). The core gate range-checks against this so each
+// per-port HAL can assume voice < JOEY_AUDIO_VOICES.
+#define JOEY_AUDIO_VOICES 3
+
// Initialize audio. Returns true if the platform has a working audio
// engine and was able to start it. Returns false silently otherwise;
// the rest of the API stays callable but produces no sound.
diff --git a/include/joey/core.h b/include/joey/core.h
index 7ecb938..3c53de0 100644
--- a/include/joey/core.h
+++ b/include/joey/core.h
@@ -8,8 +8,7 @@
typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size
- uint16_t maxSurfaces; // maximum concurrent surfaces
- uint32_t audioBytes; // audio sample and module RAM pool
+ uint32_t audioBytes; // reserved; not yet consulted by any audio engine
} jlConfigT;
// Initialize the library. Returns true on success.
@@ -35,11 +34,10 @@ const char *jlVersionString(void);
// always a hardware-level wait, not a software timer.
void jlWaitVBL(void);
-// Monotonic 16-bit frame counter. Polled by callers; ports detect
-// the rising edge inside this call (IIgs $C019, DOS $3DA, Amiga
-// VPOSR) or expose a counter maintained by a VBL ISR (Atari ST).
-// Caller must poll faster than 2 * jlFrameHz() so no edge is
-// missed. Used by benchmarks and frame-rate-independent animation.
+// Monotonic 16-bit frame counter. Caller must poll faster than
+// 2 * jlFrameHz() so no edge is missed. Used by benchmarks and
+// frame-rate-independent animation. (Per-port edge-detect / VBL-ISR
+// mechanism is documented at halFrameCount in src/core/hal.h.)
uint16_t jlFrameCount(void);
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST
@@ -50,17 +48,14 @@ uint16_t jlFrameHz(void);
// Monotonic wall-clock millisecond counter since jlInit. Decoupled
// from frame rate -- two consecutive calls separated by a long
-// render will report the true elapsed time, not the number of VBLs
-// that fired. Per-port implementation: DOS uses uclock() (PIT
-// counter 0, ~1 us resolution); ST reads the 200 Hz Timer C
-// counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL); IIgs
-// falls back to frame_count * 1000 / 60 since ORCA-C time() bursts
-// the stdio cluster bank.
+// render report the true elapsed time, not the number of VBLs that
+// fired. (Per-port millisecond timer sources are documented at
+// halMillisElapsed in src/core/hal.h.)
uint32_t jlMillisElapsed(void);
// Deterministic, seedable pseudo-random generator. Bit-identical
// output across every JoeyLib port for a given seed, since stdlib
-// rand() varies wildly between DJGPP, ORCA-C, libnix, and mintlib.
+// rand() varies wildly between platforms and C libraries.
// The underlying algorithm is xorshift32 (Marsaglia): period 2^32-1,
// no multiply, suitable for games but not for cryptography.
uint32_t jlRandom(void);
diff --git a/include/joey/platform.h b/include/joey/platform.h
index ac8f18c..6278dab 100644
--- a/include/joey/platform.h
+++ b/include/joey/platform.h
@@ -20,7 +20,7 @@
#define JOEYLIB_PLATFORM_AMIGA
#elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__)
#define JOEYLIB_PLATFORM_ATARIST
- #elif defined(__ORCAC__) || defined(__APPLE2GS__) || defined(__GNO__)
+ #elif defined(__APPLE2GS__) || defined(__GNO__)
#define JOEYLIB_PLATFORM_IIGS
#else
#error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_ explicitly via -D"
@@ -63,20 +63,6 @@
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
#endif
-// ----- ORCA-C named load segments -----
-//
-// On the IIgs the ORCA Linker fits each load segment in its own bank,
-// so spilling cross-platform .c files into named segments is the way
-// to keep monolithic IIgs binaries under the 64 KB-per-bank _ROOT
-// limit. The `segment "name";` statement is ORCA-C-specific syntax
-// (see ORCA/C ch. 30); other ports' compilers don't recognize it, so
-// the macro evaluates to nothing on Amiga/ST/DOS.
-#ifdef JOEYLIB_PLATFORM_IIGS
- #define JOEYLIB_SEGMENT(name) segment name;
-#else
- #define JOEYLIB_SEGMENT(name)
-#endif
-
// ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1
diff --git a/include/joey/present.h b/include/joey/present.h
index 5f500f9..9283039 100644
--- a/include/joey/present.h
+++ b/include/joey/present.h
@@ -15,10 +15,11 @@
#include "types.h"
// Flip the dirty regions of the stage to the display, then clear the
-// dirty state. Cheap when nothing has changed since the last call
-// (gStageAnyDirty short-circuit). Drawing primitives mark dirty as
-// a side effect, so callers only need to call jlStagePresent at the
-// end of a frame -- everything they drew shows up.
+// dirty state. Drawing primitives mark dirty as a side effect, so
+// callers only need to call jlStagePresent at the end of a frame --
+// everything they drew shows up. Rows that nothing touched since the
+// last present are skipped by the per-port present using the per-row
+// dirty bands.
//
// To present a region you didn't draw with the standard primitives
// (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on
diff --git a/include/joey/sprite.h b/include/joey/sprite.h
index 4c52b16..71fc14d 100644
--- a/include/joey/sprite.h
+++ b/include/joey/sprite.h
@@ -112,9 +112,15 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least
// (widthTiles*4) * (heightTiles*8) bytes of capacity for fully
-// in-bounds draws; for clipped draws only the visible bytes are
-// stored. The captured region's exact size is reported in
-// backup->sizeBytes.
+// in-bounds draws on chunky ports (DOS, IIgs); for clipped draws only
+// the visible bytes are stored. The captured region's exact size is
+// reported in backup->sizeBytes.
+//
+// Planar ports (Amiga, Atari ST) round x DOWN to an 8-pixel plane
+// boundary and width UP, so a sprite drawn at an odd-of-8 x captures
+// one extra plane-byte column per row. Size backup->bytes for the
+// planar worst case ((widthTiles*4) + 1) * (heightTiles*8) bytes so an
+// odd-x save cannot overrun the buffer.
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// Repaint the destination region from a jlSpriteBackupT captured by a
@@ -126,7 +132,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
// captures the destination bytes about to be overwritten, then draws
// the sprite. Both ops share validation, the destination ptr is
// computed once, and a single dirty-rect mark covers both. Saves
-// roughly one full dispatcher chain (~150 cyc on IIgs ORCA-C) per
+// roughly one full dispatcher chain (~150 cyc on IIgs) per
// frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
//
// Identical semantics to:
diff --git a/include/joey/types.h b/include/joey/types.h
index b681f88..6f5e765 100644
--- a/include/joey/types.h
+++ b/include/joey/types.h
@@ -6,10 +6,7 @@
#include
#include
-typedef enum {
- VIDEO_REGION_NTSC,
- VIDEO_REGION_PAL,
- VIDEO_REGION_VGA
-} jlVideoRegionE;
+// This header is the shared include point for stdint / stdbool across
+// every public JoeyLib header (pulled in transitively via joey.h).
#endif
diff --git a/make/iigs.mk b/make/iigs.mk
index b4cde72..696e8b7 100644
--- a/make/iigs.mk
+++ b/make/iigs.mk
@@ -1,262 +1,134 @@
-# Apple IIgs build rules.
+# Apple IIgs build rules (llvm-mos w65816: clang + llvm-mc + link816).
#
-# Uses GoldenGate's iix to drive ORCA/C 2.1.0 + ORCA Linker. The
-# toolchains/iigs/iix-build.sh wrapper handles ORCA's case-sensitivity
-# quirks (lowercase .a/.root/.sym vs linker demands for uppercase),
-# multi-source compile/link, and pragma injection for include paths
-# and the stdint/stdbool shim headers.
+# toolchains/iigs/clang-build.sh drives compile (clang) / assemble
+# (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
+# Hand-rolled asm lives in src/port/iigs/*.s (llvm-mc GAS syntax);
+# bank placement is handled by -ffunction-sections + link816
+# dead-stripping.
+#
+# STATUS: the launchable-binary pipeline (multi-segment OMF emission +
+# disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
+# compile-checks every library + example translation unit, and
+# `iigs-clang-smoke` validates the full clang -> llvm-mc -> link816 ->
+# MAME path end to end. Per-example binary targets build via
+# clang-build.sh; large examples need the multi-segment OMF wiring still
+# in progress.
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
PLATFORM := iigs
BUILD := $(REPO_DIR)/build/$(PLATFORM)
BINDIR := $(BUILD)/bin
+OBJDIR := $(BUILD)/obj
-PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c)
-# Hand-rolled .asm sources go through ORCA's macro assembler via
-# iix-build.sh's `assemble` dispatch. Each .asm declares its target
-# load segment in the START operand (e.g. peislam.asm -> PEISLAMS)
-# so the linker places its bytes in a separate bank from _ROOT.
-# See ORCA/M for IIgs ch. 6 "Load Segments" for the mechanism.
-PORT_ASM_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.asm)
+CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang
+RT_INC := $(LLVM816_ROOT)/runtime/include
+CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections
+INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen
-# audio_full.c declares its functions in the AUDIOIMPL load segment
-# (`segment "AUDIOIMPL"` at file scope, see ORCA/C ch. 30) so the
-# implementation code lives in its own bank, not _ROOT. That lets
-# the same source link into every binary, replacing the earlier
-# audio.c-stub vs audio_full.c-real split. The 34 KB NTP replayer
-# bytes still ride along via the xxd-baked header.
-PORT_C_SRCS := $(PORT_C_SRCS_ALL)
+PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c)
+# Hand-rolled asm, migrated to llvm-mc GAS .s.
+PORT_ASM_SRCS := $(wildcard $(SRC_PORT)/iigs/*.s)
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
-# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter
-# it out so the ORCA Linker doesn't pull its dead code into every
-# IIgs binary (the monolithic-link budget is tight).
+# DOS and ST share, so src/core/audioSfxMix.c is unused here.
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: 65816 emitter + cross-platform compile dispatch.
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \
$(REPO_DIR)/src/codegen/spriteCompile.c
-# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged
-# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB
-# raw 65816 binary that the IIgs audio HAL loads at runtime via
-# Memory Manager into a fixed-bank handle. NTP source is bank-internal
-# (no JSL/JML cross-bank jumps) so the player works at any bank-start
-# load address even though it was assembled with `org $0F0000`.
-NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
-NTP_BIN := $(BUILD)/audio/ntpplayer.bin
-NTP_ASM := $(BUILD)/audio/ntpdata.asm
-IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
+LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
+LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS)
-LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(PORT_ASM_SRCS_ALL) $(NTP_ASM) $(CODEGEN_SRCS)
-
-# HELLO is omitted from the disk because UBER exercises everything it
-# does and the disk was tight. PATTERN is included as the SCB / palette
-# golden-reference for cross-port debugging.
+# Example sources (single-TU unless noted).
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
-PATTERN_BIN := $(BINDIR)/PATTERN
DRAW_SRC := $(EXAMPLES)/draw/draw.c
-DRAW_BIN := $(BINDIR)/DRAW
KEYS_SRC := $(EXAMPLES)/keys/keys.c
-KEYS_BIN := $(BINDIR)/KEYS
JOY_SRC := $(EXAMPLES)/joy/joy.c
-JOY_BIN := $(BINDIR)/JOY
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
-SPRITE_BIN := $(BINDIR)/SPRITE
UBER_SRC := $(EXAMPLES)/uber/uber.c
-UBER_BIN := $(BINDIR)/UBER
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
-ADV_BIN := $(BINDIR)/ADV
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
-ADV2_BIN := $(BINDIR)/ADV2
AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
-AGI_BIN := $(BINDIR)/AGI
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.c $(EXAMPLES)/spacetaxi/stAudio.c
-# IIgs convention follows the others: app dir under bin/ with a
-# DATA/ subdir for runtime assets. The disk packager will need to
-# carry both into the 2mg image (TODO).
-STAXI_INSTALL_DIR := $(BINDIR)/STAXI
-STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI
-AUDIO_SRC := $(EXAMPLES)/audio/audio.c
-AUDIO_BIN := $(BINDIR)/AUDIO
-AUDIO_MOD := $(REPO_DIR)/assets/test.mod
-AUDIO_SFX := $(REPO_DIR)/assets/test.sfx
-AUDIO_NTP := $(BUILD)/audio/test.ntp
-AUDIO_HEADER := $(BUILD)/audio/test_assets.h
-JOEYMOD := $(REPO_DIR)/tools/joeymod/joeymod
-DISK_IMG := $(BINDIR)/joey.2mg
-IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh
+.PHONY: all iigs iigs-lib iigs-clang-smoke clean-iigs
-IIX_INCLUDES := \
- -I $(IIGS_INCLUDE_SHIM) \
- -I $(INCLUDE_DIR) \
- -I $(INCLUDE_DIR)/joey \
- -I $(SRC_CORE) \
- -I $(REPO_DIR)/src/codegen
+# Default: compile-check the library + run the end-to-end smoke test.
+all iigs: iigs-lib iigs-clang-smoke
-.PHONY: all iigs iigs-disk clean-iigs
-# Building the disk implicitly builds every binary it depends on, so
-# `make iigs` ends with a fresh joey.2mg on every change. Without this,
-# stale disk images would silently mask binary updates -- a surprise
-# when the run script always boots from joey.2mg.
-all iigs: $(DISK_IMG)
+# Compile-check every library and example translation unit with clang,
+# and assemble every .s with llvm-mc. This is the canonical "does the
+# IIgs source still build" gate while the launchable pipeline is wired.
+iigs-lib:
+ @mkdir -p $(OBJDIR)
+ @set -e; \
+ for c in $(LIB_C_SRCS); do \
+ echo " CC $$c"; \
+ $(CLANG) $(CFLAGS) $(INCLUDES) -c $$c -o $(OBJDIR)/$$(echo $$c | sed 's#[/.]#_#g').o; \
+ done; \
+ for s in $(PORT_ASM_SRCS); do \
+ echo " AS $$s"; \
+ $(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc -arch=w65816 -filetype=obj $$s -o $(OBJDIR)/$$(basename $$s .s).o; \
+ done
-$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
- @mkdir -p $(dir $@)
- @cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
- cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
- mv $(BUILD)/audio/ntpplayer $@
+# End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating
+# the ported asm computes correctly on emulated hardware.
+iigs-clang-smoke:
+ $(REPO_DIR)/toolchains/iigs/clang-smoke.sh
-# Bake the NTP replayer bytes into an ORCA-M asm file. The asm declares
-# the bytes in a `data NTPDATA` segment; ORCA's linker groups same-
-# name object segments into one load segment, and the GS/OS loader
-# places it in its own bank. Net effect: the 34 KB of NTP bytes don't
-# crowd _ROOT in any binary, so audio_full.c can link into every demo
-# (vs the old audio.c-stub split). audio_full.c references
-# gNtpPlayerBytes / gNtpPlayerBytes_len as externs (case-sensitive
-# symbol match against the asm labels).
-$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh
- @mkdir -p $(dir $@)
- $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh $(NTP_BIN) $@ NTPDATA gNtpPlayerBytes gNtpPlayerBytes_len
-
-# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
-# #pragma noroot). The example source supplies main(); libjoey sources
-# are the extras. The chtyp post-step tags the output as GS/OS S16
-# ($B3) so GS/OS recognizes it as launchable; the file-type lives in
-# a user.com.apple.FinderInfo xattr that iix and profuse preserve.
-#
-# All binaries use ORCA-C large memory model (-b). Cost: slightly
-# larger / slower compiled C per the ORCA docs. Win: 32-bit pointers
-# everywhere, so library asm can take SurfaceT* args via one
-# consistent ABI (small-mm 16-bit pointers truncated bank bytes,
-# which broke any asm that wanted to address bank-1 stage memory).
-# Per-binary header dependency files. iix-build.sh -M emits one .d
-# alongside each binary covering every header transitively included
-# by the C sources in that binary's build. Pulled in via -include at
-# the bottom of this file so editing a shared header (e.g.
-# surfaceInternal.h) triggers a rebuild of every IIgs binary that
-# transitively depends on it.
+# Per-example binaries via clang-build.sh. clang-build.sh mirrors the
+# old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These
+# link single-segment today; examples larger than one bank need the
+# multi-segment OMF wiring still in progress.
DEP_DIR := $(BUILD)/dep
-PATTERN_DEP := $(DEP_DIR)/PATTERN.d
-DRAW_DEP := $(DEP_DIR)/DRAW.d
-KEYS_DEP := $(DEP_DIR)/KEYS.d
-JOY_DEP := $(DEP_DIR)/JOY.d
-SPRITE_DEP := $(DEP_DIR)/SPRITE.d
-UBER_DEP := $(DEP_DIR)/UBER.d
-AUDIO_DEP := $(DEP_DIR)/AUDIO.d
-ADV_DEP := $(DEP_DIR)/ADV.d
-ADV2_DEP := $(DEP_DIR)/ADV2.d
-AGI_DEP := $(DEP_DIR)/AGI.d
-STAXI_DEP := $(DEP_DIR)/STAXI.d
-$(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/PATTERN: $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(PATTERN_DEP) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/PATTERN.d $(INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
-$(DRAW_BIN): $(DRAW_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/DRAW: $(DRAW_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(DRAW_DEP) $(IIX_INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/DRAW.d $(INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
-$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/KEYS: $(KEYS_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(KEYS_DEP) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/KEYS.d $(INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
-$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/JOY: $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(JOY_DEP) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
-$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/SPRITE: $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(SPRITE_DEP) $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SPRITE.d $(INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
-# UBER bumps user stack to 16 KB. ORCA-C's default user stack is small
-# (~1 KB) and vfprintf's parsing buffer + the demo's own stack-local
-# format buffers were spilling past it -- the symptom was a crash to
-# monitor on the second varargs-style joeyLogF call. The hand-rolled
-# decimal formatter in uber.c also uses larger stack-local buffers
-# (line[96], num[16]) than typical demos. 16 KB is plenty of headroom.
-$(UBER_BIN): $(UBER_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/UBER: $(UBER_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -s 16384 -M $(UBER_DEP) $(IIX_INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/UBER.d $(INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
-# Adventure has a lot of static const sprite-row arrays and walk-mask
-# strings; the auto-segmenter sees a big main TU, so the 16 KB user
-# stack matches UBER's bump for the same reason (vfprintf + format
-# buffers stay within bounds).
-$(ADV_BIN): $(ADV_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/ADV: $(ADV_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -s 16384 -M $(ADV_DEP) $(IIX_INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV.d $(INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
-$(ADV2_BIN): $(ADV2_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/ADV2: $(ADV2_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -s 16384 -M $(ADV2_DEP) $(IIX_INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
-# AGI binary spans multiple .c files (agi.c is main; agiRes.c is the
-# resource loader). iix-build.sh takes the first .c as main and the
-# rest as extras (compiled #pragma noroot); ordering matters.
-$(AGI_BIN): $(AGI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/AGI: $(AGI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -s 16384 -M $(AGI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
-# Space Taxi: multi-source like AGI; 16 KB stack for headroom on the
-# placeholder render path (full tilemap repaint + snprintf buffers).
-# Note: asset files (level *.dat, *.jas) are NOT yet packaged onto
-# the disk image -- IIGS_PACKAGE only handles binaries today. The
-# game will fail to find its assets when run; wiring data files into
-# the packager is the next step for this port on IIgs (similar to
-# the open AGI/KQ3 disk-packaging task).
-$(STAXI_BIN): $(STAXI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
+$(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -s 16384 -M $(STAXI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
+ $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
-# Convert the cross-platform .MOD asset to NinjaTrackerPlus runtime
-# format via joeymod (which shells out to ntpconverter.php). Without
-# php-cli the conversion is skipped; in that case the IIgs disk just
-# won't carry a TEST.NTP and the audio demo will report "missing data
-# file" at runtime instead of crashing on bad MOD magic.
-HAVE_PHP := $(shell command -v php >/dev/null 2>&1 && echo 1)
-
-ifeq ($(HAVE_PHP),1)
-$(AUDIO_NTP): $(AUDIO_MOD) $(JOEYMOD)
- @mkdir -p $(dir $@)
- $(JOEYMOD) $< $@
-
-AUDIO_DATA_FILES := $(AUDIO_NTP) $(AUDIO_SFX)
-else
-$(info iigs: php-cli not installed -- AUDIO demo will ship without TEST.NTP; install php-cli for real IIgs audio playback)
-AUDIO_DATA_FILES := $(AUDIO_SFX)
-endif
-
-$(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
- @mkdir -p $(dir $@) $(DEP_DIR)
- $(IIGS_BUILD) -b -M $(AUDIO_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS)
- $(IIGS_IIX) chtyp -t S16 $@
-
-# Assemble a ProDOS 2img containing the examples, ready to mount in
-# GSplus alongside a GS/OS boot volume.
-iigs-disk: $(DISK_IMG)
-
-$(DISK_IMG): $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(AUDIO_DATA_FILES) $(IIGS_PACKAGE)
- @mkdir -p $(dir $@)
- $(IIGS_PACKAGE) $@ $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) -- $(AUDIO_DATA_FILES)
+# NOTE: the audio example is omitted until the NinjaTrackerPlus replayer
+# is wired into the clang build (a path for baking the replayer bytes
+# into a data segment via llvm-mc is pending).
clean-iigs:
rm -rf $(BUILD)
-# Pull in per-binary header-dependency files generated by iix-build.sh -M.
-# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild
-# IIgs binaries that include it -- the IIgs's iix toolchain has no native
-# -MMD analog, so iix-build.sh shells out to host gcc for the scan.
--include $(PATTERN_DEP) $(DRAW_DEP) $(KEYS_DEP) $(JOY_DEP) $(SPRITE_DEP) $(UBER_DEP) $(AUDIO_DEP) $(ADV_DEP) $(ADV2_DEP) $(AGI_DEP) $(STAXI_DEP)
+-include $(wildcard $(DEP_DIR)/*.d)
diff --git a/scripts/run-iigs-mame.sh b/scripts/run-iigs-mame.sh
index bf11dde..956160d 100755
--- a/scripts/run-iigs-mame.sh
+++ b/scripts/run-iigs-mame.sh
@@ -13,7 +13,6 @@
# Outputs of a run land in /tmp/mame-iigs/:
# - debug.log MAME's debugger console output (-debuglog)
# - crash.txt Lua-hook crash dump (if anything halts the CPU)
-# - joeylog.txt Extracted from the post-run disk image
set -euo pipefail
@@ -232,7 +231,6 @@ On crash the MAME debugger halts and Lua dumps state to:
$out/crash.txt
After exit:
- $out/joeylog.txt extracted from disk image
$out/debug.log MAME debugger console output
EOF
@@ -248,23 +246,6 @@ cleanup() {
rm -rf "$out/snap"
mv "$work/snap" "$out/snap"
fi
- local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
- local mnt=$work/_mnt
- if [[ -x $profuse && -f $work/joey.2mg ]]; then
- export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
- export ORCA_ROOT="$GOLDEN_GATE"
- mkdir -p "$mnt"
- if "$profuse" -oro "$work/joey.2mg" "$mnt" 2>/dev/null; then
- for name in JOEYLOG.TXT joeylog.txt; do
- if [[ -f $mnt/$name ]]; then
- cp "$mnt/$name" "$out/joeylog.txt"
- echo "extracted joeylog.txt -> $out/joeylog.txt" >&2
- break
- fi
- done
- fusermount -u "$mnt" 2>/dev/null || true
- fi
- fi
rm -rf "$work"
}
trap cleanup EXIT
diff --git a/scripts/run-iigs.sh b/scripts/run-iigs.sh
index 87d777f..7826617 100755
--- a/scripts/run-iigs.sh
+++ b/scripts/run-iigs.sh
@@ -7,9 +7,7 @@
#
# No argument: GSplus has no way to dispatch a specific binary on boot
# (Finder is interactive), so this script just stages the disk and
-# launches the emulator. The post-run trap below extracts joeylog.txt
-# from the data disk so demos that left a breadcrumb file are visible
-# from the host shell after the emulator exits.
+# launches the emulator.
set -euo pipefail
@@ -21,12 +19,6 @@ fi
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
-# profuse looks up its FST helpers under $GOLDEN_GATE / $ORCA_ROOT and
-# falls back to /usr/share/orca which we don't install. Point both at
-# our staged GoldenGate tree.
-export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
-export ORCA_ROOT="$GOLDEN_GATE"
-
gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
@@ -49,46 +41,15 @@ done
# copies so repeated runs do not mutate the originals.
work=$(mktemp -d -t joeylib-iigs.XXXXXX)
-# After GSplus exits, mount the work copy of joey.2mg via profuse and
-# pull joeylog.txt out (if the demo wrote one) before tearing down the
-# scratch dir. This is a debug aid: the demos use joeyLog* to leave a
-# breadcrumb file on the JOEYLIB volume, but the volume only lives
-# inside the scratch dir while GSplus is running.
-log_out=$repo/build/iigs/bin/joeylog.txt
-extract_log() {
- local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
- local mnt=$work/_log_mnt
+cleanup() {
# Stash the post-run disk image for manual inspection.
if [[ -f $work/joey.2mg ]]; then
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg"
echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
fi
- if [[ ! -x $profuse ]]; then
- echo "run-iigs: profuse not found at $profuse" >&2
- elif [[ ! -f $work/joey.2mg ]]; then
- echo "run-iigs: $work/joey.2mg missing" >&2
- else
- mkdir -p "$mnt"
- if "$profuse" -oro "$work/joey.2mg" "$mnt"; then
- echo "run-iigs: mounted $mnt; contents:" >&2
- ls -la "$mnt" >&2 || true
- if [[ -f $mnt/JOEYLOG.TXT ]]; then
- cp "$mnt/JOEYLOG.TXT" "$log_out"
- echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
- elif [[ -f $mnt/joeylog.txt ]]; then
- cp "$mnt/joeylog.txt" "$log_out"
- echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
- else
- echo "run-iigs: no JOEYLOG.TXT on disk" >&2
- fi
- fusermount -u "$mnt" 2>/dev/null || true
- else
- echo "run-iigs: profuse mount FAILED" >&2
- fi
- fi
rm -rf "$work"
}
-trap extract_log EXIT
+trap cleanup EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
diff --git a/src/codegen/spriteCompile.c b/src/codegen/spriteCompile.c
index bce79fd..dad4699 100644
--- a/src/codegen/spriteCompile.c
+++ b/src/codegen/spriteCompile.c
@@ -20,26 +20,35 @@
#include "surfaceInternal.h"
-// Largest scratch buffer needed for any single emit call. 16 KB
-// covers a 32x32 sprite even on 68k (the biggest mixed-RMW byte-
-// emit at 16 bytes/byte * (16*17 dest bytes per shift) ~= 4.5 KB,
-// times shift count 2). Round up generously.
-#define SPRITE_EMIT_SCRATCH_BYTES (16u * 1024u)
+// Scratch buffer for a single emit call. 16 KB comfortably covers any
+// sprite up to a 32x32 worst-case all-mixed DRAW (16 rows*17 dest
+// bytes/shift * SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE ~= 4.5 KB on 68k,
+// the largest per-destbyte target). It is NOT trusted to bound every
+// legal input: a sprite near the create-time dimension cap
+// (widthTiles <= 40, heightTiles <= 25) can emit far more than 16 KB,
+// so every emitter ALSO bound-checks its write cursor against the
+// `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would
+// not fit. jlSpriteCompile then bails cleanly (no arena alloc,
+// sp->slot stays NULL, the dispatcher falls back to the interpreted
+// halSpriteXxxPlanes path) instead of corrupting the heap. The
+// constant is a long literal so it never wraps the 16-bit int the
+// IIgs uses for plain int arithmetic.
+#define SPRITE_EMIT_SCRATCH_BYTES (16384ul)
// Compile-time selection of the per-CPU emitter. One src/codegen/
// spriteEmit*.c file is built per platform, but the dispatch lives
// in this file so jlSpriteCompile + spriteCompiledDraw aren't
// duplicated three times.
-static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
+static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
- return spriteEmitDrawX86(out, sp, shift);
+ return spriteEmitDrawX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
- return spriteEmitDrawPlanar68k(out, sp, shift);
+ return spriteEmitDrawPlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
- return spriteEmitDrawInterleaved68k(out, sp, shift);
+ return spriteEmitDrawInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
- return spriteEmitDrawIigs(out, sp, shift);
+ return spriteEmitDrawIigs(out, cap, sp, shift);
#else
# error "jlSpriteCompile: no emitter selected for this platform"
#endif
@@ -51,33 +60,33 @@ static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shi
// src/core/sprite.c gates on routineOffsets[shift][SPRITE_OP_SAVE]
// != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy
// path otherwise.
-static uint16_t emitSaveForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
+static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
- return spriteEmitSaveX86(out, sp, shift);
+ return spriteEmitSaveX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
- return spriteEmitSavePlanar68k(out, sp, shift);
+ return spriteEmitSavePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
- return spriteEmitSaveInterleaved68k(out, sp, shift);
+ return spriteEmitSaveInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
- return spriteEmitSaveIigs(out, sp, shift);
+ return spriteEmitSaveIigs(out, cap, sp, shift);
#else
- (void)out; (void)sp; (void)shift;
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0;
#endif
}
-static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
+static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
- return spriteEmitRestoreX86(out, sp, shift);
+ return spriteEmitRestoreX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
- return spriteEmitRestorePlanar68k(out, sp, shift);
+ return spriteEmitRestorePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
- return spriteEmitRestoreInterleaved68k(out, sp, shift);
+ return spriteEmitRestoreInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
- return spriteEmitRestoreIigs(out, sp, shift);
+ return spriteEmitRestoreIigs(out, cap, sp, shift);
#else
- (void)out; (void)sp; (void)shift;
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0;
#endif
}
@@ -90,13 +99,24 @@ static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t
// land in the arena.
static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
uint32_t total;
+ uint32_t written;
uint8_t shift;
+ uint8_t op;
total = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
- total += emitDrawForTarget(scratch, sp, shift);
- total += emitSaveForTarget(scratch, sp, shift);
- total += emitRestoreForTarget(scratch, sp, shift);
+ for (op = 0; op < SPRITE_OP_COUNT; op++) {
+ switch (op) {
+ case SPRITE_OP_DRAW: written = emitDrawForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
+ case SPRITE_OP_SAVE: written = emitSaveForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
+ case SPRITE_OP_RESTORE: written = emitRestoreForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
+ default: written = 0; break;
+ }
+ if (written == SPRITE_EMIT_OVERFLOW) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ total += written;
+ }
}
return total;
}
@@ -109,8 +129,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
uint8_t op;
ArenaSlotT *slot;
uint8_t *dst;
- uint16_t written;
- uint16_t offset;
+ uint32_t written;
+ uint32_t offset;
if (sp == NULL) {
return false;
@@ -135,6 +155,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
totalSize = emitTotalSize(scratch, sp);
+ /* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single
+ * routine that did not fit the scratch is rejected here as well. */
if (totalSize > 0xFFFFu) {
free(scratch);
return false;
@@ -159,17 +181,32 @@ bool jlSpriteCompile(jlSpriteT *sp) {
offset = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
+ uint32_t capLeft;
+
+ capLeft = (uint32_t)(totalSize - offset);
switch (op) {
- case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, sp, shift); break;
- case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, sp, shift); break;
- case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, sp, shift); break;
- default: written = 0; break;
+ case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, capLeft, sp, shift); break;
+ case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, capLeft, sp, shift); break;
+ case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, capLeft, sp, shift); break;
+ default: written = 0; break;
}
- if (written == 0) {
+ if (written == SPRITE_EMIT_OVERFLOW) {
+ /* The second pass disagreed with the sizing pass (must
+ * not happen, but never write past the slot). Roll back. */
+ codegenArenaFree(slot);
+ free(scratch);
+ return false;
+ }
+ /* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED)
+ * as the "absent" sentinel, so a real offset must stay below
+ * it. totalSize <= 0xFFFF already guarantees this, but guard
+ * explicitly so a future change can't store an offset that
+ * collides with the sentinel. */
+ if (written == 0 || offset >= SPRITE_NOT_COMPILED) {
sp->routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
} else {
- sp->routineOffsets[shift][op] = offset;
- offset = (uint16_t)(offset + written);
+ sp->routineOffsets[shift][op] = (uint16_t)offset;
+ offset = offset + written;
}
}
}
@@ -189,16 +226,37 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
+#if defined(JOEYLIB_PLATFORM_IIGS) || (!defined(JOEYLIB_PLATFORM_AMIGA) && !defined(JOEYLIB_PLATFORM_ATARIST))
+
+// Re-derive the shift a chunky SAVE used from its backup. The shift is
+// a property of the save (x & 1) and is NOT stored in the backup, so
+// every chunky restore path must re-infer it. Single source of truth
+// for the two chunky restore dispatchers (IIgs + x86/68k) so the
+// formula cannot drift between them: copyBytes == spriteBytesPerRow
+// means shift 0 (even x), otherwise the +1-byte shift-1 routine.
+#define SPRITE_CHUNKY_BYTES_PER_TILE_ROW 4u
+
+static uint8_t spriteChunkyRestoreShift(const jlSpriteT *sp, const jlSpriteBackupT *backup) {
+ uint16_t copyBytes;
+ uint16_t spriteBytesPerRow;
+
+ copyBytes = (uint16_t)(backup->width >> 1);
+ spriteBytesPerRow = (uint16_t)(sp->widthTiles * SPRITE_CHUNKY_BYTES_PER_TILE_ROW);
+ return (uint8_t)((copyBytes == spriteBytesPerRow) ? 0u : 1u);
+}
+
+#endif
+
+
#if defined(JOEYLIB_PLATFORM_IIGS)
// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs;
-// declared in surfaceInternal.h. Replaces ORCA-C's __mul16 JSL with a
-// single indexed long-mode read.
+// declared in surfaceInternal.h. Uses a single indexed long-mode read
+// for the multiply.
// IIgs uses inline asm + a self-modifying call stub instead of a C
-// function-pointer cast. The build uses ORCA-C large memory model
-// (-b for sprite demos) so pointers are 24-bit and JSL works
-// cross-bank.
+// function-pointer cast. The build uses a large memory model so
+// pointers are 24-bit and JSL works cross-bank.
//
// `sta abs,Y` on 65816 uses the data bank register (DBR) for the
// high byte of the effective address, so we need DBR = dst's bank
@@ -235,23 +293,26 @@ static uint32_t gDrawStubLastFnAddr = 0xFFFFFFFFul;
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
uint8_t shift;
- uint32_t destAddr;
uint16_t destOffset;
uint8_t destBank;
uint32_t fnAddr;
{
- uint8_t *destPtr;
+ uint8_t *destPtr;
+ uint32_t destAddr;
+ const uint8_t *destB;
shift = (uint8_t)(x & 1);
destPtr = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
+ /* Byte-alias the 24-bit pointer to grab lo16 + bank without the
+ * ~LSHR4 32-bit-shift helper, matching SPLIT_POINTER below. */
destAddr = (uint32_t)destPtr;
- destOffset = (uint16_t)(destAddr & 0xFFFFu);
- destBank = (uint8_t)((destAddr >> 16) & 0xFFu);
+ destB = (const uint8_t *)&destAddr;
+ destOffset = (uint16_t)(destB[0] | ((uint16_t)destB[1] << 8));
+ destBank = destB[2];
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
}
- (void)destAddr;
if (!gDrawStubInited) {
gSpriteCallStub[ 0] = 0x8B;
@@ -284,8 +345,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
gDrawStubLastFnAddr = fnAddr;
}
- // ORCA-C compiles this function under `longa on / longi on`
- // (M=16, X=16) and emits the function epilogue assuming those
+ // This function is compiled under `longa on / longi on`
+ // (M=16, X=16) and the function epilogue assumes those
// widths at exit -- the deallocation ADC takes a 2-byte immediate
// and any LDX/LDY use 2-byte immediates. The byte writes to
// gSpriteCallStub above leave M=8, and an earlier PHP/PLP-only
@@ -294,12 +355,13 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
// swallowed the TCS, S was never adjusted, RTL popped wrong
// bytes, control fell into BSS, and the IIgs hit BRK on a zero
// byte. Force M=16/X=16 before returning to compiled C.
- asm {
- rep #0x30
- sep #0x20
- jsl gSpriteCallStub
- rep #0x30
- }
+ __asm__ volatile (
+ "rep #0x30\n\t"
+ "sep #0x20\n\t"
+ "jsl gSpriteCallStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
}
@@ -360,12 +422,10 @@ static void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank,
-// Split a 24-bit pointer into its low 16 bits + bank byte. The
-// (uint32_t) cast works correctly in ORCA/C 2.2.1 (the 2.1.0 lossy-
-// bank-byte bug is fixed). To avoid invoking the ~LSHR4 32-bit-shift
-// helper for the `>> 16` to extract the bank byte, we cast to
-// uint32_t and then byte-alias the storage -- gets the same bytes
-// with simple loads.
+// Split a 24-bit pointer into its low 16 bits + bank byte. To avoid
+// invoking a 32-bit-shift helper for the `>> 16` to extract the bank
+// byte, we cast to uint32_t and then byte-alias the storage -- gets
+// the same bytes with simple loads.
#define SPLIT_POINTER(_ptr, _outLo, _outBank) \
do { \
uint32_t spAddr_ = (uint32_t)(_ptr); \
@@ -418,7 +478,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
- copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
+ copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
screenPtr = (uint8_t *)&src->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)clippedX >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
@@ -442,9 +502,9 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
}
/* Compute the 1D index into the cached* / routineOffsets 2D arrays
- * once. ORCA-C 2.2.1 lowers `shift * SPRITE_OP_COUNT` (where
- * SPRITE_OP_COUNT==3) to a ~MUL4 helper call; (shift<<1)+shift
- * compiles to two ASLs and an ADC, no helper. */
+ * once. `shift * SPRITE_OP_COUNT` (where SPRITE_OP_COUNT==3) can
+ * lower to a multiply helper call; (shift<<1)+shift compiles to
+ * two ASLs and an ADC, no helper. */
cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
@@ -495,20 +555,19 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
}
// MVN-based routine: needs M=16 / X=16; restore M=16 on exit
- // matches ORCA-C `longa on` epilogue expectations.
- asm {
- rep #0x30
- jsl gSpriteSaveStub
- rep #0x30
- }
+ // matches the `longa on` epilogue expectations.
+ __asm__ volatile (
+ "rep #0x30\n\t"
+ "jsl gSpriteSaveStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
}
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
uint8_t shift;
uint16_t heightPx;
- uint16_t copyBytes;
- uint16_t spriteBytesPerRow;
uint16_t screenLo;
uint16_t backupLo;
uint8_t screenBank;
@@ -524,9 +583,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
sp = backup->sprite;
heightPx = backup->height;
- copyBytes = (uint16_t)(backup->width >> 1);
- spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
- shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
+ shift = spriteChunkyRestoreShift(sp, backup);
screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
@@ -580,11 +637,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
*cachedSrc = backupBank;
}
- asm {
- rep #0x30
- jsl gSpriteRestoreStub
- rep #0x30
- }
+ __asm__ volatile (
+ "rep #0x30\n\t"
+ "jsl gSpriteRestoreStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
}
#elif defined(JOEYLIB_PLATFORM_AMIGA)
@@ -602,7 +660,10 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* interpreter, which handles arbitrary x via halSpriteDrawPlanes /
* halSpriteSavePlanes / halSpriteRestorePlanes. */
-#define AMIGA_BYTES_PER_ROW_LOCAL 40
+// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40.
+// Derived from surface geometry so it cannot drift if the fixed
+// surface width ever changes.
+#define AMIGA_PLANE_BYTES_PER_ROW (SURFACE_WIDTH / 8)
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
@@ -615,7 +676,7 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
DrawFn fn;
shift = (uint8_t)(x & 7);
- byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)x >> 3));
+ byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1);
p2 = halSurfacePlanePtr(dst, 2);
@@ -646,7 +707,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
if (shift != 0u) {
widthPx = (uint16_t)(widthPx + 8u);
}
- byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)clippedX >> 3));
+ byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)clippedX >> 3));
backup->sprite = sp;
backup->x = clippedX;
@@ -683,7 +744,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* Shifted slots 1..7 all alias to the same restore bytes, so
* slot 1 stands in for any non-zero shift. */
shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u);
- byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)backup->x >> 3));
+ byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1);
@@ -708,6 +769,14 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
* falls back to halSpriteDrawPlanes. */
+
+// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
+// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
+// fact (group-interleaved layout, not chunky), so it gets its own
+// name. Each 16-pixel group is 4 plane words = 8 bytes.
+#define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8)
+#define ST_BYTES_PER_PLANE_GROUP 8u
+
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
typedef void (*DrawFn)(uint8_t *groupBase);
uint8_t shift;
@@ -734,8 +803,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
return;
}
groupBase = base
- + (uint16_t)y * 160u
- + (uint16_t)((uint16_t)x >> 4) * 8u;
+ + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(groupBase);
}
@@ -788,7 +857,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
- copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
+ copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
screenPtr = (uint8_t *)&src->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)clippedX >> 1)];
@@ -812,15 +881,11 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
jlSpriteT *sp;
uint8_t shift;
- uint16_t copyBytes;
- uint16_t spriteBytesPerRow;
uint8_t *screenPtr;
CopyFn fn;
sp = backup->sprite;
- copyBytes = (uint16_t)(backup->width >> 1);
- spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
- shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
+ shift = spriteChunkyRestoreShift(sp, backup);
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];
diff --git a/src/codegen/spriteEmit68k.c b/src/codegen/spriteEmit68k.c
index 59cc33c..23d8867 100644
--- a/src/codegen/spriteEmit68k.c
+++ b/src/codegen/spriteEmit68k.c
@@ -35,12 +35,25 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
-#define MAX_ROUTINE_BYTES 16384
+// Stack-arg displacements for the m68k cdecl calling convention used by
+// the emitted routines (args pushed right-to-left, return PC at 0(sp)):
+// draw(dst) -> dst at 4(sp)
+// save/restore(src, dst) -> src at 4(sp), dst at 8(sp)
+#define STACK_ARG0_DISP 0x0004u
+#define STACK_ARG1_DISP 0x0008u
+
+// Worst-case emitted bytes for one draw dest byte (mixed RMW: move.b
+// d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0) = 4 instrs * 4 bytes).
+#define DRAW_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
+// Bytes for the per-row adda.w stride advance.
+#define ROW_STRIDE_ADVANCE_BYTES 4u
+// Bytes for the rts epilogue.
+#define EPILOGUE_BYTES 2u
// ----- Prototypes -----
-static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
+static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
static uint16_t writeBE16(uint8_t *out, uint16_t value);
@@ -58,14 +71,20 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value);
//
// strideOnSrc=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE)
-static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
+static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
uint16_t row;
uint16_t col;
uint16_t advance;
+ uint32_t rowBytes;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) {
+ // One row: copyBytes * 2 (move.b) + the per-row adda stride.
+ rowBytes = (uint32_t)copyBytes * 2u + ROW_STRIDE_ADVANCE_BYTES;
+ if (cursor + rowBytes > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
// Unrolled: move.b (a0)+, (a1)+ -- 0x12D8.
for (col = 0; col < copyBytes; col++) {
cursor += writeBE16(out + cursor, 0x12D8u);
@@ -155,9 +174,10 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value) {
}
-// 68k draw emit. Returns bytes written.
-uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+// 68k draw emit. Returns bytes written, or SPRITE_EMIT_OVERFLOW if the
+// routine would exceed `cap`.
+uint32_t spriteEmitDraw68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
@@ -176,15 +196,21 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
- destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
+ destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
// Prologue: movea.l 4(sp), a0
+ if (cursor + 4u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x206Fu);
- cursor += writeBE16(out + cursor, 0x0004u);
+ cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
for (row = 0; row < heightPx; row++) {
if (row > 0) {
// adda.w #SURFACE_BYTES_PER_ROW, a0
+ if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0xD0FCu);
cursor += writeBE16(out + cursor, (uint16_t)SURFACE_BYTES_PER_ROW);
}
@@ -193,6 +219,11 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
if (opaqueMask == 0x00) {
continue;
}
+ // Reserve the worst case for one dest byte (16-byte mixed
+ // RMW) plus the rts epilogue.
+ if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
if (opaqueMask == 0xFFu) {
// move.b #imm, d16(a0)
// Opcode 0x117C: bits 11-9 = dst reg (0=a0), bits 8-6 =
@@ -221,14 +252,17 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
// Epilogue: rts
+ if (cursor + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
// RESTORE: copy backup -> screen. Destination has the screen stride.
-uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitRestore68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@@ -238,24 +272,33 @@ uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
+ copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
// Prologue: movea.l 4(sp), a0 (src); movea.l 8(sp), a1 (dst).
+ if (cursor + 8u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x206Fu);
- cursor += writeBE16(out + cursor, 0x0004u);
+ cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
- cursor += writeBE16(out + cursor, 0x0008u);
+ cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
- cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, false);
+ cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, false);
+ if (cursor == SPRITE_EMIT_OVERFLOW) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ if (cursor + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
// SAVE: copy screen -> backup. Source has the screen stride.
-uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitSave68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@@ -265,15 +308,24 @@ uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
+ copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
+ if (cursor + 8u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x206Fu);
- cursor += writeBE16(out + cursor, 0x0004u);
+ cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
- cursor += writeBE16(out + cursor, 0x0008u);
+ cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
- cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, true);
+ cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, true);
+ if (cursor == SPRITE_EMIT_OVERFLOW) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ if (cursor + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
diff --git a/src/codegen/spriteEmitIigs.c b/src/codegen/spriteEmitIigs.c
index f58a623..f34b93e 100644
--- a/src/codegen/spriteEmitIigs.c
+++ b/src/codegen/spriteEmitIigs.c
@@ -1,7 +1,7 @@
// IIgs (65816) sprite codegen. Emits PIC draw routines that write
// 4bpp packed surface bytes via abs,Y indexed addressing.
//
-// CALLING CONVENTION: NOT the ORCA-C fn-pointer convention. The
+// CALLING CONVENTION: NOT a C fn-pointer convention. The
// runtime never calls these routines via a C cast -- instead,
// spriteCompiledDraw (in jlSpriteCompile.c, gated on
// JOEYLIB_PLATFORM_IIGS) builds a self-modifying JSL stub that
@@ -9,7 +9,7 @@
// - M = 8-bit (set by stub before JSL)
// - X = 16-bit (set by stub)
// - Y = destRow (loaded by stub from immediate)
-// - DBR = program bank (ORCA-C default)
+// - DBR = program bank
// No stack arg, no prologue. Body executes directly.
//
// Routine shape (per-byte emit, no PEA optimization yet):
@@ -39,12 +39,10 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
-#define MAX_ROUTINE_BYTES 8192
-
// ----- Prototypes -----
-static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
+static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
@@ -116,8 +114,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
// writeLE16 was inlined at every call site. Inlining cuts a JSL/RTL
-// per emitted 16-bit immediate (4 instructions per byte * 12 sites)
-// and avoids ORCA-Linker bank-fragility around tiny-helper resolution.
+// per emitted 16-bit immediate (4 instructions per byte * 12 sites).
// Common backbone for save and restore. Both ops copy a byte-aligned
// rectangle row-by-row using MVN; only the operand banks (which buffer
@@ -150,15 +147,36 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
//
// The MVN at row R has its dstbk at routine offset (12*R + 4) and
// srcbk at (12*R + 5).
-static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
- uint16_t cursor;
+// Worst-case bytes emitted per MVN copy row: row 0 = 6 bytes, any
+// later row = 12. Plus the trailing RTL.
+#define IIGS_MVN_MAX_BYTES_PER_ROW 12u
+
+static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
+ uint32_t cursor;
uint16_t advance;
uint16_t row;
+ // MVN copies (count + 1) bytes from the LDA #(copyBytes-1) operand.
+ // A zero copyBytes would emit LDA #0xFFFF and MVN a full 64 KB --
+ // a screen-trashing blit. widthTiles==0 is rejected at sprite
+ // creation, so copyBytes>=4 in practice, but guard the helper so a
+ // future caller cannot weaponize it: emit a bare RTL no-op instead.
+ if (copyBytes == 0u) {
+ if (cap < 1u) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ out[0] = 0x6B;
+ return 1u;
+ }
+
cursor = 0;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) {
+ // Reserve the row's worst case plus the trailing RTL.
+ if (cursor + IIGS_MVN_MAX_BYTES_PER_ROW + 1u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
if (row > 0) {
out[cursor++] = advanceX ? 0x8A : 0x98; // TXA / TYA
out[cursor++] = 0x18; // CLC
@@ -174,6 +192,9 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
out[cursor++] = 0x00; // dstbk -- patched per call
out[cursor++] = 0x00; // srcbk -- patched per call
}
+ if (cursor + 1u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x6B; // RTL
return cursor;
}
@@ -184,7 +205,7 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
// by copyBytes per row. Backup rows are contiguous in memory so Y is
// already correct for the next row; screen rows are SURFACE_BYTES_PER_ROW
// apart so X needs an explicit ADC between rows.
-uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
+uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
uint16_t copyBytes;
@@ -195,16 +216,16 @@ uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
- copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
+ copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
- return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/true);
+ return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/true);
}
// RESTORE (backup -> screen). Stub passes X = backup low offset,
// Y = screen low offset. Backup is contiguous so X advances correctly
// via MVN; screen needs explicit advance, so Y is the one we ADC.
-uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
+uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
uint16_t copyBytes;
@@ -215,9 +236,9 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
- copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
+ copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
- return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/false);
+ return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/false);
}
@@ -252,8 +273,8 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
//
// Mixed bytes always run on the M=8 path because the AND/ORA in
// M=16 would clobber the adjacent byte.
-uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
@@ -273,7 +294,7 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
- destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
+ destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
wide = false;
// No prologue: caller (the inline-asm stub in jlSpriteCompile.c)
@@ -285,7 +306,19 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
if (opaqueMask == 0x00) {
continue;
}
- absOffset = (uint16_t)(row * SURFACE_BYTES_PER_ROW + col);
+ // Reserve the worst case for one dest byte: the 9-byte
+ // mixed RMW, plus a possible REP/SEP mode transition (2),
+ // plus the trailing SEP+RTL (3) at routine end.
+ if (cursor + SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS + 2u + 3u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ // Compute the dest-byte offset in a 32-bit intermediate so
+ // row*160 cannot wrap a 16-bit int used for plain
+ // int arithmetic (row up to ~199, 199*160 = 31840 is in
+ // range for legal sprites, but a clamp-bypassing caller
+ // must not silently wrap). The result still fits the 16-bit
+ // abs,Y operand for any on-surface sprite.
+ absOffset = (uint16_t)((uint32_t)row * SURFACE_BYTES_PER_ROW + col);
if (opaqueMask == 0xFFu && (col + 1) < destBytesPerRow) {
// Look ahead: if (col, col+1) are both fully opaque
@@ -341,6 +374,14 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
+ // Trailing SEP (2 bytes if still wide) + RTL (1 byte). The per-byte
+ // reserve above accounts for these whenever a byte was emitted, but
+ // an all-transparent sprite never entered the inner write, so guard
+ // the epilogue explicitly.
+ if (cursor + (wide ? 2u : 0u) + 1u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+
// Routine exits in M=8: the JSL stub assumes M=8 throughout (the
// stub itself only ever ran with M=8 and doesn't restore M). The
// asm wrapper after the JSL forces M=16 again, but be defensive
diff --git a/src/codegen/spriteEmitInterleaved68k.c b/src/codegen/spriteEmitInterleaved68k.c
index d4862ce..ef969dd 100644
--- a/src/codegen/spriteEmitInterleaved68k.c
+++ b/src/codegen/spriteEmitInterleaved68k.c
@@ -1,34 +1,15 @@
-// 68k sprite codegen for ST word-interleaved planar layout. Emits a
-// cdecl-callable routine `void draw(uint8_t *groupBase)` that walks
-// the sprite's tile data and writes plane bytes via `d16(a0)` chains.
+// 68k sprite codegen for ST word-interleaved planar layout.
//
-// ST planar layout reminder (doc/atarist_planar.md): one buffer; per
-// scanline 20 groups of 8 bytes; per group, 4 plane words back-to-
-// back. groupBase points at the FIRST group the sprite touches:
-// pd->base + y * 160 + (x >> 4) * 8
-//
-// Shift index for ST is bit 3 of x (whether the sprite starts in the
-// high half or low half of the first group). x mod 8 != 0 falls back
-// to the interpreter (returns 0 from this emitter so sp->slot stays
-// NULL for those alignments).
-//
-// Per (row, tile_col, plane) we emit one of:
-// * nothing (op byte = 0, all transparent)
-// * move.b #pbN, d16(a0) (op = 0xFF, full replace, 6 bytes)
-// * clr.b d16(a0) (op = 0xFF AND pbN = 0, 4 bytes)
-// * andi.b #~op, d16(a0) (op partial, pbN = 0, 6 bytes)
-// * ori.b #pbN, d16(a0) (op partial, pbN == op, 6 bytes)
-// * andi.b #~op + ori.b #pbN (mixed, 12 bytes)
-//
-// d16 is the byte offset from groupBase to the target plane byte.
-// Layout of the byte offset:
-// shift 0: byteOff = (col >> 1) * 8 + plane*2 + (col & 1)
-// shift 1: byteOff = ((col + 1) >> 1) * 8 + plane*2 + (1 - (col & 1))
-// Each tile column is 8 sprite pixels = exactly half a 16-pixel
-// group, alternating high (offset 0) and low (offset 1) bytes of
-// each plane word.
-//
-// Per row we adda.w #160, a0 to advance to the next scanline.
+// Phase 11 (shared-walker): the per-shift unrolled emitter was
+// retired. halSpriteDrawPlanes in src/port/atarist/hal.c now handles
+// all shifts byte-wide at runtime, including the formerly fast shifts
+// 0 and 1 -- per-cel arena cost drops to zero. The byte-aligned fast
+// path stSpriteDrawByteAligned is still called from halSpriteDrawPlanes
+// for shift==0 sprites that are fully on surface, so byte-aligned UBER
+// perf does not regress. The entry points below are kept so the
+// dispatcher API stays uniform across ports; they emit zero bytes, so
+// jlSpriteCompile takes the "totalSize == 0 -> bail" path and sp->slot
+// stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
@@ -36,194 +17,21 @@
#include "spriteInternal.h"
-// ----- Constants -----
-
-#define TILE_PIXELS 8
-#define TILE_BYTES 32
-#define TILE_BYTES_PER_ROW 4
-#define ST_BYTES_PER_ROW 160
-
-
-// ----- Helpers -----
-
-static uint16_t writeBE16(uint8_t *out, uint16_t value) {
- out[0] = (uint8_t)(value >> 8);
- out[1] = (uint8_t)(value & 0xFFu);
- return 2;
-}
-
-
-// Build the 4 plane bytes + opacity byte for one (row, tileCol)
-// pair. pbN bit 7 is sprite pixel 0 (leftmost), bit 0 is pixel 7.
-// op bit N is set iff that pixel's color != 0.
-static void buildPlaneBytes(const jlSpriteT *sp, uint16_t row, uint16_t tileCol,
- uint8_t *outPb0, uint8_t *outPb1,
- uint8_t *outPb2, uint8_t *outPb3,
- uint8_t *outOp) {
- uint16_t tileY = (uint16_t)(row >> 3);
- uint16_t inTileY = (uint16_t)(row & 7u);
- uint16_t wTiles = sp->widthTiles;
- const uint8_t *tileBytes = sp->tileData + (uint32_t)(tileY * wTiles + tileCol) * 32u;
- const uint8_t *tileRow = tileBytes + (uint32_t)inTileY * TILE_BYTES_PER_ROW;
- uint8_t pb0 = 0u;
- uint8_t pb1 = 0u;
- uint8_t pb2 = 0u;
- uint8_t pb3 = 0u;
- uint8_t op = 0u;
- uint8_t p;
- uint8_t b;
- uint8_t color;
- uint8_t bit;
-
- for (p = 0; p < 8u; p++) {
- b = tileRow[p >> 1];
- color = (p & 1u) ? (uint8_t)(b & 0x0Fu) : (uint8_t)(b >> 4);
- bit = (uint8_t)(0x80u >> p);
- if (color != 0u) {
- op = (uint8_t)(op | bit);
- if (color & 1u) pb0 = (uint8_t)(pb0 | bit);
- if (color & 2u) pb1 = (uint8_t)(pb1 | bit);
- if (color & 4u) pb2 = (uint8_t)(pb2 | bit);
- if (color & 8u) pb3 = (uint8_t)(pb3 | bit);
- }
- }
- *outPb0 = pb0;
- *outPb1 = pb1;
- *outPb2 = pb2;
- *outPb3 = pb3;
- *outOp = op;
-}
-
-
-// Emit code for one plane byte at d16(a0). Returns bytes written.
-// op=opacity byte, pb=plane byte (subset of op).
-static uint16_t emitPlaneByte(uint8_t *out, uint16_t cursor, uint16_t d16, uint8_t op, uint8_t pb) {
- uint16_t start = cursor;
-
- if (op == 0u) {
- return 0u; /* nothing to emit */
- }
- if (op == 0xFFu) {
- /* All 8 pixels opaque: replace the byte. */
- if (pb == 0u) {
- /* clr.b d16(a0). Opcode 0x4228 + d16. 4 bytes. */
- cursor += writeBE16(out + cursor, 0x4228u);
- cursor += writeBE16(out + cursor, d16);
- } else {
- /* move.b #pb, d16(a0). Opcode 0x117C + #imm word + d16. 6 bytes. */
- cursor += writeBE16(out + cursor, 0x117Cu);
- cursor += writeBE16(out + cursor, (uint16_t)pb);
- cursor += writeBE16(out + cursor, d16);
- }
- return (uint16_t)(cursor - start);
- }
- /* Partial opacity. pb is a subset of op. */
- if (pb == 0u) {
- /* All opaque pixels have plane bit 0: just clear those bits. */
- /* andi.b #~op, d16(a0). Opcode 0x0228 + #imm word + d16. 6 bytes. */
- cursor += writeBE16(out + cursor, 0x0228u);
- cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
- cursor += writeBE16(out + cursor, d16);
- return (uint16_t)(cursor - start);
- }
- if (pb == op) {
- /* All opaque pixels have plane bit 1: just set those bits. */
- /* ori.b #op, d16(a0). Opcode 0x0028 + #imm word + d16. 6 bytes. */
- cursor += writeBE16(out + cursor, 0x0028u);
- cursor += writeBE16(out + cursor, (uint16_t)op);
- cursor += writeBE16(out + cursor, d16);
- return (uint16_t)(cursor - start);
- }
- /* Mixed: clear opaque bits, then set the plane bits. */
- cursor += writeBE16(out + cursor, 0x0228u);
- cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
- cursor += writeBE16(out + cursor, d16);
- cursor += writeBE16(out + cursor, 0x0028u);
- cursor += writeBE16(out + cursor, (uint16_t)pb);
- cursor += writeBE16(out + cursor, d16);
- return (uint16_t)(cursor - start);
-}
-
-
// ----- Emit API -----
-uint16_t spriteEmitDrawInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- /* Phase 11 (shared-walker): bypass the per-shift unrolled emitter.
- * halSpriteDrawPlanes in src/port/atarist/hal.c now handles all
- * shifts byte-wide at runtime, including the formerly fast
- * shifts 0 and 1 -- per-cel arena cost drops to zero. The byte-
- * aligned fast path stSpriteDrawByteAligned is still called from
- * halSpriteDrawPlanes for shift==0 sprites that are fully on
- * surface, so byte-aligned UBER perf doesn't regress. */
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
-uint16_t spriteEmitDrawInterleaved68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
- uint16_t row;
- uint16_t col;
- uint16_t plane;
- uint16_t heightPx;
- uint16_t wTiles;
- uint8_t pb[4];
- uint8_t op;
-
- if (shift > 1u) {
- return 0u;
- }
-
- cursor = 0u;
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- wTiles = sp->widthTiles;
-
- /* Prologue: movea.l 4(sp), a0. Opcode 0x206F + d16=4. 4 bytes. */
- cursor += writeBE16(out + cursor, 0x206Fu);
- cursor += writeBE16(out + cursor, 0x0004u);
-
- for (row = 0; row < heightPx; row++) {
- if (row > 0u) {
- /* adda.w #160, a0. Opcode 0xD0FC + imm word. 4 bytes. */
- cursor += writeBE16(out + cursor, 0xD0FCu);
- cursor += writeBE16(out + cursor, (uint16_t)ST_BYTES_PER_ROW);
- }
- for (col = 0; col < wTiles; col++) {
- buildPlaneBytes(sp, row, col, &pb[0], &pb[1], &pb[2], &pb[3], &op);
- if (op == 0u) {
- continue; /* whole tile column row is transparent */
- }
- for (plane = 0; plane < 4u; plane++) {
- uint16_t d16;
- if (shift == 0u) {
- /* col 0 (high) -> +0, col 1 (low) -> +1, col 2
- * (high group 1) -> +8, ... */
- d16 = (uint16_t)((col >> 1) * 8 + plane * 2 + (col & 1u));
- } else {
- /* col 0 (low) -> +1, col 1 (high group 1) -> +8, ... */
- d16 = (uint16_t)(((col + 1u) >> 1) * 8 + plane * 2 + (1u - (col & 1u)));
- }
- cursor += emitPlaneByte(out, cursor, d16, op, pb[plane]);
- }
- }
- }
-
- /* Epilogue: rts. */
- cursor += writeBE16(out + cursor, 0x4E75u);
- return cursor;
-}
-
-
-/* Save / restore aren't implemented yet -- returning 0 so they fall
- * through to the C interpreter (halSpriteSavePlanes / halSpriteRestorePlanes
- * fast paths cover the byte-aligned case). */
-uint16_t spriteEmitSaveInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
-uint16_t spriteEmitRestoreInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
diff --git a/src/codegen/spriteEmitPlanar68k.c b/src/codegen/spriteEmitPlanar68k.c
index 9b37b1b..d50f109 100644
--- a/src/codegen/spriteEmitPlanar68k.c
+++ b/src/codegen/spriteEmitPlanar68k.c
@@ -1,40 +1,20 @@
-// Planar 68k sprite codegen for Amiga (post-Phase 9, no chunky shadow).
+// Planar 68k sprite codegen for Amiga.
//
-// Emits PIC routines that write directly to the four bitplanes via 4
-// address-register pointers (a0..a3 = plane[0..3] base + byteOff,
-// where byteOff = y*40 + x/8 -- the dispatcher pre-computes this).
+// Phase 11 (shared-walker): the Amiga port no longer emits per-cel
+// unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
+// 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
+// src/core/sprite.c routes through halSpriteXxxPlanes in
+// src/port/amiga/hal.c when sp->slot is NULL, and those HAL hooks are
+// byte-wide walkers that read sp->tileData on the fly and synthesize
+// plane bytes at draw time. Same per-byte work, shared as one static
+// lib routine instead of 21 inlined copies; the pre-shift plane-byte
+// decomposition (and its shift/transparency math) now lives ONLY in
+// halSpriteDrawPlanes, so there is one source of truth.
//
-// Calling convention (cdecl on m68k-amigaos-gcc):
-// draw(p0, p1, p2, p3):
-// args at 4(sp), 8(sp), 12(sp), 16(sp) -- one ULONG per plane.
-// loaded into a0..a3 by the prologue.
-// save(p0, p1, p2, p3, backup):
-// 5 args; backup at 20(sp), loaded into a4.
-// restore(p0, p1, p2, p3, backup):
-// same as save but reads backup, writes planes.
-//
-// Per-byte plane write encoding decisions:
-// - all-transparent (mask=0): skip the byte entirely
-// - all-opaque (mask=0xFF): move.b #imm, d16(an) (6 bytes)
-// - mixed (0
+// bail" path and sp->slot stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
@@ -42,499 +22,21 @@
#include "spriteInternal.h"
-// ----- Constants -----
-
-#define TILE_PIXELS 8
-#define TILE_BYTES 32
-#define TILE_BYTES_PER_ROW 4
-#define TRANSPARENT_NIBBLE 0
-#define AMIGA_BITPLANES 4
-#define AMIGA_BYTES_PER_ROW 40
-
-
-// ----- Instruction encoding helpers -----
-
-static uint16_t writeBE16(uint8_t *out, uint16_t value) {
- out[0] = (uint8_t)((value >> 8) & 0xFFu);
- out[1] = (uint8_t)(value & 0xFFu);
- return 2u;
-}
-
-
-// movea.l , an -- load arg at SP+disp into An.
-// Encoding: 0010 nnn 001 010 111 + disp16
-// = 0x2057 + (n << 9), where n is dst An.
-// a0: 0x206F, a1: 0x226F, a2: 0x246F, a3: 0x266F, a4: 0x286F.
-static const uint16_t kMoveaSpToAn[] = {
- 0x206Fu, 0x226Fu, 0x246Fu, 0x266Fu, 0x286Fu, 0x2A6Fu, 0x2C6Fu, 0x2E6Fu
-};
-
-
-// adda.w #imm, an -- adds 16-bit signed imm to An (sign-extended).
-// Encoding: 1101 nnn 011 111 100 + imm
-// = 0xD0FC + (n << 9).
-static const uint16_t kAddaWImmToAn[] = {
- 0xD0FCu, 0xD2FCu, 0xD4FCu, 0xD6FCu, 0xD8FCu, 0xDAFCu, 0xDCFCu, 0xDEFCu
-};
-
-
-// ANDI.B #imm, D0 -- 4 bytes (opcode word + imm word, byte in low half).
-// Opcode: 0000 0010 00 000 000 (size=byte, mode=Dn, reg=D0)
-#define ANDI_B_IMM_D0 0x0200u
-
-// ORI.B #imm, D0 -- 4 bytes (opcode word + imm word, byte in low half).
-// Opcode: 0000 0000 00 000 000
-#define ORI_B_IMM_D0 0x0000u
-
-
-// MOVE.B d16(An), D0 -- 4 bytes (opcode + disp).
-// Encoding: 0001 000 000 mode reg
-// = size=01 (byte), dst reg=000 (D0), dst mode=000 (Dn),
-// src mode=101 (d16,An), src reg=An.
-// = 0001000 000 101 nnn = 0x1028 + An.
-static const uint16_t kMoveBD16AnToD0[] = {
- 0x1028u, 0x1029u, 0x102Au, 0x102Bu
-};
-
-
-// MOVE.B D0, d16(An) -- 4 bytes (opcode + disp).
-// Encoding: 0001 nnn 101 000 000 = 0x1140 + (An << 9).
-static const uint16_t kMoveBD0ToD16An[] = {
- 0x1140u, 0x1340u, 0x1540u, 0x1740u
-};
-
-
-// MOVE.B #imm, d16(An) -- 6 bytes (opcode + imm + disp).
-// Encoding: 0001 nnn 101 111 100 = 0x117C + (An << 9).
-// (Was 0x113C earlier -- that's mode=100=predec; mode=101=d16(An)
-// is the bit difference. Predec emits a 4-byte instruction with no
-// disp word, so the byte stream went out of sync and every
-// subsequent instruction decoded into garbage.)
-static const uint16_t kMoveBImmToD16An[] = {
- 0x117Cu, 0x137Cu, 0x157Cu, 0x177Cu
-};
-
-
-// MOVE.B (a4)+, d16(An) -- 4 bytes (opcode + disp). -- used by save/restore (backup in a4)
-// Encoding: 0001 nnn 101 011 100 = 0x115C + (An << 9).
-static const uint16_t kMoveBA4PostincToD16An[] = {
- 0x115Cu, 0x135Cu, 0x155Cu, 0x175Cu
-};
-
-
-// MOVE.B d16(An), (a4)+ -- 4 bytes (opcode + disp). -- used by save (planes -> backup)
-// Encoding: 1001 100 011 mode reg
-// Wait, MOVE.B src,(a4)+ : dst mode = 011 (an+), dst reg = 100 (A4),
-// so dst reg=100, dst mode=011 -> opcode high = 0001 100 011 ...
-// = 0001100011 mode reg = 0x18C0..
-// 0001 100 011 101 nnn = 0x18E8 + An.
-static const uint16_t kMoveBD16AnToA4Postinc[] = {
- 0x18E8u, 0x18E9u, 0x18EAu, 0x18EBu
-};
-
-
-// MOVEM.L reglist, -(SP) -- 4 bytes (opcode + reglist mask).
-// Opcode 0x48E7. Predec mask is REVERSED vs all other modes:
-// bit 15 = D0, ..., bit 8 = D7, bit 7 = A0, bit 6 = A1, bit 5 = A2,
-// bit 4 = A3, bit 3 = A4, bit 2 = A5, bit 1 = A6, bit 0 = A7.
-#define MOVEM_L_PUSH_OPCODE 0x48E7u
-#define MOVEM_L_MASK_A2_A3 0x0030u /* bits 5,4 = A2,A3 (predec order) */
-#define MOVEM_L_MASK_A2_A3_A4 0x0038u /* bits 5,4,3 = A2,A3,A4 */
-
-// MOVEM.L (SP)+, reglist -- 4 bytes (opcode + reglist mask).
-// Opcode 0x4CDF. Postinc mask follows the standard layout:
-// bit 0 = D0, ..., bit 7 = D7, bit 8 = A0, ..., bit 15 = A7.
-#define MOVEM_L_POP_OPCODE 0x4CDFu
-#define MOVEM_L_MASK_POP_A2_A3 0x0C00u /* bits 11,10 = A3,A2 */
-#define MOVEM_L_MASK_POP_A2_A3_A4 0x1C00u /* bits 12,11,10 = A4,A3,A2 */
-
-// RTS opcode.
-#define OPCODE_RTS 0x4E75u
-
-
-// ----- Emit helpers -----
-
-// For shift 0 (byte-aligned x), the sprite's chunky tile data converts
-// directly to plane bytes without any sub-byte shifting. For each
-// (row, col-byte, plane) we extract the 8 plane bits from 4 chunky
-// bytes (= 8 pixels) and produce one plane byte; we also produce a
-// mask byte indicating which pixel positions are non-transparent
-// (any plane bit != 0 in the source means non-transparent if
-// transparent index is 0, the JoeyLib convention).
-//
-// Sprite layout: tileData = wTiles x hTiles tiles, each tile = 8 rows
-// x 4 chunky bytes (32 bytes). Tiles laid out row-major within the
-// sprite. For plane-byte column `c` of row `r`:
-// tileX = c (since each plane byte covers exactly one tile column)
-// tileY = r / 8
-// inTileY = r % 8
-// chunky bytes = tileData + (tileY*wTiles + tileX)*32 + inTileY*4 + 0..3
-//
-// `col` must be in [0, widthTiles); callers handle out-of-range cols
-// (used when computing shifted variants that span widthTiles+1 output
-// bytes per row) by passing a sentinel and checking against widthTiles
-// before invoking this helper.
-static void planeByteAndMaskAt(const jlSpriteT *sp, uint16_t row, uint16_t col,
- uint8_t *planeBytes /*[4]*/, uint8_t *maskByte)
-{
- uint16_t tileX;
- uint16_t tileY;
- uint16_t inTileY;
- const uint8_t *tile;
- const uint8_t *chunky;
- uint8_t nibbles[8];
- uint8_t b0, b1, b2, b3;
- uint16_t p;
- uint8_t bitMask;
- uint8_t pix;
-
- tileX = col;
- tileY = row >> 3;
- inTileY = row & 7u;
-
- tile = sp->tileData + (uint32_t)((tileY * sp->widthTiles + tileX) * 32u);
- chunky = tile + inTileY * 4u;
-
- nibbles[0] = (uint8_t)(chunky[0] >> 4);
- nibbles[1] = (uint8_t)(chunky[0] & 0x0Fu);
- nibbles[2] = (uint8_t)(chunky[1] >> 4);
- nibbles[3] = (uint8_t)(chunky[1] & 0x0Fu);
- nibbles[4] = (uint8_t)(chunky[2] >> 4);
- nibbles[5] = (uint8_t)(chunky[2] & 0x0Fu);
- nibbles[6] = (uint8_t)(chunky[3] >> 4);
- nibbles[7] = (uint8_t)(chunky[3] & 0x0Fu);
-
- b0 = 0u; b1 = 0u; b2 = 0u; b3 = 0u;
- *maskByte = 0u;
- for (p = 0; p < 8u; p++) {
- pix = nibbles[p];
- if (pix == TRANSPARENT_NIBBLE) {
- continue;
- }
- bitMask = (uint8_t)(0x80u >> p);
- *maskByte = (uint8_t)(*maskByte | bitMask);
- if (pix & 1u) b0 = (uint8_t)(b0 | bitMask);
- if (pix & 2u) b1 = (uint8_t)(b1 | bitMask);
- if (pix & 4u) b2 = (uint8_t)(b2 | bitMask);
- if (pix & 8u) b3 = (uint8_t)(b3 | bitMask);
- }
- planeBytes[0] = b0;
- planeBytes[1] = b1;
- planeBytes[2] = b2;
- planeBytes[3] = b3;
-}
-
-
-// Shifted variant: produces 4 plane bytes and 1 mask byte for output
-// column `outCol` (0..widthTiles inclusive) of row `row` when the
-// sprite is shifted right by `shift` pixels (1..7). For shift 0,
-// callers should use planeByteAndMaskAt directly (faster, no spill).
-//
-// Each output byte is composed of bits drawn from up to two source
-// plane bytes:
-// leftPart = src[outCol-1] << (8 - shift) (high (shift) bits)
-// rightPart = src[outCol] >> shift (low (8-shift) bits)
-// with src[-1] and src[widthTiles] treated as 0/transparent. The
-// resulting plane byte is leftPart | rightPart; the mask byte is the
-// shifted union of the per-byte source masks.
-static void planeByteAndMaskShifted(const jlSpriteT *sp, uint16_t row, uint16_t outCol,
- uint8_t shift, uint16_t widthTiles,
- uint8_t *planeBytes /*[4]*/, uint8_t *maskByte)
-{
- uint8_t leftPlanes[AMIGA_BITPLANES];
- uint8_t leftMask;
- uint8_t rightPlanes[AMIGA_BITPLANES];
- uint8_t rightMask;
- uint8_t i;
-
- leftMask = 0u;
- rightMask = 0u;
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- leftPlanes[i] = 0u;
- rightPlanes[i] = 0u;
- }
-
- if (outCol > 0u && (uint16_t)(outCol - 1u) < widthTiles) {
- planeByteAndMaskAt(sp, row, (uint16_t)(outCol - 1u), leftPlanes, &leftMask);
- }
- if (outCol < widthTiles) {
- planeByteAndMaskAt(sp, row, outCol, rightPlanes, &rightMask);
- }
-
- *maskByte = (uint8_t)(((leftMask << (8u - shift)) & 0xFFu) |
- ((rightMask >> shift) & 0xFFu));
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- planeBytes[i] = (uint8_t)(((leftPlanes[i] << (8u - shift)) & 0xFFu) |
- ((rightPlanes[i] >> shift) & 0xFFu));
- }
-}
-
-
-// Emit code that merges one plane byte into d16(an) where d16 is the
-// row-relative byte offset (0 since we re-base each row by adda.w).
-// The choice of all-opaque vs mixed encoding cuts code size when many
-// pixels are opaque (typical for sprite interiors).
-static uint16_t emitMergeByteToD16An(uint8_t *out, uint16_t cursor,
- uint8_t an, uint8_t disp,
- uint8_t maskByte, uint8_t srcByte)
-{
- if (maskByte == 0u) {
- return cursor; /* nothing to write */
- }
- if (maskByte == 0xFFu) {
- /* All-opaque shortcut: move.b #src, d16(an). */
- cursor += writeBE16(out + cursor, kMoveBImmToD16An[an]);
- cursor += writeBE16(out + cursor, (uint16_t)srcByte);
- cursor += writeBE16(out + cursor, (uint16_t)disp);
- return cursor;
- }
- /* Mixed: load existing, clear mask bits, OR in src, write back. */
- cursor += writeBE16(out + cursor, kMoveBD16AnToD0[an]);
- cursor += writeBE16(out + cursor, (uint16_t)disp);
- cursor += writeBE16(out + cursor, ANDI_B_IMM_D0);
- cursor += writeBE16(out + cursor, (uint16_t)((~maskByte) & 0xFFu));
- cursor += writeBE16(out + cursor, ORI_B_IMM_D0);
- cursor += writeBE16(out + cursor, (uint16_t)srcByte);
- cursor += writeBE16(out + cursor, kMoveBD0ToD16An[an]);
- cursor += writeBE16(out + cursor, (uint16_t)disp);
- return cursor;
-}
-
-
// ----- Public API -----
-/* Phase 11 (shared-walker): the Amiga port no longer emits per-cel
- * unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
- * 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
- * src/core/sprite.c routes through halSpriteXxxPlanes in src/port/amiga
- * /hal.c when sp->slot is NULL, and those HAL hooks are now byte-wide
- * walkers that read sp->tileData on the fly. Same per-byte work, just
- * shared as one static lib routine instead of 21 inlined copies.
- *
- * Returning 0 here tells emitTotalSize() the emitter has no bytes for
- * this op, so jlSpriteCompile takes the "totalSize == 0 -> bail" path
- * and sp->slot stays NULL. */
-uint16_t spriteEmitDrawPlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
- uint16_t row;
- uint16_t col;
- uint16_t heightPx;
- uint16_t widthTiles;
- uint16_t bytesPerRow; /* per plane, per row */
- uint8_t planeBytes[AMIGA_BITPLANES];
- uint8_t maskByte;
- uint8_t i;
-
- if (shift > 7u) {
- return 0u;
- }
-
- cursor = 0;
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- widthTiles = (uint16_t)sp->widthTiles;
- bytesPerRow = (uint16_t)(widthTiles + (shift == 0u ? 0u : 1u));
-
- /* Prologue: m68k cdecl callee-saves a2-a6; we clobber a2 and a3
- * loading plane pointers, so push them first. After the push, all
- * stack arg displacements shift by +8 (two longs). */
- cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3);
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)(8u + 4u + i * 4u));
- }
-
- for (row = 0; row < heightPx; row++) {
- for (col = 0; col < bytesPerRow; col++) {
- if (shift == 0u) {
- planeByteAndMaskAt(sp, row, col, planeBytes, &maskByte);
- } else {
- planeByteAndMaskShifted(sp, row, col, shift, widthTiles, planeBytes, &maskByte);
- }
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- cursor = emitMergeByteToD16An(out, cursor, i, (uint8_t)col,
- maskByte, planeBytes[i]);
- }
- }
- if (row + 1u < heightPx) {
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
- }
- }
- }
-
- /* Epilogue: restore a2-a3, rts. */
- cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3);
- cursor += writeBE16(out + cursor, OPCODE_RTS);
- return cursor;
-}
-
-
-// SAVE: planes -> backup. backup is one contiguous 4*H*W/8 byte buffer
-// laid out as 4 plane stripes, matching halSpriteSavePlanes format
-// (so cross-platform save buffer is interchangeable).
-//
-// Per row: for each plane, copy bytesPerRow bytes from d16(an) to
-// (a4)+. After the row's reads, the planes need to advance by 40,
-// while a4 advances naturally via post-increment.
-//
-// Plane stripes are sequential in backup. We could either (a) do all
-// rows of plane 0, then plane 1, etc. (matches halSpriteSavePlanes
-// layout), or (b) interleave rows of all 4 planes (different layout).
-// halSpriteSavePlanes does (a) -- 4 separate plane stripes. The
-// emitted code below matches that layout for compat.
-uint16_t spriteEmitSavePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
- uint16_t row;
- uint16_t col;
- uint16_t heightPx;
- uint16_t bytesPerRow;
- uint8_t i;
-
- /* Shifts 2..7 reuse shift 1's bytes (identical memcpy). The
- * jlSpriteCompile post-emit pass aliases their routineOffsets to
- * slot 1 so this routine is emitted once. */
- if (shift > 1u) {
- return 0u;
- }
-
- cursor = 0;
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
-
- /* Prologue: callee-save a2/a3/a4 (m68k cdecl), then load 4 plane
- * pointers + backup pointer. After the push, all stack arg disps
- * shift by +12 (three longs). */
- cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
- }
- /* a4 = backup. */
- cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
- cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
-
- /* Plane-major: for each plane, walk all rows. After this routine,
- * each An has advanced by H*40 (one frame full); we don't need to
- * unwind because the function returns. We DO need to reset An
- * back to start before walking the NEXT plane though.
- *
- * Simpler alternative: row-major (interleaved). Per row, copy
- * bytesPerRow bytes from each plane to (a4)+, then advance all
- * 4 planes by 40. Net: a4 advances by 4*H*bytesPerRow; planes
- * advance by H*40. Backup layout becomes interleaved (plane0_row0,
- * plane1_row0, plane2_row0, plane3_row0, plane0_row1, ...).
- *
- * That doesn't match halSpriteSavePlanes' plane-major layout. Need
- * to either (a) match it -- emit per-plane outer loop with a4
- * stride between planes -- or (b) change halSpriteSavePlanes to
- * interleaved. Picking (b) is simpler in emitted code, but ALSO
- * requires updating halSpriteRestorePlanes and halSpriteRestoreUnder
- * fallback math.
- *
- * For now: use plane-major matching halSpriteSavePlanes. Per
- * plane: walk rows, copy bytes from d16(an) to (a4)+, advance an
- * by 40 after each row except the last; reset an back to start
- * before next plane. */
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- for (row = 0; row < heightPx; row++) {
- for (col = 0; col < bytesPerRow; col++) {
- cursor += writeBE16(out + cursor, kMoveBD16AnToA4Postinc[i]);
- cursor += writeBE16(out + cursor, (uint16_t)col);
- }
- if (row + 1u < heightPx) {
- cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
- }
- }
- /* Reset An back to the plane base for next iteration. The
- * total advance was (heightPx - 1) * 40. Subtract that. */
- if (i + 1u < AMIGA_BITPLANES) {
- cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
- }
- }
-
- cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
- cursor += writeBE16(out + cursor, OPCODE_RTS);
- return cursor;
-}
-
-
-// RESTORE: backup -> planes. Mirror of save. Uses MOVE.B (a4)+, d16(an).
-uint16_t spriteEmitRestorePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
- uint16_t row;
- uint16_t col;
- uint16_t heightPx;
- uint16_t bytesPerRow;
- uint8_t i;
-
- if (shift > 1u) {
- return 0u;
- }
-
- cursor = 0;
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
-
- /* Callee-save a2/a3/a4; arg disps shift by +12. */
- cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
- }
- cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
- cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
-
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- for (row = 0; row < heightPx; row++) {
- for (col = 0; col < bytesPerRow; col++) {
- cursor += writeBE16(out + cursor, kMoveBA4PostincToD16An[i]);
- cursor += writeBE16(out + cursor, (uint16_t)col);
- }
- if (row + 1u < heightPx) {
- cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
- }
- }
- if (i + 1u < AMIGA_BITPLANES) {
- cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
- cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
- }
- }
-
- cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
- cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
- cursor += writeBE16(out + cursor, OPCODE_RTS);
- return cursor;
-}
-
-// ---- Phase 11 entry points: zero-byte stubs that force the dispatcher
-// ---- to the shared walker in halSpriteDrawPlanes / halSpriteSavePlanes
-// ---- / halSpriteRestorePlanes. The old emit-unrolled-bytes path is
-// ---- kept above (renamed *Old) for reference until the walker is
-// ---- thoroughly battle-tested.
-
-uint16_t spriteEmitDrawPlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
-uint16_t spriteEmitSavePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
-uint16_t spriteEmitRestorePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- (void)out; (void)sp; (void)shift;
+uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ (void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
-
diff --git a/src/codegen/spriteEmitX86.c b/src/codegen/spriteEmitX86.c
index 2be82b9..f788764 100644
--- a/src/codegen/spriteEmitX86.c
+++ b/src/codegen/spriteEmitX86.c
@@ -38,17 +38,35 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
-// Worst-case bytes per emitted routine, used to size the scratch
-// buffer. A 32x32 sprite is 16 rows * (16 dest bytes + 1 for shift1)
-// = 272 dest-byte slots, each up to 10 bytes mixed = 2720; plus
-// per-row prologues 32*6=192; plus prologue/epilogue 8. Round up
-// generously.
-#define MAX_ROUTINE_BYTES 8192
+// Stack-arg displacements for the emitted cdecl routines, after the
+// prologue saves esi (+esi/edi for save/restore):
+// draw(dst) -> dst at [esp+8] (1 push)
+// save/restore(src, dst) -> src at [esp+12], dst at [esp+16] (2 pushes)
+#define DRAW_ARG_DST_DISP 0x08u
+#define COPY_ARG_SRC_DISP 0x0Cu
+#define COPY_ARG_DST_DISP 0x10u
+
+// Largest disp8 [esi+disp8] can encode as a non-negative offset. col
+// values above this need the disp32 ModRM form (sign-extended disp8
+// would address a negative offset).
+#define DISP8_MAX 127u
+
+// Worst-case emitted bytes for one draw dest byte. The mixed RMW path
+// is mov al,[esi+d]; and al; or al; mov [esi+d],al = 3+2+2+3 = 10 with
+// disp8; the disp32 form adds 3 bytes per [esi+disp32] operand (two of
+// them), so up to 16 for a wide mixed byte.
+#define DRAW_MAX_BYTES_PER_DESTBYTE 16u
+// Bytes for the per-row `add esi, imm32` advance.
+#define ROW_STRIDE_ADVANCE_BYTES 6u
+// Bytes for the `pop esi[/edi]; ret` epilogue (worst case save/restore).
+#define EPILOGUE_BYTES 3u
// ----- Prototypes -----
-static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
+static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
+static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col);
+static uint32_t esiOperandLen(uint16_t col);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
@@ -65,7 +83,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col
//
// strideOnSrc=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE)
-static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
+static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
uint16_t row;
uint16_t dwords;
uint16_t tail;
@@ -76,6 +94,10 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
advance = (int32_t)SURFACE_BYTES_PER_ROW - (int32_t)copyBytes;
for (row = 0; row < heightPx; row++) {
+ // Worst case per row: rep movsd (7) + rep movsb (7) + add (6).
+ if (cursor + 7u + 7u + 6u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
if (dwords > 0) {
// mov ecx, dwords (B9 imm32); rep movsd (F3 A5)
out[cursor++] = 0xB9;
@@ -111,6 +133,34 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
}
+// Emit the `[esi+col]` ModRM + displacement that follows an opcode in
+// the draw stores. reg field is 000 (al / opcode-extension 0), so the
+// ModRM is 0x46 for the disp8 form (col <= 127) and 0x86 for the disp32
+// form. disp8 is sign-extended by the CPU, so any col in 128..255 would
+// address a NEGATIVE offset and corrupt pixels; wide sprites
+// (widthTiles > 31) hit this, so emit the disp32 form there. Returns
+// the new cursor.
+static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col) {
+ if (col <= DISP8_MAX) {
+ out[cursor++] = 0x46; // ModRM mod=01 [esi+disp8]
+ out[cursor++] = (uint8_t)(col & 0xFFu);
+ } else {
+ out[cursor++] = 0x86; // ModRM mod=10 [esi+disp32]
+ out[cursor++] = (uint8_t)(col & 0xFFu);
+ out[cursor++] = (uint8_t)((col >> 8) & 0xFFu);
+ out[cursor++] = 0x00;
+ out[cursor++] = 0x00;
+ }
+ return cursor;
+}
+
+
+// Bytes emitEsiOperand will write for `col` (ModRM + disp).
+static uint32_t esiOperandLen(uint16_t col) {
+ return (col <= DISP8_MAX) ? 2u : 5u;
+}
+
+
// Decompose a destination byte's contribution from the sprite into
// (value, opaqueMask) for shift in {0, 1}. opaqueMask high nibble
// 0xF0 means high dest nibble is opaque; 0x0F means low is opaque;
@@ -182,10 +232,11 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
}
-// Emit a draw routine for one shift variant. Returns bytes written.
+// Emit a draw routine for one shift variant. Returns bytes written, or
+// SPRITE_EMIT_OVERFLOW if the routine would exceed `cap`.
// Routine signature: void f(uint8_t *dst).
-uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t runEnd;
@@ -207,17 +258,23 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
- destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
+ destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
// Prologue: push esi; mov esi, [esp+8]
+ if (cursor + 5u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x56; // push esi
out[cursor++] = 0x8B; out[cursor++] = 0x74;
- out[cursor++] = 0x24; out[cursor++] = 0x08;
+ out[cursor++] = 0x24; out[cursor++] = DRAW_ARG_DST_DISP;
// Body: per row, scan dest bytes coalescing fully-opaque runs.
for (row = 0; row < heightPx; row++) {
if (row > 0) {
// add esi, SURFACE_BYTES_PER_ROW (32-bit imm)
+ if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x81; out[cursor++] = 0xC6;
out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu);
out[cursor++] = (uint8_t)((SURFACE_BYTES_PER_ROW >> 8) & 0xFFu);
@@ -231,20 +288,25 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
col++;
continue;
}
+ // Reserve the worst case for one dest byte (wide mixed RMW
+ // = 16 bytes) plus the epilogue.
+ if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
if (opaqueMask != 0xFFu) {
// Mixed: read-modify-write.
- // mov al, [esi+col] (8A 46 cc)
+ // mov al, [esi+col] (8A /r)
// and al, ~opaqueMask (24 mm)
// or al, value (0C vv)
- // mov [esi+col], al (88 46 cc)
- out[cursor++] = 0x8A; out[cursor++] = 0x46;
- out[cursor++] = (uint8_t)(col & 0xFFu);
+ // mov [esi+col], al (88 /r)
+ out[cursor++] = 0x8A;
+ cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = 0x24;
out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu);
out[cursor++] = 0x0C;
out[cursor++] = value;
- out[cursor++] = 0x88; out[cursor++] = 0x46;
- out[cursor++] = (uint8_t)(col & 0xFFu);
+ out[cursor++] = 0x88;
+ cursor = emitEsiOperand(out, cursor, col);
col++;
continue;
}
@@ -264,12 +326,15 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
// enough that re-reading per chunk beats threading a
// fixed-size buffer through.
while (runLen >= 4) {
+ // mov dword [esi+col], imm32 (C7 /0 ... ii ii ii ii)
+ if (cursor + 1u + esiOperandLen(col) + 4u + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 3), shift, spriteBytesPerRow, &v3, &m);
- // mov dword [esi+col], imm32 (C7 46 cc ii ii ii ii)
- out[cursor++] = 0xC7; out[cursor++] = 0x46;
- out[cursor++] = (uint8_t)(col & 0xFFu);
+ out[cursor++] = 0xC7;
+ cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
out[cursor++] = v1;
out[cursor++] = v2;
@@ -281,11 +346,14 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
if (runLen >= 2) {
+ // mov word [esi+col], imm16 (66 C7 /0 ... ii ii)
+ if (cursor + 2u + esiOperandLen(col) + 2u + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
- // mov word [esi+col], imm16 (66 C7 46 cc ii ii)
out[cursor++] = 0x66;
- out[cursor++] = 0xC7; out[cursor++] = 0x46;
- out[cursor++] = (uint8_t)(col & 0xFFu);
+ out[cursor++] = 0xC7;
+ cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
out[cursor++] = v1;
col = (uint16_t)(col + 2);
@@ -295,9 +363,12 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
if (runLen == 1) {
- // mov byte [esi+col], imm8 (C6 46 cc ii)
- out[cursor++] = 0xC6; out[cursor++] = 0x46;
- out[cursor++] = (uint8_t)(col & 0xFFu);
+ // mov byte [esi+col], imm8 (C6 /0 ... ii)
+ if (cursor + 1u + esiOperandLen(col) + 1u + EPILOGUE_BYTES > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ out[cursor++] = 0xC6;
+ cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
col++;
}
@@ -305,6 +376,9 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
// Epilogue: pop esi; ret
+ if (cursor + 2u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;
return cursor;
@@ -312,8 +386,8 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
// RESTORE: copy backup -> screen. Destination has the screen stride.
-uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@@ -323,17 +397,26 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
+ copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
// Prologue: push esi; push edi; mov esi,[esp+12]; mov edi,[esp+16]
+ if (cursor + 10u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x56;
out[cursor++] = 0x57;
- out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
- out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
+ out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
+ out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
- cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, false);
+ cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, false);
+ if (cursor == SPRITE_EMIT_OVERFLOW) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
// Epilogue: pop edi; pop esi; ret
+ if (cursor + 3u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x5F;
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;
@@ -342,8 +425,8 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
// SAVE: copy screen -> backup. Source has the screen stride.
-uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
- uint16_t cursor;
+uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
+ uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@@ -353,15 +436,24 @@ uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
- copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
+ copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
+ if (cursor + 10u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x56;
out[cursor++] = 0x57;
- out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
- out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
+ out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
+ out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
- cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, true);
+ cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, true);
+ if (cursor == SPRITE_EMIT_OVERFLOW) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
+ if (cursor + 3u > cap) {
+ return SPRITE_EMIT_OVERFLOW;
+ }
out[cursor++] = 0x5F;
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;
diff --git a/src/codegen/spriteEmitter.h b/src/codegen/spriteEmitter.h
index 5c7ee96..d119165 100644
--- a/src/codegen/spriteEmitter.h
+++ b/src/codegen/spriteEmitter.h
@@ -8,12 +8,25 @@
// plane bytes at draw time -- but the entry points are kept so the
// dispatcher API stays uniform across ports.
//
-// Each emit function takes the sprite + shift variant and writes
-// position-independent draw-routine bytes into `out`. Returns the
-// number of bytes written. The output bytes follow the target CPU's
-// calling convention as documented in the per-CPU file's header
-// comment, so the bytes can be called via a C function pointer cast
-// once placed in executable memory.
+// Each emit function takes the sprite + shift variant and a capacity
+// `cap` (bytes available at `out`), and writes position-independent
+// draw-routine bytes into `out`. It returns the number of bytes
+// written (uint32_t -- a single pathological large routine can exceed
+// 64 KB, so a 16-bit count would truncate and mis-size the arena
+// slot). The output bytes follow the target CPU's calling convention
+// as documented in the per-CPU file's header comment, so the bytes
+// can be called via a C function pointer cast once placed in
+// executable memory.
+//
+// Return-value contract:
+// 0 -- op not implemented for this (shift, op)
+// on this platform; dispatcher falls back
+// to the interpreted path.
+// SPRITE_EMIT_OVERFLOW -- the routine would not fit in `cap`. The
+// sizing pass and the real emit pass both
+// propagate this and jlSpriteCompile bails
+// cleanly (no arena alloc, sp->slot NULL).
+// any other value -- bytes written.
#ifndef JOEYLIB_SPRITE_EMITTER_H
#define JOEYLIB_SPRITE_EMITTER_H
@@ -21,9 +34,35 @@
#include "joey/sprite.h"
#include "spriteInternal.h"
-uint16_t spriteEmitDrawX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitDraw68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
+// Overflow sentinel returned by an emitter whose output would exceed
+// the supplied capacity. Distinct from a real byte count and from the
+// 0 "not implemented" return.
+#define SPRITE_EMIT_OVERFLOW 0xFFFFFFFFul
+
+// Worst-case emitted bytes per destination byte, per target. Used to
+// size the emit scratch and to derive the create-time dimension cap so
+// the scratch, the cap, and the per-emitter bound all share one fact:
+// 68k mixed RMW : move.b d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0)
+// = 4 instructions * 4 bytes = 16
+// x86 mixed RMW : mov al,[esi+d]; and al; or al; mov [esi+d],al = 10
+// IIgs mixed RMW: lda abs,Y; and #; ora #; sta abs,Y = 9
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K 16u
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86 10u
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS 9u
+
+#if defined(JOEYLIB_PLATFORM_DOS)
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86
+#elif defined(JOEYLIB_PLATFORM_IIGS)
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS
+#else
+// 68k chunky (unused) / planar / interleaved walkers emit 0 bytes;
+// the 68k chunky worst case is the safe upper bound for sizing.
+#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
+#endif
+
+uint32_t spriteEmitDrawX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitDraw68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Save-under and restore-under emitters. Both copy a byte-aligned
// rectangle between the destination surface and a backup buffer. The
@@ -37,12 +76,12 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
// 68k use a plain cdecl `void copy(const uint8_t *src, uint8_t *dst)`
// where the caller swaps args between SAVE (screen->backup) and
// RESTORE (backup->screen).
-uint16_t spriteEmitSaveIigs (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitSaveX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitRestoreX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitSave68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitSaveIigs (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitSave68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitRestore68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Planar 68k emitters (Amiga). Distinct from the chunky 68k emitters
// above because the destination addressing is across 4 separate
@@ -55,9 +94,9 @@ uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift)
// dispatcher). Returns 0 for shifts not yet implemented (today only
// shift 0 == byte-aligned x is emitted; shifts 1..7 fall back to the
// cross-platform interpreter).
-uint16_t spriteEmitDrawPlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitSavePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Word-interleaved planar 68k emitter (ST). Calling convention for
// the emitted bytes:
@@ -66,8 +105,8 @@ uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t
// real bytes (x mod 16 == 0 for shift 0, x mod 16 == 8 for shift 1);
// other shifts return 0 so the cross-platform dispatcher falls back
// to halSpriteDrawPlanes.
-uint16_t spriteEmitDrawInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitSaveInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
-uint16_t spriteEmitRestoreInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
+uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
#endif
diff --git a/src/core/assetLoad.c b/src/core/assetLoad.c
index 0e988f3..693bd85 100644
--- a/src/core/assetLoad.c
+++ b/src/core/assetLoad.c
@@ -26,13 +26,12 @@
#include "joey/tile.h"
#include "spriteInternal.h"
-// Push the file-IO out of the ORCA-C _ROOT segment on the IIgs. Every
+// Push the file-IO out of the root segment on the IIgs. Every
// fopen / fread / fclose call drags a chunk of the stdio cluster
// in; concentrating them in one named segment keeps the root bank
// from overflowing past 64 KB (the failure mode is the linker's
// "Code exceeds code bank size" / "Address is not in current bank"
// errors documented in the feedback memory).
-JOEYLIB_SEGMENT("ASSETLD")
// ----- Constants -----
@@ -41,6 +40,14 @@ JOEYLIB_SEGMENT("ASSETLD")
#define ASSET_PALETTE_ENTRIES 16
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows
+// Largest legitimately drawable sprite cel: the surface is 40x25 tiles
+// (320/8 x 200/8), so any cel larger than that can never be fully
+// on-screen. Reject crafted/truncated headers claiming more, which
+// would otherwise force an unbounded per-cel malloc (DoS) and feed the
+// downstream emitters/draw walkers out-of-range dimensions.
+#define ASSET_MAX_TILES_W 40
+#define ASSET_MAX_TILES_H 25
+
// Per-platform expected target byte for .tbk files. .spr files carry
// target=0 (cross-platform chunky) and pass on every build.
#if defined(JOEYLIB_PLATFORM_AMIGA)
@@ -57,10 +64,46 @@ JOEYLIB_SEGMENT("ASSETLD")
// ----- Prototypes -----
+static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
// ----- Internal helpers (alphabetical) -----
+// Open `path`, read the shared 44-byte header, and validate the magic
+// + target byte common to both .spr and .tbk. On success leaves *outFp
+// open (caller must fclose) and returns true; on any failure closes
+// the file (if opened) and returns false. When outPalette is non-NULL
+// and the header advertises a palette (header[5] != 0), the palette is
+// decoded into outPalette. Shared by jlSpriteBankLoad and
+// jlTileBankLoad so the open/read/validate/parse-palette boilerplate
+// (and its fclose-on-error pattern) lives in one place.
+static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp) {
+ FILE *fp;
+
+ fp = fopen(path, "rb");
+ if (fp == NULL) {
+ return false;
+ }
+ if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
+ fclose(fp);
+ return false;
+ }
+ if (memcmp(header, magic, 4) != 0) {
+ fclose(fp);
+ return false;
+ }
+ if (header[4] != expectedTarget) {
+ fclose(fp);
+ return false;
+ }
+ if (outPalette != NULL && header[5] != 0u) {
+ readPaletteFromHeader(header, outPalette);
+ }
+ *outFp = fp;
+ return true;
+}
+
+
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
uint8_t i;
@@ -72,6 +115,10 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
// ----- Public API (alphabetical) -----
+// Returns the number of cels successfully loaded into outCels[0..N).
+// A return value strictly less than min(headerCellCount, maxCels)
+// indicates the file was truncated/corrupt mid-stream relative to its
+// own header count; the cels that were read are still valid.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
uint16_t *outPalette) {
FILE *fp;
@@ -87,26 +134,24 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
if (path == NULL || outCels == NULL || maxCels == 0u) {
return 0u;
}
- fp = fopen(path, "rb");
- if (fp == NULL) {
- return 0u;
- }
- if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
- fclose(fp);
- return 0u;
- }
// Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
// else means the file was either baked wrong or is a stale .spr
// from the old compiled-codegen pipeline.
- if (memcmp(header, "JSP1", 4) != 0 || header[4] != 0u) {
- fclose(fp);
+ if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
return 0u;
}
widthTiles = header[6];
heightTiles = header[7];
cellCount = (uint16_t)(header[8] | (header[9] << 8));
- if (outPalette != NULL && header[5] != 0u) {
- readPaletteFromHeader(header, outPalette);
+
+ // Reject degenerate (0 in either dimension -> tileBytes 0, an
+ // implementation-defined malloc(0)) and oversized (a crafted header
+ // could force a multi-MB per-cel malloc) sprites. Matches
+ // jlSpriteCreate's invariant and the 40x25-tile surface ceiling.
+ if (widthTiles == 0u || heightTiles == 0u ||
+ widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H) {
+ fclose(fp);
+ return 0u;
}
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
@@ -135,7 +180,9 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
sp->heightTiles = heightTiles;
sp->ownsTileData = true;
sp->slot = NULL;
- memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
+ // 0xFF == SPRITE_NOT_COMPILED for every routine offset (0 is a
+ // valid offset, so zero-fill would alias a real entry).
+ memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
@@ -147,6 +194,11 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
}
+// Returns the number of tiles successfully loaded. outValid[0..maxTiles)
+// is always fully written: true for each tile read, false for the rest
+// (including slots past a truncated/short file), so the caller need not
+// pre-zero it. A return value strictly less than min(headerTileCount,
+// maxTiles) indicates the file was truncated/corrupt mid-stream.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette) {
FILE *fp;
@@ -159,28 +211,22 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
return 0u;
}
- fp = fopen(path, "rb");
- if (fp == NULL) {
- return 0u;
- }
- if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
- fclose(fp);
- return 0u;
- }
- if (memcmp(header, "JTB1", 4) != 0) {
- fclose(fp);
- return 0u;
- }
- if (header[4] != EXPECTED_TILE_TARGET) {
- // File was baked for a different target. Build-system bug
- // -- the per-target make rule should have produced a .tbk
- // with the matching target byte.
- fclose(fp);
+ // The target byte for .tbk must match the build platform. .spr
+ // files carry target=0; tiles carry the per-platform value. A
+ // mismatch is a build-system bug -- the per-target make rule
+ // should have produced a .tbk with the matching target byte.
+ if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
return 0u;
}
tileCount = (uint16_t)(header[6] | (header[7] << 8));
- if (outPalette != NULL && header[5] != 0u) {
- readPaletteFromHeader(header, outPalette);
+
+ // Define the whole outValid array up front so leftover slots read
+ // false exactly as the tile.h contract promises, regardless of
+ // caller pre-zeroing.
+ if (outValid != NULL) {
+ for (i = 0; i < maxTiles; i++) {
+ outValid[i] = false;
+ }
}
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
diff --git a/src/core/audio.c b/src/core/audio.c
index 9c83134..24ee913 100644
--- a/src/core/audio.c
+++ b/src/core/audio.c
@@ -88,8 +88,8 @@ void jlAudioTone(uint16_t freqHz) {
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
// Same reasoning as jlAudioTone: direct hardware-register
// programming, no DSP/DMA dependency. Range-check voice here so
- // the per-port HAL can assume voice < 3.
- if (voice >= 3u) {
+ // the per-port HAL can assume voice < JOEY_AUDIO_VOICES.
+ if (voice >= JOEY_AUDIO_VOICES) {
return;
}
halAudioVoice(voice, freqHz, atten);
diff --git a/src/core/audioSfxMix.c b/src/core/audioSfxMix.c
index 4f66366..ed5b5ee 100644
--- a/src/core/audioSfxMix.c
+++ b/src/core/audioSfxMix.c
@@ -1,27 +1,52 @@
// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.
+#include
#include
#include "audioSfxMixInternal.h"
+// interp()/mix arithmetic (delta * frac up to ~16.7M, and the >> 16 on
+// a possibly-negative value) requires a >= 32-bit int with arithmetic
+// right shift. DOS (DJGPP) and ST (m68k gcc) both satisfy this; the
+// file is excluded from the IIgs build (16-bit int) by
+// make/iigs.mk. Fail loudly if anyone ever shares this mixer with a
+// 16-bit-int target instead of producing silent garbage SFX.
+#if INT_MAX < 0x7FFFFFFF
+#error "audioSfxMix requires >= 32-bit int (DOS/ST only); IIgs must use NTPstreamsound"
+#endif
+
// Refill a streaming slot's prefetch buffer. Preserves the very last
// sample of the previous chunk at streamBuf[0] so linear interp can
// step across the chunk boundary without a discontinuity click. The
-// new chunk fills streamBuf[1..]; pos resets to "after byte 0" so
-// the next fetch is byte 1 (= first sample of the new chunk).
+// new chunk fills streamBuf[1..]. On a real refill (a previous chunk
+// existed) pos resets to 0 so the first read interpolates the carried
+// old-last sample at streamBuf[0] into the new chunk's first sample at
+// streamBuf[1], bridging the boundary. On the very first prime there
+// is no real previous sample, so pos resets to "after byte 0" and the
+// first read starts at the new chunk's first sample (index 1).
//
// Returns true if more samples are available, false if the stream
// has ended (caller should mark the slot inactive).
static bool streamRefill(AudioSfxSlotT *slot) {
int8_t lastSample;
uint32_t got;
+ bool haveCarry;
if (slot->streamEof) {
return false;
}
- lastSample = (slot->streamLen > 0u) ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
+ haveCarry = (slot->streamLen > 0u);
+ lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
+ // Clamp the callback's reported count to the request size so a
+ // non-conforming fill() that returns more than asked cannot make
+ // the mixer read past streamBuf. (Memory safety still requires
+ // fill() to honor count -- an over-returning callback has already
+ // over-written streamBuf+1; this caps only the subsequent reads.)
got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
+ if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
+ got = AUDIO_SFX_STREAM_BUF - 1u;
+ }
if (got == 0u) {
slot->streamEof = true;
slot->streamLen = 0u;
@@ -29,8 +54,17 @@ static bool streamRefill(AudioSfxSlotT *slot) {
}
slot->streamBuf[0] = lastSample;
slot->streamLen = (uint16_t)(got + 1u);
- // Reset pos to (1 sample, 0 frac) so the next read is at index 1.
- slot->posFp = (uint64_t)1u << 16;
+ if (haveCarry) {
+ // Bridge the chunk boundary: start at index 0 so the first
+ // read interpolates streamBuf[0] (old last sample) -> [1]
+ // (new first sample), replaying one sample with no audible
+ // discontinuity click.
+ slot->posFp = 0;
+ } else {
+ // First prime: streamBuf[0] is a synthetic zero, not a real
+ // sample. Skip it so it isn't played as a leading sample.
+ slot->posFp = (uint64_t)1u << 16;
+ }
return true;
}
@@ -39,10 +73,14 @@ static bool streamRefill(AudioSfxSlotT *slot) {
// `frac` is the 16-bit fractional part of pos; 0 = exactly at s0,
// 0xFFFF = nearly s1. Result is a signed int in [-128, +127].
static int interp(int8_t s0, int8_t s1, uint32_t frac) {
- int delta;
+ int32_t delta;
- delta = (int)s1 - (int)s0;
- return (int)s0 + ((delta * (int)frac) >> 16);
+ // delta (up to +/-255) * frac (up to 65535) is ~16.7M -- needs the
+ // explicit 32-bit width so it can't overflow a 16-bit int. The
+ // >> 16 of a possibly-negative int32_t relies on arithmetic shift,
+ // which holds on every target this file builds for (DOS/ST).
+ delta = (int32_t)s1 - (int32_t)s0;
+ return (int)s0 + (int)((delta * (int32_t)frac) >> 16);
}
diff --git a/src/core/codegenArena.c b/src/core/codegenArena.c
index 9a8e349..341592b 100644
--- a/src/core/codegenArena.c
+++ b/src/core/codegenArena.c
@@ -5,17 +5,20 @@
#if defined(JOEYLIB_PLATFORM_IIGS)
// On the IIgs the arena holds 65816 machine code that callers JSL
-// into. ORCA-C's malloc returns memory from the C heap (often bank 0,
-// which is system RAM) so JSL'ing into it lands on whatever happens
-// to live there -> instant crash. Memory Manager NewHandle with
+// into. A normal C heap allocation returns memory from bank 0 (system
+// RAM), so JSL'ing into it lands on whatever happens to live there ->
+// instant crash. Memory Manager NewHandle with
// attrFixed | attrLocked | attrPage | attrNoCross gives us a fixed
// page-aligned region in a single bank we can safely jump into.
-//
-// types.h must be included before our stdbool shim because ORCA's
-// types.h defines true/false as #define ... without #ifndef guards
-// and would re-#define our shim's macros.
-#include
-#include
+// The IIgs toolbox bindings live in . Handles are typed
+// as void* and the owner ID comes from MMStartUp(); the Memory Manager
+// attribute bits we use are defined here (values from Apple's Memory
+// Manager reference).
+#include
+#define attrPage 0x0004
+#define attrNoCross 0x0010
+#define attrFixed 0x4000
+#define attrLocked 0x8000
#endif
#include
@@ -34,16 +37,15 @@
// Memory Manager handle is locked-in-place on IIgs). Callers MUST
// treat them as read-only.
uint8_t *gCodegenArenaBase = NULL;
-// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit
-// absolute address. ORCA-C's (uint32_t)pointer cast on the IIgs
-// zeros the bank byte for some pointer expressions, so JSL targets
-// read this field directly.
+// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit absolute
+// address that JSL targets read directly (captured by copying the
+// handle pointer's raw bytes -- see codegenArenaInit).
uint32_t gCodegenArenaBaseAddr = 0;
static uint32_t gTotalBytes = 0;
static uint32_t gUsedBytes = 0;
static ArenaSlotT *gFirstSlot = NULL;
#if defined(JOEYLIB_PLATFORM_IIGS)
-static Handle gCodegenArenaBaseHandle = NULL;
+static void *gCodegenArenaBaseHandle = NULL;
#endif
@@ -224,24 +226,29 @@ bool codegenArenaInit(uint32_t totalBytes) {
return false;
}
#if defined(JOEYLIB_PLATFORM_IIGS)
- gCodegenArenaBaseHandle = NewHandle(totalBytes, _ownerid,
+ // MMStartUp() returns this application's master user ID (idempotent;
+ // the system starts the Memory Manager before the app runs). A NULL
+ // handle is the failure signal.
+ {
+ uint16_t ownerId;
+
+ ownerId = MMStartUp();
+ gCodegenArenaBaseHandle = NewHandle((unsigned long)totalBytes, ownerId,
attrFixed | attrLocked | attrPage | attrNoCross,
- NULL);
- if (gCodegenArenaBaseHandle == NULL || _toolErr != 0) {
- gCodegenArenaBaseHandle = NULL;
+ (void *)0);
+ }
+ if (gCodegenArenaBaseHandle == NULL) {
return false;
}
HLock(gCodegenArenaBaseHandle);
- // Capture the 24-bit absolute address by copying the Pointer's
- // raw bytes -- (uint32_t)pointer through a chain of expressions
- // has been observed to drop the bank byte under ORCA-C's
- // memorymodel 1, but a memcpy of the underlying 4 bytes is
- // reliable. The high byte (bytes[3]) is undefined and masked off.
+ // Capture the 24-bit absolute address by copying the master
+ // pointer's raw bytes; bytes[3] (bank-high/flags) is masked off.
{
- Pointer p;
+ void *p;
uint8_t bytes[4];
- p = *gCodegenArenaBaseHandle;
- gCodegenArenaBase = (uint8_t *)p;
+
+ p = *(void **)gCodegenArenaBaseHandle;
+ gCodegenArenaBase = (uint8_t *)p;
memcpy(bytes, &p, 4);
gCodegenArenaBaseAddr = (uint32_t)bytes[0]
| ((uint32_t)bytes[1] << 8)
diff --git a/src/core/codegenArenaInternal.h b/src/core/codegenArenaInternal.h
index 827063d..c5592a0 100644
--- a/src/core/codegenArenaInternal.h
+++ b/src/core/codegenArenaInternal.h
@@ -9,7 +9,7 @@
// compaction that moves a slot's bytes is transparent to callers.
//
// Allocations go through plain malloc; on every supported port
-// (IIgs ORCA, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
+// (IIgs, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
// readable, writable, AND executable. We are not running in a W^X
// environment.
//
diff --git a/src/core/debug.c b/src/core/debug.c
index a22b8a0..45aae4d 100644
--- a/src/core/debug.c
+++ b/src/core/debug.c
@@ -1,18 +1,19 @@
// Cross-platform "where did it hang?" logger. Holds joeylog.txt open
-// across calls; libc's stdio buffer absorbs writes (~4 KB) and the
-// final fclose at program exit (via atexit) gets the buffer to disk.
+// across calls and FLUSHES AFTER EVERY LINE: safe logs are more
+// important than fast logs. A crash, hang, or hard kill therefore
+// always leaves every line emitted so far on disk -- which is the whole
+// point of a "where did it hang?" logger.
//
-// Earlier rev opened+closed per call for crash durability ("last line
-// guaranteed on disk if we hang"); that cost ~1 second per call
-// through GoldenGate's ProDOS FST emulation -- a 50-line UBER run
-// burned ~5 minutes in IO. Even per-line fflush is too expensive
-// because every fflush forces an FST WRITE, and host-OS file IO time
-// isn't tracked by the IIgs VBL counter so wall-time logs underreport.
+// The cost is one host FST WRITE per line (seconds each through the
+// emulator's ProDOS FST emulation; that host-IO time is not tracked
+// by the IIgs VBL counter, so wall-time totals computed from the frame
+// counter slightly underreport). The 16 KB full buffer below still
+// coalesces each line's individual vfprintf writes into ONE FST write,
+// so the flush is per-line, not per-fragment.
//
-// Tradeoff: if the program crashes mid-run, buffered log lines may
-// not reach disk. For UBER and similar batch demos that's acceptable;
-// for hang-debugging where durability matters, call jlLogFlush()
-// at the suspected hang points.
+// (Earlier revs opened+closed per call, then buffered the whole run and
+// only flushed at the atexit fclose -- fast, but a mid-run crash lost
+// the buffered tail, exactly when you need it most. Safety wins.)
#include
#include
@@ -23,12 +24,10 @@
static const char *kLogPath = "joeylog.txt";
static FILE *gLogFp = NULL;
-/* 16 KB is enough for UBER's full log (~5 KB) plus generous headroom,
- * so the file never auto-flushes mid-run. ORCA-C / libnix default
- * buffers are only ~512 bytes; with that, a 50-line log triggers ~10
- * ProDOS / AmigaDOS WRITEs through the host FST, each of which is
- * untracked-host-time (seconds). Buffer the whole thing in memory and
- * let the atexit fclose flush once. */
+/* 16 KB full buffer so a single log line's many small vfprintf writes
+ * coalesce into one host FST WRITE; jlLog/jlLogF then fflush once per
+ * line. Default stdio buffers are only ~512 bytes, which would split a
+ * long line across multiple FST writes. */
#define JOEY_LOG_BUF_BYTES 16384
static char gLogBuf[JOEY_LOG_BUF_BYTES];
@@ -56,6 +55,7 @@ void jlLog(const char *msg) {
}
fputs(msg, fp);
fputc('\n', fp);
+ fflush(fp);
}
@@ -73,6 +73,7 @@ void jlLogF(const char *fmt, ...) {
vfprintf(fp, fmt, args);
va_end(args);
fputc('\n', fp);
+ fflush(fp);
}
diff --git a/src/core/draw.c b/src/core/draw.c
index edf9443..399cd21 100644
--- a/src/core/draw.c
+++ b/src/core/draw.c
@@ -21,43 +21,22 @@
// On overflow the fill silently truncates rather than crashing.
#define FLOOD_STACK_SIZE 512
+// Sentinel extents for an empty accumulated dirty bounding box. min* seed
+// above any valid coordinate and max* below any valid coordinate so the
+// first plotted point sets every edge, and a box that never receives an
+// on-surface point collapses to a no-op after clamping (min > max).
+#define DIRTY_BOX_MIN_INIT ((int16_t)0x7FFF)
+#define DIRTY_BOX_MAX_INIT ((int16_t)0x8000)
+
// ----- Prototypes -----
-static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible);
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
+static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
+static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
// ----- Internal helpers (alphabetical) -----
-// Clip a rectangle to the surface bounds. Outputs *outVisible = false
-// if the rect is entirely off-surface.
-static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible) {
- if (*w <= 0 || *h <= 0) {
- *outVisible = false;
- return;
- }
- if (*x < 0) {
- *w += *x;
- *x = 0;
- }
- if (*y < 0) {
- *h += *y;
- *y = 0;
- }
- if (*x >= SURFACE_WIDTH || *y >= SURFACE_HEIGHT) {
- *outVisible = false;
- return;
- }
- if (*x + *w > SURFACE_WIDTH) {
- *w = SURFACE_WIDTH - *x;
- }
- if (*y + *h > SURFACE_HEIGHT) {
- *h = SURFACE_HEIGHT - *y;
- }
- *outVisible = (*w > 0 && *h > 0);
-}
-
-
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
uint8_t nibble = colorIndex & 0x0F;
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
@@ -67,9 +46,16 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
uint16_t midBytes;
uint8_t *line;
+ // Planar ports have NULL s->pixels; the planar dual-write hook
+ // handles the actual plane fill. Guarding here keeps the chunky
+ // fallback from dereferencing NULL when a port has no chunky store.
+ if (s->pixels == NULL) {
+ return;
+ }
+
/* px* and midBytes are uint16_t (clipped values are non-negative)
- * so `>>1` lowers to a single LSR instead of ORCA-C's
- * ~SSHIFTRIGHT helper. Same with `<<1` for midBytes. */
+ * so `>>1` lowers to a single LSR instead of a signed-shift
+ * helper. Same with `<<1` for midBytes. */
for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x;
@@ -93,6 +79,23 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
}
+// Fill an axis-aligned span that the CALLER has already proven to be
+// fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <=
+// SURFACE_HEIGHT, w > 0, h > 0). Identical tail to jlFillRect but with
+// the 32-bit clip elided -- on an on-surface rect that clip is a
+// no-op, so callers that already hold the on-surface guarantee (rect
+// outlines, circle scanlines, axis-aligned lines) avoid re-deriving
+// it per span. Pixel output and dirty-mark word bands are bit-for-bit
+// identical to routing the same span through jlFillRect.
+static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
+ if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) {
+ fillRectClipped(s, x, y, w, h, colorIndex);
+ }
+ halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex);
+ surfaceMarkDirtyRect(s, x, y, w, h);
+}
+
+
// Smith's scanline flood fill. Implements both the unbounded and the
// boundary-stopped variants in one pass: the matching predicate is
// (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill,
@@ -164,6 +167,12 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
stackX, stackY,
&sp, FLOOD_STACK_SIZE,
&seedMatched, &leftX, &rightX)) {
+ // The asm filled the span internally but does not mark
+ // dirty -- that is the C wrapper's job. Mark the just-
+ // filled run so jlStagePresent blits it.
+ if (seedMatched) {
+ surfaceMarkDirtyRect(s, leftX, y, (int16_t)(rightX - leftX + 1), 1);
+ }
continue;
}
}
@@ -224,20 +233,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
}
}
- // Fill the span. Bypass jlFillRect's clipping wrapper: walk-out
- // already guaranteed leftX/rightX are in [0..SURFACE_WIDTH-1]
- // and the seed-pop bounds check did the same for y. We DO
- // need the planar dual-write (which jlFillRect's wrapper would
- // call), so invoke halFillRectPlanes explicitly after the
- // chunky span fill -- otherwise PLANAR_PRESENT builds (and,
- // post-Phase-9, every build) display flood-filled regions
- // as the unfilled background.
+ // Fill the span. walk-out already guaranteed leftX/rightX are in
+ // [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same
+ // for y, so the span is provably on-surface -- route it through
+ // the shared on-surface tail (chunky fill + planar dual-write +
+ // dirty mark) rather than re-clipping through jlFillRect. The
+ // planar dual-write here is load-bearing: without it
+ // PLANAR_PRESENT builds (and, post-Phase-9, every build) display
+ // flood-filled regions as the unfilled background.
{
int16_t spanW = (int16_t)(rightX - leftX + 1);
- if (!halFastFillRect(s, leftX, y, (uint16_t)spanW, 1, newNibble)) {
- fillRectClipped(s, leftX, y, spanW, 1, newNibble);
- }
- halFillRectPlanes(s, leftX, y, (uint16_t)spanW, 1, newNibble);
+ fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
}
// Scan rows above and below for run boundaries. The hot
@@ -320,6 +326,36 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
}
+// Plot a single clipped pixel WITHOUT marking the dirty rect. Shared by
+// the public jlDrawPixel (which marks per call afterward) and the
+// Bresenham line/circle fallbacks (which accumulate one bounding box and
+// mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds
+// check, halFastDrawPixel dispatch, and chunky nibble RMW fallback.
+static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
+ uint8_t *byte;
+ uint8_t nibble;
+
+ if (s == NULL) {
+ return;
+ }
+ if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
+ return;
+ }
+
+ if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) {
+ /* Cast to uint16_t before shift -- already validated x >= 0,
+ * so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
+ byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
+ nibble = colorIndex & 0x0F;
+ if (x & 1) {
+ *byte = (uint8_t)((*byte & 0xF0) | nibble);
+ } else {
+ *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
+ }
+ }
+}
+
+
// ----- Public API (alphabetical) -----
void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
@@ -327,6 +363,10 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t y;
int16_t err;
int16_t ir;
+ int16_t minX;
+ int16_t minY;
+ int16_t maxX;
+ int16_t maxY;
if (s == NULL) {
return;
@@ -338,34 +378,57 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// Fast path: when the bounding circle is fully on-surface we can
// hand off to the port asm (no per-pixel bounds check needed in
- // the inner loop) and mark the bounding box dirty once.
- ir = (int16_t)r;
- if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
- cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
- halFastDrawCircle(s, cx, cy, r, colorIndex)) {
- surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
- (uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
- return;
+ // the inner loop) and mark the bounding box dirty once. A circle
+ // with r >= SURFACE_HEIGHT can never fit on-surface, and casting
+ // such an r to int16_t can wrap negative (r > 32767), so reject it
+ // here before the cast feeds the fast path / dirty-rect extents.
+ if (r < SURFACE_HEIGHT) {
+ ir = (int16_t)r;
+ if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
+ cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
+ halFastDrawCircle(s, cx, cy, r, colorIndex)) {
+ surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
+ (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
+ return;
+ }
}
// Bresenham midpoint: maintain (x, y) on the perimeter, eight-
- // octant symmetry plots all 8 reflections each iteration. Routes
- // through jlDrawPixel so off-surface pixels clip individually.
+ // octant symmetry plots all 8 reflections each iteration. Plots
+ // through plotPixelNoMark so off-surface pixels clip individually
+ // (same per-pixel clip as jlDrawPixel) without paying the
+ // cross-segment dirty mark per pixel; one accumulated bounding box
+ // is marked after the loop instead.
+ minX = DIRTY_BOX_MIN_INIT;
+ minY = DIRTY_BOX_MIN_INIT;
+ maxX = DIRTY_BOX_MAX_INIT;
+ maxY = DIRTY_BOX_MAX_INIT;
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
- jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
- jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
- jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
- jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
- jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
- jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
- jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
- jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
+ // The 8 octants span columns cx +/- x and cx +/- y, and rows
+ // cy +/- y and cy +/- x. Fold all reflections into the box in
+ // one shot, then clamp once after the loop.
+ if (cx + x > maxX) { maxX = (int16_t)(cx + x); }
+ if (cx - x < minX) { minX = (int16_t)(cx - x); }
+ if (cx + y > maxX) { maxX = (int16_t)(cx + y); }
+ if (cx - y < minX) { minX = (int16_t)(cx - y); }
+ if (cy + x > maxY) { maxY = (int16_t)(cy + x); }
+ if (cy - x < minY) { minY = (int16_t)(cy - x); }
+ if (cy + y > maxY) { maxY = (int16_t)(cy + y); }
+ if (cy - y < minY) { minY = (int16_t)(cy - y); }
+ plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
+ plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
y++;
- /* Use `+ + 1` instead of `2 * y + 1` so ORCA-C never emits
- * the ~SMUL2 helper -- two ADDs are unconditionally cheaper. */
+ /* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits
+ * a multiply helper -- two ADDs are unconditionally cheaper. */
if (err <= 0) {
err = (int16_t)(err + y + y + 1);
} else {
@@ -373,6 +436,17 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
err = (int16_t)(err + y + y - x - x + 1);
}
}
+
+ // Clamp the accumulated box to the surface and mark it once. If the
+ // circle was entirely off-surface no pixel was plotted, so minX/minY
+ // stay above maxX/maxY (after clamping) and nothing is marked.
+ if (minX < 0) { minX = 0; }
+ if (minY < 0) { minY = 0; }
+ if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
+ if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
+ if (minX <= maxX && minY <= maxY) {
+ surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
+ }
}
@@ -384,13 +458,18 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
int16_t err;
int16_t e2;
int16_t tmp;
+ int16_t minX;
+ int16_t minY;
+ int16_t maxX;
+ int16_t maxY;
+ int32_t span;
if (s == NULL) {
return;
}
// Horizontal and vertical fast paths use jlFillRect; the general
- // case Bresenham routes per-pixel through jlDrawPixel so per-pixel
+ // case Bresenham plots per-pixel through plotPixelNoMark so per-pixel
// off-surface clipping just works.
if (y0 == y1) {
if (x0 > x1) {
@@ -398,7 +477,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
x0 = x1;
x1 = tmp;
}
- jlFillRect(s, x0, y0, (uint16_t)(x1 - x0 + 1), 1, colorIndex);
+ // Compute the span in 32-bit so far-apart off-surface endpoints
+ // (e.g. x0=-20000, x1=20000) do not overflow the 16-bit int the
+ // subtraction would otherwise promote to on a 16-bit-int target;
+ // clamp to the surface width so the downstream uint16_t narrowing
+ // is safe.
+ span = (int32_t)x1 - (int32_t)x0 + 1;
+ if (span > SURFACE_WIDTH) {
+ span = SURFACE_WIDTH;
+ }
+ // On-surface (the common case) skips jlFillRect's 32-bit clip:
+ // span is already <= SURFACE_WIDTH and x0 <= x1, so the row is
+ // fully on-surface exactly when its left edge and y are. Off-
+ // surface still routes through jlFillRect for correct clipping.
+ if (x0 >= 0 && y0 >= 0 && y0 < SURFACE_HEIGHT &&
+ (int32_t)x0 + span <= SURFACE_WIDTH) {
+ fillRectOnSurface(s, x0, y0, (int16_t)span, 1, colorIndex);
+ } else {
+ jlFillRect(s, x0, y0, (uint16_t)span, 1, colorIndex);
+ }
return;
}
if (x0 == x1) {
@@ -407,7 +504,16 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
y0 = y1;
y1 = tmp;
}
- jlFillRect(s, x0, y0, 1, (uint16_t)(y1 - y0 + 1), colorIndex);
+ span = (int32_t)y1 - (int32_t)y0 + 1;
+ if (span > SURFACE_HEIGHT) {
+ span = SURFACE_HEIGHT;
+ }
+ if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 &&
+ (int32_t)y0 + span <= SURFACE_HEIGHT) {
+ fillRectOnSurface(s, x0, y0, 1, (int16_t)span, colorIndex);
+ } else {
+ jlFillRect(s, x0, y0, 1, (uint16_t)span, colorIndex);
+ }
return;
}
@@ -426,13 +532,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
return;
}
+ // Diagonal fallback: plot through plotPixelNoMark so each pixel still
+ // clips off-surface individually (matching jlDrawPixel), but skip the
+ // per-pixel cross-segment dirty mark. Accumulate one bounding box over
+ // the plotted points and mark it once after the loop.
+ minX = DIRTY_BOX_MIN_INIT;
+ minY = DIRTY_BOX_MIN_INIT;
+ maxX = DIRTY_BOX_MAX_INIT;
+ maxY = DIRTY_BOX_MAX_INIT;
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
sx = (int16_t)((x0 < x1) ? 1 : -1);
sy = (int16_t)((y0 < y1) ? 1 : -1);
err = (int16_t)(dx + dy);
while (1) {
- jlDrawPixel(s, x0, y0, colorIndex);
+ if (x0 < minX) { minX = x0; }
+ if (x0 > maxX) { maxX = x0; }
+ if (y0 < minY) { minY = y0; }
+ if (y0 > maxY) { maxY = y0; }
+ plotPixelNoMark(s, x0, y0, colorIndex);
if (x0 == x1 && y0 == y1) {
break;
}
@@ -446,32 +564,55 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
y0 = (int16_t)(y0 + sy);
}
}
+
+ // Clamp the accumulated box to the surface and mark it once. A line
+ // entirely off-surface still ran the loop (so the box holds its raw
+ // extents); clamping collapses it to a no-op when nothing landed on
+ // the surface.
+ if (minX < 0) { minX = 0; }
+ if (minY < 0) { minY = 0; }
+ if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
+ if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
+ if (minX <= maxX && minY <= maxY) {
+ surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
+ }
}
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
- uint8_t *byte;
- uint8_t nibble;
-
if (s == NULL) {
return;
}
- if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
+#ifdef JOEYLIB_PLATFORM_IIGS
+ // Hot per-pixel path: the asm plot (halFastDrawPixel -> iigsDrawPixelInner)
+ // plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect
+ // call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table,
+ // no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) --
+ // the far-pointer code for s->pixels[...] costs more than the
+ // cross-segment JSL into the hand-tuned asm, so the asm plot stays. The
+ // single-row dirty mark is widened inline, only for the stage.
+ if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
return;
}
+ (void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
+ if (s == jlStageGet()) {
+ uint8_t word = SURFACE_WORD_INDEX(x);
- if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex)) {
- /* Cast to uint16_t before shift -- already validated x >= 0,
- * so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
- byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
- nibble = colorIndex & 0x0F;
- if (x & 1) {
- *byte = (uint8_t)((*byte & 0xF0) | nibble);
- } else {
- *byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
+ if (word < gStageMinWord[y]) {
+ gStageMinWord[y] = word;
+ }
+ if (word > gStageMaxWord[y]) {
+ gStageMaxWord[y] = word;
}
}
+#else
+ plotPixelNoMark(s, x, y, colorIndex);
+ // Public single-pixel path keeps the original per-call dirty mark
+ // with the raw (x, y); only the Bresenham fallbacks batch the mark
+ // into one accumulated box. plotPixelNoMark already no-op'd the
+ // pixel itself if it was off-surface.
surfaceMarkDirtyRect(s, x, y, 1, 1);
+#endif
}
@@ -489,6 +630,26 @@ void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
jlFillRect(s, x, y, w, h, colorIndex);
return;
}
+ // Fast path: when the OUTER rect is fully on-surface every one of
+ // the four edge spans is also fully on-surface, so each can skip
+ // jlFillRect's 32-bit clip and route straight through the shared
+ // on-surface tail. Test in 32-bit -- w/h are uint16_t, so x + w can
+ // exceed int16_t range. Output is identical because on an
+ // on-surface span jlFillRect's clip is a no-op.
+ if (x >= 0 && y >= 0 &&
+ (int32_t)x + (int32_t)w <= SURFACE_WIDTH &&
+ (int32_t)y + (int32_t)h <= SURFACE_HEIGHT) {
+ // Top edge.
+ fillRectOnSurface(s, x, y, (int16_t)w, 1, colorIndex);
+ // Bottom edge.
+ fillRectOnSurface(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex);
+ // Left edge (interior only -- top and bottom corners already drawn).
+ fillRectOnSurface(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
+ // Right edge (interior only).
+ fillRectOnSurface(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
+ return;
+ }
+ // General (possibly clipped) case: let jlFillRect clip each edge.
// Top edge.
jlFillRect(s, x, y, w, 1, colorIndex);
// Bottom edge.
@@ -504,9 +665,10 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t y;
int16_t x;
int16_t ir;
- uint16_t xx;
- uint16_t yy;
- uint16_t r2;
+ uint32_t xx;
+ uint32_t yy;
+ uint32_t r2;
+ bool onSurface;
if (s == NULL) {
return;
@@ -516,26 +678,39 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
return;
}
- ir = (int16_t)r;
- if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
- cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
- halFastFillCircle(s, cx, cy, r, colorIndex)) {
- surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
- (uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
- return;
+ // Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully
+ // on-surface, and casting such an r to int16_t can wrap negative
+ // (r > 32767), spuriously passing the bounds test and feeding the
+ // asm / dirty-rect garbage. Reject it here before the cast.
+ onSurface = false;
+ if (r < SURFACE_HEIGHT) {
+ ir = (int16_t)r;
+ if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
+ cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
+ // The whole bounding circle is on-surface, so every scanline
+ // span below is too. Remember this so the C fallback can
+ // route spans through the lighter on-surface fill instead of
+ // re-clipping each one through jlFillRect.
+ onSurface = true;
+ if (halFastFillCircle(s, cx, cy, r, colorIndex)) {
+ surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
+ (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
+ return;
+ }
+ }
}
// For each y from 0 to r, find the largest x such that x*x + y*y
// <= r*r and emit a horizontal span. Maintain xx=x*x, yy=y*y
// incrementally so the hot loop never does a 32-bit multiply --
// critical on 65816 / 68000 / 286 where mul is slow or absent.
- // (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. r is uint16_t
- // so xx, yy, r2 fit in uint16_t for any r where x*x+y*y can equal
- // r2 (i.e. r <= 255 -> r2 <= 65025).
- /* Same `+ +` pattern as jlDrawCircle so ORCA-C doesn't emit ~SMUL2 /
- * ~CUMUL2 helpers for the `2 * ...` constants. spanWidth is hoisted
+ // (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are
+ // uint32_t so r*r does not wrap for r >= 256 (a uint16_t product
+ // overflows at r == 256 -> r2 == 0).
+ /* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit
+ * multiply helpers for the `2 * ...` constants. spanWidth is hoisted
* because both jlFillRect calls in the body need it. */
- xx = (uint16_t)(r * r);
+ xx = (uint32_t)r * (uint32_t)r;
r2 = xx;
yy = 0;
x = (int16_t)r;
@@ -543,15 +718,25 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
uint16_t spanWidth;
while (xx + yy > r2) {
- xx = (uint16_t)(xx - (uint16_t)((uint16_t)x + (uint16_t)x - 1u));
+ xx = xx - (uint32_t)((uint16_t)x + (uint16_t)x - 1u);
x--;
}
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
- jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex);
- if (y > 0) {
- jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex);
+ if (onSurface) {
+ // 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and
+ // 0 <= cx - x ... cx + x < SURFACE_WIDTH for every span, so
+ // jlFillRect's clip would be a no-op -- skip it.
+ fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy + y), (int16_t)spanWidth, 1, colorIndex);
+ if (y > 0) {
+ fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy - y), (int16_t)spanWidth, 1, colorIndex);
+ }
+ } else {
+ jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex);
+ if (y > 0) {
+ jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex);
+ }
}
- yy = (uint16_t)(yy + (uint16_t)((uint16_t)y + (uint16_t)y + 1u));
+ yy = yy + (uint32_t)((uint16_t)y + (uint16_t)y + 1u);
}
}
@@ -561,25 +746,40 @@ void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
int16_t sy;
int16_t sw;
int16_t sh;
- bool visible;
+ int32_t left;
+ int32_t top;
+ int32_t right;
+ int32_t bottom;
if (s == NULL) {
return;
}
- sx = x;
- sy = y;
- sw = (int16_t)w;
- sh = (int16_t)h;
- clipRect(&sx, &sy, &sw, &sh, &visible);
- if (!visible) {
+ // Clip in 32-bit so any int16_t x/y combined with any uint16_t w/h
+ // is handled without overflow (a uint16_t w > 32767 would wrap
+ // negative if narrowed to int16_t before clipping, and a negative x
+ // with a large w must still fill out to the right surface edge).
+ // The surface is only 320x200, so the clipped result always fits
+ // int16_t.
+ left = (int32_t)x;
+ top = (int32_t)y;
+ right = left + (int32_t)w;
+ bottom = top + (int32_t)h;
+ if (left < 0) { left = 0; }
+ if (top < 0) { top = 0; }
+ if (right > SURFACE_WIDTH) { right = SURFACE_WIDTH; }
+ if (bottom > SURFACE_HEIGHT) { bottom = SURFACE_HEIGHT; }
+ if (right <= left || bottom <= top) {
return;
}
- if (!halFastFillRect(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex)) {
- fillRectClipped(s, sx, sy, sw, sh, colorIndex);
- }
- halFillRectPlanes(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex);
- surfaceMarkDirtyRect(s, sx, sy, sw, sh);
+ sx = (int16_t)left;
+ sy = (int16_t)top;
+ sw = (int16_t)(right - left);
+ sh = (int16_t)(bottom - top);
+ // The clipped rect is now provably on-surface; share the fill +
+ // planar dual-write + dirty-mark tail with the other on-surface
+ // span emitters so there is one source of truth for it.
+ fillRectOnSurface(s, sx, sy, sw, sh, colorIndex);
}
@@ -649,7 +849,7 @@ void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) {
}
nibble = colorIndex & 0x0F;
doubled = (uint8_t)((nibble << 4) | nibble);
- if (!halFastSurfaceClear(s, doubled)) {
+ if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) {
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
}
surfaceMarkDirtyAll(s);
diff --git a/src/core/hal.h b/src/core/hal.h
index 71f27f2..ca720de 100644
--- a/src/core/hal.h
+++ b/src/core/hal.h
@@ -191,8 +191,12 @@ uint16_t halFrameHz(void);
// Wall-clock millisecond counter since halInit. Source of truth
// for time-based scheduling (sound event timing, animation cadence
-// on platforms where rendering can outpace VBL). Per-port impl
-// uses the cheapest sub-frame timer available on that hardware.
+// on platforms where rendering can outpace VBL). Per-port impl uses
+// the cheapest sub-frame timer available on that hardware: DOS uses
+// uclock() (PIT counter 0, ~1 us resolution); ST reads the 200 Hz
+// Timer C counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL);
+// IIgs falls back to frame_count * 1000 / 60 since pulling in time()
+// bursts the stdio cluster bank.
uint32_t halMillisElapsed(void);
// Audio: per-port engine setup, module + SFX playback, teardown.
@@ -234,7 +238,7 @@ void halAudioFrameTick(void);
// caller should fall back to the platform-agnostic C implementation.
//
// Funneling all asm dispatches through hal.c (one TU per port) avoids
-// the cumulative ORCA Linker "Expression too complex" failure that
+// the cumulative linker "Expression too complex" failure that
// hits when multiple cross-platform TUs each call into a named load
// segment full of asm primitives. Cross-platform code in src/core/
// only ever calls into HAL, so the link-time expression cost is paid
@@ -358,7 +362,7 @@ bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
//
// Cross-platform code in src/core/*.c is unchanged -- it still calls
// halFastDrawPixel(...) etc. On IIgs the preprocessor swaps that for
-// the macro expansion before ORCA-C compiles the file. The matching
+// the macro expansion before the file is compiled. The matching
// halFast* C definitions in src/port/iigs/hal.c are deleted, since
// nothing references them once the macros take effect.
//
@@ -371,6 +375,7 @@ extern void iigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uin
extern void iigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
+extern void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord);
extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t fillWord);
extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
@@ -416,7 +421,7 @@ extern uint16_t gFloodRightX;
#undef halFastSurfaceClear
#define halFastSurfaceClear(_s, _d) \
((_s) == jlStageGet() \
- ? (iigsSurfaceClearInner((_s)->pixels, \
+ ? (iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \
true) \
: false)
@@ -438,7 +443,7 @@ extern uint16_t gFloodRightX;
// Tile primitives operate on caller-computed row pointers; just
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte
// offset within the surface. Use SURFACE_ROW_OFFSET (LUT lookup) to
-// dodge ORCA-C 2.2.1's ~CUMUL2 helper for the *160 multiply.
+// dodge a software multiply helper for the *160 multiply.
#undef halFastTileFill
#define halFastTileFill(_s, _bx, _by, _fw) \
(iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
@@ -461,8 +466,8 @@ extern uint16_t gFloodRightX;
// Tier 2/3 flood fallbacks always returned false on IIgs (the asm
// impls were deleted as unreachable). Macros to constant false so
-// ORCA-C dead-code-eliminates the never-taken fallback branches in
-// floodFillInternal.
+// the compiler dead-code-eliminates the never-taken fallback branches
+// in floodFillInternal.
#undef halFastFloodWalk
#define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false)
@@ -494,6 +499,63 @@ extern uint16_t gFloodRightX;
*(_rxOut) = (int16_t)gFloodRightX, \
true)
+// =====================================================================
+// IIgs planar dual-write elision.
+//
+// Every cross-platform primitive calls a halXxxPlanes() hook
+// unconditionally so planar ports (Amiga, ST) can mirror each draw
+// into their bitplanes. IIgs is chunky-native, so those hooks are
+// pure no-op stubs -- but the call site still costs a cross-segment
+// JSL per primitive (PHB/PHD setup + RTL) for zero work. These macros
+// expand each call to ((void)0) at preprocess time so the call site is
+// deleted entirely, exactly as the halFast* macros above eliminate the
+// wrapper layer. The matching no-op stub definitions in
+// src/port/iigs/hal.c are deleted, since nothing references them once
+// the macros take effect.
+//
+// Gated on JOEYLIB_PLATFORM_IIGS specifically (NOT JOEYLIB_NATIVE_CHUNKY):
+// DOS is also chunky-native and keeps its real stub functions, because
+// its stubs are cheap near-calls and DOS is not the perf reference.
+//
+// Arg lists match each hook's arity so the preprocessor parses and
+// discards the exact call expression; arguments with side effects are
+// not present at any halXxxPlanes call site (all args are plain lvalues
+// / field reads), so dropping them changes no behavior.
+// =====================================================================
+
+#undef halFillRectPlanes
+#define halFillRectPlanes(_s, _x, _y, _w, _h, _c) ((void)0)
+
+#undef halSurfaceCopyPlanes
+#define halSurfaceCopyPlanes(_dst, _src) ((void)0)
+
+#undef halTileFillPlanes
+#define halTileFillPlanes(_s, _bx, _by, _c) ((void)0)
+
+#undef halTileCopyPlanes
+#define halTileCopyPlanes(_dst, _dbx, _dby, _src, _sbx, _sby) ((void)0)
+
+#undef halTileCopyMaskedPlanes
+#define halTileCopyMaskedPlanes(_dst, _dbx, _dby, _src, _sbx, _sby, _t) ((void)0)
+
+#undef halTilePastePlanes
+#define halTilePastePlanes(_dst, _bx, _by, _ct) ((void)0)
+
+#undef halTilePasteMonoPlanes
+#define halTilePasteMonoPlanes(_dst, _bx, _by, _mt, _fg, _bg) ((void)0)
+
+#undef halTileSnapPlanes
+#define halTileSnapPlanes(_src, _bx, _by, _out) ((void)0)
+
+#undef halSpriteDrawPlanes
+#define halSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
+
+#undef halSpriteSavePlanes
+#define halSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
+
+#undef halSpriteRestorePlanes
+#define halSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
+
#endif /* JOEYLIB_PLATFORM_IIGS */
#endif
diff --git a/src/core/init.c b/src/core/init.c
index 386bfea..dfc2850 100644
--- a/src/core/init.c
+++ b/src/core/init.c
@@ -15,13 +15,16 @@
// 8 KB fits the largest typical sprite working set (~3-4 KB per
// 32x32 sprite at all opaque) and keeps malloc requests small enough
-// for IIgs ORCA-C's small-memory-model heap to satisfy them.
+// for the IIgs heap to satisfy them.
#define DEFAULT_CODEGEN_BYTES (8u * 1024u)
// ----- Prototypes -----
static void clearError(void);
-static void setError(const char *message);
+
+// coreSetError is the shared error setter (declared in surfaceInternal.h
+// and called by surface.c etc.); its definition lives here next to the
+// rest of the error-reporting state.
// ----- Module state -----
@@ -36,7 +39,7 @@ static void clearError(void) {
}
-static void setError(const char *message) {
+void coreSetError(const char *message) {
gLastError = message;
}
@@ -47,12 +50,12 @@ bool jlInit(const jlConfigT *config) {
clearError();
if (gInitialized) {
- setError("jlInit called while already initialized");
+ coreSetError("jlInit called while already initialized");
return false;
}
if (config == NULL) {
- setError("jlInit called with NULL config");
+ coreSetError("jlInit called with NULL config");
return false;
}
@@ -63,19 +66,19 @@ bool jlInit(const jlConfigT *config) {
// SHR setup that halInit performs.
if (!halInit(&gConfig)) {
const char *halMsg = halLastError();
- setError(halMsg != NULL ? halMsg : "halInit failed");
+ coreSetError(halMsg != NULL ? halMsg : "halInit failed");
return false;
}
if (!stageAlloc()) {
- setError("failed to allocate stage surface");
+ coreSetError("failed to allocate stage surface");
halShutdown();
return false;
}
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes
: DEFAULT_CODEGEN_BYTES)) {
- setError("failed to allocate codegen arena");
+ coreSetError("failed to allocate codegen arena");
stageFree();
halShutdown();
return false;
diff --git a/src/core/input.c b/src/core/input.c
index 22bd13d..13c8296 100644
--- a/src/core/input.c
+++ b/src/core/input.c
@@ -39,7 +39,7 @@ extern void iigsInputSnapshot(void);
// and the small button counts, and walks every array one byte per
// element. If a future change adds/removes keys or buttons the asm
// must be updated; if anyone re-types the arrays back to bool the
-// per-element size grows to ORCA-C's 2-byte _Bool and the asm reads
+// per-element size could grow past one byte and the asm reads
// the wrong bytes. Either condition declares a zero-size array
// below, which is a compile error.
typedef int joey_keycount_check [(KEY_COUNT == 60) ? 1 : -1];
@@ -53,7 +53,7 @@ typedef int joey_joybtn_size_check [(sizeof(gJoyButtonState) == JOYSTICK_COU
void jlInputPoll(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
// One asm pass for: TTL decrement + key snapshot + mouse/joy
- // button snapshots. Replaces 3 ORCA-C memcpys + the C TTL loop
+ // button snapshots. Replaces 3 memcpys + the C TTL loop
// that used to live in halInputPoll. ~0.6 ms saved per frame.
iigsInputSnapshot();
#else
@@ -88,8 +88,7 @@ void jlWaitForAnyKey(void) {
* and upper-bound check (`>= COUNT`) into a single unsigned compare.
* Index 0 (KEY_NONE / MOUSE_BUTTON_NONE) is a sentinel that no HAL
* ever writes, so reading gKeyState[0] / gMouseButtonState[0] is
- * always 0 -- the predicate result is unchanged but ORCA-C drops the
- * compound `||` into one branch each. */
+ * always 0 -- the predicate result is unchanged. */
bool jlKeyDown(jlKeyE key) {
if ((uint16_t)key >= (uint16_t)KEY_COUNT) {
return false;
@@ -172,9 +171,9 @@ int8_t jlJoystickY(jlJoystickE js) {
}
-/* Joystick button predicates: ORCA-C 2.2.1 lowers `gJoyButtonState[js][button]`
- * to a ~MUL4 helper per access. Compute the 1D byte index once and read
- * via an explicit (uint8_t *) cast -- no helpers. */
+/* Joystick button predicates: compute the 1D byte index once and read
+ * via an explicit (uint8_t *) cast so the 2D-array indexing avoids a
+ * per-access multiply helper. */
bool jlJoyDown(jlJoystickE js, jlJoyButtonE button) {
uint16_t idx;
if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) {
diff --git a/src/core/inputInternal.h b/src/core/inputInternal.h
index 4a6c455..1579e2a 100644
--- a/src/core/inputInternal.h
+++ b/src/core/inputInternal.h
@@ -12,13 +12,13 @@
#include "joey/input.h"
#include "joey/types.h"
-// Stored as uint8_t (not bool) because ORCA-C compiles _Bool as a
-// 2-byte word (Symbol.pas: size := cgWordSize). The IIgs asm fast
-// path (iigsInputSnapshot) walks these arrays one byte per element;
-// a 2-byte bool would put element k at byte offset 2*k and the asm's
-// per-byte clear would never reach the live half. uint8_t pins the
-// storage to one byte per element on every port. Public predicates
-// still return bool via implicit coercion.
+// Stored as uint8_t (not bool) because a _Bool wider than one byte
+// would break the IIgs asm fast path (iigsInputSnapshot), which walks
+// these arrays one byte per element; with a 2-byte bool element k
+// would sit at byte offset 2*k and the asm's per-byte clear would
+// never reach the live half. uint8_t pins the storage to one byte per
+// element on every port. Public predicates still return bool via
+// implicit coercion.
extern uint8_t gKeyState[KEY_COUNT];
extern uint8_t gKeyPrev [KEY_COUNT];
diff --git a/src/core/palette.c b/src/core/palette.c
index e02aa33..fb60cc1 100644
--- a/src/core/palette.c
+++ b/src/core/palette.c
@@ -10,6 +10,14 @@
#include "joey/palette.h"
#include "surfaceInternal.h"
+// jlPaletteSet / jlPaletteGet reach palette row N via byte-pointer math
+// + a single `<< 5` shift (= 32 bytes per row) to avoid a per-access
+// multiply helper. That stride is only correct while a palette row is
+// exactly SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t) bytes; pin the
+// relationship so a future palette-width change fails to compile here
+// rather than silently reading the wrong row.
+typedef char paletteRowStrideMatchesShift[((1u << 5) == (SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t))) ? 1 : -1];
+
// Standard 16-color EGA palette in IIgs $0RGB format. Used as the
// per-surface default at allocation time (paletteInitDefault) so a
@@ -45,8 +53,14 @@ void paletteInitDefault(jlSurfaceT *s) {
if (s == NULL) {
return;
}
+ // kDefaultPaletteEga is already valid $0RGB with color 0 == 0x0000,
+ // so copy it directly into each palette row instead of routing
+ // through jlPaletteSet 16 times (which would re-mask every entry
+ // and re-run the stage-dirty check on a surface that is not yet
+ // the stage). gStagePaletteDirty already defaults to true so the
+ // default palette is still uploaded on first present.
for (i = 0; i < SURFACE_PALETTE_COUNT; i++) {
- jlPaletteSet(s, i, kDefaultPaletteEga);
+ memcpy(s->palette[i], kDefaultPaletteEga, sizeof(kDefaultPaletteEga));
}
}
@@ -62,7 +76,7 @@ void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16) {
if (paletteIndex >= SURFACE_PALETTE_COUNT) {
return;
}
- /* Byte-pointer math + shift to skip the ~MUL4 helper -- see
+ /* Byte-pointer math + shift to skip the multiply helper -- see
* jlPaletteSet for the reasoning. */
row = (const uint16_t *)((const uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
memcpy(out16, row, SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t));
@@ -82,10 +96,10 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16)
}
/* Compute the row pointer via byte-pointer math + a single shift
- * (16 entries * 2 bytes = 32 = 1 << 5) so ORCA-C doesn't emit a
- * ~MUL4 helper for the 2D-array indexing. Then walk both arrays
- * with post-increment pointers so the inner loop avoids ~MUL4
- * for every `row[i]` / `colors16[i]` index multiply too. */
+ * (16 entries * 2 bytes = 32 = 1 << 5) so the 2D-array indexing
+ * avoids a multiply helper. Then walk both arrays with
+ * post-increment pointers so the inner loop avoids the index
+ * multiply for every `row[i]` / `colors16[i]` too. */
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
src = colors16;
diff --git a/src/core/random.c b/src/core/random.c
index 75bf6da..e9eeef6 100644
--- a/src/core/random.c
+++ b/src/core/random.c
@@ -1,8 +1,8 @@
// Deterministic, seedable PRNG (xorshift32 / Marsaglia).
//
-// stdlib rand() is unsuitable: DJGPP, ORCA-C, libnix, and mintlib all
-// ship different LCG constants, and ORCA-C's rand() returns a 16-bit
-// value while the others return 31 bits. Any game logic relying on
+// stdlib rand() is unsuitable: the per-platform C libraries ship
+// different LCG constants and differing result widths (some return a
+// 16-bit value, others 31 bits). Any game logic relying on
// reproducible sequences (replay, demo records, level seeds, deck
// shuffles) would diverge silently. xorshift32 has no multiply, so
// the 65816 perf floor isn't penalized by a software mul routine.
@@ -32,7 +32,12 @@ uint16_t jlRandomRange(uint16_t bound) {
if (bound == 0u) {
return 0u;
}
- return (uint16_t)(jlRandom() % bound);
+ // Multiply-high map over the top 16 (best-mixed) bits of the
+ // xorshift state. Divide-free -- the 65816/68000 lack a hardware
+ // divide, so plain `% bound` would pull in a software divide
+ // routine the multiply-free PRNG was chosen to avoid. Residual
+ // bias is the same negligible order as modulo.
+ return (uint16_t)(((uint32_t)(jlRandom() >> 16) * (uint32_t)bound) >> 16);
}
diff --git a/src/core/scb.c b/src/core/scb.c
index 178c4b2..42490cd 100644
--- a/src/core/scb.c
+++ b/src/core/scb.c
@@ -35,7 +35,6 @@ void jlScbSet(jlSurfaceT *s, uint16_t line, uint8_t paletteIndex) {
void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex) {
- uint16_t line;
uint16_t last;
if (s == NULL) {
@@ -56,12 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
return;
}
- // memset is far cheaper than the per-iter loop on ORCA-C with -b
- // (scb is uint8_t, sizeof(uint8_t)==1, so the call form below is
- // exact). On IIgs ORCA-C lowers small fixed-size memsets to MVP /
- // PEI tricks; on Amiga/ST/DOS it uses libc memset which is
- // already vectorized. Either way, much tighter than the C loop.
- (void)line;
+ // memset is far cheaper than the per-iter loop (scb is uint8_t,
+ // sizeof(uint8_t)==1, so the call form below is exact). On IIgs
+ // small fixed-size memsets lower to MVP / PEI tricks; on
+ // Amiga/ST/DOS libc memset is already vectorized. Either way, much
+ // tighter than the C loop.
memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1));
if (s == jlStageGet()) {
gStageScbDirty = true;
diff --git a/src/core/sprite.c b/src/core/sprite.c
index 968f973..466aa6d 100644
--- a/src/core/sprite.c
+++ b/src/core/sprite.c
@@ -8,16 +8,15 @@
#include
#include "joey/sprite.h"
+#include "joey/tile.h"
#include "codegenArenaInternal.h"
#include "hal.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"
-// 8x8 tiles, 4bpp packed = 4 bytes/row * 8 rows = 32 bytes/tile.
-#define TILE_BYTES 32
-#define TILE_PIXELS 8
-#define TILE_BYTES_PER_ROW 4
+// 8x8 tile geometry constants come from joey/tile.h -- one source of
+// truth for the 4bpp packed layout (32 bytes/tile, 4 bytes/row, 8 px/side).
// Color 0 is always transparent for sprites (DESIGN.md contract).
#define TRANSPARENT_NIBBLE 0
@@ -43,10 +42,10 @@
// ----- Prototypes -----
-static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY);
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble);
static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h);
static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t heightPx);
+static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
@@ -85,34 +84,6 @@ static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY,
}
-// Sample one source pixel from the sprite at (pxX, pxY) where (0,0)
-// is the sprite's top-left tile. The tile data is laid out tile by
-// tile in row-major order, each tile internally row-major with 4
-// bytes per row. High nibble = left pixel within each byte.
-static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY) {
- int16_t tileX;
- int16_t tileY;
- int16_t inTileX;
- int16_t inTileY;
- uint32_t byteOff;
- uint8_t byte;
- const uint8_t *tile;
-
- tileX = (int16_t)(pxX / TILE_PIXELS);
- tileY = (int16_t)(pxY / TILE_PIXELS);
- inTileX = (int16_t)(pxX & (TILE_PIXELS - 1));
- inTileY = (int16_t)(pxY & (TILE_PIXELS - 1));
-
- tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
- byteOff = (uint32_t)(inTileY * TILE_BYTES_PER_ROW + (inTileX >> 1));
- byte = tile[byteOff];
- if (inTileX & 1) {
- return (uint8_t)(byte & 0x0F);
- }
- return (uint8_t)(byte >> 4);
-}
-
-
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble) {
uint8_t *byte;
@@ -139,9 +110,34 @@ static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t he
}
+// Initialize a freshly-allocated jlSpriteT's fields to the
+// uncompiled/default state. routineOffsets is filled with 0xFF so
+// every (shift, op) entry reads as SPRITE_NOT_COMPILED (0xFFFF); 0
+// would be a VALID offset and could dispatch into stale arena bytes.
+static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) {
+ sp->tileData = tileData;
+ sp->widthTiles = widthTiles;
+ sp->heightTiles = heightTiles;
+ sp->ownsTileData = ownsTileData;
+ sp->slot = NULL;
+ memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
+ memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
+ memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
+ memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
+}
+
+
// Pixel-by-pixel draw. Used directly when the sprite has no compiled
// slot, and as the clip-edge fallback when a compiled draw would
// extend off the surface.
+//
+// The source sprite is tile-major (8x8 tiles, 4 bytes/row, consecutive
+// tile columns 32 bytes apart). Rather than recompute tile/in-tile
+// coordinates per pixel (the old srcNibble did two divisions + a
+// multiply EVERY pixel), this walks one source-tile-row pointer that
+// advances by TILE_BYTES each time the column crosses an 8-pixel tile
+// boundary, so the inner loop is a byte read + nibble extract with no
+// division. ~5x on the interpreted path (the every-edge-clip fallback).
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
int16_t dx;
int16_t dy;
@@ -150,17 +146,16 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
int16_t w;
int16_t h;
int16_t row;
- int16_t col;
- uint8_t nibble;
- uint8_t *dstRow;
+ uint8_t wTiles;
dx = x;
dy = y;
- w = (int16_t)(sp->widthTiles * TILE_PIXELS);
- h = (int16_t)(sp->heightTiles * TILE_PIXELS);
+ w = (int16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
+ h = (int16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) {
return;
}
+ wTiles = sp->widthTiles;
/* Skip the chunky write loop on planar ports (s->pixels == NULL).
* halSpriteDrawPlanes is called by the jlSpriteDraw caller and does
@@ -168,13 +163,40 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
* happen there. Phase 9 dropped the chunky shadow on Amiga. */
if (s->pixels != NULL) {
for (row = 0; row < h; row++) {
- dstRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW];
+ uint8_t *dstRow;
+ const uint8_t *srcTileRow;
+ uint16_t srcY;
+ uint16_t tileRowBase;
+ uint16_t inTileX;
+ int16_t col;
+
+ dstRow = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)(dy + row))];
+ srcY = (uint16_t)(sy + row);
+ /* Byte offset to tile-column 0's data for this pixel row:
+ * (tileY * wTiles) tiles in, each TILE_BYTES, plus the
+ * in-tile row. Shifts dodge multiply helpers
+ * (TILE_BYTES == 1<<5, TILE_BYTES_PER_ROW == 1<<2). */
+ tileRowBase = (uint16_t)((((uint16_t)(srcY >> 3) * wTiles) << 5)
+ + ((srcY & 7u) << 2));
+ inTileX = (uint16_t)((uint16_t)sx & 7u);
+ srcTileRow = sp->tileData + tileRowBase
+ + (((uint16_t)((uint16_t)sx >> 3)) << 5);
+
for (col = 0; col < w; col++) {
- nibble = srcNibble(sp, (int16_t)(sx + col), (int16_t)(sy + row));
- if (nibble == TRANSPARENT_NIBBLE) {
- continue;
+ uint8_t byte;
+ uint8_t nibble;
+
+ byte = srcTileRow[inTileX >> 1];
+ nibble = (inTileX & 1u) ? (uint8_t)(byte & 0x0Fu)
+ : (uint8_t)(byte >> 4);
+ if (nibble != TRANSPARENT_NIBBLE) {
+ writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
+ }
+ inTileX++;
+ if (inTileX == TILE_PIXELS_PER_SIDE) {
+ inTileX = 0;
+ srcTileRow += TILE_BYTES;
}
- writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
}
}
}
@@ -194,15 +216,7 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
if (sp == NULL) {
return NULL;
}
- sp->tileData = tileData;
- sp->widthTiles = widthTiles;
- sp->heightTiles = heightTiles;
- sp->ownsTileData = false;
- sp->slot = NULL;
- memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
- memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
- memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
- memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
+ spriteInitFields(sp, tileData, widthTiles, heightTiles, false);
return sp;
}
@@ -226,11 +240,11 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
// Source x/y must be on a tile boundary so each captured tile lands
// on whole bytes -- mid-byte snapshots would lose half a pixel at
// the left edge.
- if ((x & (TILE_PIXELS - 1)) != 0 || (y & (TILE_PIXELS - 1)) != 0) {
+ if ((x & (TILE_PIXELS_PER_SIDE - 1)) != 0 || (y & (TILE_PIXELS_PER_SIDE - 1)) != 0) {
return NULL;
}
- widthPx = (uint16_t)(widthTiles * TILE_PIXELS);
- heightPx = (uint16_t)(heightTiles * TILE_PIXELS);
+ widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
+ heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
if (x < 0 || y < 0 ||
x + (int16_t)widthPx > SURFACE_WIDTH ||
y + (int16_t)heightPx > SURFACE_HEIGHT) {
@@ -251,8 +265,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
for (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
- for (row = 0; row < TILE_PIXELS; row++) {
- srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS + row) * SURFACE_BYTES_PER_ROW];
+ for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
+ srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row) * SURFACE_BYTES_PER_ROW];
memcpy(&dstTile[row * TILE_BYTES_PER_ROW],
&srcRow[((uint16_t)x >> 1) + tx * TILE_BYTES_PER_ROW],
TILE_BYTES_PER_ROW);
@@ -275,10 +289,10 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
for (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
- for (row = 0; row < TILE_PIXELS; row++) {
- py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS + row);
- for (col = 0; col < TILE_PIXELS; col += 2) {
- px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS + col);
+ for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
+ py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row);
+ for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) {
+ px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col);
hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu);
loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu);
dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] =
@@ -294,15 +308,7 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
free(buf);
return NULL;
}
- sp->tileData = buf;
- sp->widthTiles = widthTiles;
- sp->heightTiles = heightTiles;
- sp->ownsTileData = true;
- sp->slot = NULL;
- memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
- memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
- memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
- memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
+ spriteInitFields(sp, buf, widthTiles, heightTiles, true);
return sp;
}
@@ -329,8 +335,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
if (s == NULL || sp == NULL) {
return;
}
- widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
+ widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
+ heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes + fully on surface. Off-surface draws
// fall back to the interpreter so the compiled routines never
@@ -384,8 +390,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
hTiles = sp->heightTiles;
slot = sp->slot;
- widthPx = (uint16_t)(wTiles * TILE_PIXELS);
- heightPx = (uint16_t)(hTiles * TILE_PIXELS);
+ widthPx = (uint16_t)(wTiles * TILE_PIXELS_PER_SIDE);
+ heightPx = (uint16_t)(hTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes available, fully on surface, backup
// buffer supplied. Save fills out backup->{x,y,width,height,
@@ -422,18 +428,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
}
-// .spr file format:
-// offset bytes field
-// ------ ----- --------------------------------------------
-// 0 1 widthTiles
-// 1 1 heightTiles
-// 2 2 codeSize (LE16)
-// 4 2 tileBytes (LE16) = widthTiles*heightTiles*32
-// 6 ... offsets table (JOEY_SPRITE_SHIFT_COUNT *
-// SPRITE_OP_COUNT * uint16_t)
-// ... codeSize compiled code
-// ... tileBytes raw tile data (4bpp packed, tile-major)
-//
+// Defragment the codegen arena (compiled sprite slots). The live .spr
+// on-disk format is documented beside the loader in core/assetLoad.c.
void jlSpriteCompact(void) {
codegenArenaCompact();
}
@@ -471,7 +467,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
/* Validate. Note: SURFACE_WIDTH - bx and SURFACE_HEIGHT - by stay
* in uint16_t range once bx >= 0 / by >= 0 has been checked, so
* the right-edge / bottom-edge tests don't need 32-bit arithmetic
- * (which would invoke ORCA-C's ~GRTL helper, ~50 cyc per call). */
+ * (which would invoke a costly long-compare helper). */
if (backup->bytes == NULL ||
bw == 0 || bh == 0 ||
bx < 0 || by < 0 ||
@@ -483,22 +479,32 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
}
sp = backup->sprite;
- if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS) {
+ if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS_PER_SIDE) {
uint16_t routeIdx;
uint16_t routeOffset;
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (int16_t)(bw >> 1);
- shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
- /* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
- routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
- routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1));
- if (routeOffset != SPRITE_NOT_COMPILED) {
- spriteCompiledRestoreUnder(s, backup);
- if (!COMPILED_SPRITE_WRITES_PLANES) {
- halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
+ // The compiled restore routine walks a FIXED per-row byte count
+ // derived purely from the chosen shift (spriteBytesPerRow for
+ // shift 0, spriteBytesPerRow+1 for shift 1). A horizontally
+ // clipped save records a narrower backup->width / copyBytes that
+ // matches neither, so only take the fast path when copyBytes is
+ // exactly one of those two compiled widths; any other width must
+ // fall through to the interpreted memcpy that honors copyBytes.
+ if (copyBytes == (int16_t)spriteBytesPerRow ||
+ copyBytes == (int16_t)(spriteBytesPerRow + 1)) {
+ shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
+ /* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
+ routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
+ routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1));
+ if (routeOffset != SPRITE_NOT_COMPILED) {
+ spriteCompiledRestoreUnder(s, backup);
+ if (!COMPILED_SPRITE_WRITES_PLANES) {
+ halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
+ }
+ surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
+ return;
}
- surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
- return;
}
}
@@ -526,7 +532,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
/* Only fast-path locals here. Slow-path declarations live inside
- * the slow-path block below so ORCA-C with -b doesn't reserve
+ * the slow-path block below so the compiler doesn't reserve
* stack frame for them on every fast-path call. */
uint16_t widthPx;
uint16_t heightPx;
@@ -540,8 +546,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
backup->sizeBytes = 0;
slot = sp->slot;
- widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
- heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
+ widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
+ heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Compiled fast path: fully on surface and the platform emitted
// bytes for SAVE at this shift. The compiled routine assumes a
@@ -549,8 +555,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// through to the interpreted memcpy loop below.
//
// The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as
- // explicit byte-pointer arithmetic to dodge ORCA-C 2.2.1's ~MUL4
- // helper that gets emitted for `uint16_t arr[N][M]` indexing.
+ // explicit byte-pointer arithmetic to dodge the multiply helper
+ // that gets emitted for `uint16_t arr[N][M]` indexing.
if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
uint16_t routeIdx;
uint16_t routeOffset;
diff --git a/src/core/spriteInternal.h b/src/core/spriteInternal.h
index 2a67b31..6a6fa4f 100644
--- a/src/core/spriteInternal.h
+++ b/src/core/spriteInternal.h
@@ -61,8 +61,8 @@ struct jlSpriteT {
uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
// Cached `copyBytes * heightPx` per shift for spriteCompiledSaveUnder's
- // `backup->sizeBytes` field. uint16_t * uint16_t goes through ORCA-C
- // 2.2.1's ~CUMUL2 helper (~30-50 cyc); cache hit dodges it. Filled
+ // `backup->sizeBytes` field. uint16_t * uint16_t goes through a
+ // software multiply helper (~30-50 cyc); cache hit dodges it. Filled
// lazily on first call (0 sentinel = uncached).
uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT];
};
diff --git a/src/core/surface.c b/src/core/surface.c
index 38d9599..4d5b4a8 100644
--- a/src/core/surface.c
+++ b/src/core/surface.c
@@ -30,20 +30,20 @@ uint8_t gStageMinWord[SURFACE_HEIGHT];
uint8_t gStageMaxWord[SURFACE_HEIGHT];
// "Stage SCB / palette has changed since last present-side upload."
-// Cheap flag check at present time replaces the 200+512 byte memcmps
-// the IIgs port used to run every frame in halPresentRect's
-// uploadScbAndPaletteIfNeeded -- ~7 ms / frame saved on demos that
-// don't churn palette/SCB (i.e., almost all demos).
+// A cheap flag check at present time lets a port skip re-uploading the
+// 200-byte SCB and 512-byte palette when neither changed -- ~7 ms /
+// frame saved on demos that don't churn palette/SCB (i.e., almost all
+// demos).
//
-// Initially true so the first present uploads. jlScbSet*/jlPaletteSet
-// re-mark dirty when the stage's data changes; per-port present code
-// clears the flag after consuming.
+// Initially true so the first present uploads. jlScbSet*, jlPaletteSet,
+// and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile)
+// re-mark dirty when the stage's data changes; the per-port present
+// code reads and clears the flags after uploading.
bool gStageScbDirty = true;
bool gStagePaletteDirty = true;
// ----- Internal helpers (alphabetical) -----
-#ifndef JOEYLIB_PLATFORM_IIGS
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
if (minWord < gStageMinWord[y]) {
gStageMinWord[y] = minWord;
@@ -52,7 +52,6 @@ static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
gStageMaxWord[y] = maxWord;
}
}
-#endif
// ----- Public API (alphabetical) -----
@@ -70,6 +69,13 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
memcpy(dst->palette, src->palette, sizeof(src->palette));
halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */
surfaceMarkDirtyAll(dst);
+ // Bulk-replacing the stage's SCB/palette must re-mark them dirty so
+ // the per-port present re-uploads them; surfaceMarkDirtyAll only
+ // widens the pixel bands. Matches the jlScbSet / jlPaletteSet path.
+ if (dst == gStage) {
+ gStageScbDirty = true;
+ gStagePaletteDirty = true;
+ }
}
@@ -78,12 +84,31 @@ jlSurfaceT *jlSurfaceCreate(void) {
s = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
if (s == NULL) {
+ coreSetError("out of memory allocating surface");
return NULL;
}
- /* halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
- * primary storage is the port-allocated planes via portData below. */
+ // halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
+ // primary storage is the port-allocated planes via portData below.
+ // Classify NULL by the build's storage model: chunky ports need
+ // pixels, planar ports need portData. A NULL in the required slot
+ // is an out-of-memory failure (header contract: return NULL).
s->pixels = halSurfaceAllocPixels();
s->portData = halSurfaceAllocPortData(s, false);
+#ifdef JOEYLIB_NATIVE_CHUNKY
+ if (s->pixels == NULL) {
+ halSurfaceFreePortData(s, false, s->portData);
+ free(s);
+ coreSetError("out of memory allocating surface pixels");
+ return NULL;
+ }
+#else
+ if (s->portData == NULL) {
+ halSurfaceFreePixels(s->pixels);
+ free(s);
+ coreSetError("out of memory allocating surface planes");
+ return NULL;
+ }
+#endif
paletteInitDefault(s);
return s;
}
@@ -102,30 +127,11 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
}
-// Cheapest deterministic hash that still detects per-byte changes:
-// (hash << 1) ^ byte, a single 16-bit accumulator. ORCA-C / 65816
-// compiles to ASL + EOR -- about 35 cyc per byte. A 32-bit multiply
-// FNV-style hash takes ~200 cyc per byte via ~UMUL4, which adds
-// 80+ seconds to a UBER run on IIgs. Discrimination is weaker than
-// FNV but plenty for cross-port validation: we only need "did the
-// same logical-pixel sequence produce the same hash?" -- not
-// crypto-grade collision resistance over arbitrary inputs.
-//
-// Walks the chunky pixel buffer byte-by-byte, the same logical-pixel
-// ordering on every chunky-format port (IIgs, DOS, Amiga and ST
-// while still chunky). When the planar rewrite drops s->pixels on
-// Amiga/ST this function will need a HAL hook (halSurfaceHash) to
-// read planes natively while producing the same logical hash.
-/* Cross-port FNV-style hash of pixels + SCB + palette. The hash logic
- * (multiplier streams, byte ordering for palette) is identical across
- * ports, but the pixel READS go through the port HAL so chunky ports
- * walk s->pixels and planar ports walk plane bits and assemble nibble
- * pairs into chunky bytes for the hash. Both produce the same logical-
- * pixel hash because they hash the same logical pixel sequence in the
- * same chunky byte order. SCB and palette are still hashed inline
- * here because they live in the jlSurfaceT struct on every port (no
- * port-specific storage) and the byte/value-with-explicit-byte-order
- * walks are already endian-independent. */
+// Cross-port hash of the surface's logical pixel content (plus SCB and
+// palette), delegated to the port HAL so each port reads its native
+// pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while
+// producing the same logical-pixel hash. Used by the UBER validation
+// harness to pixel-compare ports against an IIgs golden reference.
uint32_t jlSurfaceHash(const jlSurfaceT *s) {
if (s == NULL) {
return 0u;
@@ -172,6 +178,12 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
}
fclose(fp);
surfaceMarkDirtyAll(dst);
+ /* Loading a file into the stage overwrites its SCB/palette; re-mark
+ * them dirty so the next present re-uploads them. */
+ if (dst == gStage) {
+ gStageScbDirty = true;
+ gStagePaletteDirty = true;
+ }
return true;
}
@@ -204,15 +216,14 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
void surfaceMarkDirtyAll(const jlSurfaceT *s) {
- int16_t row;
-
if (s != gStage) {
return;
}
- for (row = 0; row < SURFACE_HEIGHT; row++) {
- gStageMinWord[row] = 0;
- gStageMaxWord[row] = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
- }
+ // Whole-stage mark is a constant fill of both band arrays; memset
+ // lowers to a tight fill (MVN-seeded on IIgs) and drops the 200-iter
+ // indexed-store loop. One source of truth for "all rows fully dirty".
+ memset(gStageMinWord, 0, SURFACE_HEIGHT);
+ memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
}
@@ -234,15 +245,23 @@ void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w,
if (w <= 0 || h <= 0) {
return;
}
- /* Clipped x/w are non-negative; cast to uint16_t before `>> 2` so
- * ORCA-C lowers to a pair of LSRs instead of the ~SSHIFTRIGHT
- * helper signed shifts emit. */
- minWord = (uint8_t)((uint16_t)x >> 2);
- maxWord = (uint8_t)((uint16_t)(x + w - 1) >> 2);
+ /* Clipped x/w are non-negative; SURFACE_WORD_INDEX casts to uint16_t
+ * before `>> 2` so the shift lowers to a pair of LSRs instead of a
+ * signed-shift helper. */
+ minWord = SURFACE_WORD_INDEX(x);
+ maxWord = SURFACE_WORD_INDEX(x + w - 1);
yEnd = y + h;
#ifdef JOEYLIB_PLATFORM_IIGS
- iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd,
- (uint16_t)minWord, (uint16_t)maxWord);
+ // Single-row marks (every jlDrawPixel, every horizontal span) widen
+ // inline in C; this is the common case and avoids the cross-segment
+ // JSL into iigsMarkDirtyRowsInner. Multi-row marks still use the asm
+ // marker, where the per-call JSL is amortized across the rows.
+ if (h == 1) {
+ widenRow(y, minWord, maxWord);
+ } else {
+ iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd,
+ (uint16_t)minWord, (uint16_t)maxWord);
+ }
#else
for (row = y; row < yEnd; row++) {
widenRow(row, minWord, maxWord);
@@ -261,14 +280,31 @@ bool stageAlloc(void) {
if (gStage == NULL) {
return false;
}
- /* halStageAllocPixels returns NULL on planar ports (Amiga) where
- * the chunky shadow doesn't exist; the planes from portData are
- * the source of truth. NULL pixels is no longer a failure. */
+ // halStageAllocPixels returns NULL on planar ports (Amiga) where
+ // the chunky shadow doesn't exist; the planes from portData are
+ // the source of truth. On chunky ports NULL pixels means the
+ // back-buffer alloc failed (IIgs pins a fixed address and never
+ // returns NULL, so this is a real OOM only on DOS).
gStage->pixels = halStageAllocPixels();
if (gStage->pixels != NULL) {
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
}
gStage->portData = halSurfaceAllocPortData(gStage, true);
+#ifdef JOEYLIB_NATIVE_CHUNKY
+ if (gStage->pixels == NULL) {
+ halSurfaceFreePortData(gStage, true, gStage->portData);
+ free(gStage);
+ gStage = NULL;
+ return false;
+ }
+#else
+ if (gStage->portData == NULL) {
+ halStageFreePixels(gStage->pixels);
+ free(gStage);
+ gStage = NULL;
+ return false;
+ }
+#endif
stageDirtyClearAll();
paletteInitDefault(gStage);
return true;
diff --git a/src/core/surfaceInternal.h b/src/core/surfaceInternal.h
index bf71867..2e2c105 100644
--- a/src/core/surfaceInternal.h
+++ b/src/core/surfaceInternal.h
@@ -33,13 +33,32 @@ struct jlSurfaceT {
// 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words.
// Dirty tracking grain is 16-bit words because that matches the IIgs
// PEI / PHA slam unit and the Amiga / ST c2p group is 16 px = 4 words.
+#define SURFACE_PIXELS_PER_WORD 4
#define SURFACE_WORDS_PER_ROW (SURFACE_BYTES_PER_ROW / 2)
+// x pixel -> dirty word index (4 px per 16-bit word). x is non-negative
+// at every call site (clipped to surface bounds), so it is cast to
+// uint16_t before the shift to keep the compiler from emitting a signed
+// shift helper.
+#define SURFACE_WORD_INDEX(_x) ((uint8_t)((uint16_t)(_x) >> 2))
+
+// Index of the last word in a fully-dirty row.
+#define STAGE_DIRTY_FULL_MAX ((uint8_t)(SURFACE_WORDS_PER_ROW - 1))
+
// Sentinels for "row is clean": min > max can never happen for a real
// dirty range, so the present loop tests `min > max` to skip a row.
#define STAGE_DIRTY_CLEAN_MIN 0xFFu
#define STAGE_DIRTY_CLEAN_MAX 0x00u
+// True when row `y`'s dirty band is the CLEAN sentinel (min > max).
+#define STAGE_DIRTY_ROW_CLEAN(_y) (gStageMinWord[_y] > gStageMaxWord[_y])
+
+// The per-row dirty bands are stored as uint8_t, which is only valid
+// because SURFACE_WORDS_PER_ROW - 1 (79) fits in a byte. A future width
+// change that pushes the word count past 255 must fail to compile here
+// rather than silently truncate the band arrays.
+typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : -1];
+
// Per-row dirty word bands for the stage. gStageMinWord[y] is the
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
// is the rightmost (inclusive). Both default to the CLEAN sentinels
@@ -48,8 +67,9 @@ extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b.
-// Strength-reduced to shifts so ORCA-C doesn't emit `~UMUL2` (~150 cyc
-// per call); 32 KB hashed twice -> ~5 minutes per UBER run. The
+// Strength-reduced to shifts so the compiler doesn't emit a software
+// multiply (~150 cyc per call); 32 KB hashed twice -> ~5 minutes per
+// UBER run. The
// shift form is 16-bit-equivalent (mod 2^16) so hash values are
// identical to the original `* 31u` / `* 251u` form.
// lo *= 31 == (lo << 5) - lo
@@ -94,8 +114,8 @@ void stageDirtyClearAll(void);
// On IIgs this expands to a single indexed long-mode read against
// gRowOffsetLut (built once at halInit).
//
-// The explicit (y << 1) byte-pointer arithmetic dodges ORCA-C 2.2.1's
-// `~MUL4` helper that gets emitted for `uint16_t arr[N]` indexing
+// The explicit (y << 1) byte-pointer arithmetic dodges the multiply
+// helper that gets emitted for `uint16_t arr[N]` indexing
// (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the
// compiler emits a single ASL + indexed long-mode read.
//
@@ -109,6 +129,11 @@ extern const uint16_t gRowOffsetLut[200];
#define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
#endif
+// Record a human-readable error string for jlLastError(). Defined in
+// init.c; usable by other core TUs (e.g. surface.c on allocation /
+// limit failure). The string must have static lifetime.
+void coreSetError(const char *message);
+
// Allocate and free the library-owned stage (the back-buffer surface
// that jlStagePresent flips to the display). Called from init.c during
// jlInit / jlShutdown. The stage's pixel storage is supplied by
diff --git a/src/core/tile.c b/src/core/tile.c
index ae67916..5eb3783 100644
--- a/src/core/tile.c
+++ b/src/core/tile.c
@@ -16,11 +16,8 @@
// (No -- the 4-byte-per-row inner copies are spelled out
// inline below. Avoiding memcpy / memset from the DRAWPRIMS load
-// segment keeps cross-bank relocation references out of 13/SysLib;
-// without that the ORCA Linker hits "Expression too complex" on
-// the small-binary builds.)
+// segment keeps cross-bank relocation references out of 13/SysLib.)
-JOEYLIB_SEGMENT("DRAWPRIMS")
// ----- Prototypes -----
diff --git a/src/port/amiga/audio.c b/src/port/amiga/audio.c
index cf7f667..37877a7 100644
--- a/src/port/amiga/audio.c
+++ b/src/port/amiga/audio.c
@@ -40,6 +40,7 @@ extern UBYTE mt_E8Trigger;
#define PAULA_CLOCK_PAL 3546895UL
#define PAULA_PERIOD_MIN 124 // Paula HW limit
#define PAULA_PERIOD_MAX 65535
+#define PAULA_CHANNELS 4 // hardware audio channel count
#define SFX_VOLUME_MAX 64
@@ -289,9 +290,9 @@ void halAudioStopSfx(uint8_t slot) {
// PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
// picks a channel dynamically and does not return which one. We
// can't reliably map slot -> channel after the fact, so stop all
- // four channels' SFX state. mt_stopfx is idempotent on idle
- // channels, so stopping a quiet one is harmless.
- for (i = 0; i < 4; i++) {
+ // PAULA_CHANNELS channels' SFX state. mt_stopfx is idempotent on
+ // idle channels, so stopping a quiet one is harmless.
+ for (i = 0; i < PAULA_CHANNELS; i++) {
mt_stopfx((void *)&custom, i);
}
}
diff --git a/src/port/amiga/hal.c b/src/port/amiga/hal.c
index dc2b3ce..c0aaa38 100644
--- a/src/port/amiga/hal.c
+++ b/src/port/amiga/hal.c
@@ -40,6 +40,7 @@
#include
#include
+#include
#include
#include
@@ -48,7 +49,6 @@
#include "hal.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"
-#include "draw68k_inline.h"
extern struct Custom custom;
@@ -86,6 +86,8 @@ typedef struct {
// ----- Prototypes -----
+static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES], uint16_t widthWords, uint16_t height);
+static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, bool setBits);
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color);
static void buildCopperList(const jlSurfaceT *src);
static void installCopperList(void);
@@ -124,6 +126,16 @@ static volatile uint16_t *gBplPokes[AMIGA_BITPLANES][2]; // [plane][0=PTH,1=PTL
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
+// "The previous frame's snapshot recorded at least one dirty row."
+// Lets halPresent early-out of the whole union scan + 400-byte
+// snapshot copy on consecutive idle frames: if neither the previous
+// frame nor the current frame touched any row, union(prev,current) is
+// empty AND the snapshot would copy clean->clean (a no-op), so there
+// is nothing to present and nothing to record. Seeded true to match
+// the "all dirty" bootstrap of gPrevStage* above, so the first two
+// presents still run.
+static bool gPrevAnyDirty = true;
+
// Cached SCB + palettes from the last present. halPresent* only needs
// to rebuild/install the copper list when SCB assignments or palette
// RGB values differ from what is already on screen; pure pixel updates
@@ -143,6 +155,86 @@ static bool paletteOrScbChanged(const jlSurfaceT *src);
// ----- Internal helpers (alphabetical) -----
+// Volatile view of the custom-chip register block. The Custom struct
+// fields are plain (non-volatile) UWORD/APTR, so a direct
+// `custom.bltcon0 = x` is eligible for -O2 reordering / dead-store
+// elimination. Accessing every blitter register through a volatile *
+// makes each member write volatile-qualified, which (a) keeps the
+// compiler from reordering BLTSIZE (the start trigger) ahead of the
+// setup writes and (b) stops it from dropping a "redundant" register
+// store. This is the standard idiom for poking the custom chip from C.
+static volatile struct Custom *const gCust = &custom;
+
+// Blitter geometry shared by every block op here: each plane row is
+// AMIGA_BYTES_PER_ROW (40) bytes = 20 words. BLTSIZE packs the row
+// count in the high 10 bits (<< 6) and the word count in the low 6
+// bits (a width of 0 would mean 64 words, but our rows are 20 wide).
+#define AMIGA_BLT_WORDS_PER_ROW (AMIGA_BYTES_PER_ROW / 2u) // 20
+#define AMIGA_BLT_SIZESHIFT 6
+#define AMIGA_BLT_MASK_ALL 0xFFFFu
+
+// Blitter A->D straight copy of `height` rows of `widthWords` words
+// across all four planes. src[i]/dst[i] are the per-plane start
+// addresses (already advanced to the first row/byte to copy by the
+// caller). Both must be CHIP RAM and word-aligned; rows are
+// contiguous (mod = 0) because every copy here is whole rows of a
+// 40-byte-stride plane. minterm $F0 (A_TO_D): D := A, no read of B/C/D.
+//
+// OwnBlitter once for the whole group; WaitBlit before each register
+// setup (the blitter is single-resource, so we must wait for the
+// previous plane's blit to drain before reprogramming) and a final
+// WaitBlit before DisownBlitter so the planes are fully written when
+// the caller (or the present copy) next reads them.
+static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES],
+ uint16_t widthWords, uint16_t height) {
+ uint16_t i;
+ uint16_t bltsize;
+
+ bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
+ OwnBlitter();
+ for (i = 0; i < AMIGA_BITPLANES; i++) {
+ WaitBlit();
+ gCust->bltcon0 = (uint16_t)(SRCA | DEST | A_TO_D); // $09F0
+ gCust->bltcon1 = 0u;
+ gCust->bltafwm = AMIGA_BLT_MASK_ALL;
+ gCust->bltalwm = AMIGA_BLT_MASK_ALL;
+ gCust->bltamod = 0u;
+ gCust->bltdmod = 0u;
+ gCust->bltapt = (APTR)src[i];
+ gCust->bltdpt = (APTR)dst[i];
+ gCust->bltsize = bltsize; // starts the blit
+ }
+ WaitBlit();
+ DisownBlitter();
+}
+
+
+// Blitter DEST-only block fill of one plane: `height` rows of
+// `widthWords` words, with `dstModBytes` added to the dest pointer
+// after each row (0 for a full-stride fill, (40 - rowBytes) for a
+// sub-row rect). setBits selects the constant written: $FFFF (set,
+// minterm $FF) or $0000 (clear, minterm $00). USED only -- no source
+// channel is read, so the minterm alone determines the output and the
+// fill needs no source data in memory. Caller owns the blitter and
+// has already WaitBlit'd if a prior blit may be in flight; this
+// routine WaitBlit's before its own register setup for safety when
+// called back-to-back across planes.
+static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
+ uint16_t dstModBytes, bool setBits) {
+ uint16_t minterm;
+
+ minterm = setBits ? 0x00FFu : 0x0000u;
+ WaitBlit();
+ gCust->bltcon0 = (uint16_t)(DEST | minterm);
+ gCust->bltcon1 = 0u;
+ gCust->bltafwm = AMIGA_BLT_MASK_ALL;
+ gCust->bltalwm = AMIGA_BLT_MASK_ALL;
+ gCust->bltdmod = dstModBytes;
+ gCust->bltdpt = (APTR)dst;
+ gCust->bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
+}
+
+
// Build a user copper list for per-scanline palette (SCB emulation).
// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is
// stored in gNewUCL until installCopperList swaps it onto the screen.
@@ -647,6 +739,31 @@ void halPresent(const jlSurfaceT *src) {
return;
}
+ // Idle-frame early-out. When the previous frame's snapshot recorded
+ // no dirty rows (gPrevAnyDirty == false) AND the current frame
+ // touched no row, union(prev,current) is empty: there is nothing to
+ // copy, no buffer to flip, and the snapshot below would only copy
+ // clean->clean. Skip the whole 200-row union scan and the 400-byte
+ // snapshot copy in that case. A row is dirty iff its min word was
+ // widened below STAGE_DIRTY_CLEAN_MIN, so a single byte test per row
+ // with early exit on the first dirty row suffices. Copper/palette
+ // updates already ran above, so display state stays correct. This
+ // cannot be taken while a stale band from the previous frame is
+ // still pending (gPrevAnyDirty guards that), preserving the
+ // 2-buffer staleness convergence.
+ if (!gPrevAnyDirty) {
+ bool anyDirty = false;
+ for (y = 0; y < SURFACE_HEIGHT; y++) {
+ if (gStageMinWord[y] != STAGE_DIRTY_CLEAN_MIN) {
+ anyDirty = true;
+ break;
+ }
+ }
+ if (!anyDirty) {
+ return;
+ }
+ }
+
// Phase 10 page-flip:
// * Write to the OFF-screen plane set so the beam never crosses
// in-flight pixels (no tearing regardless of copy speed).
@@ -687,6 +804,16 @@ void halPresent(const jlSurfaceT *src) {
}
if (firstRow >= 0) {
+ // The present copy reads pd->planes (the shadow) with the CPU.
+ // The bulk plane ops (clear / full-row fill / surface copy) now
+ // run on the blitter; each of those helpers WaitBlit's before
+ // returning, so the blitter is normally idle here. Add a
+ // WaitBlit anyway before the CPU read to guarantee no blit is
+ // still in flight against the shadow planes -- a CPU read racing
+ // a blitter write would copy half-written rows to the display.
+ // WaitBlit is a no-op when the blitter is already idle and does
+ // not require OwnBlitter.
+ WaitBlit();
// Each planar byte covers 8 px = 2 chunky words.
byteStart = (uint16_t)(unionMinWord >> 1);
bytesPerRow = (uint16_t)(((unionMaxWord >> 1) + 1u) - byteStart);
@@ -710,13 +837,21 @@ void halPresent(const jlSurfaceT *src) {
// Snapshot current dirty bands as "previous" for next present.
// jlStagePresent clears the live bands after halPresent returns,
- // so we have to grab them now or they're gone.
+ // so we have to grab them now or they're gone. Record whether any
+ // row was dirty so the next present can take the idle-frame early-
+ // out above. A row is dirty iff its min word stayed below the clean
+ // sentinel.
{
uint16_t i;
+ bool anyDirty = false;
for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = gStageMinWord[i];
gPrevStageMaxWord[i] = gStageMaxWord[i];
+ if (gStageMinWord[i] != STAGE_DIRTY_CLEAN_MIN) {
+ anyDirty = true;
+ }
}
+ gPrevAnyDirty = anyDirty;
}
}
@@ -746,9 +881,11 @@ static bool gVblInstalled = false;
// Server protocol: called by the interrupt dispatcher with A1 =
// is_Data, A6 = ExecBase. We compile without fbaserel so the small-
// data ABI is not in use; gFrameCount is a plain absolute global and
-// no __saveds is needed to reach it. Return Z=0 (non-zero result) to
-// keep the chain going so other VBL servers further down the priority
-// list still fire.
+// no __saveds is needed to reach it. Return 0 (D0 zero, Z flag set)
+// so the interrupt dispatcher treats this server as "not claimed" and
+// continues walking the INTB_VERTB chain, letting other VBL servers
+// further down the priority list still fire. Returning non-zero would
+// claim the interrupt and terminate the chain.
static ULONG vblServer(void) {
gFrameCount = (uint16_t)(gFrameCount + 1u);
return 0;
@@ -837,19 +974,19 @@ void halShutdown(void) {
FreeMem(gBitMap, (ULONG)sizeof(struct BitMap));
gBitMap = NULL;
}
- if (gNewUCL != NULL) {
- FreeMem(gNewUCL, sizeof(struct UCopList));
- gNewUCL = NULL;
- }
+ // gNewUCL is never live at shutdown: buildCopperList and
+ // installCopperList run back-to-back inside updateCopperIfNeeded,
+ // so the built list is always consumed (gNewUCL == NULL) before we
+ // get here. A bare FreeMem of the UCopList struct would also leak
+ // the attached CopList chain that CINIT/CWAIT/CMOVE allocate, so we
+ // deliberately do NOT attempt to free it here.
}
-// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
-// surface68k.s). Same primitives used by the Atari ST port -- the
-// stage / surfaces are an identical 4bpp packed layout on both.
-extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
-extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
-extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
+// Shared 68k fast paths from src/shared68k/surface68k.s. Only the
+// planar span fill and the planar circle outline apply on Amiga (pure
+// planar after Phase 9); the chunky-layout primitives that file also
+// exports are unused here.
extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3);
@@ -859,10 +996,14 @@ extern void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2,
// Phase 3 planar dual-write: write the bitplanes alongside the
// Phase 9: Amiga is pure planar. Every halFast* below returns true
-// to suppress the cross-platform chunky fallback path -- there is no
-// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The actual
-// planar work happens in dedicated halXxxPlanes hooks called by
-// cross-platform code AFTER each halFast*.
+// for any surface that has planar storage (s->portData != NULL) to
+// suppress the cross-platform chunky fallback path -- there is no
+// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate
+// is portData presence, NOT stage identity, so non-stage surfaces
+// created by jlSurfaceCreate use their OWN planes instead of falling
+// into the chunky C fallback (which would deref s->pixels == NULL).
+// The actual planar work happens either inline here or in dedicated
+// halXxxPlanes hooks called by cross-platform code AFTER each halFast*.
//
// halFastSurfaceClear inlines the planar fill (no separate hook).
// All other halFast* are short-circuit stubs that return true.
@@ -870,19 +1011,29 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
AmigaPlanarT *pd;
uint8_t colorIndex;
uint16_t i;
- uint8_t planeByte;
+ bool setBits;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
colorIndex = (uint8_t)(doubled & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
- if (pd != NULL) {
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- planeByte = ((colorIndex >> i) & 1u) ? 0xFFu : 0x00u;
- memset(pd->planes[i], planeByte, AMIGA_PLANE_SIZE);
- }
+
+ // Blitter DEST-only block fill of every whole plane to $00 (clear)
+ // or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
+ // BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now
+ // CHIP RAM (see halSurfaceAllocPortData) so the blitter can reach
+ // them. Own the blitter once and fill all four planes; the final
+ // WaitBlit before DisownBlitter guarantees the planes are fully
+ // written before any CPU read (a subsequent draw or the present
+ // copy) touches them.
+ OwnBlitter();
+ for (i = 0; i < AMIGA_BITPLANES; i++) {
+ setBits = ((colorIndex >> i) & 1u) != 0u;
+ amigaBlitterFillOne(pd->planes[i], AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT, 0u, setBits);
}
+ WaitBlit();
+ DisownBlitter();
return true;
}
@@ -893,7 +1044,7 @@ bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h
* partial-byte mask handling. We just claim ownership so the C
* chunky fallback never runs. */
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
return true;
@@ -962,52 +1113,26 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
yEnd = y + (int16_t)h;
/* Full-row fast path: no partial-byte RMW on either edge, so each
- * plane is a pure long-fill of (h * 40) bytes. jlFillRect 320x200
- * is the dominant case and lands here; saves 200 rows of leading
- * byte RMW + 200 of trailing byte RMW per plane = 1600 chip-bus
- * read+write cycles per plane on top of the actual data write. */
+ * plane row is a whole 40-byte (20-word) store. jlFillRect 320x200
+ * (clear-to-color, full-screen wipes) is the dominant case and
+ * lands here. With the shadow planes now in CHIP RAM the blitter
+ * can do this DEST-only block fill -- (h << 6) | 20 words, BLTDMOD
+ * = 0 because consecutive full-width rows are contiguous. Set bits
+ * write $FFFF (minterm $FF), clear bits write $0000 (minterm $00).
+ * One OwnBlitter for all four planes; WaitBlit before DisownBlitter
+ * so the planes are complete before the present copy / any later
+ * CPU draw reads them. */
if (byteFirst == 0u && byteLast == (uint16_t)(AMIGA_BYTES_PER_ROW - 1u)) {
- uint16_t totalLongs;
- uint16_t groups;
- uint16_t tail;
- uint32_t fillLong;
- uint32_t *p32;
+ uint8_t *rowBase;
- /* AMIGA_BYTES_PER_ROW = 40 = 10 longs / row. 8x-unrolled inner
- * loop amortizes the dec+bne to ~2 cyc per store; net ~12 cyc
- * per long including chip-bus contention. Tail handles the
- * 0..7 longs that don't fit a full group. */
- totalLongs = (uint16_t)((uint16_t)h * (AMIGA_BYTES_PER_ROW / 4u));
- groups = (uint16_t)(totalLongs >> 3);
- tail = (uint16_t)(totalLongs & 7u);
+ OwnBlitter();
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
- bitVal = (uint8_t)((colorIndex >> plane) & 1u);
- fullByte = bitVal ? 0xFFu : 0x00u;
- fillLong = (uint32_t)fullByte * 0x01010101UL;
- p32 = (uint32_t *)(pd->planes[plane]
- + (uint16_t)y * AMIGA_BYTES_PER_ROW);
- if (groups > 0u) {
- uint16_t n = groups;
- do {
- p32[0] = fillLong;
- p32[1] = fillLong;
- p32[2] = fillLong;
- p32[3] = fillLong;
- p32[4] = fillLong;
- p32[5] = fillLong;
- p32[6] = fillLong;
- p32[7] = fillLong;
- p32 += 8;
- } while (--n);
- }
- {
- uint16_t t = tail;
- while (t > 0u) {
- *p32++ = fillLong;
- t--;
- }
- }
+ bitVal = (uint8_t)((colorIndex >> plane) & 1u);
+ rowBase = pd->planes[plane] + (uint16_t)y * AMIGA_BYTES_PER_ROW;
+ amigaBlitterFillOne(rowBase, AMIGA_BLT_WORDS_PER_ROW, h, 0u, bitVal != 0u);
}
+ WaitBlit();
+ DisownBlitter();
return;
}
@@ -1015,9 +1140,9 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
* bytes (leftMask == rightMask == 0xFF) every byte in the row is
* a full overwrite -- no RMW needed. UBER jlFillRect 80x80 at x=120
* lands here (byteFirst=15, byteLast=24). Plane bases are
- * MEMF_FAST-allocated long-aligned, and y*40 is also a multiple
- * of 4, so rowP alignment is determined by byteFirst alone --
- * computed once, not per-row. */
+ * AllocMem(MEMF_CHIP)-returned and thus long-aligned, and y*40 is
+ * also a multiple of 4, so rowP alignment is determined by
+ * byteFirst alone -- computed once, not per-row. */
if (leftMask == 0xFFu && rightMask == 0xFFu) {
uint16_t nBytes = (uint16_t)(byteLast - byteFirst + 1u);
uint8_t alignBytes = (uint8_t)((4u - (byteFirst & 3u)) & 3u);
@@ -1108,8 +1233,8 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
/* Hoist middle-region alignment outside both per-plane and
* per-row loops. midStart = planeBase + y*40 + byteFirst + 1.
- * Plane bases are MEMF_FAST long-aligned and y*40 is a
- * multiple of 4, so midStart's alignment is determined by
+ * Plane bases are AllocMem(MEMF_CHIP) long-aligned and y*40 is
+ * a multiple of 4, so midStart's alignment is determined by
* (byteFirst + 1) & 3 alone -- constant across planes/rows. */
uint8_t midAlignBytes = (uint8_t)((4u - ((byteFirst + 1u) & 3u)) & 3u);
uint16_t midRem;
@@ -1316,55 +1441,57 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
// Phase 9 sprite save/restore plane-backup hooks.
-// Sprite save at (x, y, w, h) writes 4 plane stripes into backup
-// buffer; restore reads them back. x and w are 2-pixel aligned by
-// cross-platform code; we round x DOWN and w UP to 8-pixel boundaries
-// here so plane writes are byte-aligned. Backup layout (matches the
-// h * w/2 = 4 * h * w/8 sizing the cross-platform code allocates):
-// bytes [0 .. h*bpr ): plane 0 rows
-// bytes [h*bpr .. 2*h*bpr ): plane 1 rows
-// bytes [2*h*bpr .. 3*h*bpr ): plane 2 rows
-// bytes [3*h*bpr .. 4*h*bpr ): plane 3 rows
-// where bpr = bytesPerPlaneRow = roundedW/8.
+// Sprite save at (x, y, w, h) records the destination region into the
+// caller's backup buffer; restore writes it back. x and w are 2-pixel
+// aligned by cross-platform code.
//
-// If the rect's rounded width is wider than the chunky-sized backup
-// would hold (h * (w/2) bytes), we silently truncate -- the planar
-// stripes for partial-byte-aligned sprites won't fit. This case is
-// rare for tile-aligned sprites; document if it bites.
-
-static void amigaSpriteRoundRect(int16_t *xp, uint16_t *wp, uint16_t *bprp) {
- int16_t xIn = *xp;
- uint16_t wIn = *wp;
- int16_t xOut = (int16_t)(xIn & ~7); /* round down to 8-pixel */
- uint16_t span = (uint16_t)(((uint16_t)xIn + wIn) - (uint16_t)xOut);
- uint16_t wOut = (uint16_t)((span + 7u) & ~7u);
- *xp = xOut;
- *wp = wOut;
- *bprp = (uint16_t)(wOut >> 3);
-}
-
+// Backup layout matches the CHUNKY-packed format the cross-platform
+// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see
+// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte,
+// high nibble = even (left) pixel, low nibble = odd (right) pixel,
+// (w/2) bytes per row, (w/2)*h bytes total. This is the same format
+// the Atari ST port uses and never overruns the contract buffer --
+// unlike a plane-major layout, which would round x down to an 8-pixel
+// boundary and width up and then write (4*roundedW/8)*h = up to
+// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each
+// pixel's 4bpp color is assembled from / written to the 4 plane bits
+// at byte x>>3, bit 0x80 >> (x & 7).
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const AmigaPlanarT *pd;
- uint16_t bpr;
- uint16_t planeStripe;
- uint16_t i;
+ uint16_t pairs;
uint16_t row;
+ uint16_t pair;
uint8_t *dst;
- const uint8_t *src;
pd = (const AmigaPlanarT *)s->portData;
- if (pd == NULL || dstPlaneBytes == NULL) {
+ if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
- amigaSpriteRoundRect(&x, &w, &bpr);
- planeStripe = (uint16_t)((uint16_t)h * bpr);
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- dst = dstPlaneBytes + i * planeStripe;
- for (row = 0; row < (uint16_t)h; row++) {
- src = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
- memcpy(dst, src, bpr);
- dst += bpr;
+ pairs = (uint16_t)(w >> 1);
+ for (row = 0; row < (uint16_t)h; row++) {
+ const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ dst = dstPlaneBytes + (uint16_t)(row * pairs);
+ for (pair = 0; pair < pairs; pair++) {
+ uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
+ uint16_t byteOff = (uint16_t)(px >> 3);
+ uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
+ uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
+ uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
+ uint8_t hi = 0u;
+ uint8_t lo = 0u;
+ if (p0[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x01u); }
+ if (p1[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x02u); }
+ if (p2[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x04u); }
+ if (p3[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x08u); }
+ if (p0[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x01u); }
+ if (p1[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x02u); }
+ if (p2[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x04u); }
+ if (p3[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x08u); }
+ dst[pair] = (uint8_t)((hi << 4) | lo);
}
}
}
@@ -1372,25 +1499,41 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
AmigaPlanarT *pd;
- uint16_t bpr;
- uint16_t planeStripe;
- uint16_t i;
+ uint16_t pairs;
uint16_t row;
- uint8_t *dst;
+ uint16_t pair;
const uint8_t *src;
pd = (AmigaPlanarT *)s->portData;
- if (pd == NULL || srcPlaneBytes == NULL) {
+ if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
- amigaSpriteRoundRect(&x, &w, &bpr);
- planeStripe = (uint16_t)((uint16_t)h * bpr);
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- src = srcPlaneBytes + i * planeStripe;
- for (row = 0; row < (uint16_t)h; row++) {
- dst = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
- memcpy(dst, src, bpr);
- src += bpr;
+ pairs = (uint16_t)(w >> 1);
+ for (row = 0; row < (uint16_t)h; row++) {
+ uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
+ src = srcPlaneBytes + (uint16_t)(row * pairs);
+ for (pair = 0; pair < pairs; pair++) {
+ uint8_t b = src[pair];
+ uint8_t hi = (uint8_t)(b >> 4);
+ uint8_t lo = (uint8_t)(b & 0x0Fu);
+ uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
+ uint16_t byteOff = (uint16_t)(px >> 3);
+ uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
+ uint8_t notHi = (uint8_t)(~maskHi);
+ uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
+ uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
+ uint8_t notLo = (uint8_t)(~maskLo);
+ p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u));
+ p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u));
+ p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u));
+ p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u));
+ p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u));
+ p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u));
+ p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u));
+ p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u));
}
}
}
@@ -1422,8 +1565,12 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
* 1. reads 4 chunky bytes (one row of one 8-pixel tile column)
* 2. converts to 4 plane bytes + a shape mask (1 bit per pixel
* where the chunky nibble is non-transparent)
- * 3. shifts the per-plane bytes by (dx & 7) so the sprite can land
- * at any pixel x, not just 8-px boundaries
+ * 3. shifts the per-plane bytes by (x & 7) so the sprite can land
+ * at any pixel x, not just 8-px boundaries; the shift is derived
+ * from the ORIGINAL signed x so a left-edge clip keeps the true
+ * source-to-dest sub-byte phase
+ * 3a. each plane-byte write is guarded by a signed [0,40) dest-byte
+ * range check, which handles left, right, top and bottom clip
* 4. writes the left byte (and the spilled right byte for shifted
* sprites) via AND-mask + OR to all 4 plane buffers
*
@@ -1433,9 +1580,7 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
* code per cel that the old emitter generated. */
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
AmigaPlanarT *pd;
- int16_t dx;
int16_t dy;
- int16_t sx;
int16_t sy;
int16_t w;
int16_t h;
@@ -1445,14 +1590,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
int16_t tileY;
int16_t inTileY;
int16_t startByteX;
+ int16_t dstByteIdx;
uint16_t wTiles;
- uint16_t rowEnd;
uint8_t shift;
uint8_t bit;
uint8_t p0, p1, p2, p3, mask;
uint8_t leftMask, notMask;
uint8_t rshift, rightMask;
- uint16_t dstByteOff;
uint16_t dstRowBase;
const uint8_t *tile;
const uint8_t *chunky;
@@ -1465,37 +1609,34 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
wTiles = sp->widthTiles;
w = (int16_t)(wTiles * 8u);
h = (int16_t)(sp->heightTiles * 8u);
- dx = x;
dy = y;
- sx = 0;
sy = 0;
- /* Clip dst rect against surface (sx/sy track offset INTO sprite). */
- if (dx < 0) { sx = (int16_t)(-dx); w = (int16_t)(w - sx); dx = 0; }
+ /* Vertical clip only (sy tracks offset INTO sprite); horizontal
+ * clip is handled per-byte by the dstByteIdx range guards below so
+ * the source sub-byte phase is preserved on a left-edge clip. */
if (dy < 0) { sy = (int16_t)(-dy); h = (int16_t)(h - sy); dy = 0; }
- if (dx >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || w <= 0 || h <= 0) {
+ if (x >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || (int16_t)(x + w) <= 0 || h <= 0) {
return;
}
- if (dx + w > SURFACE_WIDTH) { w = (int16_t)(SURFACE_WIDTH - dx); }
if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); }
- shift = (uint8_t)(dx & 7u);
- startByteX = (int16_t)(dx >> 3);
+ /* Derive shift / startByteX from the ORIGINAL signed dest x so a
+ * partial left clip keeps the true source-to-dest sub-byte phase.
+ * shift in 0..7 (floored modulo), startByteX = floor(x / 8). For
+ * x = -3: shift = 5, startByteX = -1 -- source column 0 lands
+ * shifted by 5 into dest bytes -1 (off-surface, skipped) and 0. */
+ shift = (uint8_t)(((x % 8) + 8) % 8);
+ startByteX = (int16_t)((x - (int16_t)shift) / 8);
for (row = 0; row < h; row++) {
spritePy = (int16_t)(sy + row);
tileY = (int16_t)(spritePy >> 3);
inTileY = (int16_t)(spritePy & 7);
dstRowBase = (uint16_t)((uint16_t)(dy + row) * AMIGA_BYTES_PER_ROW);
- rowEnd = (uint16_t)(dstRowBase + AMIGA_BYTES_PER_ROW);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
- int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
- if (spriteBx >= (int16_t)wTiles) {
- break;
- }
-
- tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
+ tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
@@ -1515,9 +1656,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
continue;
}
- dstByteOff = (uint16_t)(dstRowBase + (uint16_t)(startByteX + byteCol));
+ /* Signed dest byte index for this source column. Guard each
+ * write against [0, AMIGA_BYTES_PER_ROW) in SIGNED math so
+ * a negative startByteX never wraps to a huge unsigned. */
+ dstByteIdx = (int16_t)(startByteX + byteCol);
leftMask = (uint8_t)(mask >> shift);
- if (leftMask != 0u && dstByteOff < rowEnd) {
+ if (leftMask != 0u && dstByteIdx >= 0 && dstByteIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
+ uint16_t dstByteOff = (uint16_t)(dstRowBase + (uint16_t)dstByteIdx);
notMask = (uint8_t)(~leftMask);
pd->planes[0][dstByteOff] = (uint8_t)((pd->planes[0][dstByteOff] & notMask) | (uint8_t)(p0 >> shift));
pd->planes[1][dstByteOff] = (uint8_t)((pd->planes[1][dstByteOff] & notMask) | (uint8_t)(p1 >> shift));
@@ -1526,17 +1671,16 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
}
if (shift != 0u) {
+ int16_t rIdx = (int16_t)(dstByteIdx + 1);
rshift = (uint8_t)(8u - shift);
rightMask = (uint8_t)(mask << rshift);
- if (rightMask != 0u) {
- uint16_t rOff = (uint16_t)(dstByteOff + 1u);
- if (rOff < rowEnd) {
- notMask = (uint8_t)(~rightMask);
- pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
- pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift));
- pd->planes[2][rOff] = (uint8_t)((pd->planes[2][rOff] & notMask) | (uint8_t)(p2 << rshift));
- pd->planes[3][rOff] = (uint8_t)((pd->planes[3][rOff] & notMask) | (uint8_t)(p3 << rshift));
- }
+ if (rightMask != 0u && rIdx >= 0 && rIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
+ uint16_t rOff = (uint16_t)(dstRowBase + (uint16_t)rIdx);
+ notMask = (uint8_t)(~rightMask);
+ pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
+ pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift));
+ pd->planes[2][rOff] = (uint8_t)((pd->planes[2][rOff] & notMask) | (uint8_t)(p2 << rshift));
+ pd->planes[3][rOff] = (uint8_t)((pd->planes[3][rOff] & notMask) | (uint8_t)(p3 << rshift));
}
}
}
@@ -1645,16 +1789,20 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd;
- uint16_t i;
dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
return;
}
- for (i = 0; i < AMIGA_BITPLANES; i++) {
- memcpy(dstPd->planes[i], srcPd->planes[i], AMIGA_PLANE_SIZE);
- }
+ // Straight blitter A->D copy of every whole plane (200 rows x 20
+ // words, contiguous). Both source and dest planes are CHIP RAM now
+ // (halSurfaceAllocPortData) so the blitter can reach both. The
+ // helper owns the blitter, copies all four planes, and WaitBlit's
+ // before DisownBlitter so the dest planes are complete before any
+ // CPU read.
+ amigaBlitterCopyPlanes(dstPd->planes, srcPd->planes,
+ AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT);
}
@@ -1758,21 +1906,19 @@ static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
AmigaPlanarT *pd;
uint8_t nibLo;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
nibLo = (uint8_t)(colorIndex & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
- if (pd != NULL) {
- amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
- }
+ amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
return true;
}
// Bresenham's diagonal line, planar-only walk. Same algorithm as
-// cross-platform jlDrawLine's fallback so the pixel set matches the
-// chunky walker (draw68kLine) bit-for-bit.
+// cross-platform jlDrawLine's fallback so the pixel set matches it
+// bit-for-bit.
static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
int16_t dx;
int16_t dy;
@@ -1815,29 +1961,25 @@ static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
AmigaPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
- if (pd != NULL) {
- amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
- }
+ amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
return true;
}
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
AmigaPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
- if (pd != NULL) {
- surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
- pd->planes[2], pd->planes[3],
- (uint16_t)cx, (uint16_t)cy, r,
- (uint8_t)(colorIndex & 0x0Fu));
- }
+ surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
+ pd->planes[2], pd->planes[3],
+ (uint16_t)cx, (uint16_t)cy, r,
+ (uint8_t)(colorIndex & 0x0Fu));
return true;
}
@@ -1904,32 +2046,30 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
int16_t spanX;
uint16_t spanW;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
- if (pd != NULL) {
- /* Inline per-span plane fill -- avoids 4 halFillRectPlanes
- * dispatches per midpoint iter (~320 dispatches for r=40). */
- x = (int16_t)r;
- y = 0;
- err = (int16_t)(1 - x);
- while (x >= y) {
- spanX = (int16_t)(cx - x);
- spanW = (uint16_t)(2 * x + 1);
- amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex);
- amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex);
- spanX = (int16_t)(cx - y);
- spanW = (uint16_t)(2 * y + 1);
- amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex);
- amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex);
- y++;
- if (err <= 0) {
- err = (int16_t)(err + y + y + 1);
- } else {
- x--;
- err = (int16_t)(err + y + y - x - x + 1);
- }
+ /* Inline per-span plane fill -- avoids 4 halFillRectPlanes
+ * dispatches per midpoint iter (~320 dispatches for r=40). */
+ x = (int16_t)r;
+ y = 0;
+ err = (int16_t)(1 - x);
+ while (x >= y) {
+ spanX = (int16_t)(cx - x);
+ spanW = (uint16_t)(2 * x + 1);
+ amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex);
+ amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex);
+ spanX = (int16_t)(cx - y);
+ spanW = (uint16_t)(2 * y + 1);
+ amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex);
+ amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex);
+ y++;
+ if (err <= 0) {
+ err = (int16_t)(err + y + y + 1);
+ } else {
+ x--;
+ err = (int16_t)(err + y + y - x - x + 1);
}
}
return true;
@@ -2375,26 +2515,20 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
* is gPlanes[], CHIP, updated only by halPresent). Non-stage uses
* these as the surface's only planes.
*
- * MEMF_FAST (no MEMF_CHIP) explicitly demands fast RAM. The
- * shadow planes are CPU-only (no blitter/copper access); freeing
- * them from chip RAM cuts halPresent's chip-bus contention in
- * half (read FAST + write CHIP instead of read CHIP + write CHIP)
- * and gives RMW drawing primitives 2-3x speedup. If fast RAM is
- * unavailable (bare A500, no expansion), we fall back to chip
- * via the loop below. */
+ * MEMF_CHIP because the bulk plane ops (clear, full-row fill,
+ * surface copy) now run on the BLITTER, which can only reach chip
+ * RAM. This is a deliberate performance tradeoff: chip RMW is
+ * slower than fast RAM for per-pixel CPU draws (line / circle /
+ * single pixel), but the blitter wins big on the full-plane
+ * clear/fill/copy paths and runs in parallel with the CPU. Net is
+ * workload-dependent; see the blitter helpers above. The planes
+ * must be MEMF_CHIP unconditionally now -- a fast-RAM fallback
+ * would put the blitter helpers out of reach and corrupt output. */
for (i = 0; i < AMIGA_BITPLANES; i++) {
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
- (ULONG)(MEMF_FAST | MEMF_CLEAR));
- if (pd->planes[i] == NULL) {
- /* No fast RAM available; fall back to chip. */
- pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
- (ULONG)(MEMF_CHIP | MEMF_CLEAR));
- jlLogF("amiga: shadow plane %u in CHIP (fast unavailable, addr=$%08lX)",
- (unsigned)i, (unsigned long)pd->planes[i]);
- } else {
- jlLogF("amiga: shadow plane %u in FAST (addr=$%08lX)",
- (unsigned)i, (unsigned long)pd->planes[i]);
- }
+ (ULONG)(MEMF_CHIP | MEMF_CLEAR));
+ jlLogF("amiga: shadow plane %u in CHIP (blitter, addr=$%08lX)",
+ (unsigned)i, (unsigned long)pd->planes[i]);
if (pd->planes[i] == NULL) {
/* Roll back any planes already allocated. */
while (i > 0u) {
diff --git a/src/port/atarist/audio.c b/src/port/atarist/audio.c
index 382f52d..64d6c35 100644
--- a/src/port/atarist/audio.c
+++ b/src/port/atarist/audio.c
@@ -46,6 +46,12 @@
#define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L)
#define ST_YM_DATA ((volatile uint8_t *)0xFFFF8802L)
+// YM mixer (reg 7) high bits 6+7 are the I/O port directions TOS
+// leaves configured (port A output, port B = centronics printer
+// output). Always preserved when we rewrite the mixer.
+#define YM_MIXER_IO_DIR 0xC0u
+#define YM_REG_MIXER 7u
+
#define MFP_TA_BIT 0x20
#define MFP_TACR_STOP 0x00
#define MFP_TACR_DIV200 0x07
@@ -81,8 +87,23 @@ static void (*gOldTimerAVec)(void) = NULL;
// Mixed in over libxmp's MOD output during halAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
+// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
+// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
+// drive the mixer through this one shadow so they share one source of
+// truth: 0x3F = all tones and noise off (PWM-DAC default). The YM
+// can't be read back, so this shadow IS the authoritative state.
+static volatile uint8_t gYmToneMask = 0x3Fu;
+
// ----- Internal helpers -----
+// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
+// TOS I/O-port-direction high bits. Supervisor context required.
+static void ymWriteMixer(void) {
+ *ST_YM_SELECT = YM_REG_MIXER;
+ *ST_YM_DATA = (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask);
+}
+
+
static long installTimerA(void) {
uint16_t i;
@@ -114,8 +135,8 @@ static long installTimerA(void) {
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
// (port A output) matches stock TOS; bit 7 set (port B output)
// matches the centronics-printer direction TOS configures.
- *ST_YM_SELECT = 7;
- *ST_YM_DATA = 0xFF; // all tones + noises off; I/O ports A+B output (TOS default)
+ gYmToneMask = 0x3Fu; // all tones + noise disabled (PWM-DAC default)
+ ymWriteMixer(); // -> 0xFF: tones+noise off, I/O ports A+B output
*ST_YM_SELECT = 8;
*ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start
*ST_YM_SELECT = 9;
@@ -271,7 +292,7 @@ void halAudioTone(uint16_t freqHz) {
static volatile uint8_t gYmVoice;
static volatile uint16_t gYmFreqHz;
static volatile uint8_t gYmAtten;
-static volatile uint8_t gYmToneMask = 0x3Fu; // bits 0-5 all off (no tones, no noise)
+// gYmToneMask is the shared YM mixer shadow, defined in module state.
static long applyYmVoice(void) {
@@ -287,8 +308,7 @@ static long applyYmVoice(void) {
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
// zero volume. Don't touch other channels' bits.
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
- *ST_YM_SELECT = 7;
- *ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask); // preserve I/O port-direction high bits
+ ymWriteMixer();
*ST_YM_SELECT = volReg;
*ST_YM_DATA = 0;
return 0;
@@ -313,8 +333,7 @@ static long applyYmVoice(void) {
// Enable tone on this channel (clear its bit in mixer).
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
- *ST_YM_SELECT = 7;
- *ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask);
+ ymWriteMixer();
return 0;
}
@@ -331,7 +350,10 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
void halAudioCriticalEnter(void) {
- // No audio ISR registered -> no critical section needed.
+ // No tick-callback ISR is registered (halAudioTickRegister returns
+ // false). The Timer A MOD ISR synchronizes with the producer via
+ // the gNeedRefill flags, not this critical section, so masking
+ // here would only starve that ~12 kHz audio ISR for no benefit.
}
@@ -340,7 +362,7 @@ void halAudioCriticalExit(void) {
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
- if (voice >= 3u) {
+ if (voice >= JOEY_AUDIO_VOICES) {
return;
}
// The PWM-via-volume engine in halAudioInit hammers YM reg 8
@@ -383,10 +405,24 @@ void halAudioStopSfx(uint8_t slot) {
// Drains ISR-raised refill flags by re-running libxmp's mixer into
// the consumed half then overlaying any active SFX. Called from the
// game loop, never from IRQ.
+//
+// Soft-real-time constraint: the producer (this function) must refill
+// a consumed half within MIX_HALF / MIX_RATE = 1024 / 12288 ~= 83 ms
+// of the ISR raising its flag, or the ISR drains into the half still
+// being rewritten (audible tearing). A full MOD-frame mix plus
+// audioSfxOverlayMix can approach that budget under load, so callers
+// should run the game loop comfortably faster than ~12 Hz.
void halAudioFrameTick(void) {
if (!gReady) {
return;
}
+ // Underrun guard: both flags set means the producer fell a full
+ // buffer behind, so the ISR is now draining a half this tick is
+ // about to rewrite. Silence first so the in-flight torn read plays
+ // as silence rather than garbage, then refill both halves below.
+ if (gNeedRefill[0] && gNeedRefill[1]) {
+ silenceMixBuffer();
+ }
if (gNeedRefill[0]) {
if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);
diff --git a/src/port/atarist/circle.s b/src/port/atarist/circle.s
index d7e21cd..8c219cc 100644
--- a/src/port/atarist/circle.s
+++ b/src/port/atarist/circle.s
@@ -24,19 +24,17 @@
| d5.w = cx (cached)
| a4 = cy (cached, sign-extended)
| a3 = base
-| a5 = bitMaskWordLut
+| a5 = _gStBitMaskWordLut
| d0,d1,d6,d7 = scratch
|
-| Scratch block (24 bytes) at sp+0..23:
-| sp+0..3: xp1 record [groupOff_w, bitMask_b, notMask_b]
-| groupOff = (x >> 4) * 8 (byte offset of group within row)
-| bitMask = byte representation of 1 << (15 - (x & 15))
-| ... wait, bitMask must be a WORD on ST not a byte.
-|
-| Actually layout differs from Amiga: ST needs a WORD bit mask, not
-| a byte. Per-record layout (8 bytes):
-| groupOff_word (2 bytes), bitMask_word (2 bytes), notMask_word
-| (2 bytes), pad (2 bytes)
+| Scratch block (40 bytes) at sp+0..39. Each xp record is 8 bytes but
+| only 2 words are actually stored: groupOff_word at +0 and
+| bitMask_word at +2 (+4/+6 are unused padding kept only for the
+| 8-byte stride). notMask is NOT cached -- PLOT_FIXED derives it per
+| plot via not.w on the bitMask. On ST the bit mask is a WORD (the
+| group is 4 plane words), unlike Amiga's byte mask.
+| xp record: groupOff = (x >> 4) * 8 (byte offset of group in row)
+| bitMask = 1 << (15 - (x & 15)) (word, from LUT)
|
| sp+0..7: xp1 record (cx + bx)
| sp+8..15: xp2 record (cx - bx)
@@ -205,9 +203,11 @@ _surface68kStCircleOutline:
movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -SP_LOCAL(%sp),%sp
- | Load base (a3) and bitMaskLut (a5).
+ | Load base (a3) and bitMaskLut (a5). The mask LUT is
+ | the shared _gStBitMaskWordLut in lineSpan.s; reference
+ | absolute (same pattern as _gStRowOffsetLut below).
move.l SP_BASE(%sp),%a3
- lea bitMaskWordLut(%pc),%a5
+ lea _gStBitMaskWordLut,%a5
| Cache cx in d5, cy (sign-extended) in a4.
move.w SP_CX(%sp),%d5
@@ -278,11 +278,5 @@ _surface68kStCircleOutline:
rts
- .align 2
-| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15.
-bitMaskWordLut:
- .word 0x8000, 0x4000, 0x2000, 0x1000
- .word 0x0800, 0x0400, 0x0200, 0x0100
- .word 0x0080, 0x0040, 0x0020, 0x0010
- .word 0x0008, 0x0004, 0x0002, 0x0001
-| (yLut now lives in lineSpan.s as the shared _gStRowOffsetLut)
+| (bitMaskWordLut and yLut both live in lineSpan.s as the shared
+| _gStBitMaskWordLut / _gStRowOffsetLut.)
diff --git a/src/port/atarist/fillCircle.s b/src/port/atarist/fillCircle.s
index 37543d6..6cf3fbe 100644
--- a/src/port/atarist/fillCircle.s
+++ b/src/port/atarist/fillCircle.s
@@ -51,13 +51,13 @@
lsr.w #4,%d0 | groupLast
sub.w %d1,%d0 | numGroups
move.w %d0,4(%sp)
- | leftMask via LUT[bitFirst]; a5 = leftMaskLut base
+ | leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base
move.w 0(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
move.w (%a5,%d0.w),%d1
move.w %d1,0(%sp)
- | rightMask via LUT[bitLast]; a6 = rightMaskLut base
+ | rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base
move.w 2(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
@@ -216,8 +216,10 @@ _surface68kStFillCircle:
| LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS).
| a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160).
- lea leftMaskLut(%pc),%a5
- lea rightMaskLut(%pc),%a6
+ | All three LUTs are the shared definitions in lineSpan.s;
+ | reference them absolute (single source of truth).
+ lea _gStLeftMaskLut,%a5
+ lea _gStRightMaskLut,%a6
lea _gStRowOffsetLut,%a2
| loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0)
@@ -299,17 +301,5 @@ _surface68kStFillCircle:
rts
- .align 2
-| leftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst (0..15)
-leftMaskLut:
- .word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
- .word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
- .word 0x00FF, 0x007F, 0x003F, 0x001F
- .word 0x000F, 0x0007, 0x0003, 0x0001
-
-| rightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast (0..15)
-rightMaskLut:
- .word 0x8000, 0xC000, 0xE000, 0xF000
- .word 0xF800, 0xFC00, 0xFE00, 0xFF00
- .word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
- .word 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF
+| (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared
+| _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.)
diff --git a/src/port/atarist/hal.c b/src/port/atarist/hal.c
index ff21511..d3e3bd2 100644
--- a/src/port/atarist/hal.c
+++ b/src/port/atarist/hal.c
@@ -38,7 +38,6 @@
#include "surfaceInternal.h"
#include "spriteInternal.h"
#include "joey/tile.h"
-#include "draw68k_inline.h"
// ----- Constants -----
@@ -63,10 +62,12 @@
// Amiga's 4 separate plane buffers -- same total bytes, very different
// access pattern.
//
-// The stage gets its own SHADOW planar buffer (NOT aliased to
-// gScreenBase) so drawing primitives don't appear until jlStagePresent
-// memcpy's shadow -> screen. Same rationale as Amiga's per-stage
-// shadow planes.
+// The stage gets its own SHADOW planar buffer (NOT aliased to a
+// displayed page) so drawing primitives don't appear until
+// jlStagePresent copies the union(prev,current) dirty rows from the
+// shadow into the off-screen page and flips the shifter to it. Same
+// rationale as Amiga's per-stage shadow planes feeding its 2-buffer
+// page flip.
typedef struct {
uint8_t *base; // 32000-byte interleaved planar buffer
uint8_t *raw; // unaligned malloc result for free()
@@ -76,6 +77,17 @@ typedef struct {
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
+// Shifter video base address registers. The shifter latches the base
+// at the start of each frame's display (during vblank), so a write
+// here mid-visible-frame takes effect on the NEXT frame -- a deferred,
+// tear-free page flip with no TOS involvement. STF exposes only the
+// high and mid bytes; there is no low byte, which is why the ST screen
+// must be 256-byte aligned (the low 8 bits of the base are hardwired
+// to zero). Both bytes are odd-addressed byte registers; byte access
+// to an odd address is legal on 68000 (only word/long faults).
+#define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16
+#define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8
+
// MFP hardware addresses.
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data
@@ -136,6 +148,7 @@ static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPl
}
static uint16_t quantizeColorToSt(uint16_t orgb);
static void flattenScbPalettes(const jlSurfaceT *src);
+static void setShifterVideoBase(const uint8_t *page);
static void writeDiagnostics(void);
static long writePrevPaletteRegs(void);
@@ -147,19 +160,51 @@ static void refreshPaletteStateIfNeeded(const
// ----- Module state -----
-// Screen buffer: enough for 320x200x4bpp planar plus padding for
-// runtime 256-byte alignment. TOS .PRG format only supports 2-byte
-// object-file alignment, so we overallocate and align the pointer
-// manually in halInit.
-static uint8_t gScreenBuffer[SURFACE_PIXELS_SIZE + ST_SCREEN_ALIGN];
+// Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
+// padding for runtime 256-byte alignment. TOS .PRG format only supports
+// 2-byte object-file alignment, so we overallocate and align the first
+// page pointer manually in halInit; the second page sits exactly one
+// page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after
+// the first, so it stays 256-byte aligned as the shifter requires.
+static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN];
+
+// Two physical screen pages. gFrontIdx selects the one the shifter is
+// currently displaying; the OFF-screen page (gFrontIdx ^ 1) is the
+// flip target written by the next present. Drawing primitives target
+// the stage's own shadow planar buffer (StPlanarT.base), NOT a page,
+// so the union(prev,cur) dirty copy at present brings the off-screen
+// page fully up to date before the flip -- mirrors the Amiga 2-buffer
+// gPrevStage* staleness pattern (shadow -> off-screen buffer -> flip).
+static uint8_t *gScreenPage[2] = { NULL, NULL };
+static uint8_t gFrontIdx = 0;
-static uint8_t *gScreenBase = NULL;
static void *gPrevPhysbase = NULL;
static void *gPrevLogbase = NULL;
static int16_t gPrevRez = 0;
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
static bool gModeSet = false;
+// Snapshot of the previous frame's dirty bands. With two physical
+// pages the off-screen page holds content from TWO frames ago, so each
+// present must refresh the union(prev, current) of dirty rows from the
+// shadow -- not just this frame's dirty rows -- or stale streaks from
+// two frames back survive on the page we are about to display. Seeded
+// to "all dirty" so the first two presents do full-screen refreshes
+// and converge both pages to the current shadow. After that we pay
+// only the small per-row union(prev,current) copy.
+static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
+static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
+
+// "The previous frame's snapshot recorded at least one dirty row."
+// Lets halPresent skip the whole 200-row union scan + 200-row snapshot
+// copy on consecutive idle frames: if neither the previous frame nor
+// the current frame touched any row, union(prev,current) is empty, the
+// off-screen page already matches the displayed page (the settling
+// flip happened on the first idle frame), and the snapshot would only
+// copy clean->clean. Seeded true to match the "all dirty" bootstrap of
+// gPrevStage* above so the first two presents still run.
+static bool gPrevAnyDirty = true;
+
// Per-scanline pre-quantized palette table. Indexed by display line;
// each row is a 16-word palette ready to be copied straight into the
// shifter registers. Written at present() time, read by the Timer B
@@ -303,12 +348,18 @@ static uint16_t quantizeColorToSt(uint16_t orgb) {
// auto-reload keep counting for the next fire. We deliberately do
// NOT stop/reload/restart the timer here: that sequence would cost
// 1-2 HBL edges each fire, and those losses compound across 7+
-// transitions into a visible "last band short" drift. Updating
-// TBDR in place is enough for variable-length bands -- the new
-// value takes effect on the fire AFTER next, which is acceptable
-// when adjacent bands have similar lengths; uniform bands (like
-// pattern.c) don't need TBDR updates at all so stay perfectly
-// aligned with no drift.
+// transitions into a visible "last band short" drift.
+//
+// MFP event-count auto-reload latches the CURRENT TBDR at each
+// underflow, so a value written here does not take effect on the
+// NEXT fire (that interval was already reloaded at the underflow
+// that triggered this ISR) but on the fire AFTER next. To time
+// variable-height bands correctly we therefore program one fire
+// ahead: in the ISR that transitions to `band` we load TBDR with
+// the band+1 -> band+2 delta (the interval that the band+1
+// underflow will reload and use to time the band+2 transition).
+// Uniform bands (like pattern.c) have all deltas equal, so the
+// one-ahead value is identical and they stay perfectly aligned.
static void timerBIsr(void) {
uint16_t band;
uint8_t palIdx;
@@ -344,14 +395,19 @@ static void timerBIsr(void) {
dst[15] = src[15];
if (band + 1 < gBandCount) {
- // Update TBDR for the fire-after-next (auto-reload at
- // the NEXT fire still uses the old value). Don't stop
- // the timer.
- nextDelta = gBandStart[band + 1] - gBandStart[band];
- if (nextDelta == 0 || nextDelta > 255) {
- nextDelta = 1;
+ // A transition to band+1 is still pending; keep Timer B
+ // running. The band+1 underflow has already had its reload
+ // value latched (set one fire earlier), so program TBDR
+ // now with the band+1 -> band+2 delta, which the band+1
+ // underflow will reload and use to time band+2. Don't
+ // stop the timer.
+ if (band + 2 < gBandCount) {
+ nextDelta = gBandStart[band + 2] - gBandStart[band + 1];
+ if (nextDelta == 0 || nextDelta > 255) {
+ nextDelta = 1;
+ }
+ *ST_MFP_TBDR = (uint8_t)nextDelta;
}
- *ST_MFP_TBDR = (uint8_t)nextDelta;
*ST_MFP_ISRA = MFP_TB_CLEAR;
return;
}
@@ -410,6 +466,19 @@ static void vblIsr(void) {
*ST_MFP_TBDR = (uint8_t)delta;
*ST_MFP_ISRA = MFP_TB_CLEAR;
*ST_MFP_TBCR = MFP_TBCR_EVENT;
+ // The restart above loads the counter from TBDR (= the band
+ // 0 -> 1 delta), so the first fire is correctly timed. Now
+ // pre-program TBDR with the band 1 -> 2 delta so the band-1
+ // underflow auto-reloads with the right interval (one fire
+ // ahead, matching timerBIsr). Writing TBDR while counting
+ // only changes the next reload, not the in-flight count.
+ if (gBandCount > 2) {
+ delta = gBandStart[2] - gBandStart[1];
+ if (delta == 0 || delta > 255) {
+ delta = 1;
+ }
+ *ST_MFP_TBDR = (uint8_t)delta;
+ }
} else {
*ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_ISRA = MFP_TB_CLEAR;
@@ -417,6 +486,23 @@ static void vblIsr(void) {
}
+// Point the shifter at a screen page by writing the high and mid base
+// bytes. The low byte does not exist on STF (base is 256-aligned), so
+// two byte writes fully specify the address. The shifter latches the
+// base at the next frame start, so calling this mid-visible-frame (as
+// halPresent does, after the app's jlWaitVBL) makes the flip take
+// effect on the following frame with no tearing. No TOS / Setscreen
+// involvement, which matters because our VBL ISR replaced TOS's and
+// does not run TOS's Setscreen-latch path.
+static void setShifterVideoBase(const uint8_t *page) {
+ uintptr_t addr;
+
+ addr = (uintptr_t)page;
+ *ST_VID_BASE_HI = (uint8_t)((addr >> 16) & 0xFFu);
+ *ST_VID_BASE_MID = (uint8_t)((addr >> 8) & 0xFFu);
+}
+
+
static long writePrevPaletteRegs(void) {
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
@@ -448,30 +534,45 @@ static void writeDiagnostics(void) {
bool halInit(const jlConfigT *config) {
uintptr_t addr;
+ uint16_t i;
(void)config;
- // Align screen buffer to 256 bytes inside the static storage.
- addr = (uintptr_t)gScreenBuffer;
- addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
- gScreenBase = (uint8_t *)addr;
+ // Align page 0 to 256 bytes inside the static storage; page 1
+ // follows exactly one 32000-byte (256-aligned) page later, so it
+ // is 256-aligned too. The shifter only displays 256-byte-aligned
+ // bases, so both pages must satisfy that.
+ addr = (uintptr_t)gScreenBuffer;
+ addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
+ gScreenPage[0] = (uint8_t *)addr;
+ gScreenPage[1] = gScreenPage[0] + SURFACE_PIXELS_SIZE;
+ gFrontIdx = 0;
- memset(gScreenBase, 0, SURFACE_PIXELS_SIZE);
+ memset(gScreenPage[0], 0, SURFACE_PIXELS_SIZE);
+ memset(gScreenPage[1], 0, SURFACE_PIXELS_SIZE);
+
+ // Bootstrap: pretend the previous frame dirtied EVERY row so the
+ // first two presents do full-page refreshes and bring both pages
+ // into sync with the shadow. After that we converge to the small
+ // per-row union(prev,current) cost.
+ for (i = 0; i < SURFACE_HEIGHT; i++) {
+ gPrevStageMinWord[i] = 0u;
+ gPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX;
+ }
gPrevPhysbase = Physbase();
gPrevLogbase = Logbase();
gPrevRez = Getrez();
// Capture current palette so we can restore exactly on shutdown.
- {
- uint16_t i;
- for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
- gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
- }
+ for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
+ gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
}
- // Switch to ST low-res: 320x200x16, mode 0.
- Setscreen((long)gScreenBase, (long)gScreenBase, 0);
+ // Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
+ // halPresent flips to the off-screen page via the shifter base
+ // registers from here on.
+ Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0);
gModeSet = true;
// Force hardware palette entry 0 to black so the overscan border
@@ -502,13 +603,23 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) {
StPlanarT *pd;
+ uint8_t *backPage;
+ uint8_t backIdx;
int16_t y;
+ int16_t firstDirty;
+ int16_t lastDirty;
+ int16_t runStart;
+ uint8_t curMin;
+ uint8_t curMax;
+ uint8_t prvMin;
+ uint8_t prvMax;
uint8_t minWord;
uint8_t maxWord;
uint16_t groupStart;
uint16_t groupEnd;
uint16_t byteStart;
uint16_t byteLen;
+ bool fullWidth;
if (src == NULL || !gModeSet) {
return;
@@ -519,23 +630,168 @@ void halPresent(const jlSurfaceT *src) {
}
refreshPaletteStateIfNeeded(src);
- // Planar buffer -> screen RAM. Each dirty word covers 4 pixels
- // (a quarter of an 8-byte group). Round to whole groups for a
- // simple aligned memcpy, since planar groups are the natural
- // copy unit.
- for (y = 0; y < SURFACE_HEIGHT; y++) {
- minWord = gStageMinWord[y];
- maxWord = gStageMaxWord[y];
- if (minWord > maxWord) {
- continue;
+ // Idle-frame early-out. When the previous frame's snapshot recorded
+ // no dirty rows (gPrevAnyDirty == false) AND the current frame
+ // touched no row, union(prev,current) is empty: both pages already
+ // match the shadow (the one settling flip after the last activity
+ // ran on the first idle frame), so there is nothing to copy, no
+ // page to flip, and the snapshot below would only copy clean->clean.
+ // Skip the whole 200-row union scan and the 200-row snapshot copy.
+ // A row is dirty iff its min word stayed at or below its max word;
+ // a single byte test per row with early exit on the first dirty row
+ // suffices. Palette/SCB updates already ran above, so display state
+ // stays correct.
+ if (!gPrevAnyDirty) {
+ bool anyDirty = false;
+ for (y = 0; y < SURFACE_HEIGHT; y++) {
+ if (gStageMinWord[y] <= gStageMaxWord[y]) {
+ anyDirty = true;
+ break;
+ }
}
- groupStart = (uint16_t)(minWord >> 2);
- groupEnd = (uint16_t)((maxWord >> 2) + 1);
- byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
- byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
- memcpy(&gScreenBase[(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
- &pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
- byteLen);
+ if (!anyDirty) {
+ return;
+ }
+ }
+
+ // PAGE FLIP: draw into the stage's shadow (pd->base), bring the
+ // OFF-screen page up to date with the union(prev,current) dirty
+ // rows, then point the shifter at it. The off-screen page holds
+ // content from TWO frames ago (strict A/B alternation), so any row
+ // dirtied in either of the last two frames may differ from the
+ // current shadow -- the union refresh covers exactly those rows.
+ // The displayed (front) page is never touched, so there is no
+ // tearing regardless of copy duration.
+ backIdx = (uint8_t)(gFrontIdx ^ 1u);
+ backPage = gScreenPage[backIdx];
+
+ // WIN 2: bound the present to the union dirty row range. The vast
+ // majority of game frames touch a small vertical band; finding the
+ // first/last union-dirty row up front lets the copy loop skip the
+ // huge clean head/tail of the page, and lets us bail the whole copy
+ // body when nothing is dirty (steady-state idle frames). The scan
+ // is 200 cheap byte compares -- far less than 200 memcpy setups.
+ firstDirty = -1;
+ lastDirty = -1;
+ for (y = 0; y < SURFACE_HEIGHT; y++) {
+ curMin = gStageMinWord[y];
+ curMax = gStageMaxWord[y];
+ prvMin = gPrevStageMinWord[y];
+ prvMax = gPrevStageMaxWord[y];
+ minWord = (curMin < prvMin) ? curMin : prvMin;
+ maxWord = (curMax > prvMax) ? curMax : prvMax;
+ if (minWord <= maxWord) {
+ if (firstDirty < 0) {
+ firstDirty = y;
+ }
+ lastDirty = y;
+ }
+ }
+
+ if (firstDirty >= 0) {
+ // Shadow -> off-screen page. Each dirty word covers 4 pixels
+ // (a quarter of an 8-byte group). Round to whole groups for a
+ // simple aligned memcpy, since planar groups are the natural
+ // copy unit.
+ //
+ // WIN 1: backPage and pd->base share the 160-byte row stride and
+ // are both contiguous, so a run of consecutive FULL-WIDTH dirty
+ // rows is one contiguous span of (rows * 160) bytes. Coalesce
+ // such runs into a single memcpy: mintlib memcpy bursts long-
+ // aligned blocks with movem.l/move.l, so one big copy beats many
+ // tiny per-row ones (fewer call setups, longer inner burst).
+ // Partial-width rows keep their own per-row group-aligned memcpy,
+ // so narrow dirty bands stay correct and cheap. runStart < 0
+ // means "no full-width run is open". The per-row band is the
+ // union(prev,current) band so a row stale on the off-screen page
+ // from two frames ago is fully refreshed, not just this frame's
+ // sub-span.
+ runStart = -1;
+ for (y = firstDirty; y <= lastDirty; y++) {
+ curMin = gStageMinWord[y];
+ curMax = gStageMaxWord[y];
+ prvMin = gPrevStageMinWord[y];
+ prvMax = gPrevStageMaxWord[y];
+ minWord = (curMin < prvMin) ? curMin : prvMin;
+ maxWord = (curMax > prvMax) ? curMax : prvMax;
+ if (minWord > maxWord) {
+ // Clean row breaks any open full-width run.
+ if (runStart >= 0) {
+ memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ &pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ (uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
+ runStart = -1;
+ }
+ continue;
+ }
+ // Defense-in-depth: an upstream clipping bug could leave a
+ // maxWord past the row; clamp so the groupEnd-driven byteLen
+ // can never push the memcpy into the next scanline.
+ if (maxWord >= SURFACE_WORDS_PER_ROW) {
+ maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
+ }
+ groupStart = (uint16_t)(minWord >> 2);
+ groupEnd = (uint16_t)((maxWord >> 2) + 1);
+ byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
+ byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
+ fullWidth = (groupStart == 0 && byteLen == (uint16_t)ST_BYTES_PER_ROW);
+
+ if (fullWidth) {
+ // Extend (or open) the contiguous full-width run; defer
+ // the copy until a non-full-width row or the range end
+ // flushes it.
+ if (runStart < 0) {
+ runStart = y;
+ }
+ continue;
+ }
+
+ // Partial-width row: flush any open full-width run first so
+ // the page stays in row order, then copy just this row's band.
+ if (runStart >= 0) {
+ memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ &pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ (uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
+ runStart = -1;
+ }
+ memcpy(&backPage [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
+ &pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
+ byteLen);
+ }
+
+ // Flush a full-width run that reached the end of the dirty range.
+ if (runStart >= 0) {
+ memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ &pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
+ (uint16_t)(lastDirty + 1 - runStart) * (uint16_t)ST_BYTES_PER_ROW);
+ }
+
+ // The off-screen page now holds the complete current image.
+ // Point the shifter at it; the latch happens at the next frame
+ // start (we are mid-visible-frame here, after the app's
+ // jlWaitVBL), so the swap is tear-free. Only flip when there was
+ // something to refresh -- flipping on a fully idle frame would
+ // oscillate the display between the two pages with no benefit.
+ setShifterVideoBase(backPage);
+ gFrontIdx = backIdx;
+ }
+
+ // Snapshot this frame's dirty bands as "previous" for the next
+ // present. jlStagePresent clears the live bands right after
+ // halPresent returns, so we must grab them now. Next present unions
+ // these with that frame's dirty rows to refresh whatever the other
+ // page is stale on. Record whether any row was dirty so the next
+ // present can take the idle-frame early-out above.
+ {
+ bool anyDirty = false;
+ for (y = 0; y < SURFACE_HEIGHT; y++) {
+ gPrevStageMinWord[y] = gStageMinWord[y];
+ gPrevStageMaxWord[y] = gStageMaxWord[y];
+ if (gStageMinWord[y] <= gStageMaxWord[y]) {
+ anyDirty = true;
+ }
+ }
+ gPrevAnyDirty = anyDirty;
}
}
@@ -577,6 +833,7 @@ uint16_t halFrameHz(void) {
// fault on user-mode access. Resolution is 5 ms, plenty for sound
// scheduling at 60 Hz AGI ticks.
static volatile uint32_t gMillisBase200 = 0;
+static volatile bool gMillisBaseSet = false;
static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL);
@@ -585,11 +842,15 @@ static long readHz200(void) {
uint32_t halMillisElapsed(void) {
uint32_t ticks;
ticks = (uint32_t)Supexec(readHz200);
- if (gMillisBase200 == 0u) {
+ // 0 is a legal counter value (power-on tick 0 and every ~248-day
+ // wrap), so use an explicit one-shot flag instead of a value-0
+ // sentinel to capture the base.
+ if (!gMillisBaseSet) {
gMillisBase200 = ticks;
+ gMillisBaseSet = true;
return 0u;
}
- // 200 Hz counter -> ms = ticks * 5.
+ // 200 Hz counter -> ms = ticks * 5. Unsigned subtraction is wrap-safe.
return (uint32_t)((ticks - gMillisBase200) * 5u);
}
@@ -626,12 +887,9 @@ void halShutdown(void) {
}
-// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
-// surface68k.s). Same primitives used by the Amiga port -- the stage /
-// surfaces are identical 4bpp packed layout on both 68k targets.
-extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
-extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
-extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
+// ST word-interleaved planar 68k fast paths. The surface68kSt*
+// primitives are ST-specific (plane-major group layout); they live in
+// src/shared68k/surface68k.s alongside the Amiga-only planar helpers.
extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
extern void surface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color);
extern void surface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color);
@@ -654,16 +912,11 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
uint8_t color;
uint32_t loLong;
uint32_t hiLong;
- uint32_t *p32;
- uint16_t groups;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
- if (pd == NULL) {
- return true;
- }
color = (uint8_t)(doubled & 0x0Fu);
/* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs.
* loLong = (p0_word << 16) | p1_word; hiLong = (p2_word << 16) | p3_word. */
@@ -671,8 +924,6 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
| ((color & 2u) ? 0x0000FFFFul : 0ul);
hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul)
| ((color & 8u) ? 0x0000FFFFul : 0ul);
- (void)p32;
- (void)groups;
surface68kStLongFill(pd->base,
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
loLong, hiLong);
@@ -686,7 +937,7 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// never runs.
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
return true;
@@ -726,13 +977,11 @@ bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
StPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
- if (pd != NULL) {
- stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
- }
+ stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
return true;
}
@@ -845,19 +1094,17 @@ static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
StPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
- if (pd != NULL) {
- // Asm walker assumes fully on-surface; partial-clip lines fall
- // back to the C walker which clips per-pixel.
- if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
- && x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
- surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
- } else {
- stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
- }
+ // Asm walker assumes fully on-surface; partial-clip lines fall
+ // back to the C walker which clips per-pixel.
+ if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
+ && x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
+ surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
+ } else {
+ stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
return true;
}
@@ -865,13 +1112,10 @@ bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
- if (pd == NULL) {
- return true;
- }
/* Off-surface circles fall back to the per-pixel C walker which
* does the clip per plot; the asm assumes fully-on-surface so it
* can drop the clip check from the inner loop. */
@@ -890,13 +1134,10 @@ bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
- if (pd == NULL) {
- return true;
- }
// Off-surface bounding box falls back to the C span walker, which
// clips each span; the asm assumes the whole circle is on-surface.
if ((int32_t)cx - (int32_t)r < 0
@@ -990,7 +1231,7 @@ bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t mat
// false would dereference NULL s->pixels.
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)bx; (void)by; (void)fillWord;
- if (s != jlStageGet()) {
+ if (s->portData == NULL) {
return false;
}
return true;
@@ -998,8 +1239,9 @@ bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
// Phase 2: allocate a shadow word-interleaved planar buffer per
-// surface. Both stage and non-stage get their own buffer (gScreenBase
-// remains the single display target).
+// surface. Both stage and non-stage get their own buffer; the two
+// displayed pages (gScreenPage[0/1]) are the flip targets, kept
+// separate from every surface's shadow.
//
// LONG alignment is required, not just word: the full-row long-fill
// path and circle.s both do `move.l` writes on this buffer, and
@@ -1605,10 +1847,20 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
* (where half_byte = dst_byte_col & 1). This replaces the old
* per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and
* murdered the frame rate on shifted sprites (passenger walk,
- * flame, anything else not on an 8-px boundary). */
+ * flame, anything else not on an 8-px boundary).
+ *
+ * Phase / placement is driven from the ORIGINAL signed x so a
+ * sub-byte left clip (x negative and not a multiple of 8) keeps
+ * its true source-to-destination phase. shift = x & 7 picks the
+ * low 3 bits of x (correct two's-complement modulo-8 even when x
+ * is negative); startByteX = x >> 3 (arithmetic) is the signed
+ * destination byte column for source byte column 0. Each source
+ * byte column is walked in full and the per-byte guards drop the
+ * off-surface portions, so the left byte and its right-carry spill
+ * are clipped independently. */
{
- uint8_t shift = (uint8_t)(spritePxStart & 7);
- int16_t startByteX = (int16_t)(spritePxStart >> 3);
+ uint8_t shift = (uint8_t)(x & 7);
+ int16_t startByteX = (int16_t)(x >> 3);
int16_t byteCol;
rowBase = pd->base + (uint16_t)spritePyStart * ST_BYTES_PER_ROW;
@@ -1618,11 +1870,7 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
int16_t inTileY = (int16_t)(spritePy & 7);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
- int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
- if (spriteBx >= (int16_t)wTiles) {
- break;
- }
- const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
+ const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
const uint8_t *chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
@@ -1645,18 +1893,20 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
/* Compute ST address for the dst byte at byte_col_dst.
* Plane bytes are interleaved word-wise inside groups of
- * 16 pixels = 8 bytes (4 planes x 2 bytes). */
+ * 16 pixels = 8 bytes (4 planes x 2 bytes). The left byte
+ * and the right-carry spill are guarded independently so
+ * a negative dstByteCol (sub-byte left clip) still lands
+ * its spill into on-surface byte dstByteCol+1. */
int16_t dstByteCol = (int16_t)(startByteX + byteCol);
- if (dstByteCol < 0 || dstByteCol >= (int16_t)(SURFACE_WIDTH / 8)) {
- continue;
- }
{
- uint16_t group = (uint16_t)(dstByteCol >> 1);
- uint16_t half = (uint16_t)(dstByteCol & 1);
uint8_t leftMask = (uint8_t)(mask >> shift);
- if (leftMask != 0u) {
- uint8_t notM = (uint8_t)(~leftMask);
- uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
+ if (leftMask != 0u
+ && dstByteCol >= 0
+ && dstByteCol < (int16_t)(SURFACE_WIDTH / 8)) {
+ uint16_t group = (uint16_t)(dstByteCol >> 1);
+ uint16_t half = (uint16_t)(dstByteCol & 1);
+ uint8_t notM = (uint8_t)(~leftMask);
+ uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 >> shift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 >> shift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 >> shift));
@@ -1665,18 +1915,18 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
if (shift != 0u) {
uint8_t rshift = (uint8_t)(8u - shift);
uint8_t rightMask = (uint8_t)(mask << rshift);
- if (rightMask != 0u) {
- int16_t rdc = (int16_t)(dstByteCol + 1);
- if (rdc < (int16_t)(SURFACE_WIDTH / 8)) {
- uint16_t rgroup = (uint16_t)(rdc >> 1);
- uint16_t rhalf = (uint16_t)(rdc & 1);
- uint8_t notM = (uint8_t)(~rightMask);
- uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
- p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
- p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
- p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
- p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
- }
+ int16_t rdc = (int16_t)(dstByteCol + 1);
+ if (rightMask != 0u
+ && rdc >= 0
+ && rdc < (int16_t)(SURFACE_WIDTH / 8)) {
+ uint16_t rgroup = (uint16_t)(rdc >> 1);
+ uint16_t rhalf = (uint16_t)(rdc & 1);
+ uint8_t notM = (uint8_t)(~rightMask);
+ uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
+ p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
+ p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
+ p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
+ p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
}
}
}
diff --git a/src/port/atarist/lineSpan.s b/src/port/atarist/lineSpan.s
index f00ca20..4ac05e9 100644
--- a/src/port/atarist/lineSpan.s
+++ b/src/port/atarist/lineSpan.s
@@ -24,7 +24,7 @@
| d7 = sx (long; moveq #1 or #-1, low word used for .w add)
| a4 = sy (long; sign-extended)
| a3 = base
-| a5 = bitMaskWordLut
+| a5 = _gStBitMaskWordLut
| a2 = scratch (per-pixel: base + byteOff)
| d0,d1 = scratch
|
@@ -46,7 +46,7 @@
| ---- DL_PLOT: 4-plane word RMW for hardcoded color ----
-| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = bitMaskWordLut
+| Inputs: d2.w = x, d3.w = y, a3 = base, a5 = _gStBitMaskWordLut
| Trashes: d0, d1, a2
.macro DL_PLOT color
@@ -127,7 +127,7 @@ _surface68kStDrawLine:
| Load base & luts.
move.l SP_BASE(%sp),%a3
- lea bitMaskWordLut(%pc),%a5
+ lea _gStBitMaskWordLut(%pc),%a5
| a6 = yLut base (yp -> yp*160) for use in DL_PLOT.
lea _gStRowOffsetLut(%pc),%a6
@@ -224,8 +224,12 @@ _surface68kStDrawLine:
.align 2
-| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15.
-bitMaskWordLut:
+| 16 word entries: _gStBitMaskWordLut[i] = 1 << (15 - i), for i in
+| 0..15. Exported (like _gStRowOffsetLut) so circle.s can reference
+| it via absolute addressing -- single source of truth for the per-
+| pixel bit mask shared by jlDrawLine and jlDrawCircle.
+ .globl _gStBitMaskWordLut
+_gStBitMaskWordLut:
.word 0x8000, 0x4000, 0x2000, 0x1000
.word 0x0800, 0x0400, 0x0200, 0x0100
.word 0x0080, 0x0040, 0x0020, 0x0010
@@ -795,7 +799,7 @@ _surface68kStFillRectMulti:
move.w %d0,%d2
and.w #15,%d2
add.w %d2,%d2
- lea frmLeftMaskLut(%pc),%a2
+ lea _gStLeftMaskLut(%pc),%a2
move.w (%a2,%d2.w),%d2 | d2 = leftMask
move.w %d2,%d4
not.w %d4 | d4 = notLeftMask
@@ -804,7 +808,7 @@ _surface68kStFillRectMulti:
move.w %d1,%d3
and.w #15,%d3
add.w %d3,%d3
- lea frmRightMaskLut(%pc),%a2
+ lea _gStRightMaskLut(%pc),%a2
move.w (%a2,%d3.w),%d3 | d3 = rightMask
move.w %d3,%d5
not.w %d5 | d5 = notRightMask
@@ -890,15 +894,20 @@ _surface68kStFillRectMulti:
.align 2
-| Same LUTs as in jlFillCircle.s; duplicated locally so each .o file's
-| PC-rel lea can reach them within its own .text segment.
-frmLeftMaskLut:
+| Leading/trailing edge-mask LUTs, exported (like _gStRowOffsetLut)
+| as the single source of truth shared with jlFillCircle.s via
+| absolute addressing.
+| _gStLeftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst
+| _gStRightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast
+ .globl _gStLeftMaskLut
+_gStLeftMaskLut:
.word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
.word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
.word 0x00FF, 0x007F, 0x003F, 0x001F
.word 0x000F, 0x0007, 0x0003, 0x0001
-frmRightMaskLut:
+ .globl _gStRightMaskLut
+_gStRightMaskLut:
.word 0x8000, 0xC000, 0xE000, 0xF000
.word 0xF800, 0xFC00, 0xFE00, 0xFF00
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
@@ -906,6 +915,14 @@ frmRightMaskLut:
.align 2
+| ST surface stride. Single asm-side source of truth, mirroring
+| include/joey/surface.h SURFACE_BYTES_PER_ROW (160). A future stride
+| change must touch both; the guard below fails the assemble if this
+| drifts from 160.
+ .equ ST_BYTES_PER_ROW, 160
+ .if ST_BYTES_PER_ROW != 160
+ .error "ST_BYTES_PER_ROW must stay 160 (== surface.h SURFACE_BYTES_PER_ROW)"
+ .endif
| Shared y -> y*160 LUT. Used by jlDrawLine (DL_PLOT), jlDrawCircle
| (YP_REC), jlFillCircle (SPAN_BODY). 200 words = 400 bytes.
| Replaces a 44-cyc lsl.w #5 + lsl.w #7 + add.w shift chain with
@@ -915,7 +932,7 @@ frmRightMaskLut:
_gStRowOffsetLut:
.set li_y, 0
.rept 200
- .word li_y * 160
+ .word li_y * ST_BYTES_PER_ROW
.set li_y, li_y + 1
.endr
@@ -924,19 +941,22 @@ _gStRowOffsetLut:
|
| Bulk long-fill helper for full-row fills (jlSurfaceClear, jlFillRect
| 320x200). Writes numGroups groups of 8 bytes (loLong, hiLong)
-| starting at dst. Uses movem.l d2-d7 (3 groups = 24 bytes per
-| batch) plus a tail pair to amortize loop overhead.
+| starting at dst. Register-pumps movem.l d2-d7/a1-a6 (12 longs =
+| 6 groups = 48 bytes per batch) so the per-batch lea/subq/bne loop
+| overhead is amortised over twice the data of the old 24-byte batch.
+| a1 is caller-saved scratch (free); a2-a6 and d2-d7 are callee-saved
+| and restored on exit.
|
| void surface68kStLongFill(uint8_t *dst,
| uint16_t numGroups,
| uint32_t loLong,
| uint32_t hiLong);
|
-| Per-batch cost: movem.l (56 cyc) + subq (8) + bne (10) = 74 cyc
-| for 24 bytes -- ~3 cyc/byte vs ~5 cyc/byte for the straight C
-| do-while of two move.l writes.
+| STF has no blitter, so this CPU movem.l burst is the fast path; the
+| fill is memory-bandwidth bound, so the win is in cutting loop
+| overhead (one lea/subq/bne per 48 bytes instead of per 24).
- .equ SP_LF_SAVED, 24 | d2-d7 = 6 longs
+ .equ SP_LF_SAVED, 44 | d2-d7 + a2-a6 = 11 longs
.equ SP_LF_OFF, (SP_LF_SAVED + 4)
.equ SP_LF_DST, SP_LF_OFF + 0
.equ SP_LF_NGROUPS, SP_LF_OFF + 4 + 2
@@ -947,45 +967,51 @@ _gStRowOffsetLut:
.globl _surface68kStLongFill
_surface68kStLongFill:
- movem.l %d2-%d7,-(%sp)
+ movem.l %d2-%d7/%a2-%a6,-(%sp)
move.l SP_LF_DST(%sp),%a0
move.l SP_LF_LO(%sp),%d2
move.l SP_LF_HI(%sp),%d3
move.w SP_LF_NGROUPS(%sp),%d0
- | Set up d2-d7 = lo, hi, lo, hi, lo, hi (movem writes
- | in d-reg order, so this gives the right alternation
- | for 3 consecutive 8-byte groups).
+ | Spread the {lo,hi} pattern across 12 registers in the
+ | exact order movem.l writes them (d2-d7 then a1-a6, low
+ | to high address) = 6 consecutive 8-byte groups:
+ | lo -> d2,d4,d6,a1,a3,a5 ; hi -> d3,d5,d7,a2,a4,a6
move.l %d2,%d4
move.l %d2,%d6
+ move.l %d2,%a1
+ move.l %d2,%a3
+ move.l %d2,%a5
move.l %d3,%d5
move.l %d3,%d7
+ move.l %d3,%a2
+ move.l %d3,%a4
+ move.l %d3,%a6
- | numBatches = numGroups / 3 (quotient), tail = remainder
+ | numBatches = numGroups / 6 (quotient), tail = remainder
ext.l %d0
- divu.w #3,%d0
+ divu.w #6,%d0
move.l %d0,%d1
- swap %d1 | d1.w = remainder
+ swap %d1 | d1.w = remainder (0..5)
tst.w %d0 | quotient
beq.s .Llf_tail
.Llf_loop:
- movem.l %d2-%d7,(%a0)
- lea 24(%a0),%a0
+ movem.l %d2-%d7/%a1-%a6,(%a0) | 48 bytes = 6 groups
+ lea 48(%a0),%a0
subq.w #1,%d0
bne.s .Llf_loop
.Llf_tail:
- | Remainder: 0, 1, or 2 groups of 8 bytes
+ | Remainder: 0..5 groups of 8 bytes
tst.w %d1
beq.s .Llf_done
+.Llf_tailLoop:
move.l %d2,(%a0)+
move.l %d3,(%a0)+
subq.w #1,%d1
- beq.s .Llf_done
- move.l %d2,(%a0)+
- move.l %d3,(%a0)+
+ bne.s .Llf_tailLoop
.Llf_done:
- movem.l (%sp)+,%d2-%d7
+ movem.l (%sp)+,%d2-%d7/%a2-%a6
rts
diff --git a/src/port/dos/audio.c b/src/port/dos/audio.c
index 8e24044..f9a7590 100644
--- a/src/port/dos/audio.c
+++ b/src/port/dos/audio.c
@@ -94,9 +94,15 @@
// ----- Mix configuration -----
+// MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can
+// only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The
+// SB hardware rate is the fixed one, so the effective rate derived from
+// the same time constant (see gEffRate, set in halAudioInit) is the
+// single source of truth handed to libxmp and the SFX resampler.
#define MIX_RATE 22050
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
#define MIX_HALF (MIX_DMA_BUFFER / 2)
+#define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base
#define ISR_LOCK_SIZE 4096
@@ -109,6 +115,12 @@ static uint8_t gDspMajor = 0;
static uint8_t gDspMinor = 0;
static bool gDspPresent = false;
+// Effective SB playback rate derived from the DSP time constant. Set
+// once in halAudioInit and used as the single rate source of truth for
+// libxmp's mixer and the SFX resampler, so they match what the DAC
+// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
+static uint16_t gEffRate = MIX_RATE;
+
// Conventional-memory DMA buffer.
static int gDmaSelector = 0;
static uint32_t gDmaLinear = 0;
@@ -153,6 +165,7 @@ static void dspWrite(uint8_t value);
static void freeDmaBuffer(void);
static void haltDspDma(void);
static void parseBlasterEnv(void);
+static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit);
static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq);
static int picVectorForIrq(uint8_t irq);
@@ -324,13 +337,13 @@ static void programDmaController(void) {
}
-// 8-bit DSP time constant: tc = 256 - 1000000 / rate. SB plays at
-// 1000000 / (256 - tc) Hz, so we round to the nearest representable
-// rate. For 22050 Hz: tc = 256 - 45 = 211.
+// 8-bit DSP time constant: tc = 256 - SB_DSP_CLOCK_HZ / rate. SB plays
+// at SB_DSP_CLOCK_HZ / (256 - tc) Hz, so we round to the nearest
+// representable rate. For 22050 Hz: tc = 256 - 45 = 211.
static uint8_t dspTimeConst(uint16_t rate) {
uint32_t tc;
- tc = 256u - (1000000u / rate);
+ tc = 256u - (SB_DSP_CLOCK_HZ / rate);
if (tc < 4) {
tc = 4;
}
@@ -364,6 +377,19 @@ static void haltDspDma(void) {
}
+// Decode an IRQ number to its PIC Interrupt Mask Register port and the
+// mask bit within that register. IRQ 0-7 live in PIC1, 8-15 in PIC2.
+static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit) {
+ if (irq < 8) {
+ *port = PIC1_IMR;
+ *bit = (uint8_t)(1u << irq);
+ } else {
+ *port = PIC2_IMR;
+ *bit = (uint8_t)(1u << (irq - 8));
+ }
+}
+
+
// Unmask the IRQ line in the PIC's Interrupt Mask Register so the IRQ
// can reach the CPU. Some environments (DOSBox in particular) leave the
// SB IRQ masked at boot; without this the hooked vector never fires
@@ -373,13 +399,7 @@ static void picUnmaskIrq(uint8_t irq) {
uint8_t bit;
uint8_t mask;
- if (irq < 8) {
- port = PIC1_IMR;
- bit = (uint8_t)(1u << irq);
- } else {
- port = PIC2_IMR;
- bit = (uint8_t)(1u << (irq - 8));
- }
+ picIrqPortBit(irq, &port, &bit);
mask = inportb(port);
mask = (uint8_t)(mask & ~bit);
outportb(port, mask);
@@ -398,13 +418,7 @@ static void picMaskIrq(uint8_t irq) {
uint8_t bit;
uint8_t mask;
- if (irq < 8) {
- port = PIC1_IMR;
- bit = (uint8_t)(1u << irq);
- } else {
- port = PIC2_IMR;
- bit = (uint8_t)(1u << (irq - 8));
- }
+ picIrqPortBit(irq, &port, &bit);
mask = inportb(port);
mask = (uint8_t)(mask | bit);
outportb(port, mask);
@@ -474,6 +488,11 @@ bool halAudioInit(void) {
gDspMajor = (uint8_t)major;
gDspMinor = (uint8_t)minor;
+ // Lock in the effective playback rate from the same time constant
+ // the SB DAC will receive, so libxmp and the SFX resampler agree
+ // with the hardware instead of using the nominal MIX_RATE.
+ gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));
+
if (!allocDmaBuffer()) {
return false;
}
@@ -484,6 +503,20 @@ bool halAudioInit(void) {
return false;
}
+ // Allocate the iret wrapper first so its failure path needs no
+ // unlock; the page locks below are only required once the vector
+ // is actually hooked.
+ vector = picVectorForIrq(gSbIrq);
+ _go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
+ gNewIrqVec.pm_offset = (unsigned long)sbIsr;
+ gNewIrqVec.pm_selector = _go32_my_cs();
+ if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
+ xmp_free_context(gXmpCtx);
+ gXmpCtx = NULL;
+ freeDmaBuffer();
+ return false;
+ }
+
// Lock ISR code + the data sbIsr touches at IRQ time. The mixer
// refill no longer runs in interrupt context, so refillHalf and
// libxmp internals do not need locking.
@@ -493,16 +526,6 @@ bool halAudioInit(void) {
_go32_dpmi_lock_data((void *)&gSbBase, sizeof(gSbBase));
_go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq));
- vector = picVectorForIrq(gSbIrq);
- _go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
- gNewIrqVec.pm_offset = (unsigned long)sbIsr;
- gNewIrqVec.pm_selector = _go32_my_cs();
- if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
- xmp_free_context(gXmpCtx);
- gXmpCtx = NULL;
- freeDmaBuffer();
- return false;
- }
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
gIrqHooked = true;
@@ -569,7 +592,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
return;
}
gXmpLoaded = true;
- if (xmp_start_player(gXmpCtx, MIX_RATE,
+ if (xmp_start_player(gXmpCtx, gEffRate,
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
@@ -588,7 +611,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
- audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
+ audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate);
}
@@ -596,7 +619,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
- audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
+ audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate);
}
diff --git a/src/port/dos/hal.c b/src/port/dos/hal.c
index 72101c6..7e63c2f 100644
--- a/src/port/dos/hal.c
+++ b/src/port/dos/hal.c
@@ -1,4 +1,4 @@
-// DOS HAL: VGA mode 13h, 8-bit DAC, nibble-expand present.
+// DOS HAL: VGA mode 13h, 6-bit DAC, nibble-expand present.
//
// JoeyLib surfaces are 4bpp chunky with a 200-entry SCB table selecting
// one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of
@@ -17,6 +17,7 @@
#include
#include // uclock / UCLOCKS_PER_SEC
+#include
#include
#include
#include
@@ -40,6 +41,7 @@
static void crashHandler(int sig);
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
static void installCrashLog(void);
+static void rebuildExpandLut(uint8_t palBase);
static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src);
@@ -49,48 +51,74 @@ static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false;
static FILE *gCrashLog = NULL;
+// Frame counter via $3DA bit 3 polling; rising edge marks the start
+// of vertical retrace. Caller polls fast enough that no edge is
+// missed (UBER's hot loop is far below 70 Hz period even on a 386).
+static uint16_t gFrameCount;
+static uint8_t gPrevInVret;
+
// Cached palette from the last present. VGA DAC programming is ~768
// outportb calls; skip it when the source palette is unchanged.
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false;
-// Scratch row buffer for halPresent. expandAndWriteLine builds the
-// 8bpp form of a dirty band here, then a single memcpy lowers to REP
-// MOVS to push the whole band into VGA RAM in one bus-batched run.
-// The original per-pixel loop wrote one byte at a time, which stalled
-// the CPU on every ISA cycle (~125 ns at 8 MHz bus); REP MOVS pipelines
-// 16-bit ISA writes back-to-back, much better on slow CPUs.
-static uint8_t gExpandRow[SURFACE_WIDTH];
+// Expand LUT for halPresent. Maps a 4bpp-packed source byte to the
+// 2-byte little-endian mode-13h output pair for the current scanline's
+// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
+// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
+// pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
+// write therefore lands the left pixel at the lower address, matching
+// the old two-byte sequence exactly. Rebuilt only when the SCB-derived
+// palBase changes from the previous row (most stages run long runs of
+// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
+static uint16_t gExpandLut[256];
+static uint8_t gExpandLutBase = 0;
+static bool gExpandLutValid = false;
// ----- Internal helpers (alphabetical) -----
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
uint8_t palBase;
const uint8_t *srcBytes;
- uint8_t *out;
+ uint16_t *out;
uint16_t pairs;
uint16_t i;
- uint8_t byte;
- palBase = (uint8_t)(src->scb[y] << 4);
+ palBase = (uint8_t)(src->scb[y] << 4);
+ if (!gExpandLutValid || palBase != gExpandLutBase) {
+ rebuildExpandLut(palBase);
+ }
// Dirty bands are chunky-word aligned (halPresent computes
// pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source
- // byte (= 2 nibbles) maps to 2 destination bytes -- no per-pixel
- // odd/even shift branch needed.
+ // byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps
+ // &dst[x] 16-bit aligned, so each pair is one aligned 16-bit store
+ // straight into VGA RAM -- no scratch row, no trailing memcpy.
srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
- out = gExpandRow;
+ out = (uint16_t *)&dst[x];
pairs = (uint16_t)(w >> 1);
for (i = 0; i < pairs; i++) {
- byte = *srcBytes++;
- *out++ = (uint8_t)(palBase | (byte >> 4));
- *out++ = (uint8_t)(palBase | (byte & 0x0Fu));
+ *out++ = gExpandLut[*srcBytes++];
}
+}
- // Single bus-batched copy to VGA. memcpy in DJGPP libc lowers to
- // REP MOVS, which queues 16-bit ISA write cycles back-to-back
- // instead of stalling the CPU on every byte store.
- memcpy(&dst[x], gExpandRow, w);
+
+static void rebuildExpandLut(uint8_t palBase) {
+ uint16_t byte;
+ uint8_t leftPixel;
+ uint8_t rightPixel;
+
+ for (byte = 0; byte < 256u; byte++) {
+ // Packed high nibble = LEFT pixel (lower x); packed low nibble
+ // = RIGHT pixel. The little-endian word puts the LEFT pixel in
+ // the low byte so a 16-bit store lands it at the lower (even)
+ // VGA address -- byte-for-byte identical to the old pair store.
+ leftPixel = (uint8_t)(palBase | (byte >> 4));
+ rightPixel = (uint8_t)(palBase | (byte & 0x0Fu));
+ gExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
+ }
+ gExpandLutBase = palBase;
+ gExpandLutValid = true;
}
@@ -102,6 +130,12 @@ static void uploadPalette(const jlSurfaceT *src) {
uint8_t g;
uint8_t b;
+ // The whole index+data burst relies on the DAC's auto-increment
+ // index latch staying in step across the 768 data writes. Mask
+ // interrupts so no installed ISR can desync the latch by touching
+ // $3C8/$3C9 mid-burst. Invariant: no installed ISR may write the
+ // VGA DAC ports while a palette upload is in progress.
+ disable();
outportb(DAC_INDEX_PORT, 0);
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
@@ -109,11 +143,14 @@ static void uploadPalette(const jlSurfaceT *src) {
r = (uint8_t)((rgb >> 8) & 0x0F);
g = (uint8_t)((rgb >> 4) & 0x0F);
b = (uint8_t)(rgb & 0x0F);
+ // VGA DAC is 6-bit per channel; expand JoeyLib's 4-bit
+ // channel to 6 bits (0..63).
outportb(DAC_DATA_PORT, (uint8_t)((r << 2) | (r >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2)));
}
}
+ enable();
}
@@ -256,6 +293,13 @@ void halPresent(const jlSurfaceT *src) {
if (minWord > maxWord) {
continue;
}
+ // Defense-in-depth: an upstream clipping bug that wrote
+ // maxWord >= SURFACE_WORDS_PER_ROW would push the expand copy
+ // past the 320-byte VGA row into the next scanline. Clamp to
+ // the in-contract maximum word index.
+ if (maxWord >= SURFACE_WORDS_PER_ROW) {
+ maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
+ }
pixelX = (int16_t)((uint16_t)minWord << 2);
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
@@ -279,12 +323,6 @@ void halWaitVBL(void) {
}
-// Frame counter via $3DA bit 3 polling; rising edge marks the start
-// of vertical retrace. Caller polls fast enough that no edge is
-// missed (UBER's hot loop is far below 70 Hz period even on a 386).
-static uint16_t gFrameCount = 0;
-static uint8_t gPrevInVret = 0;
-
uint16_t halFrameCount(void) {
uint8_t now;
@@ -322,9 +360,12 @@ extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive;
-static uclock_t gMillisBase = 0;
+static uclock_t gMillisBase = 0;
+static bool gMillisBaseSet = false;
uint32_t halMillisElapsed(void) {
+ uclock_t delta;
+
if (gAudioIsrActive && gAudioIsrHz > 0u) {
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
// wall-clock-correct and monotonic. uint64 promotion in
@@ -333,11 +374,20 @@ uint32_t halMillisElapsed(void) {
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
/ (uint32_t)gAudioIsrHz);
}
- if (gMillisBase == 0) {
- gMillisBase = uclock();
+ // Use an explicit one-shot flag rather than gMillisBase == 0, so a
+ // legitimate uclock() value of 0 on the first call cannot re-arm
+ // the base every call.
+ if (!gMillisBaseSet) {
+ gMillisBase = uclock();
+ gMillisBaseSet = true;
return 0u;
}
- return (uint32_t)((uclock() - gMillisBase) / (UCLOCKS_PER_SEC / 1000u));
+ // Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of
+ // dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not
+ // 1193.18), which would run the clock ~0.015% fast. The uint64
+ // intermediate keeps delta*1000 from overflowing.
+ delta = uclock() - gMillisBase;
+ return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
}
diff --git a/src/port/dos/input.c b/src/port/dos/input.c
index b968ba6..c8f18f7 100644
--- a/src/port/dos/input.c
+++ b/src/port/dos/input.c
@@ -269,20 +269,30 @@ static void joystickPoll(void) {
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0;
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_1] = (btn & BIOS_BTN_B2) == 0;
+ // Announce presence from the successful switches read that produced
+ // live button state, independent of the axis decode below -- matches
+ // the ST/IIgs convention. Doing it here (not after the position read)
+ // keeps connected status consistent with the button state.
+ gJoyConnected[JOYSTICK_0] = true;
+ gJoyConnected[JOYSTICK_1] = true;
+
memset(&r, 0, sizeof(r));
r.h.ah = BIOS_FN_GAMEPORT;
r.x.dx = BIOS_GAMEPORT_DX_POS;
__dpmi_int(BIOS_INT, &r);
if (r.x.flags & BIOS_FLAGS_CARRY) {
+ // A failed position read leaves the stick connected (buttons are
+ // live), but report centered axes rather than stale values.
+ gJoyAxisX[JOYSTICK_0] = 0;
+ gJoyAxisY[JOYSTICK_0] = 0;
+ gJoyAxisX[JOYSTICK_1] = 0;
+ gJoyAxisY[JOYSTICK_1] = 0;
return;
}
gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax);
gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx);
gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx);
gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx);
-
- gJoyConnected[JOYSTICK_0] = true;
- gJoyConnected[JOYSTICK_1] = true;
}
@@ -296,6 +306,12 @@ static void mousePoll(void) {
memset(&r, 0, sizeof(r));
r.x.ax = MOUSE_FN_GET_STATE;
__dpmi_int(MOUSE_INT, &r);
+ // Assumes a mode-13h-aware driver (DOSBox / DOSBox-X) that returns
+ // native 0..SURFACE_WIDTH-1 X. Some real MS-compatible drivers keep
+ // an internal 640-wide virtual space in mode 13h where fn 7/8 only
+ // clamp min/max (not scale), so X comes back doubled and would need
+ // a >>1; that scale-detection is not done here. Y is unaffected
+ // (only X is virtualized in 320-wide modes).
gMouseX = (int16_t)r.x.cx;
gMouseY = (int16_t)r.x.dx;
btn = r.x.bx;
diff --git a/src/port/iigs/audio_full.c b/src/port/iigs/audio_full.c
index fd478cb..702ed13 100644
--- a/src/port/iigs/audio_full.c
+++ b/src/port/iigs/audio_full.c
@@ -1,13 +1,8 @@
// Apple IIgs audio HAL -- single source linked into every IIgs demo.
-// Earlier we split this into an audio.c no-op stub and an audio_full.c
-// real implementation, filtering audio_full.c out of non-AUDIO source
-// sets, because pulling Memory Manager + the 34 KB NTP replayer into
-// every binary blew the ORCA Linker's blank-segment / "Expression too
-// complex" budget. Now that we know how to name load segments (see
-// ORCA/C ch. 30 "segment statement"), we put every function in this
-// file into a named AUDIOIMPL load segment; the GS/OS loader places
-// it in its own bank, so non-AUDIO binaries pay only for the data
-// references, not the implementation code.
+// Memory Manager + the 34 KB NTP replayer only need to be present in
+// AUDIO binaries; link-time dead-stripping drops this implementation
+// code from non-AUDIO binaries, which pay only for the data
+// references they actually use.
//
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
@@ -18,25 +13,27 @@
#include
-#include
-#include
+// Memory Manager via . Handles/pointers are void* and the
+// owner ID comes from MMStartUp(); _ownerid maps to MMStartUp() and
+// _toolErr collapses to 0 so each `|| _toolErr != 0` guard reduces to the
+// NULL-handle failure check. Memory Manager attribute bits are defined
+// here (values from Apple's Memory Manager reference).
+#include
+typedef void *Handle;
+typedef void *Pointer;
+#define _ownerid (MMStartUp())
+#define _toolErr 0
+#define attrPage 0x0004
+#define attrNoCross 0x0010
+#define attrFixed 0x4000
+#define attrLocked 0x8000
#include "hal.h"
#include "joey/audio.h"
-// Place every function defined below in the shared DRAWPRIMS overflow
-// load segment so the linker keeps the implementation code out of
-// _ROOT in every binary that includes this TU. (See ORCA/C ch. 30
-// "segment statement". Reusing the same segment as draw.c / tile.c
-// rather than picking a unique name keeps the linker's symbol-
-// resolution expressions flat.)
-//
-// The 34 KB NTP replayer bytes are NOT in this segment -- ORCA/C's
-// `segment` statement only relocates functions, not data. They live
-// in their own NTPDATA load segment, declared in build/iigs/audio/
-// ntpdata.asm (auto-generated from ntpplayer.bin by make/iigs.mk).
-// We just extern the symbols here.
-segment "DRAWPRIMS";
+// The 34 KB NTP replayer bytes live in their own data segment
+// (build/iigs/audio/ntpdata.asm, auto-generated from ntpplayer.bin by
+// make/iigs.mk). We just extern the symbols here.
extern const unsigned char gNtpPlayerBytes[];
extern const unsigned long gNtpPlayerBytes_len;
@@ -99,8 +96,8 @@ static bool gNTPPlaying = false;
// Per-slot config cache. halAudioPlaySfx's biggest cost is the
// per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's
-// unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms in
-// ORCA-C. Most callers play the same SFX repeatedly into the same
+// unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms.
+// Most callers play the same SFX repeatedly into the same
// slot. Cache (sample ptr, length, rate) per slot; on cache hit
// (same sample re-triggered) skip the byte copy AND the struct
// rebuild, just re-fire NTPstreamsound.
@@ -129,17 +126,14 @@ static uint16_t gSfxSlotRateHz[JOEY_AUDIO_SFX_SLOTS] = { 0 };
// `jsl gCallStub` followed by `rep #0x30` -- no global-operand
// references in the asm block.
//
-// The trailing REP is required to satisfy ORCA-C's `longa on /
-// longi on` exit contract: the byte writes the call sites do to
-// patch this stub leave M=8, and NTP's RTL leaves M/X in whatever
-// state NTP chose. Without REP, the C epilogue's ADC/LDX widths
-// decode wrong and the function returns to garbage. See
-// project_iigs_inline_asm_mode.md.
+// The trailing REP restores M=16/X=16 for the function epilogue: the
+// byte writes the call sites do to patch this stub leave M=8, and
+// NTP's RTL leaves M/X in whatever state NTP chose. Without REP, the
+// C epilogue's ADC/LDX widths decode wrong and the function returns
+// to garbage. See project_iigs_inline_asm_mode.md.
//
-// (We tried the obvious `lda gAsmGlobal / jsl gJslStub` shape first;
-// ORCA's inline assembler accepts the first absolute-global operand
-// in a block but rejects the second and third with "invalid operand".
-// Folding the loads into the stub side-steps the issue entirely.)
+// Folding the X/Y/A loads and the JSL target into the stub keeps the
+// inline asm block free of absolute-global operands entirely.
//
// Layout (15 bytes):
// 00: C2 30 REP #$30 ; 16-bit M/X
@@ -183,7 +177,7 @@ static bool loadNTP(void) {
return false;
}
HLock(h);
- p = *h;
+ p = *(void **)h;
if (p == NULL) {
DisposeHandle(h);
return false;
@@ -222,7 +216,7 @@ bool halAudioInit(void) {
}
HLock(modHandle);
gModuleHandle = modHandle;
- gModuleBase = (uint32_t)*modHandle;
+ gModuleBase = (uint32_t)*(void **)modHandle;
// Third fixed-bank handle for SFX sample storage. Same idea as
// the module handle: NTPstreamsound wants a 24-bit pointer, and
@@ -239,7 +233,7 @@ bool halAudioInit(void) {
}
HLock(sfxHandle);
gSfxHandle = sfxHandle;
- gSfxBase = (uint32_t)*sfxHandle;
+ gSfxBase = (uint32_t)*(void **)sfxHandle;
gNTPReady = true;
return true;
@@ -312,17 +306,19 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
(uint16_t)(gModuleBase & 0xFFFFu),
(uint16_t)((gModuleBase >> 16) & 0xFFu),
0);
- asm {
- jsl gCallStub
- rep #0x30
- }
+ __asm__ volatile (
+ "jsl gCallStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
// NTPplay(loopFlag in A). 0 = loop forever, 1 = play once.
buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1);
- asm {
- jsl gCallStub
- rep #0x30
- }
+ __asm__ volatile (
+ "jsl gCallStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
gNTPPlaying = true;
}
@@ -353,7 +349,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
// for this slot? Then the slot's DOC sample bytes and stream
// struct are already correct -- skip the 4 KB byte-XOR loop and
// the 15-byte struct rebuild, both of which together can run
- // ~50 ms per call in ORCA-C.
+ // ~50 ms per call.
if (sample == gSfxSlotSample[slot] &&
length == gSfxSlotLength[slot] &&
rateHz == gSfxSlotRateHz[slot]) {
@@ -401,10 +397,11 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
(uint16_t)(structAddr & 0xFFFFu),
(uint16_t)((structAddr >> 16) & 0xFFu),
0);
- asm {
- jsl gCallStub
- rep #0x30
- }
+ __asm__ volatile (
+ "jsl gCallStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
}
@@ -466,10 +463,11 @@ void halAudioStopMod(void) {
return;
}
buildCallStub(gNTPBase + 6, 0, 0, 0);
- asm {
- jsl gCallStub
- rep #0x30
- }
+ __asm__ volatile (
+ "jsl gCallStub\n\t"
+ "rep #0x30"
+ ::: "memory"
+ );
gNTPPlaying = false;
}
@@ -487,17 +485,13 @@ void halAudioStopSfx(uint8_t slot) {
// NTP has no SFX-stop entry point. We poke DOC oscillator control
// registers directly to halt the slot's osc pair. SEI/CLI keeps
// NTP's DOC IRQ from racing our sound_address writes.
- asm {
- sei
- }
+ __asm__ volatile ("sei" ::: "memory");
*DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME;
for (i = 0; i < SFX_OSCS_PER_SLOT; i++) {
*DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i);
*DOC_SOUND_DATA = DOC_OSC_HALT_BITS;
}
- asm {
- cli
- }
+ __asm__ volatile ("cli" ::: "memory");
}
diff --git a/src/port/iigs/hal.c b/src/port/iigs/hal.c
index 8b3d054..cd4b4c6 100644
--- a/src/port/iigs/hal.c
+++ b/src/port/iigs/hal.c
@@ -7,20 +7,19 @@
// $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB
//
// NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on.
-// ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so
-// that the long addresses resolve to bank $E1.
+// Long (24-bit) addresses are used so they resolve to bank $E1.
//
// DIRTY-WALK + PEI-SLAM PRESENT
// -----------------------------
// halPresent walks the per-row dirty bands maintained by drawing
// primitives in src/core/*.c. Fully-dirty rows go through the PEI
-// slam in src/port/iigs/peislam.asm (~530 cyc/row, ~55% faster than
-// memcpy/MVN); partial-dirty rows use memcpy, which ORCA-C lowers
+// slam in src/port/iigs/peislam.s (~530 cyc/row, ~55% faster than
+// memcpy/MVN); partial-dirty rows use memcpy, which lowers
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into
// bank $E1 when the dirty band is too narrow to amortize the slam's
// per-call AUXWRITE/RAMRD/shadow toggle.
//
-// peislam.asm declares its load segment as DRAWPRIMS so the linker
+// peislam.s declares its load segment as DRAWPRIMS so the linker
// places it in its own bank, separate from AUDIO's _ROOT (where
// audio_full.c + Memory Manager + stdio + NTPstreamsound already
// crowd up against the 64 KB-per-bank limit).
@@ -33,7 +32,7 @@
#include "hal.h"
#include "surfaceInternal.h"
-/* GetTick wrapper in peislam.asm: invokes the Misc Toolset GetTick
+/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick
* ($2503) and returns the low 16 bits of the system's tick counter
* (firmware VBL ISR-driven). Polling $C019 from C user code missed
* transitions for any op over ~1 ms; the system's tick counter is
@@ -48,10 +47,9 @@ static uint16_t gFrameHz = 60u;
// hal.c is the single TU that calls into joeyDraw.asm. Cross-
// platform draw.c / tile.c / etc. dispatch through halFast*
// functions defined here; they never reference the asm symbols
-// directly. This avoids the cumulative ORCA-Linker-Expression-
-// too-complex-in-13/SysLib failure that hit when each cross-
-// platform TU brought its own asm extern.
-JOEYLIB_SEGMENT("DRAWPRIMS")
+// directly. Keeping the asm externs in a single TU avoids the
+// link-time fragility seen when each cross-platform TU brought its
+// own asm extern.
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
@@ -81,9 +79,10 @@ extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_
extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
// Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette.
// Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette.
-// Replaces ORCA-C's memcpy path which silently fails when called
-// from halPresent (DBR-state quirk after prior asm primitives).
-extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr);
+// The asm performs the upload directly rather than a C-side memcpy,
+// which is unreliable when called from halPresent (DBR-state quirk
+// after prior asm primitives).
+extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by halFastFloodWalkAndScans.
extern uint16_t gFloodSeedMatch;
@@ -102,7 +101,7 @@ extern void iigsInitRowLut(void);
// screen presents (halPresentRect). srcOffset is the byte offset
// within bank $01 of the FIRST byte to copy on the FIRST row;
// subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs
-// ORCA-C memcpy's ~30 cyc/byte.
+// C memcpy's ~30 cyc/byte.
extern void iigsBlitRectStageToShr(uint16_t srcOffset, uint16_t copyBytes, uint16_t rowsLeft);
// PEI-slam variant of the per-row rect blit. ~3 cyc/byte vs MVN's
// ~9 cyc/byte. Constraints: copyBytes must be even and 2..80
@@ -165,7 +164,7 @@ static bool gModeSet = false;
// SCB / palette upload skipping is now driven by gStageScbDirty /
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp-
-// against-cached-copy approach was costing ~7 ms / frame on ORCA-C.
+// against-cached-copy approach was costing ~7 ms / frame.
// PEI slam scratch. File-scope non-static so the asm can `ext` them;
// all accesses inside the slam use long-mode `>` addressing so they
@@ -175,8 +174,10 @@ volatile uint8_t gPeiOrigShadow;
volatile uint16_t gPeiTempRowBase;
volatile uint16_t gPeiCurRow; // row counter saved across slam (stack is hijacked)
volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across slam (Y reg storage)
+volatile uint16_t gPeiMvnSrc; // narrow-extent present: MVN source offset
+volatile uint16_t gPeiMvnCount; // narrow-extent present: MVN byte count
-// peislam.asm's per-row peiSlamFullRow helper is no longer wired in;
+// peislam.s's per-row peiSlamFullRow helper is no longer wired in;
// the present pipeline now does its own PEI-slam loop inside
// iigsBlitStageToShr above (with dirty-row skip).
@@ -184,7 +185,7 @@ volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across sl
// Upload SCB / palette into bank-$E1 SHR memory only when the
// matching dirty flag is set. Replaces a per-frame 712-byte memcmp
-// pair (~7 ms / frame on ORCA-C with -b) with a 2-cyc flag check.
+// pair (~7 ms / frame) with a 2-cyc flag check.
// gStageScbDirty / gStagePaletteDirty live in core/surface.c; they
// start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
@@ -231,16 +232,22 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) {
+ uint16_t uploadFlags;
+
if (src == NULL) {
return;
}
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
- // ORCA-C's C-side memcpy to bank $E1 has been unreliable from
+ // C-side memcpy to bank $E1 has been unreliable from
// halPresent's calling context, so we route everything through
- // the asm path. Future: re-introduce per-row dirty-band logic
- // for partial-screen updates (currently we always blit 32K).
- iigsBlitStageToShr(src->scb, &src->palette[0][0]);
+ // the asm path. uploadFlags gates the optional SCB / palette
+ // uploads on the dirty bits so we don't re-push 712 unchanged
+ // bytes every frame: bit 0 = SCB, bit 1 = palette (pixels always).
+ uploadFlags = (uint16_t)gStageScbDirty | ((uint16_t)gStagePaletteDirty << 1);
+ iigsBlitStageToShr(src->scb, &src->palette[0][0], uploadFlags);
+ gStagePaletteDirty = false;
+ gStageScbDirty = false;
}
@@ -266,7 +273,7 @@ void halShutdown(void) {
// halFastFillRect: macro-dispatched in core/hal.h, same as the other
// halFast* primitives. The C wrapper that used to live here was kept
-// as load-bearing _ROOT mass to defeat ORCA-Linker bank fragility;
+// as load-bearing _ROOT mass to defeat linker bank fragility;
// since the CORESYS migration drained _ROOT, the macro form is safe.
@@ -298,50 +305,9 @@ void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
}
-// IIgs SHR is chunky-native; no bitplanes to update.
-void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
- (void)s;
- (void)x;
- (void)y;
- (void)w;
- (void)h;
- (void)colorIndex;
-}
-
-
-void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
- (void)dst;
- (void)src;
-}
-
-
-void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
- (void)s; (void)bx; (void)by; (void)colorIndex;
-}
-void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
- (void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy;
-}
-void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
- (void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy; (void)transparentIndex;
-}
-void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
- (void)dst; (void)bx; (void)by; (void)chunkyTile;
-}
-void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
- (void)dst; (void)bx; (void)by; (void)monoTile; (void)fgColor; (void)bgColor;
-}
-void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyTileOut) {
- (void)src; (void)bx; (void)by; (void)chunkyTileOut;
-}
-void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
- (void)s; (void)sp; (void)x; (void)y;
-}
-void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
- (void)s; (void)x; (void)y; (void)w; (void)h; (void)dstPlaneBytes;
-}
-void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
- (void)s; (void)x; (void)y; (void)w; (void)h; (void)srcPlaneBytes;
-}
+// IIgs is chunky-native: the hal*Planes hooks are compiled out by
+// no-op macros in core/hal.h (gated on JOEYLIB_PLATFORM_IIGS), so no
+// stub functions are defined here.
/* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like
@@ -436,7 +402,7 @@ void halWaitVBL(void) {
// context where ToolBox interrupt setup would be intrusive.
//
// gFrameCount uses an explicit lda+adc+sta read-modify-write rather
-// than `gFrameCount++` because ORCA-C lowers the post-increment to
+// than `gFrameCount++` because the post-increment can lower to
// `inc |gFrameCount` (the only INC abs form on 65816 -- there is no
// INC long-abs). With this file in the DRAWPRIMS load segment but
// halFrameCount called from CORESYS via JSL, DBR isn't pointing at
@@ -454,8 +420,7 @@ uint16_t halFrameHz(void) {
}
-// IIgs: frame-counter approximation. ORCA-C's time() bursts the
-// stdio cluster bank limit (see feedback_orca_c_time_segment), so
+// IIgs: frame-counter approximation. time() is avoided here, so
// we approximate wall-clock ms from the 16-bit frame counter at
// gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so
// callers tracking deltas should use this for short intervals only
diff --git a/src/port/iigs/input.c b/src/port/iigs/input.c
index 5cc0b34..42e0270 100644
--- a/src/port/iigs/input.c
+++ b/src/port/iigs/input.c
@@ -36,8 +36,6 @@
#include
-#include
-
#include "hal.h"
#include "inputInternal.h"
#include "joey/surface.h"
@@ -118,9 +116,8 @@ static int8_t thresholdPaddle(uint8_t v);
// ----- Module state -----
-// ASCII -> jlKeyE, filled once at halInputInit. ORCA/C is C89 and
-// does not accept designated initializers; runtime fill keeps lookup
-// O(1) instead of a 40-plus-case switch.
+// ASCII -> jlKeyE, filled once at halInputInit. Runtime fill keeps
+// lookup O(1) instead of a 40-plus-case switch.
static uint8_t gAsciiToKey[ASCII_TABLE_SIZE];
static int16_t gMouseAbsX = SURFACE_WIDTH / 2;
@@ -230,9 +227,7 @@ static int8_t thresholdPaddle(uint8_t v) {
// Each paddle read triggers an RC scan via $C070 and then polls the
// paddle softswitch until bit 7 clears; the iteration count
// approximates the paddle's 0..255 position (the Apple firmware
-// PREAD routine works the same way). The two reads are inlined here
-// rather than factored into a helper because ORCA/C 2.1 trips over
-// Auto-disconnect tracking. The paddle one-shot timer takes ~3 ms to
+// PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to
// charge at full deflection; if NO joystick is wired up, the BUSY bit
// stays set forever and the busy-wait runs the full PADDLE_TIMEOUT
// every frame -- ~3 ms wasted per frame on a stick that isn't there.
@@ -251,10 +246,8 @@ static bool gJoyDisconnectLatched = false;
// to the digital threshold mapping. gJoyRecalibrate is set by
// halJoystickReset and cleared on the next successful poll, which
// captures the new center.
-// uint8_t (not bool) so the per-element stride is 1 byte. ORCA-C's
-// _Bool is 2 bytes, which forces a ~MUL4 helper for every index
-// multiply -- even when the index is a constant the compiler doesn't
-// fold. Storage is still 0 or 1 either way.
+// uint8_t (not bool) so the per-element stride is a known 1 byte.
+// Storage is still 0 or 1 either way.
static uint8_t gJoyCenterX [JOYSTICK_COUNT];
static uint8_t gJoyCenterY [JOYSTICK_COUNT];
static uint8_t gJoyCenterValid [JOYSTICK_COUNT];
@@ -290,10 +283,8 @@ static void pollJoystick(void) {
bool yResolved;
// Buttons are I/O reads -- always cheap, do them every frame.
- // ORCA-C 2.2.1 doesn't constant-fold the row-stride multiply for
- // 2D arrays even when both indices are constants, so each
- // gJoyButtonState[i][j] write emits a ~MUL4 helper. Indexing
- // through a (uint8_t *) cast collapses to a literal byte offset.
+ // Indexing through a (uint8_t *) cast collapses each
+ // gJoyButtonState[i][j] write to a literal byte offset.
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0]
= (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0;
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_1]
@@ -324,11 +315,11 @@ static void pollJoystick(void) {
// stick. One resolves => stick is present, reset the counter.
//
// gJoyConsecutiveTimeouts uses a local-var read-modify-write rather
- // than `++`. ORCA-C lowers `++` to `inc abs` (the only INC abs form
- // on 65816), which depends on DBR pointing at this static's bank.
- // Cross-segment JSL doesn't update DBR, so a caller in a different
- // load segment would silently mutate the wrong byte. Long-mode
- // lda+sta is DBR-independent.
+ // than `++` so the update is DBR-independent: an `inc abs` on the
+ // static would depend on DBR pointing at this static's bank, and a
+ // cross-segment JSL doesn't update DBR, so a caller in a different
+ // load segment could silently mutate the wrong byte. Long-mode
+ // lda+sta avoids that.
if (!xResolved && !yResolved) {
uint16_t timeouts;
diff --git a/src/port/iigs/joeyDraw.asm b/src/port/iigs/joeyDraw.asm
deleted file mode 100644
index ebc44a6..0000000
--- a/src/port/iigs/joeyDraw.asm
+++ /dev/null
@@ -1,4269 +0,0 @@
-* joeyDraw.asm - Apple IIgs ASM inner loops for graphics primitives.
-*
-* Each function is the FAST INNER LOOP only. The C wrappers in
-* src/core/draw.c and src/core/tile.c handle:
-* - NULL / out-of-range argument validation
-* - clipping math (Cohen-Sutherland for lines, simple AABB clip
-* for rects, etc.)
-* - dirty-rect marking after the fact (surfaceMarkDirtyRect /
-* surfaceMarkDirtyAll)
-* The inner loops trust their inputs: pre-clipped rect, byte-aligned
-* destination, no NULL pointers, etc.
-*
-* Calling convention: ORCA-C cross-load-segment cdecl.
-* - These routines live in the IIGSASM load segment; the C
-* wrappers in iigs/hal.c live in DRAWPRIMS. ORCA-C emits JSL
-* (3-byte return: PCL, PCH, PCB) for cross-load-segment calls
-* regardless of memory model, so we use RTL to return and the
-* stack offsets below are the same whether the binary was built
-* with -b or not.
-* - Caller pushes args right-to-left.
-* - On JSL entry: SP+4 is the first arg byte 0.
-* - After PHP at function entry: SP+5 is the first arg byte 0
-* (PHP shifts every offset by +1). After PHP+PHB: SP+6. After
-* PHP+PHB+PHD: SP+8.
-* - Pointer args are 4 bytes (low 16, then high byte = bank, then
-* 1 padding byte).
-* - 8-bit args (uint8_t etc.) are passed as int-promoted 16-bit.
-*
-* Each function declares its own ORCA object segment with a load-
-* segment name of DRAWPRIMS, so the GS/OS loader places them
-* together in their own bank rather than in _ROOT (see
-* docs/DESIGN.md and the original ORCA/M for IIgs ch. 6 "Load
-* Segments").
-
- keep JOEYDRAW
- case on
-
-
-****************************************************************
-* Repeated patterns inlined throughout this file (ORCA/M's macro
-* support did not accept our positional-parameter syntax so we expand
-* by hand). Two recurring sequences worth naming:
-*
-* y -> row byte offset (a.k.a. y*160) via gRowOffsetLut. A holds y on
-* entry; A holds y*160 on exit. Trashes X. Used in every primitive
-* that needs row offsets:
-* asl a ; A = y*2 (LUT byte offset)
-* tax
-* lda >gRowOffsetLut,x ; A = y*160
-*
-* High-nibble RMW: byte at [PTR],Y in DP-indirect-long form, with the
-* new (nib<<4) byte in NIBHI:
-* lda [PTR],y
-* and #$0F
-* ora >NIBHI
-* sta [PTR],y
-*
-* Low-nibble RMW: same shape, with the new nibble (low half only) in
-* NIBLO:
-* lda [PTR],y
-* and #$F0
-* ora >NIBLO
-* sta [PTR],y
-****************************************************************
-
-
-****************************************************************
-* iigsProbeArgs(uint8_t *ptr, uint16_t a, uint16_t b)
-*
-* Pure diagnostic. Captures the 16-bit value at each of stack
-* offsets 1..15 (after our PHP) into the gProbe1..gProbe15 scratch
-* slots. C reads them back and prints to JOEYLOG. Lets us nail
-* down ORCA's actual cdecl/-b convention by experiment instead of
-* guessing.
-****************************************************************
-
-iigsProbeArgs start IIGSASM
- php
- rep #$30
-
- lda 1,s
- sta >gProbe1
- lda 2,s
- sta >gProbe2
- lda 3,s
- sta >gProbe3
- lda 4,s
- sta >gProbe4
- lda 5,s
- sta >gProbe5
- lda 6,s
- sta >gProbe6
- lda 7,s
- sta >gProbe7
- lda 8,s
- sta >gProbe8
- lda 9,s
- sta >gProbe9
- lda 10,s
- sta >gProbe10
- lda 11,s
- sta >gProbe11
- lda 12,s
- sta >gProbe12
- lda 13,s
- sta >gProbe13
- lda 14,s
- sta >gProbe14
- lda 15,s
- sta >gProbe15
-
- plp
- rtl
- end
-
-
-gProbe1 data DRAWPRIMS
- ds 2
- end
-gProbe2 data DRAWPRIMS
- ds 2
- end
-gProbe3 data DRAWPRIMS
- ds 2
- end
-gProbe4 data DRAWPRIMS
- ds 2
- end
-gProbe5 data DRAWPRIMS
- ds 2
- end
-gProbe6 data DRAWPRIMS
- ds 2
- end
-gProbe7 data DRAWPRIMS
- ds 2
- end
-gProbe8 data DRAWPRIMS
- ds 2
- end
-gProbe9 data DRAWPRIMS
- ds 2
- end
-gProbe10 data DRAWPRIMS
- ds 2
- end
-gProbe11 data DRAWPRIMS
- ds 2
- end
-gProbe12 data DRAWPRIMS
- ds 2
- end
-gProbe13 data DRAWPRIMS
- ds 2
- end
-gProbe14 data DRAWPRIMS
- ds 2
- end
-gProbe15 data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord)
-*
-* MVN-based fill of the 32000-byte stage at $01:$2000-$9CFF with
-* fillWord (16-bit pattern, both bytes the same). Seed first 2
-* bytes via long-mode STA, then MVN $01,$01 propagates the seed to
-* the rest. ~7 cyc/byte; stays in M=16 throughout (no SEP/REP toggle
-* in the body). SEI brackets to mask IRQs across MVN.
-*
-* MUST live in DRAWPRIMS load segment (same as the C caller
-* halFastSurfaceClear). Putting this function in its own load segment
-* causes ORCA-C's cross-segment call to silently land in a stub that
-* RTLs without invoking the body -- looks like a no-op.
-*
-* The `pixels` arg is ignored; the stage is hardcoded at $01:$2000
-* (the C wrapper guarantees s == jlStageGet() before calling).
-*
-* Args after PHP only: pixels at SP+5..8, fillWord at SP+9..10.
-****************************************************************
-
-****************************************************************
-* iigsInitRowLut(void)
-*
-* One-shot init for the y*160 lookup table at gRowOffsetLut. C calls
-* this once from halInit; afterwards every primitive that needs to
-* convert y -> row byte offset can do `lda >gRowOffsetLut,x` (X = y*2)
-* in 6 cyc instead of the 7-instruction shift-add (~22 cyc). 200
-* entries x 2 bytes = 400 bytes in DRAWPRIMS data.
-****************************************************************
-
-iigsInitRowLut start DRAWPRIMS
- php
- rep #$30
- LONGA ON
- LONGI ON
- ldx #0 ; X = byte offset into LUT
- lda #0 ; A = current y*160
-initLutLoop anop
- sta >gRowOffsetLut,x
- clc
- adc #160
- inx
- inx
- cpx #400
- bcc initLutLoop
- LONGA OFF
- LONGI OFF
- plp
- rtl
- end
-
-
-gRowOffsetLut data DRAWPRIMS
- ds 400
- end
-
-
-iigsSurfaceClearInner start DRAWPRIMS
- php
- sei
- rep #$30 ; M=16, X=16
- LONGA ON
- LONGI ON
-
- lda 9,s ; A = fillWord
- sta >$012000 ; seed first 2 bytes
-
- ldx #$2000
- ldy #$2002
- lda #31997
- mvn $010000,$010000
-
- LONGA OFF
- LONGI OFF
- plp
- rtl
- end
-
-
-sclrPixelsCap data DRAWPRIMS
- ds 2
- end
-sclrFillCap data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* Single 2-byte slot for jlSurfaceClear's saved stack pointer. Other
-* asm primitives (when they land) should keep state in registers
-* and on the original stack frame -- adding more cross-segment
-* data references tips the ORCA Linker's "Expression too complex"
-* threshold for the smaller binaries.
-****************************************************************
-
-sclrSavedSp data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsTileFillInner(uint8_t *dstRow0, uint16_t fillWord)
-*
-* 16 STA abs,X stores at fixed offsets along a 160-byte stride.
-* ~120 cyc per call vs ORCA-C's ~300.
-****************************************************************
-
-iigsTileFillInner start IIGSASM
- php
- rep #$30
- LONGA ON
- LONGI ON
-
- lda 9,s ; fillWord
- tay
- lda 5,s ; dst offset
- tax
-
- phb
- sep #$20
- LONGA OFF
- lda 8,s ; bank byte (post-PHB)
- pha
- plb
- rep #$20
- LONGA ON
-
- tya
- sta |0,x
- sta |2,x
- sta |160,x
- sta |162,x
- sta |320,x
- sta |322,x
- sta |480,x
- sta |482,x
- sta |640,x
- sta |642,x
- sta |800,x
- sta |802,x
- sta |960,x
- sta |962,x
- sta |1120,x
- sta |1122,x
-
- LONGA OFF
- LONGI OFF
- plb
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsFillRectStageInner(uint8_t *firstRow, uint16_t bytesPerRow,
-* uint16_t rows, uint16_t fillWord)
-*
-* Per-row MVN fill at $01:firstRow + N*160 for N in [0..rows). Each
-* row: seed first 2 bytes via long-mode indexed STA, then MVN $01,$01
-* propagates the seed to the rest of the row (count = bytesPerRow-3
-* so MVN copies bytesPerRow-2 bytes; total = 2 seed + bytesPerRow-2 =
-* bytesPerRow bytes). Stays in M=16 throughout, SEI bracketed.
-*
-* MUST be in DRAWPRIMS load segment (see iigsSurfaceClearInner).
-*
-* Args after PHP only:
-* firstRow at SP+5..8 (LO word at SP+5..6 -- only the LO word is
-* used; bank is hardcoded $01)
-* bytesPerRow at SP+9..10
-* rows at SP+11..12
-* fillWord at SP+13..14
-*
-* Loop scratch in bank-1 free space below the stage at $01:$1F00:
-* $01:$1F00 = curRow (uint16_t)
-* $01:$1F02 = rowsLeft (uint16_t)
-* $01:$1F04 = bytesPerRow (uint16_t, cached for inner loop)
-* $01:$1F06 = fillWord seed (uint16_t)
-* Bank-1 below the stage is verified writable; jlSurfaceClear's MVN
-* operates in the same bank. We avoid ORCA-M `data DRAWDATA` labels
-* because they were resolving to addresses that hung the function.
-****************************************************************
-
-iigsFillRectStageInner start DRAWPRIMS
-* Per-row MVN fill of `bytesPerRow` doubled bytes per row across
-* `rows` rows, advancing 160 bytes per row. firstRow's low 16 bits
-* point into bank $01 (the stage). Each row: M=8 seed of one byte,
-* then MVN $01,$01 propagates the seed to the rest of the row.
-*
-* Args after PHP+SEI:
-* firstRow ptr at SP+5..8 (low 16 = byte offset within bank 1)
-* bytesPerRow at SP+9..10
-* rows at SP+11..12
-* fillWord at SP+13..14 (low byte = doubled fill byte)
- php
- sei
- rep #$30
- LONGA ON
- LONGI ON
-
-* Stash args into scratch slots (they're easier to work with via long
-* mode than re-reading from sp,s every iteration).
- lda 5,s
- sta >fillrCurRow ; running row offset within bank 1
- lda 9,s
- sta >fillrBytes ; bytesPerRow
- lda 11,s
- sta >fillrRowsLeft ; rows
- lda 13,s
- sta >fillrFillWord ; doubled-byte fillWord
-
-fillrRowLoop anop
- lda >fillrRowsLeft
- bne fillrRowDoIt
- brl fillrDone
-fillrRowDoIt anop
-
-* Seed the first byte of this row with the doubled fill byte. Stay
-* in M=16 to load curRow (16-bit), drop to M=8 just for the byte
-* store. STX has no long-abs form, so go via TAX.
- lda >fillrCurRow
- tax
- sep #$20
- LONGA OFF
- lda >fillrFillWord ; M=8: just the low byte
- sta >$010000,x
- rep #$20
- LONGA ON
-
-* If bytesPerRow < 2 we are done with this row (seed sufficed).
- lda >fillrBytes
- cmp #2
- bcc fillrNextRow
-
-* Set up MVN: src=curRow (X), dst=curRow+1 (Y), count=bytesPerRow-2.
- lda >fillrCurRow
- tax ; X = src offset
- inc a
- tay ; Y = dst offset = src + 1
- lda >fillrBytes
- dec a
- dec a ; A = bytesPerRow - 2
- mvn $010000,$010000
-
-fillrNextRow anop
- lda >fillrCurRow
- clc
- adc #160
- sta >fillrCurRow
- lda >fillrRowsLeft
- dec a
- sta >fillrRowsLeft
- brl fillrRowLoop
-
-fillrDone anop
- LONGA OFF
- LONGI OFF
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y,
-* uint16_t w, uint16_t h, uint16_t nibble)
-*
-* Full jlFillRect, including the leading- and trailing-partial-byte
-* nibble handling that used to live in halFastFillRect's C wrapper.
-* Macro-dispatched directly from cross-platform draw.c (see hal.h's
-* IIgs block) so the wrapper layer is gone.
-*
-* Stage-only: hard-coded MVN bank $01 in the middle-bytes loop.
-* Caller (the macro) checks `s == jlStageGet()` before invoking.
-*
-* Per-row work: leading partial-byte RMW (if x odd) + middle MVN
-* (seed + MVN copies seed forward) + trailing partial-byte RMW
-* (if (x+w) odd). All long-mode stores so DBR doesn't matter.
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8):
-* pixels D+0..3 (long ptr; only the bank-1 16-bit offset of the
-* first row's start matters since asm hard-codes
-* bank 1 anyway -- but pass it for symmetry)
-* x D+4..5
-* y D+6..7
-* w D+8..9
-* h D+10..11
-* nibble D+12..13 (low byte = 0..15)
-****************************************************************
-
-iigsFillRectInner start DRAWPRIMS
-fri_pix equ 0
-fri_x equ 4
-fri_y equ 6
-fri_w equ 8
-fri_h equ 10
-fri_nib equ 12
-
- php
- sei
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
-* pxStart = x; pxEnd = x + w.
- lda fri_x
- sta >friPxStart
- clc
- adc fri_w
- sta >friPxEnd
-
-* leadingByte = x >> 1; hasLeading = x & 1.
- lda fri_x
- lsr a
- sta >friLeadingByte
- lda fri_x
- and #1
- sta >friHasLeading
- beq friStartReady
- lda >friPxStart
- inc a
- sta >friPxStart
-friStartReady anop
-
-* midStart = pxStart >> 1.
- lda >friPxStart
- lsr a
- sta >friMidStart
-
-* spanLen = pxEnd - pxStart; hasTrailing = spanLen & 1; midBytes = spanLen >> 1.
- lda >friPxEnd
- sec
- sbc >friPxStart
- sta >friMidBytes ; temporary hold spanLen
- and #1
- sta >friHasTrailing
- lda >friMidBytes
- lsr a
- sta >friMidBytes
-
-* trailingByte = midStart + midBytes.
- lda >friMidStart
- clc
- adc >friMidBytes
- sta >friTrailingByte
-
-* nibble decoding: friNibLo = nib (low half), friNibHi = nib<<4
-* (high half), friDoubled = (nib<<4)|nib (full byte fill).
- sep #$20
- LONGA OFF
- lda fri_nib
- and #$0F
- sta >friNibLo
- asl a
- asl a
- asl a
- asl a
- sta >friNibHi
- ora >friNibLo
- sta >friDoubled
- rep #$20
- LONGA ON
-
-* curRow = $2000 + y*160 (LUT-driven).
- lda fri_y
- asl a
- tax
- lda >gRowOffsetLut,x
- clc
- adc #$2000
- sta >friCurRow
-
- lda fri_h
- sta >friRowsLeft
-
-friRowLoop anop
- lda >friRowsLeft
- bne friDoRow
- brl friExit
-friDoRow anop
-
-* Leading partial-byte RMW.
- lda >friHasLeading
- beq friNoLead
- lda >friCurRow
- clc
- adc >friLeadingByte
- tax
- sep #$20
- LONGA OFF
- lda >$010000,x
- and #$F0
- ora >friNibLo
- sta >$010000,x
- rep #$20
- LONGA ON
-friNoLead anop
-
-* Middle bytes: if midBytes == 0, skip; if midBytes == 1, just seed
-* one byte; if midBytes >= 2, seed first byte then MVN propagates the
-* seed across the rest of the row.
- lda >friMidBytes
- bne friMidNonZero
- brl friNoMid
-friMidNonZero anop
- lda >friCurRow
- clc
- adc >friMidStart
- tax
- sep #$20
- LONGA OFF
- lda >friDoubled
- sta >$010000,x
- rep #$20
- LONGA ON
- lda >friMidBytes
- cmp #2
- bcc friNoMid
- lda >friCurRow
- clc
- adc >friMidStart
- tax ; src offset
- inc a
- tay ; dst offset = src+1
- lda >friMidBytes
- dec a
- dec a ; count = midBytes-2 (MVN copies count+1)
- mvn $010000,$010000
-friNoMid anop
-
-* Trailing partial-byte RMW.
- lda >friHasTrailing
- beq friNoTrail
- lda >friCurRow
- clc
- adc >friTrailingByte
- tax
- sep #$20
- LONGA OFF
- lda >$010000,x
- and #$0F
- ora >friNibHi
- sta >$010000,x
- rep #$20
- LONGA ON
-friNoTrail anop
-
-* Advance to next row.
- lda >friCurRow
- clc
- adc #160
- sta >friCurRow
- lda >friRowsLeft
- dec a
- sta >friRowsLeft
- brl friRowLoop
-
-friExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-friPxStart data DRAWPRIMS
- ds 2
- end
-friPxEnd data DRAWPRIMS
- ds 2
- end
-friLeadingByte data DRAWPRIMS
- ds 2
- end
-friHasLeading data DRAWPRIMS
- ds 2
- end
-friMidStart data DRAWPRIMS
- ds 2
- end
-friMidBytes data DRAWPRIMS
- ds 2
- end
-friHasTrailing data DRAWPRIMS
- ds 2
- end
-friTrailingByte data DRAWPRIMS
- ds 2
- end
-friNibLo data DRAWPRIMS
- ds 2
- end
-friNibHi data DRAWPRIMS
- ds 2
- end
-friDoubled data DRAWPRIMS
- ds 2
- end
-friCurRow data DRAWPRIMS
- ds 2
- end
-friRowsLeft data DRAWPRIMS
- ds 2
- end
-
-
-fillrSeed data DRAWPRIMS
- ds 2 ; 2 bytes -- read as 16-bit fillWord
- end
-fillrBytes data DRAWPRIMS
- ds 2
- end
-fillrCurRow data DRAWPRIMS
- ds 2
- end
-fillrRowsLeft data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* Scratch slots for jlFillRect's saved SP / loop counters. Same
-* rationale as sclrSavedSp -- avoid cross-segment data references
-* from the inner loop.
-****************************************************************
-
-fillrSavedSp data DRAWPRIMS
- ds 2
- end
-
-fillrWordCount data DRAWPRIMS
- ds 2
- end
-
-fillrSpAdvance data DRAWPRIMS
- ds 2
- end
-
-fillrFillWord data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* Tile primitives (iigsTileCopyInner / iigsTileCopyMaskedInner /
-* iigsTilePasteInner / iigsTileSnapInner)
-*
-* Common pattern: D is set to point into the calling stack frame
-* so [0],y reaches the dst long pointer and [4],y reaches the src
-* long pointer. Lets us cross banks (stage at $01, heap surfaces
-* anywhere) without touching DBR. Dst/src banks may differ.
-*
-* Stack layout after PHP/PHB/PHD on entry to a 2-pointer routine
-* (4 prologue bytes total). The empirical offset for "first arg
-* byte 0" matches the existing iigsSurfaceClearInner / iigsTile-
-* FillInner pattern: with PHP only, first arg is at SP+4; each
-* extra prologue byte shifts by 1, so PHP+PHB+PHD puts first arg
-* at SP+7. TSC + ADC #7 + TCD makes D = SP+7, then [0] reaches
-* the dst long pointer and [4] reaches src.
-*
-* For jlTileCopyMasked the third arg (transparent, 16-bit) sits at
-* D+8 with the same prologue.
-****************************************************************
-
-
-****************************************************************
-* iigsTileCopyInner(uint8_t *dst, const uint8_t *src)
-*
-* Opaque copy of an 8x8 tile region (32 bytes) between two 4bpp
-* surfaces with 160-byte row stride. Dst and src may be in any
-* banks. ~340 cyc per tile vs ORCA-C's ~900.
-****************************************************************
-
-iigsTileCopyInner start IIGSASM
-dst equ 0
-src equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd ; D = SP+7 = dst arg
-
- ldy #0
- lda [src],y
- sta [dst],y
- ldy #2
- lda [src],y
- sta [dst],y
- ldy #160
- lda [src],y
- sta [dst],y
- ldy #162
- lda [src],y
- sta [dst],y
- ldy #320
- lda [src],y
- sta [dst],y
- ldy #322
- lda [src],y
- sta [dst],y
- ldy #480
- lda [src],y
- sta [dst],y
- ldy #482
- lda [src],y
- sta [dst],y
- ldy #640
- lda [src],y
- sta [dst],y
- ldy #642
- lda [src],y
- sta [dst],y
- ldy #800
- lda [src],y
- sta [dst],y
- ldy #802
- lda [src],y
- sta [dst],y
- ldy #960
- lda [src],y
- sta [dst],y
- ldy #962
- lda [src],y
- sta [dst],y
- ldy #1120
- lda [src],y
- sta [dst],y
- ldy #1122
- lda [src],y
- sta [dst],y
-
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsTilePasteInner(uint8_t *dst, const uint8_t *src)
-*
-* Paste a packed jlTileT buffer (4 bytes/row * 8 rows = 32 bytes
-* tight) onto a surface row at `dst`. Src stride 4, dst stride 160.
-* Both args are 4-byte large-model pointers.
-****************************************************************
-
-iigsTilePasteInner start IIGSASM
-dst equ 0
-src equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd
-
-* Row 0: src word 0 = 0, dst word 0 = 0
- ldy #0
- lda [src],y
- sta [dst],y
- ldy #2
- lda [src],y
- sta [dst],y
-* Row 1: src 4/6, dst 160/162
- ldy #4
- lda [src],y
- ldy #160
- sta [dst],y
- ldy #6
- lda [src],y
- ldy #162
- sta [dst],y
-* Row 2: src 8/10, dst 320/322
- ldy #8
- lda [src],y
- ldy #320
- sta [dst],y
- ldy #10
- lda [src],y
- ldy #322
- sta [dst],y
-* Row 3: src 12/14, dst 480/482
- ldy #12
- lda [src],y
- ldy #480
- sta [dst],y
- ldy #14
- lda [src],y
- ldy #482
- sta [dst],y
-* Row 4: src 16/18, dst 640/642
- ldy #16
- lda [src],y
- ldy #640
- sta [dst],y
- ldy #18
- lda [src],y
- ldy #642
- sta [dst],y
-* Row 5: src 20/22, dst 800/802
- ldy #20
- lda [src],y
- ldy #800
- sta [dst],y
- ldy #22
- lda [src],y
- ldy #802
- sta [dst],y
-* Row 6: src 24/26, dst 960/962
- ldy #24
- lda [src],y
- ldy #960
- sta [dst],y
- ldy #26
- lda [src],y
- ldy #962
- sta [dst],y
-* Row 7: src 28/30, dst 1120/1122
- ldy #28
- lda [src],y
- ldy #1120
- sta [dst],y
- ldy #30
- lda [src],y
- ldy #1122
- sta [dst],y
-
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsTileSnapInner(uint8_t *dst, const uint8_t *src)
-*
-* Snapshot an 8x8 region of a surface into a packed jlTileT buffer.
-* Src stride 160, dst stride 4. Mirrors jlTilePaste with stride
-* values swapped.
-****************************************************************
-
-iigsTileSnapInner start IIGSASM
-dst equ 0
-src equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd
-
-* Row 0: src 0/2, dst 0/2
- ldy #0
- lda [src],y
- sta [dst],y
- ldy #2
- lda [src],y
- sta [dst],y
-* Row 1: src 160/162, dst 4/6
- ldy #160
- lda [src],y
- ldy #4
- sta [dst],y
- ldy #162
- lda [src],y
- ldy #6
- sta [dst],y
-* Row 2: src 320/322, dst 8/10
- ldy #320
- lda [src],y
- ldy #8
- sta [dst],y
- ldy #322
- lda [src],y
- ldy #10
- sta [dst],y
-* Row 3: src 480/482, dst 12/14
- ldy #480
- lda [src],y
- ldy #12
- sta [dst],y
- ldy #482
- lda [src],y
- ldy #14
- sta [dst],y
-* Row 4: src 640/642, dst 16/18
- ldy #640
- lda [src],y
- ldy #16
- sta [dst],y
- ldy #642
- lda [src],y
- ldy #18
- sta [dst],y
-* Row 5: src 800/802, dst 20/22
- ldy #800
- lda [src],y
- ldy #20
- sta [dst],y
- ldy #802
- lda [src],y
- ldy #22
- sta [dst],y
-* Row 6: src 960/962, dst 24/26
- ldy #960
- lda [src],y
- ldy #24
- sta [dst],y
- ldy #962
- lda [src],y
- ldy #26
- sta [dst],y
-* Row 7: src 1120/1122, dst 28/30
- ldy #1120
- lda [src],y
- ldy #28
- sta [dst],y
- ldy #1122
- lda [src],y
- ldy #30
- sta [dst],y
-
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsTileCopyMaskedInner(uint8_t *dst, const uint8_t *src,
-* uint16_t transparent)
-*
-* Same shape as jlTileCopy (160/160 strides, 32 bytes), but source
-* nibbles equal to `transparent` are skipped. Per-byte fast path:
-* if both src nibbles == transparent the whole byte is skipped;
-* otherwise build the output by mixing src/dst nibbles.
-*
-* Args: dst at SP+8..11, src at SP+12..15, transparent at SP+16..17.
-****************************************************************
-
-iigsTileCopyMaskedInner start IIGSASM
-dst equ 0
-src equ 4
-trans equ 8 ; transparent (16-bit, low byte at D+8)
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd ; D = SP+7 (dst arg)
-
-* Pre-compute scratch values from `transparent` (at D+8..9).
- sep #$20 ; M=8
- LONGA OFF
-
- lda trans
- and #$0F
- sta >tmaskTLo ; tLo = T
-
- asl a
- asl a
- asl a
- asl a
- sta >tmaskTHi ; tHi = T<<4
-
- ora >tmaskTLo
- sta >tmaskDoubled ; doubled = (T<<4)|T
-
-* Walk 8 rows of 4 bytes. Y holds the byte offset; after the 3
-* in-row INYs Y is at row*160+3, so +157 lands at next row's
-* start (row*160+160).
- ldx #8 ; row counter
- ldy #0 ; byte offset
-
-tmaskRowLoop anop
- jsr tmaskByte ; byte 0
- iny
- jsr tmaskByte ; byte 1
- iny
- jsr tmaskByte ; byte 2
- iny
- jsr tmaskByte ; byte 3
-
- rep #$20 ; M=16 for arithmetic on Y
- LONGA ON
- tya
- clc
- adc #157 ; +156 stride, +1 because we're not at row+4 yet (we're at row+3)
- tay
- sep #$20 ; back to M=8
- LONGA OFF
-
- dex
- bne tmaskRowLoop
-
- rep #$20 ; M=16 before epilogue
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
-
-* tmaskByte: in M=8 X=16, Y holds byte offset.
-* - load src byte
-* - if both nibbles transparent: skip
-* - else: assemble output from src/dst nibbles per nibble's
-* non-transparency, store
-tmaskByte anop
- lda [src],y ; A = src byte
- cmp >tmaskDoubled
- beq tmaskSkip ; both nibbles == transparent
-
- sta >tmaskSrc
-
-* Decide hi nibble.
- and #$F0
- cmp >tmaskTHi
- beq tmaskUseDstHi
- sta >tmaskOutHi ; src hi (kept in $F0 form)
- bra tmaskHiDone
-tmaskUseDstHi anop
- lda [dst],y
- and #$F0
- sta >tmaskOutHi
-tmaskHiDone anop
-
-* Decide lo nibble.
- lda >tmaskSrc
- and #$0F
- cmp >tmaskTLo
- beq tmaskUseDstLo
- sta >tmaskOutLo ; src lo
- bra tmaskLoDone
-tmaskUseDstLo anop
- lda [dst],y
- and #$0F
- sta >tmaskOutLo
-tmaskLoDone anop
-
- lda >tmaskOutHi
- ora >tmaskOutLo
- sta [dst],y
-
-tmaskSkip anop
- rts
- end
-
-
-****************************************************************
-* Scratch for the masked-tile inner loop. Lives outside the
-* primitive's stack frame because the per-byte subroutine call
-* would otherwise need to thread state through registers.
-****************************************************************
-
-tmaskTLo data DRAWPRIMS
- ds 1
- end
-
-tmaskTHi data DRAWPRIMS
- ds 1
- end
-
-tmaskDoubled data DRAWPRIMS
- ds 1
- end
-
-tmaskSrc data DRAWPRIMS
- ds 1
- end
-
-tmaskOutHi data DRAWPRIMS
- ds 1
- end
-
-tmaskOutLo data DRAWPRIMS
- ds 1
- end
-
-
-****************************************************************
-* iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y,
-* uint16_t nibble)
-*
-* Plot one 4bpp pixel at (x,y) into a 320x200 surface. Caller has
-* already validated bounds and stripped the high nibbles of `nibble`
-* (only low 4 bits used). pixels is a 4-byte large-model pointer.
-*
-* Offset math: byte = y*160 + (x>>1). 160 = 128 + 32 = (y<<7)+(y<<5).
-* Nibble half: x&1 == 0 picks high nibble (left pixel), else low.
-****************************************************************
-
-iigsDrawPixelInner start IIGSASM
-pix equ 0 ; pixels long ptr
-xx equ 4 ; x (16-bit)
-yy equ 6 ; y (16-bit)
-nib equ 8 ; nibble (low byte at D+8)
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd
-
-* Compute byte offset = y*160 + (x>>1) into A. Use the LUT.
- lda yy
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dpxlTmp
- lda xx
- lsr a ; A = x >> 1
- clc
- adc >dpxlTmp ; A = byte offset within surface
- tay
-
- sep #$20 ; M=8
- LONGA OFF
-
- lda xx ; A = x low byte (parity in LSB)
- and #1
- bne dpxlOdd
-
-* Even x -> high nibble.
- lda nib
- asl a
- asl a
- asl a
- asl a
- sta >dpxlNibPart
- lda [pix],y
- and #$0F
- ora >dpxlNibPart
- sta [pix],y
- bra dpxlDone
-
-dpxlOdd anop
-* Odd x -> low nibble.
- lda nib
- and #$0F
- sta >dpxlNibPart
- lda [pix],y
- and #$F0
- ora >dpxlNibPart
- sta [pix],y
-
-dpxlDone anop
- rep #$20 ; back to M=16 for epilogue
- LONGA ON
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-dpxlTmp data DRAWPRIMS
- ds 2
- end
-
-dpxlNibPart data DRAWPRIMS
- ds 1
- end
-
-
-****************************************************************
-* iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0,
-* uint16_t x1, uint16_t y1, uint16_t nibble)
-*
-* Bresenham line plot. Caller has clipped both endpoints into
-* [0..319] x [0..199], so no per-pixel bounds check.
-*
-* Walks (dx, dy, sx, sy, err) and plots inline (no per-pixel
-* function call). Each plot does the same parity-aware nibble
-* RMW as jlDrawPixel. State stored in DRAWDATA scratch because
-* registers are too few for the full Bresenham state plus
-* current pixel address.
-****************************************************************
-
-iigsDrawLineInner start IIGSASM
-pix equ 0 ; pixels long ptr (D+0..3)
-ax0 equ 4 ; x0 arg slot
-ay0 equ 6 ; y0
-ax1 equ 8 ; x1
-ay1 equ 10 ; y1
-anib equ 12 ; nibble
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd ; D = SP+7 = pixels arg
-
-* dx = |x1 - x0|, sx = sign(x1 - x0)
- lda ax1
- sec
- sbc ax0
- bpl dlnDxPos
- eor #$FFFF
- clc
- adc #1 ; A = x0 - x1 (positive |dx|)
- sta >dlnDx
- lda #$FFFF
- sta >dlnSx ; sx = -1
- bra dlnDxDone
-dlnDxPos anop
- sta >dlnDx
- lda #1
- sta >dlnSx
-dlnDxDone anop
-
-* dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy.
- lda ay1
- sec
- sbc ay0
- bpl dlnDyPos
- sta >dlnDy ; A is negative; that's dy with the sign already
- lda #$FFFF
- sta >dlnSy ; sy = -1
- bra dlnDyDone
-dlnDyPos anop
-* y1 >= y0 -> raw dy positive; negate for Bresenham (-|y1-y0|).
- eor #$FFFF
- clc
- adc #1
- sta >dlnDy
- lda #1
- sta >dlnSy
-dlnDyDone anop
-
-* err = dx + dy
- lda >dlnDx
- clc
- adc >dlnDy
- sta >dlnErr
-
-* Copy x0, y0 to running state slots.
- lda ax0
- sta >dlnX
- lda ay0
- sta >dlnY
-
-* Cache x1, y1 so the loop's compare is fast.
- lda ax1
- sta >dlnX1
- lda ay1
- sta >dlnY1
-
-* Cache nibble in 8-bit form.
- sep #$20
- LONGA OFF
- lda anib
- and #$0F
- sta >dlnNib
- rep #$20
- LONGA ON
-
-dlnLoop anop
-* --- Plot pixel at (dlnX, dlnY) --- LUT path.
- lda >dlnY
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dlnTmp
- lda >dlnX
- lsr a ; x >> 1
- clc
- adc >dlnTmp ; byte offset
- tay
-
- sep #$20
- LONGA OFF
-
- lda >dlnX ; x low byte parity
- and #1
- bne dlnPlotOdd
-
- lda >dlnNib
- asl a
- asl a
- asl a
- asl a
- sta >dlnNibPart
- lda [pix],y
- and #$0F
- ora >dlnNibPart
- sta [pix],y
- bra dlnPlotDone
-dlnPlotOdd anop
- lda >dlnNib
- sta >dlnNibPart
- lda [pix],y
- and #$F0
- ora >dlnNibPart
- sta [pix],y
-dlnPlotDone anop
- rep #$20
- LONGA ON
-
-* --- Loop test: if X==X1 and Y==Y1, done ---
- lda >dlnX
- cmp >dlnX1
- bne dlnStep
- lda >dlnY
- cmp >dlnY1
- bne dlnStep
- brl dlnExit
-
-dlnStep anop
-* e2 = err << 1 (signed). Compare e2 against dy and dx.
- lda >dlnErr
- asl a ; e2 = err * 2
- sta >dlnE2
-
-* if (e2 >= dy) { err += dy; X += sx; }
-* dy is negative, so signed-compare with BMI/BPL needed.
-* Test e2 - dy >= 0 via SEC; SBC dy; BPL.
- sec
- sbc >dlnDy
- bmi dlnSkipX ; e2 < dy
-
- lda >dlnErr
- clc
- adc >dlnDy
- sta >dlnErr
-
- lda >dlnX
- clc
- adc >dlnSx
- sta >dlnX
-
-dlnSkipX anop
-* if (e2 <= dx) { err += dx; Y += sy; }
-* Test dx - e2 >= 0 via dx - e2 (BPL means e2 <= dx).
- lda >dlnDx
- sec
- sbc >dlnE2
- bmi dlnSkipY ; dx < e2
-
- lda >dlnErr
- clc
- adc >dlnDx
- sta >dlnErr
-
- lda >dlnY
- clc
- adc >dlnSy
- sta >dlnY
-
-dlnSkipY anop
- brl dlnLoop
-
-dlnExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-dlnDx data DRAWPRIMS
- ds 2
- end
-dlnDy data DRAWPRIMS
- ds 2
- end
-dlnSx data DRAWPRIMS
- ds 2
- end
-dlnSy data DRAWPRIMS
- ds 2
- end
-dlnErr data DRAWPRIMS
- ds 2
- end
-dlnE2 data DRAWPRIMS
- ds 2
- end
-dlnX data DRAWPRIMS
- ds 2
- end
-dlnY data DRAWPRIMS
- ds 2
- end
-dlnX1 data DRAWPRIMS
- ds 2
- end
-dlnY1 data DRAWPRIMS
- ds 2
- end
-dlnNib data DRAWPRIMS
- ds 1
- end
-dlnNibPart data DRAWPRIMS
- ds 1
- end
-dlnTmp data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy,
-* uint16_t r, uint16_t nibble)
-*
-* Bresenham midpoint circle outline. Caller has verified that the
-* whole bounding box (cx-r..cx+r, cy-r..cy+r) fits inside the
-* surface, so the inner loop plots all 8 octants without per-pixel
-* clip checks. Each iteration computes 4 row-base byte offsets
-* (cy +/- y) * 160, (cy +/- x) * 160 then plots 8 pixels via the
-* dcPlotPx subroutine.
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8 = pixels arg):
-* pix at D+0..3 (long ptr, used via [pix],y)
-* cx at D+4..5
-* cy at D+6..7
-* r at D+8..9
-* nibble at D+10..11
-****************************************************************
-
-iigsDrawCircleInner start IIGSASM
-pix equ 0
-acx equ 4
-acy equ 6
-ar equ 8
-anib equ 10
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd ; D = SP+8 = pix arg
-
-* Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4.
- lda anib
- and #$000F
- sta >dcNibLo ; low byte = nib, high byte = 0
- asl a
- asl a
- asl a
- asl a
- sta >dcNibHi ; low byte = nib<<4
-
-* x = r, y = 0, err = 1 - x.
- lda ar
- sta >dcX
- lda #0
- sta >dcY
- lda #1
- sec
- sbc ar
- sta >dcErr
-
-dcLoop anop
-* Loop guard: if x < y we are done.
- lda >dcX
- cmp >dcY
- bcs dcLoopBody ; x >= y -> continue
- brl dcExit
-
-dcLoopBody anop
-* Compute 4 row bases.
- lda acy
- clc
- adc >dcY
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dcRowYP
- lda acy
- sec
- sbc >dcY
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dcRowYN
- lda acy
- clc
- adc >dcX
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dcRowXP
- lda acy
- sec
- sbc >dcX
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >dcRowXN
-
-* 8 octant plots, fully inlined. Each plot:
-* 1. col = (acx +/- dcX|dcY) -> A
-* 2. save col -> dcSavedCol (for parity test)
-* 3. byteIdx = col >> 1; byte addr = byteIdx + rowBase -> Y
-* 4. test col & 1; do high or low nibble RMW
-* Skips the JSR/RTS to dcPlotPx (~12 cyc) and the load-row-via-X
-* dance (sta dcSavedCol + tax + reload). Per plot: ~60 cyc vs ~80
-* cyc with JSR. ~20 cyc/plot x ~2560 plots in the demo's 4 circles
-* = ~18 ms.
-*
-* Each plot has its own dcOddN / dcDoneN labels (ORCA-M needs unique).
-
-* Octants 1-4: y-row pair (cx +/- x, cy +/- y).
-* Plot 1: (cx+x, cy+y)
- lda acx
- clc
- adc >dcX
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowYP
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd1
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone1
-dcOdd1 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone1 anop
- rep #$20
- LONGA ON
-
-* Plot 2: (cx-x, cy+y)
- lda acx
- sec
- sbc >dcX
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowYP
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd2
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone2
-dcOdd2 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone2 anop
- rep #$20
- LONGA ON
-
-* Plot 3: (cx+x, cy-y)
- lda acx
- clc
- adc >dcX
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowYN
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd3
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone3
-dcOdd3 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone3 anop
- rep #$20
- LONGA ON
-
-* Plot 4: (cx-x, cy-y)
- lda acx
- sec
- sbc >dcX
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowYN
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd4
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone4
-dcOdd4 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone4 anop
- rep #$20
- LONGA ON
-
-* Octants 5-8: x-row pair (cx +/- y, cy +/- x).
-* Plot 5: (cx+y, cy+x)
- lda acx
- clc
- adc >dcY
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowXP
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd5
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone5
-dcOdd5 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone5 anop
- rep #$20
- LONGA ON
-
-* Plot 6: (cx-y, cy+x)
- lda acx
- sec
- sbc >dcY
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowXP
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd6
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone6
-dcOdd6 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone6 anop
- rep #$20
- LONGA ON
-
-* Plot 7: (cx+y, cy-x)
- lda acx
- clc
- adc >dcY
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowXN
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd7
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone7
-dcOdd7 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone7 anop
- rep #$20
- LONGA ON
-
-* Plot 8: (cx-y, cy-x)
- lda acx
- sec
- sbc >dcY
- sta >dcSavedCol
- lsr a
- clc
- adc >dcRowXN
- tay
- lda >dcSavedCol
- sep #$20
- LONGA OFF
- and #1
- bne dcOdd8
- lda [pix],y
- and #$0F
- ora >dcNibHi
- sta [pix],y
- bra dcDone8
-dcOdd8 anop
- lda [pix],y
- and #$F0
- ora >dcNibLo
- sta [pix],y
-dcDone8 anop
- rep #$20
- LONGA ON
-
-* Update Bresenham: y++; if err <= 0: err += 2y+1; else x--; err += 2(y-x)+1.
- lda >dcY
- inc a
- sta >dcY ; y now = old y + 1
-
- lda >dcErr
- bmi dcErrLE ; err < 0 -> err <= 0
- bne dcErrGT ; err > 0
-* err == 0: take the LE branch (err += 2y+1).
-dcErrLE anop
- lda >dcY
- asl a
- clc
- adc #1
- clc
- adc >dcErr
- sta >dcErr
- brl dcLoop
-dcErrGT anop
- lda >dcX
- dec a
- sta >dcX
-* err += 2*(y-x) + 1
- lda >dcY
- sec
- sbc >dcX
- asl a
- clc
- adc #1
- clc
- adc >dcErr
- sta >dcErr
- brl dcLoop
-
-dcExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
-
-* dcMul160 deleted -- callers now expand the y160lut macro inline.
-
-* dcPlotPx deleted -- the 8 octant plot sites now expand the plot
-* logic inline (no JSR/RTS, no STA dcSavedCol / TAX / reload-col
-* round-trip per plot).
- end
-
-
-****************************************************************
-* Scratch slots for circle outline state. Live in DRAWPRIMS so the
-* asm subroutine's `>label` accesses match the load segment of the
-* parent function.
-****************************************************************
-
-dcX data DRAWPRIMS
- ds 2
- end
-dcY data DRAWPRIMS
- ds 2
- end
-dcErr data DRAWPRIMS
- ds 2
- end
-dcNibLo data DRAWPRIMS
- ds 2
- end
-dcNibHi data DRAWPRIMS
- ds 2
- end
-dcRowYP data DRAWPRIMS
- ds 2
- end
-dcRowYN data DRAWPRIMS
- ds 2
- end
-dcRowXP data DRAWPRIMS
- ds 2
- end
-dcRowXN data DRAWPRIMS
- ds 2
- end
-dcMulTmp data DRAWPRIMS
- ds 2
- end
-dcSavedCol data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy,
-* uint16_t r, uint16_t fillWord)
-*
-* Filled circle with horizontal-span scanline output. Caller has
-* verified the bbox fits inside the surface, so the inner loop
-* fills every span unconditionally. Maintains xx = x*x and
-* yy = y*y incrementally so the hot path uses only 16-bit add /
-* sub / cmp -- no 65816 multiply at all (other than one r*r at
-* setup, which is a small once-off shift-and-add).
-*
-* fillWord is the doubled byte (low byte) replicated, e.g. nibble 3
-* -> $3333 (we only use the low byte for nibble RMW; the duplicate
-* high byte simplifies the C wrapper math).
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8):
-* pix at D+0..3 (long ptr)
-* cx at D+4..5
-* cy at D+6..7
-* r at D+8..9
-* fillWord at D+10..11
-****************************************************************
-
-iigsFillCircleInner start IIGSASM
-fpix equ 0
-fcx equ 4
-fcy equ 6
-fr equ 8
-ffill equ 10
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
-
- tsc
- clc
- adc #8
- tcd ; D = SP+8
-
-* Cache fill nibble bytes. ffill low byte = doubled byte (e.g., $33).
-* fcFillByte = doubled low byte (used for full byte fills + low nibble).
-* fcFillHi = (nibble << 4) only (used for high nibble RMW).
-* fcFillLo = nibble only (used for low nibble RMW).
- lda ffill
- and #$00FF
- sta >fcFillByte ; doubled byte e.g. $33
- lsr a
- lsr a
- lsr a
- lsr a ; A = nibble
- sta >fcFillLo
- asl a
- asl a
- asl a
- asl a ; A = nibble << 4
- sta >fcFillHi
-
-* Compute r*r via 16-bit shift-and-add. Only done once.
- lda fr
- sta >fcMulA
- lda fr
- sta >fcMulB
- jsr fcMul16
- sta >fcR2
- sta >fcXX ; xx = r*r initially
-
-* yy = 0, x = r, y = 0.
- lda #0
- sta >fcYY
- sta >fcY
- lda fr
- sta >fcX
-
-fcOuterLoop anop
-* Inner: while (xx + yy) > r2 then xx -= 2x - 1; x--.
-fcInnerLoop anop
- lda >fcXX
- clc
- adc >fcYY
- cmp >fcR2
- bcc fcInnerDone ; xx+yy < r2
- beq fcInnerDone ; xx+yy == r2 (acceptable)
-* xx + yy > r2 -> decrement
- lda >fcX
- asl a ; A = 2x
- sec
- sbc #1 ; A = 2x - 1
- sta >fcDec
- lda >fcXX
- sec
- sbc >fcDec
- sta >fcXX
- lda >fcX
- dec a
- sta >fcX
- bra fcInnerLoop
-fcInnerDone anop
-
-* leftCol = cx - x, rightCol = cx + x for both lower and (if y>0) upper rows.
- lda fcx
- sec
- sbc >fcX
- sta >fcLeftCol
- lda fcx
- clc
- adc >fcX
- sta >fcRightCol
-
-* Lower row: rowBase = (cy + y) * 160
- lda fcy
- clc
- adc >fcY
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >fcSpanRowBase
- jsr fcDoSpan
-
-* If y > 0: upper row at (cy - y).
- lda >fcY
- beq fcSkipUpper
- lda fcy
- sec
- sbc >fcY
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- sta >fcSpanRowBase
- jsr fcDoSpan
-fcSkipUpper anop
-
-* yy += 2*y + 1
- lda >fcY
- asl a
- clc
- adc #1
- clc
- adc >fcYY
- sta >fcYY
-
-* y++
- lda >fcY
- inc a
- sta >fcY
-
-* if y > r: exit, else loop
- cmp fr
- bcc fcContinue
- beq fcContinue
- brl fcExit
-fcContinue anop
- brl fcOuterLoop
-
-fcExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
-
-* fcMul160 deleted -- callers now expand the y160lut macro inline.
-
-****************************************************************
-* fcMul16: A = fcMulA * fcMulB (16-bit unsigned, low 16 of result).
-* Standard shift-and-add. 16 iterations. Caller has M=16, X=16.
-****************************************************************
-fcMul16 anop
- LONGA ON
- LONGI ON
- lda #0
- sta >fcMulRes
- ldx #16
-fcMulLoop anop
-* result <<= 1
- lda >fcMulRes
- asl a
- sta >fcMulRes
-* multiplier <<= 1, MSB -> C
- lda >fcMulA
- asl a
- sta >fcMulA
- bcc fcMulSkip
- lda >fcMulRes
- clc
- adc >fcMulB
- sta >fcMulRes
-fcMulSkip anop
- dex
- bne fcMulLoop
- lda >fcMulRes
- rts
-
-****************************************************************
-* fcDoSpan: fill horizontal span from fcLeftCol to fcRightCol on
-* row whose byte base is in fcSpanRowBase. Uses byte-aligned fill
-* for the middle bytes and per-pixel RMW for the partial-nibble
-* leading/trailing bytes.
-*
-* Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values).
-* Trashes A, X, Y, P. Preserves D and B (relies on D = SP+8).
-****************************************************************
-fcDoSpan anop
- LONGA ON
- LONGI ON
-* leftByte = leftCol >> 1, leftPartial = leftCol & 1
- lda >fcLeftCol
- lsr a
- sta >fcLeftByte
- lda >fcLeftCol
- and #1
- sta >fcLeftPartial
-
-* rightByte = rightCol >> 1, rightPartial = !(rightCol & 1)
- lda >fcRightCol
- lsr a
- sta >fcRightByte
- lda >fcRightCol
- and #1
- eor #1
- sta >fcRightPartial
-
-* If leftByte == rightByte: single-byte case
- lda >fcLeftByte
- cmp >fcRightByte
- bne fcSpanMulti
- brl fcSpanSingle
-fcSpanMulti anop
-
-* Multi-byte case.
-* Leading partial (low nibble of leftByte) if leftPartial.
- lda >fcLeftPartial
- beq fcSkipLP
- lda >fcSpanRowBase
- clc
- adc >fcLeftByte
- tay
- sep #$20
- LONGA OFF
- lda [fpix],y
- and #$F0
- ora >fcFillLo
- sta [fpix],y
- rep #$20
- LONGA ON
-fcSkipLP anop
-
-* Trailing partial (high nibble of rightByte) if rightPartial.
- lda >fcRightPartial
- beq fcSkipRP
- lda >fcSpanRowBase
- clc
- adc >fcRightByte
- tay
- sep #$20
- LONGA OFF
- lda [fpix],y
- and #$0F
- ora >fcFillHi
- sta [fpix],y
- rep #$20
- LONGA ON
-fcSkipRP anop
-
-* Mid bytes: from (leftByte + leftPartial) to (rightByte - rightPartial) inclusive.
- lda >fcLeftByte
- clc
- adc >fcLeftPartial
- sta >fcMidStart
- lda >fcRightByte
- sec
- sbc >fcRightPartial
- sta >fcMidEnd
- cmp >fcMidStart
- bcs fcMidGo
- rts ; midEnd < midStart
-fcMidGo anop
- lda >fcMidEnd
- sec
- sbc >fcMidStart
- inc a
- sta >fcMidCount
-
- lda >fcSpanRowBase
- clc
- adc >fcMidStart
- tay ; Y = byte offset
- lda >fcMidCount
- tax ; X = byte counter
-
- sep #$20
- LONGA OFF
- lda >fcFillByte ; doubled byte
-fcFillLoop anop
- sta [fpix],y
- iny
- dex
- bne fcFillLoop
- rep #$20
- LONGA ON
- rts
-
-fcSpanSingle anop
-* Single-byte case: leftByte == rightByte. Three sub-cases:
-* leftPartial=1 -> low nibble only (single pixel, leftCol odd, rightCol==leftCol)
-* rightPartial=1 -> high nibble only (single pixel, leftCol==rightCol even)
-* neither -> full byte (leftCol even, rightCol odd, two adjacent pixels)
- lda >fcSpanRowBase
- clc
- adc >fcLeftByte
- tay
- sep #$20
- LONGA OFF
- lda >fcLeftPartial
- beq fcSbCheckRP
- lda [fpix],y
- and #$F0
- ora >fcFillLo
- sta [fpix],y
- rep #$20
- LONGA ON
- rts
-fcSbCheckRP anop
- LONGA OFF ; reached via BEQ from M=8 path
- lda >fcRightPartial
- beq fcSbFull
- lda [fpix],y
- and #$0F
- ora >fcFillHi
- sta [fpix],y
- rep #$20
- LONGA ON
- rts
-fcSbFull anop
- LONGA OFF ; reached via BEQ from M=8 path
- lda >fcFillByte
- sta [fpix],y
- rep #$20
- LONGA ON
- rts
- end
-
-
-****************************************************************
-* Scratch slots for filled circle.
-****************************************************************
-
-fcX data DRAWPRIMS
- ds 2
- end
-fcY data DRAWPRIMS
- ds 2
- end
-fcXX data DRAWPRIMS
- ds 2
- end
-fcYY data DRAWPRIMS
- ds 2
- end
-fcR2 data DRAWPRIMS
- ds 2
- end
-fcDec data DRAWPRIMS
- ds 2
- end
-fcMulA data DRAWPRIMS
- ds 2
- end
-fcMulB data DRAWPRIMS
- ds 2
- end
-fcMulRes data DRAWPRIMS
- ds 2
- end
-fcLeftCol data DRAWPRIMS
- ds 2
- end
-fcRightCol data DRAWPRIMS
- ds 2
- end
-fcSpanRowBase data DRAWPRIMS
- ds 2
- end
-fcLeftByte data DRAWPRIMS
- ds 2
- end
-fcRightByte data DRAWPRIMS
- ds 2
- end
-fcLeftPartial data DRAWPRIMS
- ds 2
- end
-fcRightPartial data DRAWPRIMS
- ds 2
- end
-fcMidStart data DRAWPRIMS
- ds 2
- end
-fcMidEnd data DRAWPRIMS
- ds 2
- end
-fcMidCount data DRAWPRIMS
- ds 2
- end
-fcFillByte data DRAWPRIMS
- ds 2
- end
-fcFillLo data DRAWPRIMS
- ds 2
- end
-fcFillHi data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsFloodWalkInner / fwTest deleted -- the C fallback path that
-* called halFastFloodWalk (which delegated here) is unreachable on
-* IIgs because halFastFloodWalkAndScans always succeeds first. The
-* gFloodSeedMatch / gFloodLeftX / gFloodRightX globals below are
-* still used by iigsFloodWalkAndScansInner and stay.
-****************************************************************
-
-****************************************************************
-* Globals for jlFloodFill asm walk results. Live in DRAWPRIMS so
-* both asm and C see them at the same long address.
-****************************************************************
-
-gFloodSeedMatch data DRAWPRIMS
- ds 2
- end
-gFloodLeftX data DRAWPRIMS
- ds 2
- end
-gFloodRightX data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr)
-*
-* Three-step blit:
-* 1. Pixels: MVN 32000 bytes from $01:$2000 to $E1:$2000.
-* 2. SCB: 200 bytes from caller's SCB to $E1:$9D00.
-* 3. Pal: 512 bytes from caller's palette to $E1:$9E00.
-*
-* After the pixel MVN, DBR is left at $E1 (MVN sets DBR to dst bank).
-* That lets the SCB/palette loops use sta abs,Y (16-bit absolute,
-* DBR-relative) instead of long-mode -- which has no Y-indexed form.
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8):
-* scbPtr at D+0..3 (long ptr)
-* palettePtr at D+4..7 (long ptr)
-****************************************************************
-
-iigsBlitStageToShr start DRAWPRIMS
-bscb equ 0
-bpal equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
-* 1. SCB upload (200 bytes) via MVN. Done BEFORE the PEI-slam so the
-* SEI window only spans the slam itself (~38 ms). Source bank is
-* runtime-patched into the MVN instruction (encoding: $54 dst src,
-* so byte +2 is src).
- sep #$20
- LONGA OFF
- lda bscb+2
- sta >mvnScbInst+2
- rep #$20
- LONGA ON
- lda bscb
- tax
- ldy #$9D00
- lda #199
-mvnScbInst mvn $000000,$E10000
-
-* 2. Palette upload (512 bytes) via MVN. Same trick.
- sep #$20
- LONGA OFF
- lda bpal+2
- sta >mvnPalInst+2
- rep #$20
- LONGA ON
- lda bpal
- tax
- ldy #$9E00
- lda #511
-mvnPalInst mvn $000000,$E10000
-
-* 3. Pixel blit via PEI-slam, with per-row dirty skip and chunked SEI.
-* PEI-slam: SP hijacked into the SHR shadow region of bank $01, AUXWRITE
-* + RAMRD remap bank-$00 stack pushes to bank $01, SHR shadow mirrors
-* bank-$01 writes to $E1. Result: PEI dp pushes from DP=$01:row_start
-* land at $E1:row_start (160 bytes / row at ~6 cyc per 2 bytes).
-* ~480 cyc/row vs MVN's ~1120 cyc/row -- 2.3x faster per row.
-*
-* Chunked SEI: the slam runs in 5 chunks of up to 40 rows each, with
-* CLI between chunks so audio (DOC IRQ) and other periodic interrupts
-* aren't starved. Each chunk holds SEI for ~7.6 ms worst case (40
-* dirty rows). Per-chunk teardown re-saves the original soft-switch
-* state, then the next chunk re-arms it. Original SP / shadow are
-* captured once before the loop, so the per-chunk reset just reloads
-* them.
-*
-* Dirty skip: rows where gStageMinWord[y] > gStageMaxWord[y] are
-* clean and not slammed. Saves big on sparse-update demos; for
-* full-screen presents (DRAW), every row slams.
-*
-* Register usage during the chunk loop:
-* X = absolute row counter (0..200)
-* Y = in-chunk row counter (0..40)
-* Both are clobbered by the per-row slam (TCD/TCS) so we save through
-* gPeiCurRow / gPeiChunkRow long-mode scratch around each slam.
-
- tsc
- sta >gPeiOrigSp
- sep #$20
- LONGA OFF
- lda >$00C035
- sta >gPeiOrigShadow
- rep #$20
- LONGA ON
-
- ldx #0 ; X = absolute row counter
-
-peiChunkBegin anop
- ldy #0 ; Y = in-chunk row counter
-
- sei
-
- sep #$20
- LONGA OFF
- lda >gPeiOrigShadow
- and #$F1 ; clear bits 1,2,3 -> SHR shadow ON
- sta >$00C035
- lda #0
- sta >$00C005 ; AUXWRITE on
- sta >$00C003 ; RAMRD on
- rep #$20
- LONGA ON
-
-peiRowLoop anop
- cpx #200
- bcc peiNotAllDone ; X < 200 -> still rows to do
- brl peiChunkEnd ; 8-bit BCS can't reach across the 80-PEI body
-peiNotAllDone anop
- cpy #40
- bcc peiCheckDirty ; Y < 40 -> chunk has room
- brl peiChunkEnd
-peiCheckDirty anop
- sep #$20
- LONGA OFF
- lda >gStageMinWord,x
- cmp >gStageMaxWord,x
- rep #$20
- LONGA ON
- bcc peiSlamRow ; min < max -> dirty
- beq peiSlamRow ; min == max -> 1-word dirty
- inx ; clean row, skip
- iny
- brl peiRowLoop
-
-peiSlamRow anop
-* Save X and Y into long-mode scratch (stack is hijacked into $E1,
-* can't PHX/PHY without polluting the pixel data we're slamming).
- txa
- sta >gPeiCurRow
- tya
- sta >gPeiChunkRow
- lda >gPeiCurRow
- asl a ; A = y*2 (LUT byte offset)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- clc
- adc #$2000 ; A = row_start
- tcd ; D = row_start (PEI dp base)
- clc
- adc #159
- tcs ; SP = row_start + 159
-
-* 80 PEIs from DP+$9E down to DP+$00. Each pushes 2 bytes; SP decreases
-* by 2 each PEI. Final SP = row_start - 1. Bytes land at $E1:row_start
-* through $E1:row_start+159 (in correct memory order because we walk
-* DP offsets backwards).
- pei $9E
- pei $9C
- pei $9A
- pei $98
- pei $96
- pei $94
- pei $92
- pei $90
- pei $8E
- pei $8C
- pei $8A
- pei $88
- pei $86
- pei $84
- pei $82
- pei $80
- pei $7E
- pei $7C
- pei $7A
- pei $78
- pei $76
- pei $74
- pei $72
- pei $70
- pei $6E
- pei $6C
- pei $6A
- pei $68
- pei $66
- pei $64
- pei $62
- pei $60
- pei $5E
- pei $5C
- pei $5A
- pei $58
- pei $56
- pei $54
- pei $52
- pei $50
- pei $4E
- pei $4C
- pei $4A
- pei $48
- pei $46
- pei $44
- pei $42
- pei $40
- pei $3E
- pei $3C
- pei $3A
- pei $38
- pei $36
- pei $34
- pei $32
- pei $30
- pei $2E
- pei $2C
- pei $2A
- pei $28
- pei $26
- pei $24
- pei $22
- pei $20
- pei $1E
- pei $1C
- pei $1A
- pei $18
- pei $16
- pei $14
- pei $12
- pei $10
- pei $0E
- pei $0C
- pei $0A
- pei $08
- pei $06
- pei $04
- pei $02
- pei $00
-
- lda >gPeiCurRow
- tax
- inx
- lda >gPeiChunkRow
- tay
- iny
- brl peiRowLoop
-
-peiChunkEnd anop
-* Per-chunk teardown: restore SP, shadow, AUXWRITE/RAMRD, then CLI to
-* let pending IRQs run. If more rows remain, the next iteration of
-* peiChunkBegin re-arms everything.
- lda >gPeiOrigSp
- tcs
- sep #$20
- LONGA OFF
- lda >gPeiOrigShadow
- sta >$00C035
- lda #0
- sta >$00C004 ; AUXWRITE off
- sta >$00C002 ; RAMRD off
- rep #$20
- LONGA ON
- cli
-
- cpx #200
- bcs peiAllDone
- brl peiChunkBegin
-
-peiAllDone anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp ; restores I (pre-SEI value)
- rtl
- end
-
-
-****************************************************************
-* iigsBlitRectStageToShr(uint16_t srcOffset, uint16_t copyBytes,
-* uint16_t rowsLeft)
-*
-* Per-row MVN blit of `copyBytes` bytes from $01:srcOffset to
-* $E1:srcOffset, advancing srcOffset by 160 each row for `rowsLeft`
-* rows. Used by halPresentRect for partial-screen presents (e.g.,
-* sprite blits where only the union of old/new ball rect is dirty).
-*
-* MVN at ~9 cyc/byte (with $E1 slow-RAM wait states) beats ORCA-C's
-* per-byte memcpy loop (~30 cyc/byte). For a 16x16 sprite rect (16
-* rows of ~8 bytes) the savings are ~2 KB cyc per present (~0.7 ms),
-* which adds up across an animation loop running at full tilt.
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8):
-* srcOffset at D+0..1 (within bank-$01, e.g. $2000..$9C60)
-* copyBytes at D+2..3 (>= 1)
-* rowsLeft at D+4..5 (>= 1)
-****************************************************************
-
-iigsBlitRectStageToShr start DRAWPRIMS
-brsOff equ 0
-brsBytes equ 2
-brsRows equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
-* MVN expects (count - 1) in A. Cache once.
- lda brsBytes
- dec a
- sta >brsBytesM1
-
-brsRowLoop anop
- lda brsRows
- bne brsRowGo
- brl brsExit
-brsRowGo anop
-
-* X = src offset in bank $01, Y = dst offset in bank $E1 (same value).
- lda brsOff
- tax
- tay
- lda >brsBytesM1
- mvn $010000,$E10000
-
-* Advance offset by SURFACE_BYTES_PER_ROW (160).
- clc
- lda brsOff
- adc #160
- sta brsOff
-
- lda brsRows
- dec a
- sta brsRows
- brl brsRowLoop
-
-brsExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-brsBytesM1 data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsBlitRectStageToShrPEI(srcOffset, copyBytes, rowsLeft)
-*
-* PEI-slam variant of iigsBlitRectStageToShr for partial-rect
-* presents. Uses the SHR shadow trick + AUXWRITE/RAMRD stack hijack
-* to push pixel words from $01:row to $E1:row at ~3 cyc/byte instead
-* of MVN's ~9 cyc/byte (against $E1 wait states). For a 16x16 sprite
-* present (16 rows x 8 bytes) that's ~640 cyc vs MVN's ~1300 cyc.
-*
-* Caller contract:
-* - copyBytes must be even and >= 2 and <= 80. Caller (C wrapper)
-* verifies; this asm assumes the contract holds.
-* - srcOffset is the byte offset within bank $01 of the FIRST byte
-* of the FIRST row to copy. Rows advance by 160.
-*
-* SEI window for the duration: copyBytes/2 PEIs * rowsLeft + setup
-* per row. For a 16x16 sprite that's ~700 cyc = ~0.25 ms; safe for
-* DOC IRQ. For larger rects the C wrapper falls back to MVN to keep
-* the SEI window tiny.
-*
-* Args after PHP+PHB+PHD (TCD = SP+8):
-* srcOffset at D+0..1
-* copyBytes at D+2..3
-* rowsLeft at D+4..5
-****************************************************************
-
-iigsBlitRectStageToShrPEI start RECTPEI
-brpOff equ 0
-brpBytes equ 2
-brpRows equ 4
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
-* Save SP and shadow state for teardown.
- tsc
- sta >brpOrigSp
- sep #$20
- LONGA OFF
- lda >$00C035
- sta >brpOrigShadow
- rep #$20
- LONGA ON
-
-* Stash inputs into long-mode globals so they survive TCD changes.
-* Per-row code does TCD = rowBase, which means D-relative reads no
-* longer reach the original args at D+0..5. Everything we still need
-* per-row goes into a long-mode global below.
- lda brpOff
- sta >brpRowBase
- lda brpRows
- sta >brpRowsRem
- lda brpBytes
- dec a
- sta >brpBytesM1Saved ; copyBytes - 1, for TCS = base + bytes - 1
-
-* Compute jump entry into the unrolled PEI sequence.
-* words = copyBytes / 2
-* entry = peiSeqEnd - words * 2 (each PEI dp is 2 bytes; sequence
-* ends at peiSeqEnd with PEI $00 as
-* the LAST entry; offsets descend so
-* jumping `words*2` bytes BEFORE the
-* end starts at PEI $(2*(words-1)).
- lda brpBytes
- lsr a ; A = words
- asl a ; A = words * 2 (bytes of PEI to execute)
- sta >brpJmpDelta
- lda #peiSeqEnd
- sec
- sbc >brpJmpDelta
- sta >brpJmpTarget+1 ; patch JMP abs operand
-
- sei
-
- sep #$20
- LONGA OFF
- lda >brpOrigShadow
- and #$F1 ; SHR shadow ON (clear bits 1,2,3)
- sta >$00C035
- lda #0
- sta >$00C005 ; AUXWRITE on
- sta >$00C003 ; RAMRD on
- rep #$20
- LONGA ON
-
-brpRowLoop anop
- lda >brpRowsRem
- bne brpDoRow
- brl brpExit
-brpDoRow anop
-
-* Per-row: set DP = row base (so PEI dp pulls from the source row),
-* set SP = row base + copyBytes - 1 (so PEIs decrement-push into the
-* row in-place; bytes mirror to $E1 via SHR shadow).
-* NB: brpBytes is at original D+2 -- after TCD = rowBase that read
-* would land in pixel data. Use the long-mode brpBytesM1Saved instead.
- lda >brpRowBase
- clc
- adc >brpBytesM1Saved
- tcs ; SP = row base + copyBytes - 1
- lda >brpRowBase
- tcd ; D = row base
-
-* Jump into the unrolled PEI sequence at the right offset. operand
-* low byte was patched above; high byte is fixed at link time.
-brpJmpTarget anop
- jmp peiSeqEnd ; operand low byte is patched per call
-
-* ----- Unrolled PEI sequence: 40 PEIs, walking DP offsets DOWN from
-* $4E to $00 in 2-byte steps. JMP target lands at the right offset
-* so only `words` PEIs execute. Each PEI: 6 cyc, pushes 2 bytes to
-* SP (which mirrors to $E1 via shadow). Falls through to row
-* advance after PEI $00.
- pei $4E
- pei $4C
- pei $4A
- pei $48
- pei $46
- pei $44
- pei $42
- pei $40
- pei $3E
- pei $3C
- pei $3A
- pei $38
- pei $36
- pei $34
- pei $32
- pei $30
- pei $2E
- pei $2C
- pei $2A
- pei $28
- pei $26
- pei $24
- pei $22
- pei $20
- pei $1E
- pei $1C
- pei $1A
- pei $18
- pei $16
- pei $14
- pei $12
- pei $10
- pei $0E
- pei $0C
- pei $0A
- pei $08
- pei $06
- pei $04
- pei $02
- pei $00
-peiSeqEnd anop
-
-* Advance row base by 160 and decrement rows-remaining.
- lda >brpRowBase
- clc
- adc #160
- sta >brpRowBase
- lda >brpRowsRem
- dec a
- sta >brpRowsRem
- brl brpRowLoop
-
-brpExit anop
- lda >brpOrigSp
- tcs
- sep #$20
- LONGA OFF
- lda >brpOrigShadow
- sta >$00C035
- lda #0
- sta >$00C004 ; AUXWRITE off
- sta >$00C002 ; RAMRD off
- rep #$20
- LONGA ON
-
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp ; restores I (pre-SEI value)
- rtl
- end
-
-
-brpOrigSp data RECTPEI
- ds 2
- end
-brpOrigShadow data RECTPEI
- ds 1
- end
-brpRowBase data RECTPEI
- ds 2
- end
-brpRowsRem data RECTPEI
- ds 2
- end
-brpJmpDelta data RECTPEI
- ds 2
- end
-brpBytesM1Saved data RECTPEI
- ds 2
- end
-
-
-****************************************************************
-* iigsMarkDirtyRowsInner(yStart, yEnd, minWord, maxWord)
-*
-* Tight inline of surfaceMarkDirtyRect's per-row widenRow loop.
-* For each row in [yStart, yEnd) (yEnd exclusive), widen
-* gStageMinWord[row] DOWN to minWord (if smaller) and
-* gStageMaxWord[row] UP to maxWord (if larger).
-*
-* Replaces the ORCA-C version's per-row JSL/PHB/RTL/PLB chain
-* (~150 cyc/row) with a ~35 cyc/row asm body. Two calls per
-* sprite frame (one for restore, one for draw) on a 16-row
-* sprite saves ~3500 cyc/frame; cumulative savings across all
-* draw primitives that mark dirty are larger.
-*
-* Caller is responsible for clipping yStart/yEnd to [0, 200);
-* matches the original C contract (no internal bounds check).
-*
-* Args after PHP+PHB+PHD (TCD = SP+8):
-* yStart at D+0..1
-* yEnd at D+2..3 (exclusive)
-* minWord at D+4..5 (low byte used; high byte ignored)
-* maxWord at D+6..7 (low byte used; high byte ignored)
-****************************************************************
-
-iigsMarkDirtyRowsInner start MARKDIRTY
-mdrYStart equ 0
-mdrYEnd equ 2
-mdrMinWord equ 4
-mdrMaxWord equ 6
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
- ldx mdrYStart
- cpx mdrYEnd
- bcs mdrExit ; empty range -> nothing to do
-
- sep #$20
- LONGA OFF
-
-mdrLoop anop
- lda mdrMinWord
- cmp >gStageMinWord,x
- bcs mdrNoMin ; A >= existing -> existing already <= minWord
- sta >gStageMinWord,x ; widen down
-mdrNoMin anop
- lda mdrMaxWord
- cmp >gStageMaxWord,x
- bcc mdrNoMax ; A < existing -> existing already >= maxWord
- beq mdrNoMax
- sta >gStageMaxWord,x ; widen up
-mdrNoMax anop
- inx
- cpx mdrYEnd
- bcc mdrLoop
-
- rep #$20
- LONGA ON
-
-mdrExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsPollJoystickInner(void)
-*
-* Reads the IIgs paddle ports (PDL0/PDL1) at the period-correct 1 MHz
-* CPU speed, ported from the old joeylib's jIIgs.asm. The polling rate
-* and the paddle one-shot timer rate must match for the count value to
-* mean what every other IIgs/Apple II joystick game expects.
-*
-* Algorithm:
-* 1. PHP / SEI / save DBR / DBR=0 (so abs $C0xx I/O reads work).
-* 2. Save current $C036 CYAREG; clear bit 7 to force 1 MHz.
-* 3. PTRIG ($C070) to start both paddle timers in parallel.
-* 4. Tight interleaved poll loop:
-* - Both busy: lda PDL0; if done -> pdl1-only loop. Else inx;
-* inx; lda PDL1; if done -> pdl0-only loop. Else iny; loop.
-* - X overflow (256 iter * 2) marks pdl0 unresolved (timeout).
-* - Y overflow (256 iter * 1) marks pdl1 unresolved.
-* 5. Y is incremented half as often as X (mirrors old jIIgs.asm), so
-* the trailing `asl a` doubles it back to the same scale.
-* 6. Restore $C036, restore DBR, restore P (incl I).
-*
-* Outputs (DRAWPRIMS scratch):
-* gJoyPx -- pdl0 count, 0..254 (unconnected/timeout: 0)
-* gJoyPy -- pdl1 count, 0..254
-* gJoyResolved -- bit 0 set iff pdl0 fired before timeout;
-* bit 1 set iff pdl1 fired
-****************************************************************
-
-iigsPollJoystickInner start IIGSASM
- php
- sei
-
- rep #$30
- LONGA ON
- LONGI ON
-
-* Save caller's DBR; force DBR=0 so abs $C0xx hits the I/O page.
- phb
- pea $0000
- plb ; pull low byte of pushed word -> DBR=0
- plb ; pull high byte (discard, was 0)
-
- sep #$30 ; M=8, X=8 (tight inner loop)
- LONGA OFF
- LONGI OFF
-
-* Save CYAREG and force 1 MHz (clear bit 7).
- lda $C036
- sta >gJoyOrigSpeed
- and #$7F
- sta $C036
-
- ldx #0 ; pdl0 count (increments by 2)
- ldy #0 ; pdl1 count (increments by 1; asl at end)
- lda #0
- sta >gJoyResolved
-
-* PTRIG: start both paddle one-shot timers.
- lda $C070
-
-* Both-busy poll loop. ~25 cyc / iter; X reaches ~240 at full
-* deflection, X wraps (8-bit) at ~3.2 ms (paddle timer max).
-joyChkPdl0 anop
- lda $C064 ; PDL0
- bpl joyGotPdl0 ; bit7=0 -> pdl0 done
- inx
- inx
- beq joyTimeoutX ; X wrapped -> pdl0 never fired
- lda $C065 ; PDL1
- bmi joyNoGots ; bit7=1 -> pdl1 still busy
-* pdl1 just fired. Mark resolved, switch to pdl0-only loop.
- lda #$02
- sta >gJoyResolved
-joyPdl0Only anop
- lda $C064
- bpl joyAllDone
- inx
- inx
- bne joyPdl0Only
- bra joyTimeoutX
-joyNoGots anop
- iny
- bne joyChkPdl0
- bra joyTimeoutY
-
-joyGotPdl0 anop
- lda #$01
- sta >gJoyResolved
-joyPdl1Only anop
- lda $C065
- bpl joyAllDone
- iny
- bne joyPdl1Only
- bra joyTimeoutY
-
-joyAllDone anop
- lda >gJoyResolved
- ora #$03
- sta >gJoyResolved
- bra joyExit
-
-joyTimeoutX anop
- lda >gJoyResolved
- and #$02 ; clear bit 0 (pdl0 unresolved)
- sta >gJoyResolved
- bra joyExit
-
-joyTimeoutY anop
- lda >gJoyResolved
- and #$01 ; clear bit 1 (pdl1 unresolved)
- sta >gJoyResolved
-
-joyExit anop
-* STX/STY have no long-abs form. Stash via TXA/TYA.
- txa
- sta >gJoyPx
- tya
- asl a ; scale Y from 0..127 to 0..254
- sta >gJoyPy
-
-* Restore CYAREG (other bits preserved by saving full byte).
- lda >gJoyOrigSpeed
- sta $C036
-
- rep #$30
- LONGA ON
- LONGI ON
- plb ; restore caller DBR
-
- plp ; restores I from pre-SEI value
- rtl
- end
-
-
-gJoyPx data DRAWPRIMS
- ds 2
- end
-gJoyPy data DRAWPRIMS
- ds 2
- end
-gJoyResolved data DRAWPRIMS
- ds 2
- end
-gJoyOrigSpeed data DRAWPRIMS
- ds 2
- end
-
-
-****************************************************************
-* iigsInputSnapshot(void)
-*
-* Per-frame input bookkeeping done in one tight asm pass instead of
-* three C memcpys. Saves ~0.5 ms per frame in animated demos.
-*
-* Two combined operations:
-* 1. Snapshot gKeyState -> gKeyPrev (KEY_COUNT bytes via long-mode
-* lda/sta loop, ~15 cyc/byte).
-* 2. Snapshot gMouseButtonState/gJoyButtonState (4 bytes each) via
-* 4 inline lda/sta pairs.
-*
-* The TTL-decay loop this used to run has been removed: the IIgs
-* port now derives release directly from $C010 bit 7 in halInputPoll
-* (the live "any key currently held" flag), so the inferred-release
-* TTL mechanism is no longer needed.
-*
-* IMPORTANT: KEY_COUNT is hard-coded at 60 below. If you add or
-* remove a key in joey/input.h, bump the constant or the loop bounds
-* will silently miss / spill. The C side has a STATIC_ASSERT-ish
-* check via a build-time array dimension trick (see input.c).
-****************************************************************
-
-iigsInputSnapshot start IIGSASM
- php
- rep #$30
- LONGA ON
- LONGI ON
- sep #$20
- LONGA OFF
-
-* Snapshot gKeyState -> gKeyPrev (60 bytes), long-mode loop.
- ldx #59
-isnKeyLoop anop
- lda >gKeyState,x
- sta >gKeyPrev,x
- dex
- bpl isnKeyLoop
-
-* Snapshot gMouseButtonState -> gMouseButtonPrev (4 bytes inline).
- lda >gMouseButtonState
- sta >gMouseButtonPrev
- lda >gMouseButtonState+1
- sta >gMouseButtonPrev+1
- lda >gMouseButtonState+2
- sta >gMouseButtonPrev+2
- lda >gMouseButtonState+3
- sta >gMouseButtonPrev+3
-
-* Snapshot gJoyButtonState -> gJoyButtonPrev (4 bytes inline,
-* JOYSTICK_COUNT * JOY_BUTTON_COUNT = 2*2).
- lda >gJoyButtonState
- sta >gJoyButtonPrev
- lda >gJoyButtonState+1
- sta >gJoyButtonPrev+1
- lda >gJoyButtonState+2
- sta >gJoyButtonPrev+2
- lda >gJoyButtonState+3
- sta >gJoyButtonPrev+3
-
- rep #$30
- LONGA ON
- LONGI ON
- plp
- rtl
- end
-
-
-****************************************************************
-* iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y,
-* uint16_t matchColor, uint16_t newColor,
-* uint16_t matchEqual,
-* int16_t *stackX, int16_t *stackY,
-* uint16_t *spInOut, uint16_t maxSp)
-*
-* Combined per-popped-seed work. Replaces the C dispatch loop's three
-* halFast* calls (walk + scan-above + scan-below) with one cross-
-* segment call. All three sub-operations share the cached row addr,
-* match-byte, new-byte, eq-flag, and matchByte-vs-newByte decoders.
-*
-* Outputs (DRAWPRIMS globals, same as iigsFloodWalkInner):
-* gFloodSeedMatch -- 1 if seed pixel matched
-* gFloodLeftX -- leftmost matching column (only valid if matched)
-* gFloodRightX -- rightmost matching column
-* Caller is responsible for the 1-row halFastFillRect after we return,
-* using gFloodLeftX..gFloodRightX. We DON'T fill here -- the C-side
-* fill is already as fast as the asm fill (both end up in
-* iigsFillRectStageInner).
-*
-* Args after PHP+PHB+PHD (TCD makes D = SP+8):
-* pixels D+0..3 (long ptr)
-* x D+4..5
-* y D+6..7
-* matchColor D+8..9 (low byte)
-* newColor D+10..11
-* matchEqual D+12..13
-* stackX D+14..17 (long ptr to int16_t[])
-* stackY D+18..21 (long ptr)
-* spInOut D+22..25 (long ptr to int16_t)
-* maxSp D+26..27
-****************************************************************
-
-iigsFloodWalkAndScansInner start IIGSASM
-* wsRow occupies the same 4 DP bytes as the pixels arg (D+0..3): we
-* compute row = pixels + y*160 and overwrite the pixels slot with the
-* result so that `lda [wsRow],y` (DP-indirect-long indexed Y) reads
-* directly from the row in the inner loops -- no separate scratch ptr.
-* Likewise wsScanRow overlaps wsMatch+wsNew (D+8..11): those args get
-* cached to wsMatchByte/wsNewByte first, then we reuse the slot.
-wsRow equ 0 ; D+0..3 (was wsPixels arg)
-wsX equ 4
-wsY equ 6
-wsScanRow equ 8 ; D+8..11 (was wsMatch+wsNew args)
-wsMatch equ 8 ; alias of wsScanRow during initial cache
-wsNew equ 10 ; alias of wsScanRow+2 during initial cache
-wsEq equ 12
-wsStackX equ 14
-wsStackY equ 18
-wsSpInOut equ 22
-wsMaxSp equ 26
-wsPixels equ 0 ; alias of wsRow for the initial row-addr compute
-wsMidEnd equ 4 ; alias of wsX, used by inline fill (post-walk)
-* DP scratch for the rewritten scan inner loop (2 pixels per byte read,
-* DP-relative for ~1 cyc faster than long-mode `>fpScratch` access).
-* All free post-walk / post-cache; sequential phases don't overlap.
-wsScanCurX equ 4 ; alias wsX/wsMidEnd, 16-bit
-wsScanByte equ 12 ; alias wsEq.lo, 8-bit
-wsScanPrevHit equ 26 ; alias wsMaxSp.lo, 8-bit
-wsScanCurHit equ 27 ; alias wsMaxSp.hi, 8-bit
-
- php
- phb
- phd
- rep #$30
- LONGA ON
- LONGI ON
- tsc
- clc
- adc #8
- tcd
-
-* Cache 8-bit constants used across walk + scan + fill.
-* wsNewHigh = newNibble << 4 (used for fill's trailing high-nibble RMW).
-* wsDoubledByte = (newNibble << 4) | newNibble (full-byte fill value).
- sep #$20
- LONGA OFF
- lda wsMatch
- and #$0F
- sta >wsMatchByte
- lda wsNew
- and #$0F
- sta >wsNewByte
- ora #0
- pha ; save newByte for doubled compute
- asl a
- asl a
- asl a
- asl a ; A = newByte << 4
- sta >wsNewHigh
- ora 1,s ; A = (newByte<<4) | newByte
- sta >wsDoubledByte
- pla ; clean stack (A=byte, then we overwrite)
- lda wsEq
- sta >wsEqByte
- rep #$20
- LONGA ON
-
-* Compute rowAddr (long ptr) = pixels + lut[y*2]. LUT lookup beats
-* the shift-add by ~7 cyc per call. Then store row.lo / row.hi over
-* the pixels arg slot (= wsRow).
- lda wsY
- asl a ; A = y*2 (byte offset into LUT)
- tax
- lda >gRowOffsetLut,x ; A = y*160
- clc
- adc wsPixels ; A = pixels.lo + y*160 = row.lo
- tax ; X = row.lo (carry preserved through tax)
- lda wsPixels+2 ; A = pixels.hi
- adc #0 ; A = row.hi (carry from prev add)
- sta wsRow+2 ; D+2..3 = row.hi
- txa
- sta wsRow ; D+0..1 = row.lo
-
-* === SEED TEST + WALK LEFT + WALK RIGHT ===
-* Specialized per matchEqual: branch once, then run inlined-test loops.
-* Kills the per-pixel JSR/RTS to wsTestPix (~12 cyc) and the per-pixel
-* matchEqual branch inside the test (~8 cyc) -- ~20 cyc per pixel
-* across ~3600 walked pixels in the demo.
-*
-* leftX / rightX are held in DP slots (wsScanCurX = DP+4 alias of wsX)
-* during the walk; the gFloodLeftX/RightX globals are only written at
-* the end so we don't pay long-mode store cost on every step.
- sep #$20
- LONGA OFF
- lda >wsEqByte
- rep #$20
- LONGA ON
- bne wsWalkEqEntry
- brl wsWalkBndEntry
-
-***** EQUAL MODE WALK *****
-* Seed + walk-left + walk-right with the pixel test inlined (kills
-* JSR/RTS overhead per pixel) and per-iter long-mode access to
-* gFloodLeftX/RightX replaced by DP-relative wsMatchByte / rep #$20
-wsWalkEqEntry anop
-* --- SEED TEST EQ (inline) ---
- lda wsX
- lsr a
- tay
- sep #$20
- LONGA OFF
- bcs wsSeedEqOdd
- lda [wsRow],y
- lsr a
- lsr a
- lsr a
- lsr a
- bra wsSeedEqHave
-wsSeedEqOdd anop
- lda [wsRow],y
- and #$0F
-wsSeedEqHave anop
- cmp >wsMatchByte
- rep #$20
- LONGA ON
- bne wsSeedEqMiss
- lda #1
- sta >gFloodSeedMatch
- bra wsWalkEqSeedOk
-wsSeedEqMiss anop
- lda #0
- sta >gFloodSeedMatch
- brl wsExit
-wsWalkEqSeedOk anop
-
-* --- WALK LEFT EQ (byte-cached: 1 byte read per 2 walked pixels) ---
-* Splits the loop into evenEntry / oddEntry paths.
-* evenEntry: currentX even -> test column C-1 (odd, low nib of byte Y-1).
-* Read NEW byte at Y-1; cache; test low nib.
-* oddEntry: currentX odd -> test column C-1 (even, high nib of byte Y).
-* Reuse CACHED byte from previous iter; extract high nib.
-* Initial: peel parity once to seed the cache and pick entry point.
- lda wsX
- sta wsMatchByte
- rep #$20
- LONGA ON
- bne wsLeftEqDone
- dec wsMatchByte
- rep #$20
- LONGA ON
- bne wsLeftEqDone
- dec gFloodLeftX
-
-* --- WALK RIGHT EQ (byte-cached) ---
-* evenEntry: currentX even -> test C+1 (odd, low nib of CACHED byte at Y).
-* oddEntry: currentX odd -> test C+1 (even, high nib of byte Y+1).
-* Inc Y, read NEW byte, cache, test high nib.
- lda wsX
- sta wsMatchByte
- rep #$20
- LONGA ON
- bne wsRightEqDone
- inc wsMatchByte
- rep #$20
- LONGA ON
- bne wsRightEqDone
- inc gFloodRightX
- brl wsAfterWalk
-
-***** BOUNDARY MODE WALK *****
-wsWalkBndEntry anop
-* --- SEED TEST BND (inline) ---
- lda wsX
- lsr a
- tay
- sep #$20
- LONGA OFF
- bcs wsSeedBndOdd
- lda [wsRow],y
- lsr a
- lsr a
- lsr a
- lsr a
- bra wsSeedBndHave
-wsSeedBndOdd anop
- lda [wsRow],y
- and #$0F
-wsSeedBndHave anop
- cmp >wsMatchByte
- beq wsSeedBndMiss
- cmp >wsNewByte
- beq wsSeedBndMiss
- rep #$20
- LONGA ON
- lda #1
- sta >gFloodSeedMatch
- bra wsWalkBndSeedOk
-wsSeedBndMiss anop
- rep #$20
- LONGA ON
- lda #0
- sta >gFloodSeedMatch
- brl wsExit
-wsWalkBndSeedOk anop
-
-* --- WALK LEFT BND (byte-cached) ---
- lda wsX
- sta wsMatchByte
- beq wsLBndStop
- cmp >wsNewByte
- beq wsLBndStop
- rep #$20
- LONGA ON
- dec wsMatchByte
- beq wsLBndStop2
- cmp >wsNewByte
- beq wsLBndStop2
- rep #$20
- LONGA ON
- dec gFloodLeftX
-
-* --- WALK RIGHT BND (byte-cached) ---
- lda wsX
- sta wsMatchByte
- beq wsRBndStop
- cmp >wsNewByte
- beq wsRBndStop
- rep #$20
- LONGA ON
- inc wsMatchByte
- beq wsRBndStop2
- cmp >wsNewByte
- beq wsRBndStop2
- rep #$20
- LONGA ON
- inc gFloodRightX
-
-wsAfterWalk anop
-
-* Cache leftX/rightX/spanLen for the scans.
- lda >gFloodLeftX
- sta >wsLeftX
- lda >gFloodRightX
- sta >wsRightX
- sec
- sbc >wsLeftX
- inc a
- sta >wsSpanLen
-
-* === FILL THE SPAN ===
-* Inline 1-row fill of row[leftX..rightX] with newNibble. Walk-out
-* validated leftX/rightX bounds. Saves a cross-segment halFastFillRect
-* call per popped seed; the C side now skips it when this asm path
-* runs. wsMidEnd uses DP+4 (alias of wsX, free post-walk) so DP-direct
-* `cpy wsMidEnd` works (CPY has no long-abs form).
- lda >wsLeftX
- lsr a ; A = leadingByte; carry = leftX & 1
- tay ; Y = leadingByte
- bcc wsFillNoLead
-* Leading partial byte: low nibble of byte[Y] = newNibble.
- sep #$20
- LONGA OFF
- lda [wsRow],y
- and #$F0
- ora >wsNewByte
- sta [wsRow],y
- rep #$20
- LONGA ON
- iny ; midStart = leadingByte + 1
-wsFillNoLead anop
-
-* midEnd = (rightX + 1) >> 1; trailing partial only if rightX even.
- lda >wsRightX
- inc a ; A = pxEnd = rightX + 1
- lsr a ; A = midEnd; carry = pxEnd & 1
- sta wsMidEnd ; DP store (so DP-direct CPY works)
- bcs wsFillSetTrail
- lda #0
- sta >wsHasTrail
- bra wsFillMidLoop
-wsFillSetTrail anop
- lda #1
- sta >wsHasTrail
-
-wsFillMidLoop anop
- sep #$20
- LONGA OFF
- lda >wsDoubledByte
-wsFillMidIter anop
- cpy wsMidEnd ; DP-direct CPY (X=16-bit reads 2 bytes)
- bcs wsFillMidDone
- sta [wsRow],y
- iny
- bra wsFillMidIter
-wsFillMidDone anop
- rep #$20
- LONGA ON
-
- lda >wsHasTrail
- beq wsFillDone
-* Trailing partial byte: high nibble of byte[wsMidEnd] = newNibble.
- ldy wsMidEnd ; Y = trailing byte index
- sep #$20
- LONGA OFF
- lda [wsRow],y
- and #$0F
- ora >wsNewHigh
- sta [wsRow],y
- rep #$20
- LONGA ON
-wsFillDone anop
-
-* Load sp from *spInOut, cache maxSp.
- ldy #0
- lda [wsSpInOut],y
- sta >wsSp
- lda wsMaxSp
- sta >wsMaxSpCache
-
-* === SCAN ABOVE (if y > 0) ===
- lda wsY
- bne wsHasAbove
- brl wsSkipAbove
-wsHasAbove anop
- lda wsRow
- sec
- sbc #160
- sta wsScanRow
- lda wsRow+2
- sbc #0
- sta wsScanRow+2
- lda wsY
- dec a
- sta >wsScanY
- jsr wsScanAndPush
-wsSkipAbove anop
-
-* === SCAN BELOW (if y < 199) ===
- lda wsY
- cmp #199
- bcc wsHasBelow
- brl wsSkipBelow
-wsHasBelow anop
- lda wsRow
- clc
- adc #160
- sta wsScanRow
- lda wsRow+2
- adc #0
- sta wsScanRow+2
- lda wsY
- inc a
- sta >wsScanY
- jsr wsScanAndPush
-wsSkipBelow anop
-
-* Store updated sp back to *spInOut.
- ldy #0
- lda >wsSp
- sta [wsSpInOut],y
-
-wsExit anop
- LONGA OFF
- LONGI OFF
- pld
- plb
- plp
- rtl
-
-* wsTestEq / wsTestBnd helpers deleted -- the seed test and walk-out
-* loops now expand the test inline (no JSR/RTS overhead per walked
-* pixel; saves ~12 cyc/pixel x ~3600 walked pixels in the demo).
-
-
-* wsScanAndPush: walk wsScanRow[wsLeftX..wsRightX] for run-edge
-* transitions. Pushes (curX-1, wsScanY) on falling edges and
-* (rightX, wsScanY) at end of trailing run.
-*
-* This version processes 2 pixels per byte read (one byte = high+low
-* nibble) and keeps hot scratch (curX, byte, prevHit, curHit) in DP
-* slots. Three phases: optional leading single low-nibble (if leftX
-* odd), then a pair loop reading whole bytes, then optional trailing
-* high-nibble (if curX == rightX after the pair loop).
-*
-* Y is maintained as the byte index within scanRow across the whole
-* function -- no more phy/ply per pixel.
-wsScanAndPush anop
- sep #$20
- LONGA OFF
- lda #0
- sta wsEqByte
- rep #$20
- LONGA ON
- bne wsEqEntry2
- brl wsBoundEntry2
-
-***** EQUAL MODE *****
-wsEqEntry2 anop
- lda >wsLeftX
- sta wsMatchByte
- beq wsEqLeadHit2
- lda #0
- bra wsEqLeadStore2
-wsEqLeadHit2 anop
- lda #1
-wsEqLeadStore2 anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsEqLeadNoFall2 anop
- lda wsRightX
- bcc wsEqDoPair2
- beq wsEqDoPair2
- brl wsEqTrailing2
-
-wsEqDoPair2 anop
- sep #$20
- LONGA OFF
- lda [wsScanRow],y
- sta wsMatchByte
- beq wsEqHi2Hit
- lda #0
- bra wsEqHi2Store
-wsEqHi2Hit anop
- lda #1
-wsEqHi2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsEqHi2NoFall anop
- lda wsMatchByte
- beq wsEqLo2Hit
- lda #0
- bra wsEqLo2Store
-wsEqLo2Hit anop
- lda #1
-wsEqLo2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsEqLo2NoFall anop
- lda wsRightX
- bcc wsEqDoTrail2
- beq wsEqDoTrail2
- brl wsScanDone
-
-wsEqDoTrail2 anop
- sep #$20
- LONGA OFF
- lda [wsScanRow],y
- lsr a
- lsr a
- lsr a
- lsr a ; A = high nibble
- cmp >wsMatchByte
- beq wsEqTr2Hit
- lda #0
- bra wsEqTr2Store
-wsEqTr2Hit anop
- lda #1
-wsEqTr2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsEqTr2NoFall anop
- lda wsLeftX
- sta wsMatchByte
- beq wsBndLeadMiss2
- cmp >wsNewByte
- beq wsBndLeadMiss2
- lda #1
- bra wsBndLeadStore2
-wsBndLeadMiss2 anop
- lda #0
-wsBndLeadStore2 anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsBndLeadNoFall2 anop
- lda wsRightX
- bcc wsBndDoPair2
- beq wsBndDoPair2
- brl wsBndTrailing2
-
-wsBndDoPair2 anop
- sep #$20
- LONGA OFF
- lda [wsScanRow],y
- sta wsMatchByte
- beq wsBndHi2Miss
- cmp >wsNewByte
- beq wsBndHi2Miss
- lda #1
- bra wsBndHi2Store
-wsBndHi2Miss anop
- lda #0
-wsBndHi2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsBndHi2NoFall anop
- lda wsMatchByte
- beq wsBndLo2Miss
- cmp >wsNewByte
- beq wsBndLo2Miss
- lda #1
- bra wsBndLo2Store
-wsBndLo2Miss anop
- lda #0
-wsBndLo2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsBndLo2NoFall anop
- lda wsRightX
- bcc wsBndDoTrail2
- beq wsBndDoTrail2
- brl wsScanDone
-
-wsBndDoTrail2 anop
- sep #$20
- LONGA OFF
- lda [wsScanRow],y
- lsr a
- lsr a
- lsr a
- lsr a ; A = high nibble
- cmp >wsMatchByte
- beq wsBndTr2Miss
- cmp >wsNewByte
- beq wsBndTr2Miss
- lda #1
- bra wsBndTr2Store
-wsBndTr2Miss anop
- lda #0
-wsBndTr2Store anop
- sta wsPushX
- jsr wsPushXY
- sep #$20
- LONGA OFF
-wsBndTr2NoFall anop
- lda wsRightX
- sta >wsPushX
- jsr wsPushXY
-wsScanReturn anop
- rts
-
-
-* wsPushXY: push (wsPushX, wsScanY) at sp if spwsSp
- cmp >wsMaxSpCache
- bcs wsPushSkip
- asl a
- tay
- lda >wsPushX
- sta [wsStackX],y
- lda >wsScanY
- sta [wsStackY],y
- lda >wsSp
- inc a
- sta >wsSp
-wsPushSkip anop
- ply
- pla
- rts
- end
-
-
-wsRowMulTmp data DRAWPRIMS
- ds 2
- end
-wsLeftX data DRAWPRIMS
- ds 2
- end
-wsRightX data DRAWPRIMS
- ds 2
- end
-wsScanY data DRAWPRIMS
- ds 2
- end
-wsMatchByte data DRAWPRIMS
- ds 2
- end
-wsNewByte data DRAWPRIMS
- ds 2
- end
-wsEqByte data DRAWPRIMS
- ds 2
- end
-wsSp data DRAWPRIMS
- ds 2
- end
-wsMaxSpCache data DRAWPRIMS
- ds 2
- end
-wsSpanLen data DRAWPRIMS
- ds 2
- end
-wsCurX data DRAWPRIMS
- ds 2
- end
-wsByte data DRAWPRIMS
- ds 2
- end
-wsPixNib data DRAWPRIMS
- ds 2
- end
-wsPushX data DRAWPRIMS
- ds 2
- end
-wsPrevHit data DRAWPRIMS
- ds 2
- end
-wsCurHit data DRAWPRIMS
- ds 2
- end
-wsNewHigh data DRAWPRIMS
- ds 2
- end
-wsDoubledByte data DRAWPRIMS
- ds 2
- end
-wsHasTrail data DRAWPRIMS
- ds 2
- end
diff --git a/src/port/iigs/joeyDraw.s b/src/port/iigs/joeyDraw.s
new file mode 100644
index 0000000..2de396b
--- /dev/null
+++ b/src/port/iigs/joeyDraw.s
@@ -0,0 +1,4248 @@
+; joeyDraw.s - Apple IIgs ASM inner loops for graphics primitives.
+;
+; GAS syntax for the llvm-mos w65816 toolchain. Each function is the
+; FAST INNER LOOP only;
+; the C wrappers in src/core/draw.c and src/core/tile.c handle NULL /
+; range validation, clipping, and dirty-rect marking. The inner loops
+; trust their inputs (pre-clipped, byte-aligned, non-NULL).
+;
+; Calling convention (llvm-mos w65816 cdecl), as probed from clang -S:
+; - arg0 in registers: A:X for a 32-bit pointer/long, A for a 16-bit
+; word. (clang puts arg0 in registers, which is why the
+; per-function stack offsets below are small.)
+; - args 1.. on the stack, pushed right-to-left, first stack arg at
+; 4,s (after the 3-byte JSL return), each word +2, each ptr/long +4.
+; - After PHP every stack offset shifts by +1.
+; - uint16_t return in A; 32-bit return in A:X.
+;
+; Memory model (llvm-mos small model): DBR = 0 for C code, and globals
+; live in bank 0, so 16-bit absolute reaches both globals and bank-0
+; soft switches ($00Cxxx) -- no long prefix is needed on those.
+; The SHR stage at $01:2000 is written with full 24-bit literals, which
+; the assembler selects long addressing for automatically (value >64K).
+;
+; llvm-mc defaults to M=I=16; the explicit .a8/.a16/.i8/.i16 directives
+; below pair with sep/rep so immediates are sized correctly.
+
+ .text
+
+; ----------------------------------------------------------------
+; void iigsInitRowLut(void)
+;
+; One-shot init for the y*160 lookup table at gRowOffsetLut. C calls
+; this once from halInit; afterwards every primitive that needs y ->
+; row byte offset can do `lda gRowOffsetLut,x` (X = y*2) in a few cyc
+; instead of a shift-add chain. 200 entries x 2 bytes = 400 bytes.
+; ----------------------------------------------------------------
+ .section .text.iigsInitRowLut,"ax"
+ .globl iigsInitRowLut
+iigsInitRowLut:
+ php
+ rep #0x30 ; M=16, X=16
+ .a16
+ .i16
+ ldx #0 ; X = byte offset into LUT
+ lda #0 ; A = current y*160
+initLutLoop:
+ sta gRowOffsetLut, x
+ clc
+ adc #160
+ inx
+ inx
+ cpx #400
+ bcc initLutLoop
+ .a8
+ .i8
+ plp
+ rtl
+
+; ----------------------------------------------------------------
+; void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord)
+;
+; Fast constant fill of the 32000-byte stage at $01:$2000-$9CFF with
+; fillWord (both bytes the same). Unrolled run of STA long-absolute
+; indexed stores (`sta >$0120xx,x`, 6 cyc / 2 bytes). 40 stores cover
+; 80 bytes; X steps by 80 over 400 rows. Long addressing targets bank
+; $01 explicitly, so no DBR / AUXWRITE / shadow manipulation and no SEI.
+;
+; `pixels` (arg0) arrives in A:X and is ignored; the stage is hardcoded
+; at $01:$2000 (the C wrapper guarantees s == jlStageGet()). fillWord
+; (arg1) is at 5,s after PHP.
+; ----------------------------------------------------------------
+ .section .text.iigsSurfaceClearInner,"ax"
+ .globl iigsSurfaceClearInner
+iigsSurfaceClearInner:
+ php
+ rep #0x30 ; M=16, X=16
+ .a16
+ .i16
+ ldx #0 ; X = row byte base (0,80,...,31920)
+clrRowLoop:
+ lda 5, s ; A = fillWord (S never moves; 5,s stays valid)
+ sta 0x012000, x
+ sta 0x012002, x
+ sta 0x012004, x
+ sta 0x012006, x
+ sta 0x012008, x
+ sta 0x01200a, x
+ sta 0x01200c, x
+ sta 0x01200e, x
+ sta 0x012010, x
+ sta 0x012012, x
+ sta 0x012014, x
+ sta 0x012016, x
+ sta 0x012018, x
+ sta 0x01201a, x
+ sta 0x01201c, x
+ sta 0x01201e, x
+ sta 0x012020, x
+ sta 0x012022, x
+ sta 0x012024, x
+ sta 0x012026, x
+ sta 0x012028, x
+ sta 0x01202a, x
+ sta 0x01202c, x
+ sta 0x01202e, x
+ sta 0x012030, x
+ sta 0x012032, x
+ sta 0x012034, x
+ sta 0x012036, x
+ sta 0x012038, x
+ sta 0x01203a, x
+ sta 0x01203c, x
+ sta 0x01203e, x
+ sta 0x012040, x
+ sta 0x012042, x
+ sta 0x012044, x
+ sta 0x012046, x
+ sta 0x012048, x
+ sta 0x01204a, x
+ sta 0x01204c, x
+ sta 0x01204e, x
+ txa
+ clc
+ adc #80
+ tax
+ cpx #32000
+ bcs clrExit ; X >= 32000 -> done
+ brl clrRowLoop ; loop back is >127 bytes -> needs brl
+clrExit:
+ .a8
+ .i8
+ plp
+ rtl
+
+; ----------------------------------------------------------------
+; void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord)
+;
+; Post-init fast clear: PHA stack-slam (4 cyc / 2 bytes) vs the STA
+; long,X path (6 cyc / 2 bytes). SP is relocated to the top of the $01
+; stage region ($2000..$9CFF); AUXWRITE ($C005) redirects the bank-$00
+; stack writes to the $01 stage bank, and SHR shadow is already
+; inhibited by halInit. ~1.5x faster.
+;
+; SEI guards the AUXWRITE/SP-hijack window; SP is restored before CLI.
+; SAFE ONLY POST-halInit. Soft switches are bank-0 16-bit absolute
+; (DBR = 0). `pixels` (arg0) in A:X is ignored; fillWord (arg1) at 5,s.
+; ----------------------------------------------------------------
+ .section .text.iigsSurfaceClearFastInner,"ax"
+ .globl iigsSurfaceClearFastInner
+iigsSurfaceClearFastInner:
+ php
+ rep #0x30
+ .a16
+ .i16
+ lda 5, s ; A = fillWord (read before SP/AUXWRITE change)
+ sta sclrFillCap
+ sei
+ tsc
+ sta sclrSavedSp ; save original SP
+ sep #0x20
+ .a8
+ sta 0xc005 ; AUXWRITE on: $00:0200-BFFF writes -> bank $01
+ rep #0x20
+ .a16
+ lda #0x9cff
+ tcs ; SP = top of $01 stage region
+ lda sclrFillCap ; A = fillWord = the pushed value
+ ldx #200 ; 200 blocks x 80 PHA = 16000 pushes = 32000 bytes
+clrFastBlock:
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ pha
+ dex
+ bne clrFastBlock
+ lda sclrSavedSp
+ tcs ; restore original SP
+ sep #0x20
+ .a8
+ sta 0xc004 ; AUXWRITE off
+ rep #0x20
+ .a16
+ .a8
+ .i8
+ plp ; restores I (CLI)
+ rtl
+
+; ----- DRAWPRIMS data (uninitialized; bank 0 BSS) -----
+
+ .section .bss.gRowOffsetLut,"aw"
+ .globl gRowOffsetLut
+gRowOffsetLut:
+ .zero 400
+
+ .section .bss.sclrFillCap,"aw"
+ .globl sclrFillCap
+sclrFillCap:
+ .zero 2
+
+ .section .bss.sclrSavedSp,"aw"
+ .globl sclrSavedSp
+sclrSavedSp:
+ .zero 2
+
+; Flood-fill seed/match scratch (asm-only data; no C definition exists).
+; iigsFloodWalkAndScansInner writes the seed match result and the left/
+; right span extents here for the C wrapper to read back.
+ .section .bss.gFloodSeedMatch,"aw"
+ .globl gFloodSeedMatch
+gFloodSeedMatch:
+ .zero 2
+
+ .section .bss.gFloodLeftX,"aw"
+ .globl gFloodLeftX
+gFloodLeftX:
+ .zero 2
+
+ .section .bss.gFloodRightX,"aw"
+ .globl gFloodRightX
+gFloodRightX:
+ .zero 2
+
+; ===== ported: tile primitives =====
+; ====================================================================
+; frag_tiles.s - tile inner loops (llvm-mc w65816)
+;
+; Shared ABI fact (clang w65816 cdecl, per PORT_SPEC.md):
+; - arg0 (pointer/long) arrives in A=low16, X=high16 (bank byte =
+; low byte of X). It is NOT on the stack.
+; - arg1, arg2, ... are pushed right-to-left and live on the stack;
+; on JSL entry arg1 byte0 is at 4,s. A word arg is 2 bytes, a
+; pointer/long arg is 4 bytes.
+; - Each prologue push shifts every stack offset up: php=+1, phb=+1,
+; phd=+2, pha/phx=+2.
+; ====================================================================
+
+
+; --------------------------------------------------------------------
+; Bank-0 scratch pointer block used by the two-pointer tile routines
+; (Copy / Paste / Snap / CopyMasked). The clang ABI hands us dst in
+; A:X and src on the stack, but the original copy loops assume a
+; D-frame with dst at [D+0] and src at [D+4]. We rebuild that frame in
+; this bank-0 scratch, point D at it, and run the original [0],y /
+; [4],y indirect-long loads unchanged.
+; tileScratchPtrs+0..2 = dst (low16 + bank) <- [0],y
+; tileScratchPtrs+4..6 = src (low16 + bank) <- [4],y
+; (bytes +3 / +7 are unused padding, mirroring the 4-byte ptr layout)
+; --------------------------------------------------------------------
+.section .bss.tileScratchPtrs,"aw"
+.globl tileScratchPtrs
+tileScratchPtrs:
+.zero 8
+
+
+; ====================================================================
+; iigsTileFillInner(uint8_t *dstRow0, uint16_t fillWord)
+;
+; Original: 16 STA abs,X stores at fixed offsets along a 160-byte
+; stride (DBR set to the dst bank via PHB/PLB so abs,X rode the right
+; bank). ~120 cyc per call vs the C version's ~300.
+;
+; ABI: arg0 dstRow0 in A:X (A = dst low16, X = dst high16 incl. bank).
+; arg1 fillWord (word) on the stack: @4,s on JSL entry; after
+; PHP+PHB+PHD(+4) it sits @8,s. Only fillWord is on the stack and
+; dst comes in A:X.
+;
+; PORTING NOTE (addressing mode): the original used `sta |off,x`
+; (16-bit absolute,X, DBR = dst bank). llvm-mc here unconditionally
+; shrinks `sta off,x` with a small constant `off` to DIRECT-PAGE,X
+; (opcode 0x95), which is bank-0 / D-relative and would target the
+; wrong memory; and this build accepts none of the abs-forcing
+; modifiers (`!`/`|`/`mos16()`). To stay bank-correct AND assemble, we
+; keep the identical 16 store offsets and the identical fill word but
+; reach the destination via DP-indirect-long `[dst],y` with Y = the
+; offset -- the same mechanism the two-pointer tile routines use. We
+; stash dst (A:X) in tileScratchPtrs and point D there ([0] = dst).
+; D-frame: tileScratchPtrs+0..2 = dst (low16 + bank) -> [0],y
+; ====================================================================
+.section .text.iigsTileFillInner,"ax"
+.globl iigsTileFillInner
+iigsTileFillInner:
+ php
+ phb
+ phd ; save caller D
+ rep #0x30
+ .a16
+ .i16
+
+ ; A = dst low16, X = dst high16 (bank in low byte).
+ sta tileScratchPtrs+0 ; dst low16
+ stx tileScratchPtrs+2 ; dst high16 (bank + pad)
+ lda 8,s ; fillWord (post +4 push)
+ pha ; stash fillWord (+2)
+ lda #tileScratchPtrs
+ tcd ; D -> scratch; [0] = dst
+ pla ; A = fillWord again
+ ; (PLA balances the PHA so the stack is clean for PLD.)
+
+ ldy #0
+ sta [0], y
+ ldy #2
+ sta [0], y
+ ldy #160
+ sta [0], y
+ ldy #162
+ sta [0], y
+ ldy #320
+ sta [0], y
+ ldy #322
+ sta [0], y
+ ldy #480
+ sta [0], y
+ ldy #482
+ sta [0], y
+ ldy #640
+ sta [0], y
+ ldy #642
+ sta [0], y
+ ldy #800
+ sta [0], y
+ ldy #802
+ sta [0], y
+ ldy #960
+ sta [0], y
+ ldy #962
+ sta [0], y
+ ldy #1120
+ sta [0], y
+ ldy #1122
+ sta [0], y
+
+ .a8
+ .i8
+ rep #0x30
+ .a16
+ .i16
+ pld ; restore caller D
+ plb
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsTileCopyInner(uint8_t *dst, const uint8_t *src)
+;
+; Opaque copy of an 8x8 tile region (32 bytes) between two 4bpp
+; surfaces with 160-byte row stride. Dst and src may be in any banks.
+; ~340 cyc per tile vs the C version's ~900.
+;
+; ABI: arg0 dst in A:X (A=low16, X=high16). arg1 src (4-byte ptr) on
+; the stack: @4,s on JSL entry; after PHP+PHB+PHD(+4) src low16
+; @8,s, src bank @10,s. We point D at tileScratchPtrs, store dst
+; (A:X) at +0..2 and src (from the stack) at +4..6, then run the
+; original [0],y (dst) / [4],y (src) loads unchanged. PLD restores
+; the caller's D (saved by our PHD).
+; ====================================================================
+.section .text.iigsTileCopyInner,"ax"
+.globl iigsTileCopyInner
+iigsTileCopyInner:
+ php
+ phb
+ phd ; save caller D
+ rep #0x30
+ .a16
+ .i16
+
+ ; Build the dst/src D-frame in bank-0 scratch.
+ sta tileScratchPtrs+0 ; dst low16
+ stx tileScratchPtrs+2 ; dst high16 (bank + pad)
+ lda 8,s ; src low16 (post +4 push)
+ sta tileScratchPtrs+4
+ lda 10,s ; src bank
+ sta tileScratchPtrs+6
+ lda #tileScratchPtrs
+ tcd ; D -> scratch; [0]=dst [4]=src
+
+ ldy #0
+ lda [4], y
+ sta [0], y
+ ldy #2
+ lda [4], y
+ sta [0], y
+ ldy #160
+ lda [4], y
+ sta [0], y
+ ldy #162
+ lda [4], y
+ sta [0], y
+ ldy #320
+ lda [4], y
+ sta [0], y
+ ldy #322
+ lda [4], y
+ sta [0], y
+ ldy #480
+ lda [4], y
+ sta [0], y
+ ldy #482
+ lda [4], y
+ sta [0], y
+ ldy #640
+ lda [4], y
+ sta [0], y
+ ldy #642
+ lda [4], y
+ sta [0], y
+ ldy #800
+ lda [4], y
+ sta [0], y
+ ldy #802
+ lda [4], y
+ sta [0], y
+ ldy #960
+ lda [4], y
+ sta [0], y
+ ldy #962
+ lda [4], y
+ sta [0], y
+ ldy #1120
+ lda [4], y
+ sta [0], y
+ ldy #1122
+ lda [4], y
+ sta [0], y
+
+ .a8
+ .i8
+ rep #0x30
+ .a16
+ .i16
+ pld ; restore caller D
+ plb
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsTilePasteInner(uint8_t *dst, const uint8_t *src)
+;
+; Paste a packed jlTileT buffer (4 bytes/row * 8 rows = 32 bytes tight)
+; onto a surface row at `dst`. Src stride 4, dst stride 160. Both args
+; are 4-byte large-model pointers.
+;
+; ABI: identical to iigsTileCopyInner -- arg0 dst in A:X, arg1 src on
+; the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD). D points
+; at tileScratchPtrs; [0],y = dst, [4],y = src.
+; ====================================================================
+.section .text.iigsTilePasteInner,"ax"
+.globl iigsTilePasteInner
+iigsTilePasteInner:
+ php
+ phb
+ phd ; save caller D
+ rep #0x30
+ .a16
+ .i16
+
+ sta tileScratchPtrs+0 ; dst low16
+ stx tileScratchPtrs+2 ; dst high16 (bank + pad)
+ lda 8,s ; src low16
+ sta tileScratchPtrs+4
+ lda 10,s ; src bank
+ sta tileScratchPtrs+6
+ lda #tileScratchPtrs
+ tcd ; D -> scratch; [0]=dst [4]=src
+
+ ; Row 0: src word 0 = 0, dst word 0 = 0
+ ldy #0
+ lda [4], y
+ sta [0], y
+ ldy #2
+ lda [4], y
+ sta [0], y
+ ; Row 1: src 4/6, dst 160/162
+ ldy #4
+ lda [4], y
+ ldy #160
+ sta [0], y
+ ldy #6
+ lda [4], y
+ ldy #162
+ sta [0], y
+ ; Row 2: src 8/10, dst 320/322
+ ldy #8
+ lda [4], y
+ ldy #320
+ sta [0], y
+ ldy #10
+ lda [4], y
+ ldy #322
+ sta [0], y
+ ; Row 3: src 12/14, dst 480/482
+ ldy #12
+ lda [4], y
+ ldy #480
+ sta [0], y
+ ldy #14
+ lda [4], y
+ ldy #482
+ sta [0], y
+ ; Row 4: src 16/18, dst 640/642
+ ldy #16
+ lda [4], y
+ ldy #640
+ sta [0], y
+ ldy #18
+ lda [4], y
+ ldy #642
+ sta [0], y
+ ; Row 5: src 20/22, dst 800/802
+ ldy #20
+ lda [4], y
+ ldy #800
+ sta [0], y
+ ldy #22
+ lda [4], y
+ ldy #802
+ sta [0], y
+ ; Row 6: src 24/26, dst 960/962
+ ldy #24
+ lda [4], y
+ ldy #960
+ sta [0], y
+ ldy #26
+ lda [4], y
+ ldy #962
+ sta [0], y
+ ; Row 7: src 28/30, dst 1120/1122
+ ldy #28
+ lda [4], y
+ ldy #1120
+ sta [0], y
+ ldy #30
+ lda [4], y
+ ldy #1122
+ sta [0], y
+
+ .a8
+ .i8
+ rep #0x30
+ .a16
+ .i16
+ pld ; restore caller D
+ plb
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0)
+;
+; Snapshot an 8x8 region of a surface into a packed jlTileT buffer.
+; Src stride 160, dst stride 4. Mirrors jlTilePaste with stride values
+; swapped.
+;
+; ABI: arg0 dst (the packed tile buffer) in A:X, arg1 src (the surface
+; row) on the stack (low16 @8,s / bank @10,s after PHP+PHB+PHD).
+; D points at tileScratchPtrs; [0],y = dst, [4],y = src.
+; ====================================================================
+.section .text.iigsTileSnapInner,"ax"
+.globl iigsTileSnapInner
+iigsTileSnapInner:
+ php
+ phb
+ phd ; save caller D
+ rep #0x30
+ .a16
+ .i16
+
+ sta tileScratchPtrs+0 ; dst low16
+ stx tileScratchPtrs+2 ; dst high16 (bank + pad)
+ lda 8,s ; src low16
+ sta tileScratchPtrs+4
+ lda 10,s ; src bank
+ sta tileScratchPtrs+6
+ lda #tileScratchPtrs
+ tcd ; D -> scratch; [0]=dst [4]=src
+
+ ; Row 0: src 0/2, dst 0/2
+ ldy #0
+ lda [4], y
+ sta [0], y
+ ldy #2
+ lda [4], y
+ sta [0], y
+ ; Row 1: src 160/162, dst 4/6
+ ldy #160
+ lda [4], y
+ ldy #4
+ sta [0], y
+ ldy #162
+ lda [4], y
+ ldy #6
+ sta [0], y
+ ; Row 2: src 320/322, dst 8/10
+ ldy #320
+ lda [4], y
+ ldy #8
+ sta [0], y
+ ldy #322
+ lda [4], y
+ ldy #10
+ sta [0], y
+ ; Row 3: src 480/482, dst 12/14
+ ldy #480
+ lda [4], y
+ ldy #12
+ sta [0], y
+ ldy #482
+ lda [4], y
+ ldy #14
+ sta [0], y
+ ; Row 4: src 640/642, dst 16/18
+ ldy #640
+ lda [4], y
+ ldy #16
+ sta [0], y
+ ldy #642
+ lda [4], y
+ ldy #18
+ sta [0], y
+ ; Row 5: src 800/802, dst 20/22
+ ldy #800
+ lda [4], y
+ ldy #20
+ sta [0], y
+ ldy #802
+ lda [4], y
+ ldy #22
+ sta [0], y
+ ; Row 6: src 960/962, dst 24/26
+ ldy #960
+ lda [4], y
+ ldy #24
+ sta [0], y
+ ldy #962
+ lda [4], y
+ ldy #26
+ sta [0], y
+ ; Row 7: src 1120/1122, dst 28/30
+ ldy #1120
+ lda [4], y
+ ldy #28
+ sta [0], y
+ ldy #1122
+ lda [4], y
+ ldy #30
+ sta [0], y
+
+ .a8
+ .i8
+ rep #0x30
+ .a16
+ .i16
+ pld ; restore caller D
+ plb
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsTileCopyMaskedInner(uint8_t *dst, const uint8_t *src,
+; uint16_t transparent)
+;
+; Same shape as jlTileCopy (160/160 strides, 32 bytes), but source
+; nibbles equal to `transparent` are skipped. Per-byte fast path: if
+; both src nibbles == transparent the whole byte is skipped; otherwise
+; build the output by mixing src/dst nibbles.
+;
+; ABI: arg0 dst in A:X. arg1 src (4-byte ptr) and arg2 transparent
+; (word) on the stack. JSL entry: src @4,s, transparent @8,s.
+; After PHP+PHB+PHD(+4): src low16 @8,s, src bank @10,s,
+; transparent @12,s. The original read `trans` via the D-frame
+; ([D+8]); here D points at tileScratchPtrs instead, so we read
+; transparent's low byte from the stack (@12,s in M=8) during
+; setup. [0],y = dst, [4],y = src in the per-byte subroutine.
+; ====================================================================
+.section .text.iigsTileCopyMaskedInner,"ax"
+.globl iigsTileCopyMaskedInner
+iigsTileCopyMaskedInner:
+ php
+ phb
+ phd ; save caller D
+ rep #0x30
+ .a16
+ .i16
+
+ ; Build the dst/src D-frame in bank-0 scratch.
+ sta tileScratchPtrs+0 ; dst low16
+ stx tileScratchPtrs+2 ; dst high16 (bank + pad)
+ lda 8,s ; src low16
+ sta tileScratchPtrs+4
+ lda 10,s ; src bank
+ sta tileScratchPtrs+6
+ lda #tileScratchPtrs
+ tcd ; D -> scratch; [0]=dst [4]=src
+
+ ; Pre-compute scratch values from `transparent`.
+ ; Original read D+8 (the stack arg); here transparent's
+ ; low byte is at 12,s (see ABI note).
+ sep #0x20 ; M=8
+ .a8
+
+ lda 12,s ; transparent (low byte)
+ and #0x0F
+ sta tmaskTLo ; tLo = T
+
+ asl a
+ asl a
+ asl a
+ asl a
+ sta tmaskTHi ; tHi = T<<4
+
+ ora tmaskTLo
+ sta tmaskDoubled ; doubled = (T<<4)|T
+
+ ; Walk 8 rows of 4 bytes. Y holds the byte offset; after
+ ; the 3 in-row INYs Y is at row*160+3, so +157 lands at
+ ; next row's start (row*160+160).
+ ldx #8 ; row counter
+ ldy #0 ; byte offset
+
+tmaskRowLoop:
+ jsr tmaskByte ; byte 0
+ iny
+ jsr tmaskByte ; byte 1
+ iny
+ jsr tmaskByte ; byte 2
+ iny
+ jsr tmaskByte ; byte 3
+
+ rep #0x20 ; M=16 for arithmetic on Y
+ .a16
+ tya
+ clc
+ adc #157 ; +156 stride, +1 (we're at row+3)
+ tay
+ sep #0x20 ; back to M=8
+ .a8
+
+ dex
+ bne tmaskRowLoop
+
+ rep #0x20 ; M=16 before epilogue
+ .a16
+ .i16
+ pld ; restore caller D
+ plb
+ plp
+ rtl
+
+; tmaskByte: in M=8 X=16, Y holds byte offset.
+; - load src byte
+; - if both nibbles transparent: skip
+; - else: assemble output from src/dst nibbles per nibble's
+; non-transparency, store
+tmaskByte:
+ ; Entered (via JSR from the M=8 row loop) with M=8, X=16.
+ ; Tell the assembler so the immediates below size as 1
+ ; byte. RTS returns to the still-M=8 row loop, so no
+ ; width restore is needed here.
+ .a8
+ .i16
+ lda [4], y ; A = src byte
+ cmp tmaskDoubled
+ beq tmaskSkip ; both nibbles == transparent
+
+ sta tmaskSrc
+
+ ; Decide hi nibble.
+ and #0xF0
+ cmp tmaskTHi
+ beq tmaskUseDstHi
+ sta tmaskOutHi ; src hi (kept in $F0 form)
+ bra tmaskHiDone
+tmaskUseDstHi:
+ lda [0], y
+ and #0xF0
+ sta tmaskOutHi
+tmaskHiDone:
+
+ ; Decide lo nibble.
+ lda tmaskSrc
+ and #0x0F
+ cmp tmaskTLo
+ beq tmaskUseDstLo
+ sta tmaskOutLo ; src lo
+ bra tmaskLoDone
+tmaskUseDstLo:
+ lda [0], y
+ and #0x0F
+ sta tmaskOutLo
+tmaskLoDone:
+
+ lda tmaskOutHi
+ ora tmaskOutLo
+ sta [0], y
+
+tmaskSkip:
+ rts
+
+
+; --------------------------------------------------------------------
+; Scratch for the masked-tile inner loop. Lives outside the
+; primitive's stack frame because the per-byte subroutine call would
+; otherwise need to thread state through registers. (bank-0 globals)
+; --------------------------------------------------------------------
+.section .bss.tmaskTLo,"aw"
+.globl tmaskTLo
+tmaskTLo:
+.zero 1
+
+.section .bss.tmaskTHi,"aw"
+.globl tmaskTHi
+tmaskTHi:
+.zero 1
+
+.section .bss.tmaskDoubled,"aw"
+.globl tmaskDoubled
+tmaskDoubled:
+.zero 1
+
+.section .bss.tmaskSrc,"aw"
+.globl tmaskSrc
+tmaskSrc:
+.zero 1
+
+.section .bss.tmaskOutHi,"aw"
+.globl tmaskOutHi
+tmaskOutHi:
+.zero 1
+
+.section .bss.tmaskOutLo,"aw"
+.globl tmaskOutLo
+tmaskOutLo:
+.zero 1
+
+; ===== ported: fillrect / pixel / line =====
+; ====================================================================
+; JoeyLib IIgs draw primitives (llvm-mc GAS, w65816)
+; Functions: iigsFillRectInner, iigsDrawPixelInner, iigsDrawLineInner
+; ====================================================================
+
+
+; ====================================================================
+; iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y,
+; uint16_t w, uint16_t h, uint16_t nibble)
+;
+; Fill an axis-aligned rectangle of 4bpp pixels into the IIgs SHR
+; stage at bank $01. FillRect hard-codes bank 1 and derives the row
+; address from y via gRowOffsetLut, so the `pixels` arg (A:X) is unused.
+;
+; ABI: clang cdecl. pixels arrives in A:X (UNUSED). x,y,w,h,nibble are
+; on the stack (right-to-left). On JSL entry: x@4,s y@6,s w@8,s h@10,s
+; nib@12,s. After php+phb+phd (+4) and tsc+adc#8+tcd (D = SP+8), the
+; equates are:
+; fri_x=0 fri_y=2 fri_w=4 fri_h=6 fri_nib=8
+; Prologue (php/phb/phd, tsc+adc#8+tcd) otherwise unchanged.
+; ====================================================================
+
+.section .text.iigsFillRectInner,"ax"
+.globl iigsFillRectInner
+iigsFillRectInner:
+fri_x = 0
+fri_y = 2
+fri_w = 4
+fri_h = 6
+fri_nib = 8
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+ tsc
+ clc
+ adc #8
+ tcd
+
+; pxStart = x; pxEnd = x + w.
+ lda fri_x
+ sta friPxStart
+ clc
+ adc fri_w
+ sta friPxEnd
+
+; leadingByte = x >> 1; hasLeading = x & 1.
+ lda fri_x
+ lsr a
+ sta friLeadingByte
+ lda fri_x
+ and #1
+ sta friHasLeading
+ beq friStartReady
+ lda friPxStart
+ inc a
+ sta friPxStart
+friStartReady:
+
+; midStart = pxStart >> 1.
+ lda friPxStart
+ lsr a
+ sta friMidStart
+
+; spanLen = pxEnd - pxStart; hasTrailing = spanLen & 1; midBytes = spanLen >> 1.
+ lda friPxEnd
+ sec
+ sbc friPxStart
+ sta friMidBytes ; temporary hold spanLen
+ and #1
+ sta friHasTrailing
+ lda friMidBytes
+ lsr a
+ sta friMidBytes
+
+; trailingByte = midStart + midBytes.
+ lda friMidStart
+ clc
+ adc friMidBytes
+ sta friTrailingByte
+
+; nibble decoding: friNibLo = nib (low half), friNibHi = nib<<4
+; (high half), friDoubled = (nib<<4)|nib (full byte fill).
+ sep #0x20
+ .a8
+ lda fri_nib
+ and #0x0F
+ sta friNibLo
+ asl a
+ asl a
+ asl a
+ asl a
+ sta friNibHi
+ ora friNibLo
+ sta friDoubled
+ rep #0x20
+ .a16
+
+; curRow = $2000 + y*160 (LUT-driven).
+ lda fri_y
+ asl a
+ tax
+ lda gRowOffsetLut,x
+ clc
+ adc #0x2000
+ sta friCurRow
+
+ lda fri_h
+ sta friRowsLeft
+
+; friFillWord = friDoubled replicated into both bytes (16-bit pattern).
+ lda friDoubled
+ and #0x00FF
+ sta friFillWord
+ xba ; A = byte in high half
+ ora friFillWord
+ sta friFillWord
+
+friRowLoop:
+ lda friRowsLeft
+ bne friDoRow
+ brl friExit
+friDoRow:
+
+; Leading partial-byte RMW.
+ lda friHasLeading
+ beq friNoLead
+ lda friCurRow
+ clc
+ adc friLeadingByte
+ tax
+ sep #0x20
+ .a8
+ lda 0x010000,x
+ and #0xF0
+ ora friNibLo
+ sta 0x010000,x
+ rep #0x20
+ .a16
+friNoLead:
+
+; Middle bytes: if midBytes == 0, skip; if midBytes == 1, just seed
+; one byte; if midBytes >= 2, seed first byte then MVN propagates the
+; seed across the rest of the row.
+ lda friMidBytes
+ bne friMidNonZero
+ brl friNoMid
+friMidNonZero:
+; Computed-jump slam for ANY middle width. nWords = midBytes>>1 words are
+; slammed through the unrolled STA long,X table by entering it at store
+; #(80-nWords), with X biased so those stores cover
+; [curRow+midStart .. +2*nWords-1] at 3 cyc/byte. Replaces the old
+; friFullWidth-only slam plus seed+MVN-for-partials (7 cyc/byte). The entry
+; is reached with an RTS computed-goto: it stays in the program bank and
+; uses the normal stack, so there is no bank-0 indirect-vector hazard.
+; nWords==0 (a 1-byte middle) lands just past the table on the odd-byte
+; writer. An odd trailing middle byte is written after the table.
+ lda friMidBytes
+ lsr a
+ sta friSlamWords ; nWords = midBytes >> 1
+; Push (entry - 1); entry = friMidSlamTbl + (80 - nWords) * 4 (each store is 4 bytes).
+ lda #80
+ sec
+ sbc friSlamWords
+ asl a
+ asl a
+ clc
+ adc #friMidSlamTbl-1
+ pha
+; X = curRow + midStart + 2*nWords - 160, so store #(80-nWords) hits curRow+midStart.
+ lda friSlamWords
+ asl a
+ clc
+ adc friCurRow
+ clc
+ adc friMidStart
+ sec
+ sbc #160
+ tax
+ lda friFillWord
+ rts ; computed-goto into friMidSlamTbl
+friMidSlamTbl:
+ sta 0x010000,x
+ sta 0x010002,x
+ sta 0x010004,x
+ sta 0x010006,x
+ sta 0x010008,x
+ sta 0x01000A,x
+ sta 0x01000C,x
+ sta 0x01000E,x
+ sta 0x010010,x
+ sta 0x010012,x
+ sta 0x010014,x
+ sta 0x010016,x
+ sta 0x010018,x
+ sta 0x01001A,x
+ sta 0x01001C,x
+ sta 0x01001E,x
+ sta 0x010020,x
+ sta 0x010022,x
+ sta 0x010024,x
+ sta 0x010026,x
+ sta 0x010028,x
+ sta 0x01002A,x
+ sta 0x01002C,x
+ sta 0x01002E,x
+ sta 0x010030,x
+ sta 0x010032,x
+ sta 0x010034,x
+ sta 0x010036,x
+ sta 0x010038,x
+ sta 0x01003A,x
+ sta 0x01003C,x
+ sta 0x01003E,x
+ sta 0x010040,x
+ sta 0x010042,x
+ sta 0x010044,x
+ sta 0x010046,x
+ sta 0x010048,x
+ sta 0x01004A,x
+ sta 0x01004C,x
+ sta 0x01004E,x
+ sta 0x010050,x
+ sta 0x010052,x
+ sta 0x010054,x
+ sta 0x010056,x
+ sta 0x010058,x
+ sta 0x01005A,x
+ sta 0x01005C,x
+ sta 0x01005E,x
+ sta 0x010060,x
+ sta 0x010062,x
+ sta 0x010064,x
+ sta 0x010066,x
+ sta 0x010068,x
+ sta 0x01006A,x
+ sta 0x01006C,x
+ sta 0x01006E,x
+ sta 0x010070,x
+ sta 0x010072,x
+ sta 0x010074,x
+ sta 0x010076,x
+ sta 0x010078,x
+ sta 0x01007A,x
+ sta 0x01007C,x
+ sta 0x01007E,x
+ sta 0x010080,x
+ sta 0x010082,x
+ sta 0x010084,x
+ sta 0x010086,x
+ sta 0x010088,x
+ sta 0x01008A,x
+ sta 0x01008C,x
+ sta 0x01008E,x
+ sta 0x010090,x
+ sta 0x010092,x
+ sta 0x010094,x
+ sta 0x010096,x
+ sta 0x010098,x
+ sta 0x01009A,x
+ sta 0x01009C,x
+ sta 0x01009E,x
+; Odd trailing middle byte: the slam wrote 2*nWords bytes; if midBytes is
+; odd, write the last middle byte at curRow + midStart + 2*nWords.
+ lda friMidBytes
+ and #1
+ beq friMidNoOdd
+ lda friSlamWords
+ asl a
+ clc
+ adc friCurRow
+ clc
+ adc friMidStart
+ tax
+ sep #0x20
+ .a8
+ lda friDoubled
+ sta 0x010000,x
+ rep #0x20
+ .a16
+friMidNoOdd:
+friNoMid:
+
+; Trailing partial-byte RMW.
+ lda friHasTrailing
+ beq friNoTrail
+ lda friCurRow
+ clc
+ adc friTrailingByte
+ tax
+ sep #0x20
+ .a8
+ lda 0x010000,x
+ and #0x0F
+ ora friNibHi
+ sta 0x010000,x
+ rep #0x20
+ .a16
+friNoTrail:
+
+; Advance to next row.
+ lda friCurRow
+ clc
+ adc #160
+ sta friCurRow
+ lda friRowsLeft
+ dec a
+ sta friRowsLeft
+
+ brl friRowLoop
+
+friExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+.section .bss.friPxStart,"aw"
+.globl friPxStart
+friPxStart:
+ .zero 2
+.section .bss.friPxEnd,"aw"
+.globl friPxEnd
+friPxEnd:
+ .zero 2
+.section .bss.friLeadingByte,"aw"
+.globl friLeadingByte
+friLeadingByte:
+ .zero 2
+.section .bss.friHasLeading,"aw"
+.globl friHasLeading
+friHasLeading:
+ .zero 2
+.section .bss.friMidStart,"aw"
+.globl friMidStart
+friMidStart:
+ .zero 2
+.section .bss.friMidBytes,"aw"
+.globl friMidBytes
+friMidBytes:
+ .zero 2
+.section .bss.friHasTrailing,"aw"
+.globl friHasTrailing
+friHasTrailing:
+ .zero 2
+.section .bss.friTrailingByte,"aw"
+.globl friTrailingByte
+friTrailingByte:
+ .zero 2
+.section .bss.friNibLo,"aw"
+.globl friNibLo
+friNibLo:
+ .zero 2
+.section .bss.friNibHi,"aw"
+.globl friNibHi
+friNibHi:
+ .zero 2
+.section .bss.friDoubled,"aw"
+.globl friDoubled
+friDoubled:
+ .zero 2
+.section .bss.friCurRow,"aw"
+.globl friCurRow
+friCurRow:
+ .zero 2
+.section .bss.friRowsLeft,"aw"
+.globl friRowsLeft
+friRowsLeft:
+ .zero 2
+.section .bss.friFillWord,"aw"
+.globl friFillWord
+friFillWord:
+ .zero 2
+.section .bss.friSlamWords,"aw"
+.globl friSlamWords
+friSlamWords:
+ .zero 2
+
+
+; ====================================================================
+; iigsDrawPixelInner(uint8_t *pixels, uint16_t x, uint16_t y,
+; uint16_t nibble)
+;
+; Plot one 4bpp pixel at (x,y) into a 320x200 surface. Caller has
+; already validated bounds and stripped the high nibbles of `nibble`
+; (only low 4 bits used). pixels is a 4-byte large-model pointer that
+; IS dereferenced via DP-indirect-long.
+;
+; Offset math: byte = y*160 + (x>>1). Nibble half: x&1 == 0 picks high
+; nibble (left pixel), else low.
+;
+; ABI: clang cdecl. pixels arrives in A:X; x,y,nibble are on the stack.
+; llvm-mc only encodes DP-indirect-long
+; ([dp],y) for an operand that fits the direct page, so we cannot use a
+; 16-bit bss symbol as the pointer slot. Instead, faithful to the spec's
+; "pointer arg0 that IS used" recipe, we repoint D at a bank-0 scratch
+; struct (dpxlScratch): store pixels (A:X) at dpxlScratch+0..2 and use
+; [pix],y with pix=0. Because D no longer overlays the stack frame, the
+; x,y,nibble args are read stack-relative.
+; Prologue pushes: php(+1)+phb(+1)+phd(+2)=+4. On JSL entry x@4,s y@6,s
+; nib@8,s; after the 4 pushes: x@8,s y@10,s nib@12,s.
+; pix=0 (D-relative, = dpxlScratch+0)
+; xx=8,s yy=10,s nib=12,s
+; pix is a 3-byte far pointer (A low16 + X bank byte); we store A then X
+; as words -> a harmless 4th filler byte in the 4-byte scratch.
+; ====================================================================
+
+.section .text.iigsDrawPixelInner,"ax"
+.globl iigsDrawPixelInner
+iigsDrawPixelInner:
+pix = 0 ; pixels far ptr (D+0..2 = dpxlScratch)
+
+; Stash pixels far pointer (A:X) into bank-0 scratch before any push.
+ rep #0x30
+ .a16
+ .i16
+ sta dpxlScratch ; low 16 bits of pixels
+ stx dpxlScratch+2 ; bank byte (+ filler)
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+; Repoint D at the scratch struct so [pix],y (pix=0) is DP-indirect-long.
+ lda #dpxlScratch
+ tcd
+
+; Compute byte offset = y*160 + (x>>1) into A. Use the LUT.
+ lda 10,s ; y
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut,x ; A = y*160
+ sta dpxlTmp
+ lda 8,s ; x
+ lsr a ; A = x >> 1
+ clc
+ adc dpxlTmp ; A = byte offset within surface
+ tay
+
+ sep #0x20 ; M=8
+ .a8
+
+ lda 8,s ; A = x low byte (parity in LSB)
+ and #1
+ bne dpxlOdd
+
+; Even x -> high nibble.
+ lda 12,s ; nibble
+ asl a
+ asl a
+ asl a
+ asl a
+ sta dpxlNibPart
+ lda [pix],y
+ and #0x0F
+ ora dpxlNibPart
+ sta [pix],y
+ bra dpxlDone
+
+dpxlOdd:
+; Odd x -> low nibble.
+ lda 12,s ; nibble
+ and #0x0F
+ sta dpxlNibPart
+ lda [pix],y
+ and #0xF0
+ ora dpxlNibPart
+ sta [pix],y
+
+dpxlDone:
+ rep #0x20 ; back to M=16 for epilogue
+ .a16
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+.section .bss.dpxlTmp,"aw"
+.globl dpxlTmp
+dpxlTmp:
+ .zero 2
+.section .bss.dpxlNibPart,"aw"
+.globl dpxlNibPart
+dpxlNibPart:
+ .zero 1
+.section .bss.dpxlScratch,"aw"
+.globl dpxlScratch
+dpxlScratch:
+ .zero 4
+
+
+; ====================================================================
+; iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0,
+; uint16_t x1, uint16_t y1, uint16_t nibble)
+;
+; Bresenham line plot. Caller has clipped both endpoints into
+; [0..319] x [0..199], so no per-pixel bounds check.
+;
+; Walks (dx, dy, sx, sy, err) and plots inline (no per-pixel
+; function call). Each plot does the same parity-aware nibble RMW as
+; iigsDrawPixel. State stored in DRAWPRIMS scratch because registers
+; are too few for the full Bresenham state plus current pixel address.
+;
+; ABI: clang cdecl. pixels arrives in A:X (dereferenced via [pix],y);
+; x0,y0,x1,y1,nibble on the stack. Same hybrid as DrawPixel: llvm-mc
+; cannot encode [dp],y from a 16-bit bss symbol, so we repoint D at a
+; bank-0 scratch struct (dlnScratch), stash pixels (A:X) at
+; dlnScratch+0..2, and use [pix],y with pix=0 in the loop. The five
+; word args are copied into DRAWPRIMS scratch up front (stack-relative),
+; so the loop never touches the stack frame again.
+; Prologue pushes: php(+1)+phb(+1)+phd(+2)=+4. On JSL entry x0@4,s
+; y0@6,s x1@8,s y1@10,s nib@12,s; after the 4 pushes:
+; x0@8,s y0@10,s x1@12,s y1@14,s nib@16,s
+; pix=0 (D-relative, = dlnScratch+0)
+; ====================================================================
+
+.section .text.iigsDrawLineInner,"ax"
+.globl iigsDrawLineInner
+iigsDrawLineInner:
+pix = 0 ; pixels far ptr (D+0..2 = dlnScratch)
+
+; Stash pixels far pointer (A:X) into bank-0 scratch before any push.
+ rep #0x30
+ .a16
+ .i16
+ sta dlnScratch ; low 16 bits of pixels
+ stx dlnScratch+2 ; bank byte (+ filler)
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+; Repoint D at the scratch struct so [pix],y (pix=0) is DP-indirect-long.
+ lda #dlnScratch
+ tcd
+
+; dx = |x1 - x0|, sx = sign(x1 - x0)
+ lda 12,s ; x1
+ sec
+ sbc 8,s ; x0
+ bpl dlnDxPos
+ eor #0xFFFF
+ clc
+ adc #1 ; A = x0 - x1 (positive |dx|)
+ sta dlnDx
+ lda #0xFFFF
+ sta dlnSx ; sx = -1
+ bra dlnDxDone
+dlnDxPos:
+ sta dlnDx
+ lda #1
+ sta dlnSx
+dlnDxDone:
+
+; dy = -|y1 - y0|, sy = sign(y1 - y0). Bresenham uses negative dy.
+ lda 14,s ; y1
+ sec
+ sbc 10,s ; y0
+ bpl dlnDyPos
+ sta dlnDy ; A is negative; that's dy with the sign already
+ lda #0xFFFF
+ sta dlnSy ; sy = -1
+ bra dlnDyDone
+dlnDyPos:
+; y1 >= y0 -> raw dy positive; negate for Bresenham (-|y1-y0|).
+ eor #0xFFFF
+ clc
+ adc #1
+ sta dlnDy
+ lda #1
+ sta dlnSy
+dlnDyDone:
+
+; err = dx + dy
+ lda dlnDx
+ clc
+ adc dlnDy
+ sta dlnErr
+
+; Copy x0, y0 to running state slots.
+ lda 8,s ; x0
+ sta dlnX
+ lda 10,s ; y0
+ sta dlnY
+
+; Cache x1, y1 so the loop's compare is fast.
+ lda 12,s ; x1
+ sta dlnX1
+ lda 14,s ; y1
+ sta dlnY1
+
+; Precompute both 16-bit nibble OR-values ONCE (stays in M=16 the
+; whole loop -- no per-pixel SEP/REP flip). The nibble RMW reads the
+; target byte plus its adjacent high byte (16-bit), masks so only the
+; target nibble changes, and writes both bytes back; the high byte is
+; preserved by the mask + zero OR-high-byte, so the result is identical
+; to the old 8-bit single-byte RMW. dlnNibHi/dlnNibLo have low byte =
+; (nib<<4) / nib and high byte = 0.
+ lda 16,s ; nibble
+ and #0x000F
+ sta dlnNibLo ; low byte = nib, high byte = 0
+ asl a
+ asl a
+ asl a
+ asl a
+ sta dlnNibHi ; low byte = nib<<4, high byte = 0
+
+dlnLoop:
+; --- Plot pixel at (dlnX, dlnY) --- LUT path. Stays in M=16.
+ lda dlnY
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut,x ; A = y*160
+ sta dlnTmp
+ lda dlnX
+ lsr a ; x >> 1
+ clc
+ adc dlnTmp ; byte offset
+ tay
+
+ lda dlnX ; x parity
+ and #1
+ bne dlnPlotOdd
+
+; Even x -> high nibble. Mask keeps low nibble of target byte and the
+; whole adjacent high byte; OR sets nib<<4 into the high nibble.
+ lda [pix],y
+ and #0xFF0F
+ ora dlnNibHi
+ sta [pix],y
+ bra dlnPlotDone
+dlnPlotOdd:
+; Odd x -> low nibble. Mask keeps high nibble of target byte and the
+; whole adjacent high byte; OR sets nib into the low nibble.
+ lda [pix],y
+ and #0xF0FF
+ ora dlnNibLo
+ sta [pix],y
+dlnPlotDone:
+
+; --- Loop test: if X==X1 and Y==Y1, done ---
+ lda dlnX
+ cmp dlnX1
+ bne dlnStep
+ lda dlnY
+ cmp dlnY1
+ bne dlnStep
+ brl dlnExit
+
+dlnStep:
+; e2 = err << 1 (signed). Compare e2 against dy and dx.
+ lda dlnErr
+ asl a ; e2 = err * 2
+ sta dlnE2
+
+; if (e2 >= dy) { err += dy; X += sx; }
+; dy is negative, so signed-compare with BMI/BPL needed.
+; Test e2 - dy >= 0 via SEC; SBC dy; BPL.
+ sec
+ sbc dlnDy
+ bmi dlnSkipX ; e2 < dy
+
+ lda dlnErr
+ clc
+ adc dlnDy
+ sta dlnErr
+
+ lda dlnX
+ clc
+ adc dlnSx
+ sta dlnX
+
+dlnSkipX:
+; if (e2 <= dx) { err += dx; Y += sy; }
+; Test dx - e2 >= 0 via dx - e2 (BPL means e2 <= dx).
+ lda dlnDx
+ sec
+ sbc dlnE2
+ bmi dlnSkipY ; dx < e2
+
+ lda dlnErr
+ clc
+ adc dlnDx
+ sta dlnErr
+
+ lda dlnY
+ clc
+ adc dlnSy
+ sta dlnY
+
+dlnSkipY:
+ brl dlnLoop
+
+dlnExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+.section .bss.dlnDx,"aw"
+.globl dlnDx
+dlnDx:
+ .zero 2
+.section .bss.dlnDy,"aw"
+.globl dlnDy
+dlnDy:
+ .zero 2
+.section .bss.dlnSx,"aw"
+.globl dlnSx
+dlnSx:
+ .zero 2
+.section .bss.dlnSy,"aw"
+.globl dlnSy
+dlnSy:
+ .zero 2
+.section .bss.dlnErr,"aw"
+.globl dlnErr
+dlnErr:
+ .zero 2
+.section .bss.dlnE2,"aw"
+.globl dlnE2
+dlnE2:
+ .zero 2
+.section .bss.dlnX,"aw"
+.globl dlnX
+dlnX:
+ .zero 2
+.section .bss.dlnY,"aw"
+.globl dlnY
+dlnY:
+ .zero 2
+.section .bss.dlnX1,"aw"
+.globl dlnX1
+dlnX1:
+ .zero 2
+.section .bss.dlnY1,"aw"
+.globl dlnY1
+dlnY1:
+ .zero 2
+.section .bss.dlnNibLo,"aw"
+.globl dlnNibLo
+dlnNibLo:
+ .zero 2
+.section .bss.dlnNibHi,"aw"
+.globl dlnNibHi
+dlnNibHi:
+ .zero 2
+.section .bss.dlnTmp,"aw"
+.globl dlnTmp
+dlnTmp:
+ .zero 2
+.section .bss.dlnScratch,"aw"
+.globl dlnScratch
+dlnScratch:
+ .zero 4
+
+; ===== ported: circles =====
+; ================================================================
+; iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy,
+; uint16_t r, uint16_t nibble)
+;
+; Bresenham midpoint circle outline. Caller has verified that the
+; whole bounding box (cx-r..cx+r, cy-r..cy+r) fits inside the
+; surface, so the inner loop plots all 8 octants without per-pixel
+; clip checks. Each iteration computes 4 row-base byte offsets
+; (cy +/- y)*160, (cy +/- x)*160 then plots 8 pixels inline.
+;
+; ABI (llvm-mos w65816 cdecl):
+; pix -> arg0 in A:X (A=offset low16, X low byte=bank). USED via
+; [pix],y, so it must live in the direct page. We stash it
+; into bank-0 scratch dcPixPtr (offset@+0, bank@+2) BEFORE
+; setting D, then point D at dcPixPtr so the original
+; `[pix],y` (pix equ 0 -> [D+0],y) loads are byte-identical.
+; php/phb/phd do not touch A/X, so A:X still hold pix when
+; we capture them after the pushes.
+; cx,cy,r,nibble -> on the stack. On JSL entry cx@4,s cy@6,s r@8,s
+; nib@10,s. After php(+1)+phb(+1)+phd(+2)=+4: cx@8,s
+; cy@10,s r@12,s nib@14,s. S never moves in the body
+; (no pushes in the loop), so these N,s reads stay valid.
+; They are stack reads because D hosts the pix pointer.
+; ================================================================
+ .section .text.iigsDrawCircleInner,"ax"
+ .globl iigsDrawCircleInner
+pix = 0 ; D-relative: D points at dcPixPtr
+dcArgCx = 8 ; cx @ 8,s after +4 prologue
+dcArgCy = 10 ; cy @ 10,s
+dcArgR = 12 ; r @ 12,s
+dcArgNib = 14 ; nibble @ 14,s
+iigsDrawCircleInner:
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+; Stash the pix far pointer (still in A:X) into bank-0 scratch and
+; point D at it so [pix],y works unchanged.
+ sta dcPixPtr ; offset low 16 bits
+ txa
+ sta dcPixPtr+2 ; low byte = bank (high byte harmless)
+ lda #dcPixPtr
+ tcd ; D = &dcPixPtr ; [pix],y == [0],y
+
+; Cache the nibble in 8-bit form: dcNibLo = nib, dcNibHi = nib<<4.
+ lda dcArgNib, s
+ and #0x000F
+ sta dcNibLo ; low byte = nib, high byte = 0
+ asl a
+ asl a
+ asl a
+ asl a
+ sta dcNibHi ; low byte = nib<<4
+
+; x = r, y = 0, err = 1 - x.
+ lda dcArgR, s
+ sta dcX
+ lda #0
+ sta dcY
+ lda #1
+ sec
+ sbc dcArgR, s
+ sta dcErr
+
+; Initialize the 4 octant row bases once, then maintain them
+; incrementally in the Bresenham update (instead of 4 LUT lookups per
+; iteration). At entry x=r, y=0: RowYP=RowYN=acy*160,
+; RowXP=(acy+r)*160, RowXN=(acy-r)*160.
+ lda dcArgCy, s
+ asl a
+ tax
+ lda gRowOffsetLut, x
+ sta dcRowYP
+ sta dcRowYN
+ lda dcArgCy, s
+ clc
+ adc dcArgR, s
+ asl a
+ tax
+ lda gRowOffsetLut, x
+ sta dcRowXP
+ lda dcArgCy, s
+ sec
+ sbc dcArgR, s
+ asl a
+ tax
+ lda gRowOffsetLut, x
+ sta dcRowXN
+
+dcLoop:
+; Loop guard: if x < y we are done.
+ lda dcX
+ cmp dcY
+ bcs dcLoopBody ; x >= y -> continue
+ brl dcExit
+
+dcLoopBody:
+; Row bases (dcRowYP/YN/XP/XN) are maintained incrementally in the
+; Bresenham update -- no per-iteration LUT lookups here.
+;
+; 8 octant plots, fully inlined. Each plot:
+; 1. col = (acx +/- dcX|dcY) -> A
+; 2. stash col in X (free across the plots); the parity test reads it
+; back with txa instead of a memory round-trip.
+; 3. byteIdx = col >> 1; byte addr = byteIdx + rowBase -> Y
+; 4. test col & 1 (via txa); do high or low nibble RMW
+;
+; All 8 octant plots stay in M=16 -- no per-plot SEP/REP flip. The
+; nibble RMW reads the target byte plus its adjacent high byte (16-bit
+; load), masks so only the target nibble changes ($FF0F keeps low
+; nibble + whole high byte for even/high-nibble x, $F0FF keeps high
+; nibble + whole high byte for odd/low-nibble x), then writes both
+; bytes back.
+
+; Octants 1-4: y-row pair (cx +/- x, cy +/- y).
+; Plot 1: (cx+x, cy+y)
+ lda dcArgCx, s
+ clc
+ adc dcX
+ tax
+ lsr a
+ clc
+ adc dcRowYP
+ tay
+ txa
+ and #1
+ bne dcOdd1
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone1
+dcOdd1:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone1:
+
+; Plot 2: (cx-x, cy+y)
+ lda dcArgCx, s
+ sec
+ sbc dcX
+ tax
+ lsr a
+ clc
+ adc dcRowYP
+ tay
+ txa
+ and #1
+ bne dcOdd2
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone2
+dcOdd2:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone2:
+
+; Plot 3: (cx+x, cy-y)
+ lda dcArgCx, s
+ clc
+ adc dcX
+ tax
+ lsr a
+ clc
+ adc dcRowYN
+ tay
+ txa
+ and #1
+ bne dcOdd3
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone3
+dcOdd3:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone3:
+
+; Plot 4: (cx-x, cy-y)
+ lda dcArgCx, s
+ sec
+ sbc dcX
+ tax
+ lsr a
+ clc
+ adc dcRowYN
+ tay
+ txa
+ and #1
+ bne dcOdd4
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone4
+dcOdd4:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone4:
+
+; Octants 5-8: x-row pair (cx +/- y, cy +/- x).
+; Plot 5: (cx+y, cy+x)
+ lda dcArgCx, s
+ clc
+ adc dcY
+ tax
+ lsr a
+ clc
+ adc dcRowXP
+ tay
+ txa
+ and #1
+ bne dcOdd5
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone5
+dcOdd5:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone5:
+
+; Plot 6: (cx-y, cy+x)
+ lda dcArgCx, s
+ sec
+ sbc dcY
+ tax
+ lsr a
+ clc
+ adc dcRowXP
+ tay
+ txa
+ and #1
+ bne dcOdd6
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone6
+dcOdd6:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone6:
+
+; Plot 7: (cx+y, cy-x)
+ lda dcArgCx, s
+ clc
+ adc dcY
+ tax
+ lsr a
+ clc
+ adc dcRowXN
+ tay
+ txa
+ and #1
+ bne dcOdd7
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone7
+dcOdd7:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone7:
+
+; Plot 8: (cx-y, cy-x)
+ lda dcArgCx, s
+ sec
+ sbc dcY
+ tax
+ lsr a
+ clc
+ adc dcRowXN
+ tay
+ txa
+ and #1
+ bne dcOdd8
+ lda [pix], y
+ and #0xFF0F
+ ora dcNibHi
+ sta [pix], y
+ bra dcDone8
+dcOdd8:
+ lda [pix], y
+ and #0xF0FF
+ ora dcNibLo
+ sta [pix], y
+dcDone8:
+
+; Update Bresenham: y++; if err<=0: err+=2y+1; else x--; err+=2(y-x)+1.
+ lda dcY
+ inc a
+ sta dcY ; y now = old y + 1
+; y increased by 1: RowYP moves down a row (+160), RowYN moves up (-160).
+ lda dcRowYP
+ clc
+ adc #160
+ sta dcRowYP
+ lda dcRowYN
+ sec
+ sbc #160
+ sta dcRowYN
+
+ lda dcErr
+ bmi dcErrLE ; err < 0 -> err <= 0
+ bne dcErrGT ; err > 0
+; err == 0: take the LE branch (err += 2y+1).
+dcErrLE:
+ lda dcY
+ asl a
+ clc
+ adc #1
+ clc
+ adc dcErr
+ sta dcErr
+ brl dcLoop
+dcErrGT:
+ lda dcX
+ dec a
+ sta dcX
+; x decreased by 1: RowXP moves up a row (-160), RowXN moves down (+160).
+ lda dcRowXP
+ sec
+ sbc #160
+ sta dcRowXP
+ lda dcRowXN
+ clc
+ adc #160
+ sta dcRowXN
+; err += 2*(y-x) + 1
+ lda dcY
+ sec
+ sbc dcX
+ asl a
+ clc
+ adc #1
+ clc
+ adc dcErr
+ sta dcErr
+ brl dcLoop
+
+dcExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+; ================================================================
+; iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy,
+; uint16_t r, uint16_t fillWord)
+;
+; Filled circle with horizontal-span scanline output. Caller has
+; verified the bbox fits inside the surface, so the inner loop fills
+; every span unconditionally. Maintains xx=x*x and yy=y*y
+; incrementally so the hot path uses only 16-bit add/sub/cmp -- no
+; 65816 multiply at all (other than one r*r at setup).
+;
+; fillWord is the doubled byte (low byte) replicated, e.g. nibble 3
+; -> $3333 (we only use the low byte for nibble RMW).
+;
+; ABI (llvm-mos w65816 cdecl):
+; pix -> arg0 in A:X. USED via [fpix],y, so stashed into bank-0
+; scratch fcPixPtr (offset@+0, bank@+2) and D pointed at
+; it; fpix equ 0 -> [D+0],y.
+; cx,cy,r,fillWord -> on the stack. JSL entry cx@4,s cy@6,s r@8,s
+; fill@10,s. After php+phb+phd (+4): cx@8,s cy@10,s
+; r@12,s fill@14,s. S is constant in the outer body; the
+; jsr-called helpers (fcMul16/fcDoSpan) only touch
+; scratch globals + [fpix],y, never N,s, so the inner
+; jsr-pushed return PC never disturbs an arg read.
+; ================================================================
+ .section .text.iigsFillCircleInner,"ax"
+ .globl iigsFillCircleInner
+fpix = 0 ; D-relative: D points at fcPixPtr
+fcArgCx = 8 ; cx @ 8,s after +4 prologue
+fcArgCy = 10 ; cy @ 10,s
+fcArgR = 12 ; r @ 12,s
+fcArgFill = 14 ; fillWord @ 14,s
+iigsFillCircleInner:
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+; Stash the pix far pointer (A:X) into bank-0 scratch and point D at it.
+ sta fcPixPtr ; offset low 16 bits
+ txa
+ sta fcPixPtr+2 ; low byte = bank
+ lda #fcPixPtr
+ tcd ; D = &fcPixPtr ; [fpix],y == [0],y
+
+; Cache fill nibble bytes. ffill low byte = doubled byte (e.g., $33).
+; fcFillByte = doubled low byte (full byte fills + low nibble).
+; fcFillHi = (nibble << 4) only (high nibble RMW).
+; fcFillLo = nibble only (low nibble RMW).
+ lda fcArgFill, s
+ and #0x00FF
+ sta fcFillByte ; doubled byte e.g. $33
+ lsr a
+ lsr a
+ lsr a
+ lsr a ; A = nibble
+ sta fcFillLo
+ asl a
+ asl a
+ asl a
+ asl a ; A = nibble << 4
+ sta fcFillHi
+
+; Compute r*r via 16-bit shift-and-add. Only done once.
+ lda fcArgR, s
+ sta fcMulA
+ lda fcArgR, s
+ sta fcMulB
+ jsr fcMul16
+ sta fcR2
+ sta fcXX ; xx = r*r initially
+
+; yy = 0, x = r, y = 0.
+ lda #0
+ sta fcYY
+ sta fcY
+ lda fcArgR, s
+ sta fcX
+
+fcOuterLoop:
+; Inner: while (xx + yy) > r2 then xx -= 2x - 1; x--.
+fcInnerLoop:
+ lda fcXX
+ clc
+ adc fcYY
+ cmp fcR2
+ bcc fcInnerDone ; xx+yy < r2
+ beq fcInnerDone ; xx+yy == r2 (acceptable)
+; xx + yy > r2 -> decrement
+ lda fcX
+ asl a ; A = 2x
+ sec
+ sbc #1 ; A = 2x - 1
+ sta fcDec
+ lda fcXX
+ sec
+ sbc fcDec
+ sta fcXX
+ lda fcX
+ dec a
+ sta fcX
+ bra fcInnerLoop
+fcInnerDone:
+
+; leftCol = cx - x, rightCol = cx + x for both lower and (if y>0) upper.
+ lda fcArgCx, s
+ sec
+ sbc fcX
+ sta fcLeftCol
+ lda fcArgCx, s
+ clc
+ adc fcX
+ sta fcRightCol
+
+; Lower row: rowBase = (cy + y) * 160
+ lda fcArgCy, s
+ clc
+ adc fcY
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut, x ; A = y*160
+ sta fcSpanRowBase
+ jsr fcDoSpan
+
+; If y > 0: upper row at (cy - y).
+ lda fcY
+ beq fcSkipUpper
+ lda fcArgCy, s
+ sec
+ sbc fcY
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut, x ; A = y*160
+ sta fcSpanRowBase
+ jsr fcDoSpan
+fcSkipUpper:
+
+; yy += 2*y + 1
+ lda fcY
+ asl a
+ clc
+ adc #1
+ clc
+ adc fcYY
+ sta fcYY
+
+; y++
+ lda fcY
+ inc a
+ sta fcY
+
+; if y > r: exit, else loop
+ cmp fcArgR, s
+ bcc fcContinue
+ beq fcContinue
+ brl fcExit
+fcContinue:
+ brl fcOuterLoop
+
+fcExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+; ----------------------------------------------------------------
+; fcMul16: A = fcMulA * fcMulB (16-bit unsigned, low 16 of result).
+; Standard shift-and-add. 16 iterations. Caller has M=16, X=16.
+; ----------------------------------------------------------------
+fcMul16:
+ .a16
+ .i16
+ lda #0
+ sta fcMulRes
+ ldx #16
+fcMulLoop:
+; result <<= 1
+ lda fcMulRes
+ asl a
+ sta fcMulRes
+; multiplier <<= 1, MSB -> C
+ lda fcMulA
+ asl a
+ sta fcMulA
+ bcc fcMulSkip
+ lda fcMulRes
+ clc
+ adc fcMulB
+ sta fcMulRes
+fcMulSkip:
+ dex
+ bne fcMulLoop
+ lda fcMulRes
+ rts
+
+; ----------------------------------------------------------------
+; fcDoSpan: fill horizontal span from fcLeftCol to fcRightCol on row
+; whose byte base is in fcSpanRowBase. Byte-aligned fill for the
+; middle bytes and per-pixel RMW for the partial-nibble leading /
+; trailing bytes.
+;
+; Inputs: fcLeftCol, fcRightCol, fcSpanRowBase (M=16 values).
+; Trashes A, X, Y, P. Preserves D and B (relies on D = &fcPixPtr).
+; ----------------------------------------------------------------
+fcDoSpan:
+ .a16
+ .i16
+; leftByte = leftCol >> 1, leftPartial = leftCol & 1
+ lda fcLeftCol
+ lsr a
+ sta fcLeftByte
+ lda fcLeftCol
+ and #1
+ sta fcLeftPartial
+
+; rightByte = rightCol >> 1, rightPartial = !(rightCol & 1)
+ lda fcRightCol
+ lsr a
+ sta fcRightByte
+ lda fcRightCol
+ and #1
+ eor #1
+ sta fcRightPartial
+
+; If leftByte == rightByte: single-byte case
+ lda fcLeftByte
+ cmp fcRightByte
+ bne fcSpanMulti
+ brl fcSpanSingle
+fcSpanMulti:
+
+; Multi-byte case.
+; Leading partial (low nibble of leftByte) if leftPartial.
+ lda fcLeftPartial
+ beq fcSkipLP
+ lda fcSpanRowBase
+ clc
+ adc fcLeftByte
+ tay
+ sep #0x20
+ .a8
+ lda [fpix], y
+ and #0xF0
+ ora fcFillLo
+ sta [fpix], y
+ rep #0x20
+ .a16
+fcSkipLP:
+
+; Trailing partial (high nibble of rightByte) if rightPartial.
+ lda fcRightPartial
+ beq fcSkipRP
+ lda fcSpanRowBase
+ clc
+ adc fcRightByte
+ tay
+ sep #0x20
+ .a8
+ lda [fpix], y
+ and #0x0F
+ ora fcFillHi
+ sta [fpix], y
+ rep #0x20
+ .a16
+fcSkipRP:
+
+; Mid bytes: (leftByte+leftPartial) .. (rightByte-rightPartial) incl.
+ lda fcLeftByte
+ clc
+ adc fcLeftPartial
+ sta fcMidStart
+ lda fcRightByte
+ sec
+ sbc fcRightPartial
+ sta fcMidEnd
+ cmp fcMidStart
+ bcs fcMidGo
+ rts ; midEnd < midStart
+fcMidGo:
+ lda fcMidEnd
+ sec
+ sbc fcMidStart
+ inc a
+ sta fcMidCount
+
+ lda fcSpanRowBase
+ clc
+ adc fcMidStart
+ tay ; Y = byte offset
+ lda fcMidCount
+ tax ; X = byte counter
+
+ sep #0x20
+ .a8
+ lda fcFillByte ; doubled byte
+fcFillLoop:
+ sta [fpix], y
+ iny
+ dex
+ bne fcFillLoop
+ rep #0x20
+ .a16
+ rts
+
+fcSpanSingle:
+; Single-byte case: leftByte == rightByte. Three sub-cases:
+; leftPartial=1 -> low nibble only
+; rightPartial=1 -> high nibble only
+; neither -> full byte (two adjacent pixels)
+ lda fcSpanRowBase
+ clc
+ adc fcLeftByte
+ tay
+ sep #0x20
+ .a8
+ lda fcLeftPartial
+ beq fcSbCheckRP
+ lda [fpix], y
+ and #0xF0
+ ora fcFillLo
+ sta [fpix], y
+ rep #0x20
+ .a16
+ rts
+fcSbCheckRP:
+ .a8 ; reached via BEQ from M=8 path
+ lda fcRightPartial
+ beq fcSbFull
+ lda [fpix], y
+ and #0x0F
+ ora fcFillHi
+ sta [fpix], y
+ rep #0x20
+ .a16
+ rts
+fcSbFull:
+ .a8 ; reached via BEQ from M=8 path
+ lda fcFillByte
+ sta [fpix], y
+ rep #0x20
+ .a16
+ rts
+
+
+; ================================================================
+; Scratch slots (bank-0 BSS). dcSavedCol is kept for byte-faithful
+; parity with the original DRAWPRIMS layout even though the inlined
+; plots no longer round-trip through it. dcPixPtr / fcPixPtr hold the
+; stashed arg0 far pointer for the [pix],y / [fpix],y direct-page
+; indirect-long loads.
+; ================================================================
+
+ .section .bss.dcPixPtr,"aw"
+ .globl dcPixPtr
+dcPixPtr:
+ .zero 4
+
+ .section .bss.dcX,"aw"
+ .globl dcX
+dcX:
+ .zero 2
+
+ .section .bss.dcY,"aw"
+ .globl dcY
+dcY:
+ .zero 2
+
+ .section .bss.dcErr,"aw"
+ .globl dcErr
+dcErr:
+ .zero 2
+
+ .section .bss.dcNibLo,"aw"
+ .globl dcNibLo
+dcNibLo:
+ .zero 2
+
+ .section .bss.dcNibHi,"aw"
+ .globl dcNibHi
+dcNibHi:
+ .zero 2
+
+ .section .bss.dcRowYP,"aw"
+ .globl dcRowYP
+dcRowYP:
+ .zero 2
+
+ .section .bss.dcRowYN,"aw"
+ .globl dcRowYN
+dcRowYN:
+ .zero 2
+
+ .section .bss.dcRowXP,"aw"
+ .globl dcRowXP
+dcRowXP:
+ .zero 2
+
+ .section .bss.dcRowXN,"aw"
+ .globl dcRowXN
+dcRowXN:
+ .zero 2
+
+ .section .bss.dcSavedCol,"aw"
+ .globl dcSavedCol
+dcSavedCol:
+ .zero 2
+
+ .section .bss.fcPixPtr,"aw"
+ .globl fcPixPtr
+fcPixPtr:
+ .zero 4
+
+ .section .bss.fcX,"aw"
+ .globl fcX
+fcX:
+ .zero 2
+
+ .section .bss.fcY,"aw"
+ .globl fcY
+fcY:
+ .zero 2
+
+ .section .bss.fcXX,"aw"
+ .globl fcXX
+fcXX:
+ .zero 2
+
+ .section .bss.fcYY,"aw"
+ .globl fcYY
+fcYY:
+ .zero 2
+
+ .section .bss.fcR2,"aw"
+ .globl fcR2
+fcR2:
+ .zero 2
+
+ .section .bss.fcDec,"aw"
+ .globl fcDec
+fcDec:
+ .zero 2
+
+ .section .bss.fcMulA,"aw"
+ .globl fcMulA
+fcMulA:
+ .zero 2
+
+ .section .bss.fcMulB,"aw"
+ .globl fcMulB
+fcMulB:
+ .zero 2
+
+ .section .bss.fcMulRes,"aw"
+ .globl fcMulRes
+fcMulRes:
+ .zero 2
+
+ .section .bss.fcLeftCol,"aw"
+ .globl fcLeftCol
+fcLeftCol:
+ .zero 2
+
+ .section .bss.fcRightCol,"aw"
+ .globl fcRightCol
+fcRightCol:
+ .zero 2
+
+ .section .bss.fcSpanRowBase,"aw"
+ .globl fcSpanRowBase
+fcSpanRowBase:
+ .zero 2
+
+ .section .bss.fcLeftByte,"aw"
+ .globl fcLeftByte
+fcLeftByte:
+ .zero 2
+
+ .section .bss.fcRightByte,"aw"
+ .globl fcRightByte
+fcRightByte:
+ .zero 2
+
+ .section .bss.fcLeftPartial,"aw"
+ .globl fcLeftPartial
+fcLeftPartial:
+ .zero 2
+
+ .section .bss.fcRightPartial,"aw"
+ .globl fcRightPartial
+fcRightPartial:
+ .zero 2
+
+ .section .bss.fcMidStart,"aw"
+ .globl fcMidStart
+fcMidStart:
+ .zero 2
+
+ .section .bss.fcMidEnd,"aw"
+ .globl fcMidEnd
+fcMidEnd:
+ .zero 2
+
+ .section .bss.fcMidCount,"aw"
+ .globl fcMidCount
+fcMidCount:
+ .zero 2
+
+ .section .bss.fcFillByte,"aw"
+ .globl fcFillByte
+fcFillByte:
+ .zero 2
+
+ .section .bss.fcFillLo,"aw"
+ .globl fcFillLo
+fcFillLo:
+ .zero 2
+
+ .section .bss.fcFillHi,"aw"
+ .globl fcFillHi
+fcFillHi:
+ .zero 2
+
+; ===== ported: blit / dirty / input / flood =====
+; ====================================================================
+; frag_blitflood.s - llvm-mc GAS (w65816):
+; iigsBlitStageToShr, iigsMarkDirtyRowsInner, iigsPollJoystickInner,
+; iigsInputSnapshot, iigsFloodWalkAndScansInner
+;
+; Shared ABI facts (clang w65816 cdecl, per PORT_SPEC.md):
+; - arg0 (pointer/long) arrives in A=low16, X=high16; word arg0 in A.
+; It is NOT on the stack.
+; - arg1, arg2, ... are pushed right-to-left; on JSL entry arg1 byte0
+; is at 4,s. A word arg is 2 bytes, a pointer/long arg is 4 bytes.
+; - Each prologue push shifts every stack offset up: php=+1, phb=+1,
+; phd=+2, pha/phx=+2.
+; - Memory model: DBR=0 for C-called code; all globals + bank-0 soft
+; switches reachable via 16-bit absolute (no long prefix needed;
+; `$00Cxxx` operands are plain 16-bit abs).
+; ====================================================================
+
+
+; ====================================================================
+; iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr,
+; uint16_t uploadFlags)
+;
+; SHR upload: optional SCB (200 bytes) and palette (512 bytes) uploads
+; via direct-$E1 MVN, then a per-row dirty-skipped, chunked-SEI PEI
+; slam of the 32000-byte pixel buffer from the bank-$01 back buffer to
+; the $E1 SHR frame buffer. WIDE dirty rows take the unrolled 80-PEI
+; full-row slam; NARROW dirty rows take an exact-extent MVN.
+;
+; Original args (D-frame, D=SP+8, every arg on stack):
+; scbPtr D+0..3 (long ptr)
+; palettePtr D+4..7 (long ptr)
+; uploadFlags D+8..9 (uint16_t bitmask)
+;
+; ABI: under clang scbPtr (arg0, ptr) arrives in A:X and is NOT on the
+; stack; palettePtr (arg1, ptr) and uploadFlags (arg2, word) are on the
+; stack. JSL entry: palettePtr @4,s, uploadFlags @8,s; after
+; php+phb+phd (+4): palettePtr @8,s, uploadFlags @12,s.
+;
+; The original read scbPtr/palettePtr/uploadFlags via a D-frame
+; (bscb=D+0, bpal=D+4, bflags=D+8). We rebuild that exact frame in a
+; private bank-0 scratch block (bsFrame) -- D+0..3 must be writable
+; scratch and cannot overlap the saved registers / return address the
+; way the original's stack args did -- copy scbPtr from A:X to +0..3,
+; palettePtr from the stack to +4..7, uploadFlags to +8, then point D
+; at bsFrame so all `bscb`/`bpal`/`bflags` reads work unchanged. Phase
+; 3 (the slam) clobbers D freely via TCD to row bases (exactly as the
+; original); the final PLD restores the caller's D (saved by our PHD)
+; AND therefore restores DBR=0 expectations -- DBR is restored by PLB.
+; ====================================================================
+.section .text.iigsBlitStageToShr,"ax"
+.globl iigsBlitStageToShr
+iigsBlitStageToShr:
+bscb = 0
+bpal = 4
+bflags = 8
+; A dirty row with (maxWord-minWord) < PEI_NARROW_MAX uses an exact MVN
+; instead of the full-row PEI slam. 32 keeps a safe margin below the
+; ~34-word MVN/PEI break-even.
+PEI_NARROW_MAX = 32
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+ ; Rebuild the D-frame (scbPtr/palettePtr/flags) in
+ ; bank-0 scratch. scbPtr is in A:X; palettePtr/flags are
+ ; on the stack (post +4 push: palettePtr@8,s flags@12,s).
+ sta bsFrame+0 ; scbPtr.lo (A)
+ stx bsFrame+2 ; scbPtr.hi (X, bank+pad)
+ lda 8,s ; palettePtr.lo
+ sta bsFrame+4
+ lda 10,s ; palettePtr.hi (bank+pad)
+ sta bsFrame+6
+ lda 12,s ; uploadFlags
+ sta bsFrame+8
+ lda #bsFrame
+ tcd ; D -> bsFrame (bscb/bpal/bflags)
+
+; 1. SCB upload (200 bytes) via MVN, only when the SCB dirty bit is
+; set. Source bank is runtime-patched into the MVN instruction
+; (encoding: $54 dst src, so byte +2 is src).
+ lda bflags
+ and #1
+ beq bsSkipScb
+ sep #0x20
+ .a8
+ lda bscb+2
+ sta mvnScbInst+2
+ rep #0x20
+ .a16
+ lda bscb
+ tax
+ ldy #0x9D00
+ lda #199
+mvnScbInst: mvn 0xE1, 0x00 ; mvn dst,src (src bank runtime-patched)
+bsSkipScb:
+
+; 2. Palette upload (512 bytes) via MVN, only when the palette dirty
+; bit is set. Same trick.
+ lda bflags
+ and #2
+ beq bsSkipPal
+ sep #0x20
+ .a8
+ lda bpal+2
+ sta mvnPalInst+2
+ rep #0x20
+ .a16
+ lda bpal
+ tax
+ ldy #0x9E00
+ lda #511
+mvnPalInst: mvn 0xE1, 0x00
+bsSkipPal:
+
+; 3. Pixel blit via PEI-slam, with per-row dirty skip and chunked SEI.
+ tsc
+ sta gPeiOrigSp
+ sep #0x20
+ .a8
+ lda 0xc035
+ sta gPeiOrigShadow
+ rep #0x20
+ .a16
+
+ ldx #0 ; X = absolute row counter
+
+peiChunkBegin:
+ ldy #0 ; Y = in-chunk row counter
+
+ sei
+
+ sep #0x20
+ .a8
+ lda gPeiOrigShadow
+ and #0xF1 ; clear bits 1,2,3 -> SHR shadow ON
+ sta 0xc035
+ lda #0
+ sta 0xc005 ; AUXWRITE on
+ sta 0xc003 ; RAMRD on
+ rep #0x20
+ .a16
+
+peiRowLoop:
+ cpx #200
+ bcc peiNotAllDone ; X < 200 -> still rows to do
+ brl peiChunkEnd
+peiNotAllDone:
+ cpy #40
+ bcc peiCheckDirty ; Y < 40 -> chunk has room
+ brl peiChunkEnd
+peiCheckDirty:
+ sep #0x20
+ .a8
+ lda gStageMinWord,x
+ cmp gStageMaxWord,x
+ rep #0x20
+ .a16
+ bcc peiRowDirty ; min < max -> dirty
+ beq peiRowDirty ; min == max -> dirty (1 word)
+ inx ; clean row, skip
+ iny
+ brl peiRowLoop
+
+; Dirty row: narrow spans take the exact-extent MVN; wide spans take
+; the unchanged full-row PEI slam.
+peiRowDirty:
+ sep #0x20
+ .a8
+ lda gStageMaxWord,x
+ sec
+ sbc gStageMinWord,x ; maxWord - minWord (>= 0)
+ rep #0x20
+ .a16
+ and #0x00FF
+ cmp #PEI_NARROW_MAX
+ bcs peiSlamRow ; wide span -> full-row PEI slam
+ brl peiMvnRow ; narrow span -> exact-extent MVN
+
+peiSlamRow:
+; Save X and Y into long-mode scratch (stack is hijacked into $E1).
+ txa
+ sta gPeiCurRow
+ tya
+ sta gPeiChunkRow
+ lda gPeiCurRow
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut,x ; A = y*160
+ clc
+ adc #0x2000 ; A = row_start
+ tcd ; D = row_start (PEI dp base)
+ clc
+ adc #159
+ tcs ; SP = row_start + 159
+
+; 80 PEIs from DP+$9E down to DP+$00.
+ pei 0x9E
+ pei 0x9C
+ pei 0x9A
+ pei 0x98
+ pei 0x96
+ pei 0x94
+ pei 0x92
+ pei 0x90
+ pei 0x8E
+ pei 0x8C
+ pei 0x8A
+ pei 0x88
+ pei 0x86
+ pei 0x84
+ pei 0x82
+ pei 0x80
+ pei 0x7E
+ pei 0x7C
+ pei 0x7A
+ pei 0x78
+ pei 0x76
+ pei 0x74
+ pei 0x72
+ pei 0x70
+ pei 0x6E
+ pei 0x6C
+ pei 0x6A
+ pei 0x68
+ pei 0x66
+ pei 0x64
+ pei 0x62
+ pei 0x60
+ pei 0x5E
+ pei 0x5C
+ pei 0x5A
+ pei 0x58
+ pei 0x56
+ pei 0x54
+ pei 0x52
+ pei 0x50
+ pei 0x4E
+ pei 0x4C
+ pei 0x4A
+ pei 0x48
+ pei 0x46
+ pei 0x44
+ pei 0x42
+ pei 0x40
+ pei 0x3E
+ pei 0x3C
+ pei 0x3A
+ pei 0x38
+ pei 0x36
+ pei 0x34
+ pei 0x32
+ pei 0x30
+ pei 0x2E
+ pei 0x2C
+ pei 0x2A
+ pei 0x28
+ pei 0x26
+ pei 0x24
+ pei 0x22
+ pei 0x20
+ pei 0x1E
+ pei 0x1C
+ pei 0x1A
+ pei 0x18
+ pei 0x16
+ pei 0x14
+ pei 0x12
+ pei 0x10
+ pei 0x0E
+ pei 0x0C
+ pei 0x0A
+ pei 0x08
+ pei 0x06
+ pei 0x04
+ pei 0x02
+ pei 0x00
+
+ lda gPeiCurRow
+ tax
+ inx
+ lda gPeiChunkRow
+ tay
+ iny
+ brl peiRowLoop
+
+; Narrow dirty span: exact-extent MVN of words [minWord..maxWord] from
+; bank-$01 back buffer to $E1 SHR frame buffer. src $01 -> dst $E1;
+; GAS spells MVN dst,src so `mvn 0xE1, 0x01`.
+peiMvnRow:
+ txa
+ sta gPeiCurRow
+ tya
+ sta gPeiChunkRow
+; rowStart = $2000 + y*160
+ lda gPeiCurRow
+ asl a
+ tax
+ lda gRowOffsetLut,x
+ clc
+ adc #0x2000
+ sta gPeiMvnSrc
+; src/dst offset = rowStart + minWord*2
+ lda gPeiCurRow
+ tax
+ sep #0x20
+ .a8
+ lda gStageMinWord,x
+ rep #0x20
+ .a16
+ and #0x00FF
+ asl a
+ clc
+ adc gPeiMvnSrc
+ sta gPeiMvnSrc
+; count = (maxWord - minWord)*2 + 1
+ sep #0x20
+ .a8
+ lda gStageMaxWord,x
+ sec
+ sbc gStageMinWord,x
+ rep #0x20
+ .a16
+ and #0x00FF
+ asl a
+ inc a
+ sta gPeiMvnCount
+; Block move: X=src off, Y=dst off (same), A=count-1; src $01, dst $E1.
+ lda gPeiMvnSrc
+ tax
+ tay
+ lda gPeiMvnCount
+ mvn 0xE1, 0x01
+; Restore row counters and advance.
+ lda gPeiCurRow
+ tax
+ inx
+ lda gPeiChunkRow
+ tay
+ iny
+ brl peiRowLoop
+
+peiChunkEnd:
+; Per-chunk teardown: restore SP, shadow, AUXWRITE/RAMRD, then CLI.
+ lda gPeiOrigSp
+ tcs
+ sep #0x20
+ .a8
+ lda gPeiOrigShadow
+ sta 0xc035
+ lda #0
+ sta 0xc004 ; AUXWRITE off
+ sta 0xc002 ; RAMRD off
+ rep #0x20
+ .a16
+ cli
+
+ cpx #200
+ bcs peiAllDone
+ brl peiChunkBegin
+
+peiAllDone:
+ .a8
+ .i8
+ pld
+ plb
+ plp ; restores I (pre-SEI value)
+ rtl
+
+
+; ====================================================================
+; iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd,
+; uint16_t minWord, uint16_t maxWord)
+;
+; For each row in [yStart, yEnd) widen gStageMinWord[row] DOWN to
+; minWord (if smaller) and gStageMaxWord[row] UP to maxWord (if larger).
+; Caller clips yStart/yEnd to [0,200); no internal bounds check.
+;
+; Original args (D-frame, D=SP+8, all on stack):
+; yStart D+0..1, yEnd D+2..3, minWord D+4..5, maxWord D+6..7
+;
+; ABI: ALL FOUR ARGS ARE WORDS. arg0 yStart arrives in A; arg1 yEnd,
+; arg2 minWord, arg3 maxWord are on the stack. JSL entry: yEnd @4,s,
+; minWord @6,s, maxWord @8,s; after php+phb+phd (+4): yEnd @8,s.
+; The original D-frame had yStart at D+0; under clang yStart is in A
+; and is only ever loaded ONCE into X (the loop never re-reads it), so
+; we just TAX it -- no D+0 slot is needed. The three stack args slid
+; DOWN by 2 (one word), so set D = SP+6 (adc #6 not #8): then
+; yEnd@8,s = D+2, minWord = D+4, maxWord = D+6 (original equates hold).
+; D+0..1 would overlap the return address, but mdrYStart is never
+; touched in the frame, so that is harmless.
+; ====================================================================
+.section .text.iigsMarkDirtyRowsInner,"ax"
+.globl iigsMarkDirtyRowsInner
+iigsMarkDirtyRowsInner:
+mdrYEnd = 2
+mdrMinWord = 4
+mdrMaxWord = 6
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+ tax ; X = yStart (arg0 in A); never re-read
+ tsc
+ clc
+ adc #6
+ tcd ; D = SP+6 (yEnd@D+2, min@D+4, max@D+6)
+
+ cpx mdrYEnd
+ bcs mdrExit ; empty range -> nothing to do
+
+ sep #0x20
+ .a8
+
+mdrLoop:
+ lda mdrMinWord
+ cmp gStageMinWord,x
+ bcs mdrNoMin ; A >= existing -> already <= minWord
+ sta gStageMinWord,x ; widen down
+mdrNoMin:
+ lda mdrMaxWord
+ cmp gStageMaxWord,x
+ bcc mdrNoMax ; A < existing -> already >= maxWord
+ beq mdrNoMax
+ sta gStageMaxWord,x ; widen up
+mdrNoMax:
+ inx
+ cpx mdrYEnd
+ bcc mdrLoop
+
+ rep #0x20
+ .a16
+
+mdrExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsPollJoystickInner(void)
+;
+; Reads the IIgs paddle ports (PDL0/PDL1) at 1 MHz, ported from the old
+; joeylib jIIgs.asm. Outputs gJoyPx/gJoyPy/gJoyResolved (DRAWPRIMS
+; scratch). No args.
+;
+; ABI: void; no register/stack args. DBR forced to 0 for the abs $C0xx
+; I/O reads (restored via PLB before return).
+; ====================================================================
+.section .text.iigsPollJoystickInner,"ax"
+.globl iigsPollJoystickInner
+iigsPollJoystickInner:
+ php
+ sei
+
+ rep #0x30
+ .a16
+ .i16
+
+; Save caller's DBR; force DBR=0 so abs $C0xx hits the I/O page.
+ phb
+ pea 0x0000
+ plb ; pull low byte -> DBR=0
+ plb ; pull high byte (discard)
+
+ sep #0x30 ; M=8, X=8
+ .a8
+ .i8
+
+; Save CYAREG and force 1 MHz (clear bit 7).
+ lda 0xc036
+ sta gJoyOrigSpeed
+ and #0x7F
+ sta 0xc036
+
+ ldx #0 ; pdl0 count (increments by 2)
+ ldy #0 ; pdl1 count (increments by 1; asl at end)
+ lda #0
+ sta gJoyResolved
+
+; PTRIG: start both paddle one-shot timers.
+ lda 0xc070
+
+joyChkPdl0:
+ lda 0xc064 ; PDL0
+ bpl joyGotPdl0 ; bit7=0 -> pdl0 done
+ inx
+ inx
+ beq joyTimeoutX ; X wrapped -> pdl0 never fired
+ lda 0xc065 ; PDL1
+ bmi joyNoGots ; bit7=1 -> pdl1 still busy
+; pdl1 just fired. Mark resolved, switch to pdl0-only loop.
+ lda #0x02
+ sta gJoyResolved
+joyPdl0Only:
+ lda 0xc064
+ bpl joyAllDone
+ inx
+ inx
+ bne joyPdl0Only
+ bra joyTimeoutX
+joyNoGots:
+ iny
+ bne joyChkPdl0
+ bra joyTimeoutY
+
+joyGotPdl0:
+ lda #0x01
+ sta gJoyResolved
+joyPdl1Only:
+ lda 0xc065
+ bpl joyAllDone
+ iny
+ bne joyPdl1Only
+ bra joyTimeoutY
+
+joyAllDone:
+ lda gJoyResolved
+ ora #0x03
+ sta gJoyResolved
+ bra joyExit
+
+joyTimeoutX:
+ lda gJoyResolved
+ and #0x02 ; clear bit 0 (pdl0 unresolved)
+ sta gJoyResolved
+ bra joyExit
+
+joyTimeoutY:
+ lda gJoyResolved
+ and #0x01 ; clear bit 1 (pdl1 unresolved)
+ sta gJoyResolved
+
+joyExit:
+; STX/STY have no long-abs form. Stash via TXA/TYA.
+ txa
+ sta gJoyPx
+ tya
+ asl a ; scale Y from 0..127 to 0..254
+ sta gJoyPy
+
+; Restore CYAREG.
+ lda gJoyOrigSpeed
+ sta 0xc036
+
+ rep #0x30
+ .a16
+ .i16
+ plb ; restore caller DBR
+
+ plp ; restores I from pre-SEI value
+ rtl
+
+
+; ====================================================================
+; iigsInputSnapshot(void)
+;
+; Snapshot gKeyState->gKeyPrev (KEY_COUNT=60 bytes) plus
+; gMouseButtonState/gJoyButtonState (4 bytes each). No args.
+;
+; ABI: void; no register/stack args. DBR stays 0; all globals via
+; 16-bit abs.
+; ====================================================================
+.section .text.iigsInputSnapshot,"ax"
+.globl iigsInputSnapshot
+iigsInputSnapshot:
+ php
+ rep #0x30
+ .a16
+ .i16
+ sep #0x20
+ .a8
+
+; Snapshot gKeyState -> gKeyPrev (60 bytes), long-mode loop.
+ ldx #59
+isnKeyLoop:
+ lda gKeyState,x
+ sta gKeyPrev,x
+ dex
+ bpl isnKeyLoop
+
+; Snapshot gMouseButtonState -> gMouseButtonPrev (4 bytes inline).
+ lda gMouseButtonState
+ sta gMouseButtonPrev
+ lda gMouseButtonState+1
+ sta gMouseButtonPrev+1
+ lda gMouseButtonState+2
+ sta gMouseButtonPrev+2
+ lda gMouseButtonState+3
+ sta gMouseButtonPrev+3
+
+; Snapshot gJoyButtonState -> gJoyButtonPrev (4 bytes inline).
+ lda gJoyButtonState
+ sta gJoyButtonPrev
+ lda gJoyButtonState+1
+ sta gJoyButtonPrev+1
+ lda gJoyButtonState+2
+ sta gJoyButtonPrev+2
+ lda gJoyButtonState+3
+ sta gJoyButtonPrev+3
+
+ rep #0x30
+ .a16
+ .i16
+ plp
+ rtl
+
+
+; ====================================================================
+; iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y,
+; uint16_t matchColor, uint16_t newColor,
+; uint16_t matchEqual,
+; int16_t *stackX, int16_t *stackY,
+; uint16_t *spInOut, uint16_t maxSp)
+;
+; Combined per-popped-seed flood work: seed test + walk-left + walk-
+; right + span fill + scan-above + scan-below (each scan pushes new
+; seeds onto the explicit stackX/stackY scan stack via *spInOut/maxSp).
+; Outputs gFloodSeedMatch / gFloodLeftX / gFloodRightX.
+;
+; Original args (D-frame, D=SP+8, all on stack):
+; pixels D+0..3, x D+4..5, y D+6..7, matchColor D+8..9,
+; newColor D+10..11, matchEqual D+12..13, stackX D+14..17,
+; stackY D+18..21, spInOut D+22..25, maxSp D+26..27
+;
+; ABI: pixels (arg0, ptr) arrives in A:X and is NOT on the stack; the
+; other 9 args are on the stack. The body needs ALL of D+0..D+27 to be
+; writable DP scratch -- it overwrites the pixels slot (wsRow) with the
+; computed row pointer and uses `[wsRow],y`, `[wsScanRow],y`,
+; `[wsStackX],y`, `[wsStackY],y`, `[wsSpInOut],y` DP-indirect-long.
+; The clang stack offers no such writable 28-byte window above the
+; saved regs, so we point D at a private bank-0 scratch frame (wsFrame)
+; and copy EVERY arg into it at the ORIGINAL D-offsets, leaving all the
+; ws* equates and indirect loads unchanged.
+;
+; Stack source offsets (after php+phb+phd, +4):
+; x@8,s y@10,s matchColor@12,s newColor@14,s matchEqual@16,s
+; stackX@18,s(4) stackY@22,s(4) spInOut@26,s(4) maxSp@30,s
+; Copied to: wsFrame+4 / +6 / +8 / +10 / +12 / +14..17 / +18..21 /
+; +22..25 / +26 ; pixels(A:X) -> wsFrame+0..3. PLD restores caller D.
+; ====================================================================
+.section .text.iigsFloodWalkAndScansInner,"ax"
+.globl iigsFloodWalkAndScansInner
+iigsFloodWalkAndScansInner:
+; wsRow occupies D+0..3 (was pixels arg): row = pixels + y*160 is stored
+; over the pixels slot so `lda [wsRow],y` reads the row directly.
+; wsScanRow overlaps wsMatch+wsNew (D+8..11): those args are cached to
+; wsMatchByte/wsNewByte first, then the slot is reused.
+wsRow = 0 ; D+0..3 (was wsPixels arg)
+wsX = 4
+wsY = 6
+wsScanRow = 8 ; D+8..11 (was wsMatch+wsNew args)
+wsMatch = 8 ; alias of wsScanRow during initial cache
+wsNew = 10 ; alias of wsScanRow+2 during initial cache
+wsEq = 12
+wsStackX = 14
+wsStackY = 18
+wsSpInOut = 22
+wsMaxSp = 26
+wsPixels = 0 ; alias of wsRow for the initial row-addr compute
+wsMidEnd = 4 ; alias of wsX, used by inline fill (post-walk)
+; DP scratch for the rewritten scan inner loop (DP-relative reads).
+wsScanCurX = 4 ; alias wsX/wsMidEnd, 16-bit
+wsScanByte = 12 ; alias wsEq.lo, 8-bit
+wsScanPrevHit = 26 ; alias wsMaxSp.lo, 8-bit
+wsScanCurHit = 27 ; alias wsMaxSp.hi, 8-bit
+
+ php
+ phb
+ phd
+ rep #0x30
+ .a16
+ .i16
+
+ ; Rebuild the full D-frame in bank-0 scratch.
+ ; pixels in A:X; the 9 stack args at +8/+10/.../+30,s.
+ sta wsFrame+0 ; pixels.lo (A)
+ stx wsFrame+2 ; pixels.hi (X)
+ lda 8,s ; x
+ sta wsFrame+4
+ lda 10,s ; y
+ sta wsFrame+6
+ lda 12,s ; matchColor
+ sta wsFrame+8
+ lda 14,s ; newColor
+ sta wsFrame+10
+ lda 16,s ; matchEqual
+ sta wsFrame+12
+ lda 18,s ; stackX.lo
+ sta wsFrame+14
+ lda 20,s ; stackX.hi
+ sta wsFrame+16
+ lda 22,s ; stackY.lo
+ sta wsFrame+18
+ lda 24,s ; stackY.hi
+ sta wsFrame+20
+ lda 26,s ; spInOut.lo
+ sta wsFrame+22
+ lda 28,s ; spInOut.hi
+ sta wsFrame+24
+ lda 30,s ; maxSp
+ sta wsFrame+26
+ lda #wsFrame
+ tcd ; D -> wsFrame
+
+; Cache 8-bit constants used across walk + scan + fill.
+ sep #0x20
+ .a8
+ lda wsMatch
+ and #0x0F
+ sta wsMatchByte
+ lda wsNew
+ and #0x0F
+ sta wsNewByte
+ ora #0
+ pha ; save newByte for doubled compute
+ asl a
+ asl a
+ asl a
+ asl a ; A = newByte << 4
+ sta wsNewHigh
+ ora 1,s ; A = (newByte<<4) | newByte
+ sta wsDoubledByte
+ pla ; clean stack
+ lda wsEq
+ sta wsEqByte
+ rep #0x20
+ .a16
+
+; Compute rowAddr (long ptr) = pixels + lut[y*2]; store over wsRow.
+ lda wsY
+ asl a ; A = y*2 (LUT byte offset)
+ tax
+ lda gRowOffsetLut,x ; A = y*160
+ clc
+ adc wsPixels ; A = pixels.lo + y*160 = row.lo
+ tax ; X = row.lo (carry preserved)
+ lda wsPixels+2 ; A = pixels.hi
+ adc #0 ; A = row.hi (carry from prev add)
+ sta wsRow+2 ; D+2..3 = row.hi
+ txa
+ sta wsRow ; D+0..1 = row.lo
+
+; === SEED TEST + WALK LEFT + WALK RIGHT ===
+ sep #0x20
+ .a8
+ lda wsEqByte
+ rep #0x20
+ .a16
+ bne wsWalkEqEntry
+ brl wsWalkBndEntry
+
+; ***** EQUAL MODE WALK *****
+wsWalkEqEntry:
+; --- SEED TEST EQ (inline) ---
+ lda wsX
+ lsr a
+ tay
+ sep #0x20
+ .a8
+ bcs wsSeedEqOdd
+ lda [wsRow],y
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ bra wsSeedEqHave
+wsSeedEqOdd:
+ lda [wsRow],y
+ and #0x0F
+wsSeedEqHave:
+ cmp wsMatchByte
+ rep #0x20
+ .a16
+ bne wsSeedEqMiss
+ lda #1
+ sta gFloodSeedMatch
+ bra wsWalkEqSeedOk
+wsSeedEqMiss:
+ lda #0
+ sta gFloodSeedMatch
+ brl wsExit
+wsWalkEqSeedOk:
+
+; --- WALK LEFT EQ (byte-cached) ---
+ lda wsX
+ sta wsScanCurX
+ lsr a
+ tay ; Y = byteIdx
+ bcc wsLEqEvenEntry
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ rep #0x20
+ .a16
+ bra wsLEqOddEntry
+
+wsLEqEvenEntry:
+ lda wsScanCurX
+ beq wsLeftEqDone
+ dey
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ and #0x0F
+ cmp wsMatchByte
+ rep #0x20
+ .a16
+ bne wsLeftEqDone
+ dec wsScanCurX
+wsLEqOddEntry:
+ lda wsScanCurX
+ beq wsLeftEqDone
+ sep #0x20
+ .a8
+ lda wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ cmp wsMatchByte
+ rep #0x20
+ .a16
+ bne wsLeftEqDone
+ dec wsScanCurX
+ bra wsLEqEvenEntry
+wsLeftEqDone:
+ lda wsScanCurX
+ sta gFloodLeftX
+
+; --- WALK RIGHT EQ (byte-cached) ---
+ lda wsX
+ sta wsScanCurX
+ lsr a
+ tay
+ bcs wsREqOddEntry
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ rep #0x20
+ .a16
+ bra wsREqEvenEntry
+
+wsREqEvenEntry:
+ lda wsScanCurX
+ cmp #319
+ bcs wsRightEqDone
+ sep #0x20
+ .a8
+ lda wsScanByte
+ and #0x0F
+ cmp wsMatchByte
+ rep #0x20
+ .a16
+ bne wsRightEqDone
+ inc wsScanCurX
+wsREqOddEntry:
+ lda wsScanCurX
+ cmp #319
+ bcs wsRightEqDone
+ iny
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ cmp wsMatchByte
+ rep #0x20
+ .a16
+ bne wsRightEqDone
+ inc wsScanCurX
+ bra wsREqEvenEntry
+wsRightEqDone:
+ lda wsScanCurX
+ sta gFloodRightX
+ brl wsAfterWalk
+
+; ***** BOUNDARY MODE WALK *****
+wsWalkBndEntry:
+; --- SEED TEST BND (inline) ---
+ lda wsX
+ lsr a
+ tay
+ sep #0x20
+ .a8
+ bcs wsSeedBndOdd
+ lda [wsRow],y
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ bra wsSeedBndHave
+wsSeedBndOdd:
+ lda [wsRow],y
+ and #0x0F
+wsSeedBndHave:
+ cmp wsMatchByte
+ beq wsSeedBndMiss
+ cmp wsNewByte
+ beq wsSeedBndMiss
+ rep #0x20
+ .a16
+ lda #1
+ sta gFloodSeedMatch
+ bra wsWalkBndSeedOk
+wsSeedBndMiss:
+ rep #0x20
+ .a16
+ lda #0
+ sta gFloodSeedMatch
+ brl wsExit
+wsWalkBndSeedOk:
+
+; --- WALK LEFT BND (byte-cached) ---
+ lda wsX
+ sta wsScanCurX
+ lsr a
+ tay
+ bcc wsLBndEvenEntry
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ rep #0x20
+ .a16
+ bra wsLBndOddEntry
+
+wsLBndEvenEntry:
+ lda wsScanCurX
+ beq wsLeftBndDone
+ dey
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ and #0x0F
+ cmp wsMatchByte
+ beq wsLBndStop
+ cmp wsNewByte
+ beq wsLBndStop
+ rep #0x20
+ .a16
+ dec wsScanCurX
+ bra wsLBndOddEntry2
+wsLBndStop:
+ rep #0x20
+ .a16
+ bra wsLeftBndDone
+wsLBndOddEntry2:
+wsLBndOddEntry:
+ lda wsScanCurX
+ beq wsLeftBndDone
+ sep #0x20
+ .a8
+ lda wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ cmp wsMatchByte
+ beq wsLBndStop2
+ cmp wsNewByte
+ beq wsLBndStop2
+ rep #0x20
+ .a16
+ dec wsScanCurX
+ bra wsLBndEvenEntry
+wsLBndStop2:
+ rep #0x20
+ .a16
+wsLeftBndDone:
+ lda wsScanCurX
+ sta gFloodLeftX
+
+; --- WALK RIGHT BND (byte-cached) ---
+ lda wsX
+ sta wsScanCurX
+ lsr a
+ tay
+ bcs wsRBndOddEntry
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ rep #0x20
+ .a16
+ bra wsRBndEvenEntry
+
+wsRBndEvenEntry:
+ lda wsScanCurX
+ cmp #319
+ bcs wsRightBndDone
+ sep #0x20
+ .a8
+ lda wsScanByte
+ and #0x0F
+ cmp wsMatchByte
+ beq wsRBndStop
+ cmp wsNewByte
+ beq wsRBndStop
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ bra wsRBndOddEntry2
+wsRBndStop:
+ rep #0x20
+ .a16
+ bra wsRightBndDone
+wsRBndOddEntry2:
+wsRBndOddEntry:
+ lda wsScanCurX
+ cmp #319
+ bcs wsRightBndDone
+ iny
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ sta wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a
+ cmp wsMatchByte
+ beq wsRBndStop2
+ cmp wsNewByte
+ beq wsRBndStop2
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ bra wsRBndEvenEntry
+wsRBndStop2:
+ rep #0x20
+ .a16
+wsRightBndDone:
+ lda wsScanCurX
+ sta gFloodRightX
+
+wsAfterWalk:
+
+; Cache leftX/rightX/spanLen for the scans.
+ lda gFloodLeftX
+ sta wsLeftX
+ lda gFloodRightX
+ sta wsRightX
+ sec
+ sbc wsLeftX
+ inc a
+ sta wsSpanLen
+
+; === FILL THE SPAN ===
+ lda wsLeftX
+ lsr a ; A = leadingByte; carry = leftX & 1
+ tay ; Y = leadingByte
+ bcc wsFillNoLead
+; Leading partial byte: low nibble of byte[Y] = newNibble.
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ and #0xF0
+ ora wsNewByte
+ sta [wsRow],y
+ rep #0x20
+ .a16
+ iny ; midStart = leadingByte + 1
+wsFillNoLead:
+
+; midEnd = (rightX + 1) >> 1; trailing partial only if rightX even.
+ lda wsRightX
+ inc a ; A = pxEnd = rightX + 1
+ lsr a ; A = midEnd; carry = pxEnd & 1
+ sta wsMidEnd ; DP store (so DP-direct CPY works)
+ bcs wsFillSetTrail
+ lda #0
+ sta wsHasTrail
+ bra wsFillMidLoop
+wsFillSetTrail:
+ lda #1
+ sta wsHasTrail
+
+wsFillMidLoop:
+ sep #0x20
+ .a8
+ lda wsDoubledByte
+wsFillMidIter:
+ cpy wsMidEnd ; DP-direct CPY (X=16-bit reads 2 bytes)
+ bcs wsFillMidDone
+ sta [wsRow],y
+ iny
+ bra wsFillMidIter
+wsFillMidDone:
+ rep #0x20
+ .a16
+
+ lda wsHasTrail
+ beq wsFillDone
+; Trailing partial byte: high nibble of byte[wsMidEnd] = newNibble.
+ ldy wsMidEnd ; Y = trailing byte index
+ sep #0x20
+ .a8
+ lda [wsRow],y
+ and #0x0F
+ ora wsNewHigh
+ sta [wsRow],y
+ rep #0x20
+ .a16
+wsFillDone:
+
+; Load sp from *spInOut, cache maxSp.
+ ldy #0
+ lda [wsSpInOut],y
+ sta wsSp
+ lda wsMaxSp
+ sta wsMaxSpCache
+
+; === SCAN ABOVE (if y > 0) ===
+ lda wsY
+ bne wsHasAbove
+ brl wsSkipAbove
+wsHasAbove:
+ lda wsRow
+ sec
+ sbc #160
+ sta wsScanRow
+ lda wsRow+2
+ sbc #0
+ sta wsScanRow+2
+ lda wsY
+ dec a
+ sta wsScanY
+ jsr wsScanAndPush
+wsSkipAbove:
+
+; === SCAN BELOW (if y < 199) ===
+ lda wsY
+ cmp #199
+ bcc wsHasBelow
+ brl wsSkipBelow
+wsHasBelow:
+ lda wsRow
+ clc
+ adc #160
+ sta wsScanRow
+ lda wsRow+2
+ adc #0
+ sta wsScanRow+2
+ lda wsY
+ inc a
+ sta wsScanY
+ jsr wsScanAndPush
+wsSkipBelow:
+
+; Store updated sp back to *spInOut.
+ ldy #0
+ lda wsSp
+ sta [wsSpInOut],y
+
+wsExit:
+ .a8
+ .i8
+ pld
+ plb
+ plp
+ rtl
+
+
+; wsScanAndPush: walk wsScanRow[wsLeftX..wsRightX] for run-edge
+; transitions. Pushes (curX-1, wsScanY) on falling edges and
+; (rightX, wsScanY) at end of trailing run. 2 pixels per byte read.
+wsScanAndPush:
+ sep #0x20
+ .a8
+ lda #0
+ sta wsScanPrevHit
+ lda wsEqByte
+ rep #0x20
+ .a16
+ bne wsEqEntry2
+ brl wsBoundEntry2
+
+; ***** EQUAL MODE *****
+wsEqEntry2:
+ lda wsLeftX
+ sta wsScanCurX ; DP write 16-bit
+ lsr a ; A = byteIdx; carry = leftX & 1
+ tay ; Y = byteIdx
+ bcc wsEqAfterLead2
+
+; LEADING: low nibble of byte at Y (curX = leftX, odd).
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ and #0x0F
+ cmp wsMatchByte
+ beq wsEqLeadHit2
+ lda #0
+ bra wsEqLeadStore2
+wsEqLeadHit2:
+ lda #1
+wsEqLeadStore2:
+ sta wsScanCurHit
+ bne wsEqLeadNoFall2
+ lda wsScanPrevHit
+ beq wsEqLeadNoFall2
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsEqLeadNoFall2:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ iny
+
+wsEqAfterLead2:
+; Pair loop: while curX+1 <= rightX, process high+low of one byte.
+wsEqPairLoop2:
+ lda wsScanCurX
+ inc a
+ cmp wsRightX
+ bcc wsEqDoPair2
+ beq wsEqDoPair2
+ brl wsEqTrailing2
+
+wsEqDoPair2:
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ sta wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a ; A = high nibble (pixel at curX)
+ cmp wsMatchByte
+ beq wsEqHi2Hit
+ lda #0
+ bra wsEqHi2Store
+wsEqHi2Hit:
+ lda #1
+wsEqHi2Store:
+ sta wsScanCurHit
+ bne wsEqHi2NoFall
+ lda wsScanPrevHit
+ beq wsEqHi2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsEqHi2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+
+ sep #0x20
+ .a8
+ lda wsScanByte
+ and #0x0F ; A = low nibble (pixel at curX after inc)
+ cmp wsMatchByte
+ beq wsEqLo2Hit
+ lda #0
+ bra wsEqLo2Store
+wsEqLo2Hit:
+ lda #1
+wsEqLo2Store:
+ sta wsScanCurHit
+ bne wsEqLo2NoFall
+ lda wsScanPrevHit
+ beq wsEqLo2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsEqLo2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ iny
+ brl wsEqPairLoop2
+
+wsEqTrailing2:
+ lda wsScanCurX
+ cmp wsRightX
+ bcc wsEqDoTrail2
+ beq wsEqDoTrail2
+ brl wsScanDone
+
+wsEqDoTrail2:
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ lsr a
+ lsr a
+ lsr a
+ lsr a ; A = high nibble
+ cmp wsMatchByte
+ beq wsEqTr2Hit
+ lda #0
+ bra wsEqTr2Store
+wsEqTr2Hit:
+ lda #1
+wsEqTr2Store:
+ sta wsScanCurHit
+ bne wsEqTr2NoFall
+ lda wsScanPrevHit
+ beq wsEqTr2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsEqTr2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ brl wsScanDone
+
+; ***** BOUNDARY MODE *****
+wsBoundEntry2:
+ lda wsLeftX
+ sta wsScanCurX
+ lsr a
+ tay
+ bcc wsBndAfterLead2
+
+; LEADING: low nibble of byte at Y, boundary mode.
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ and #0x0F
+ cmp wsMatchByte
+ beq wsBndLeadMiss2
+ cmp wsNewByte
+ beq wsBndLeadMiss2
+ lda #1
+ bra wsBndLeadStore2
+wsBndLeadMiss2:
+ lda #0
+wsBndLeadStore2:
+ sta wsScanCurHit
+ bne wsBndLeadNoFall2
+ lda wsScanPrevHit
+ beq wsBndLeadNoFall2
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsBndLeadNoFall2:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ iny
+
+wsBndAfterLead2:
+wsBndPairLoop2:
+ lda wsScanCurX
+ inc a
+ cmp wsRightX
+ bcc wsBndDoPair2
+ beq wsBndDoPair2
+ brl wsBndTrailing2
+
+wsBndDoPair2:
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ sta wsScanByte
+ lsr a
+ lsr a
+ lsr a
+ lsr a ; A = high nibble
+ cmp wsMatchByte
+ beq wsBndHi2Miss
+ cmp wsNewByte
+ beq wsBndHi2Miss
+ lda #1
+ bra wsBndHi2Store
+wsBndHi2Miss:
+ lda #0
+wsBndHi2Store:
+ sta wsScanCurHit
+ bne wsBndHi2NoFall
+ lda wsScanPrevHit
+ beq wsBndHi2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsBndHi2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+
+ sep #0x20
+ .a8
+ lda wsScanByte
+ and #0x0F ; A = low nibble
+ cmp wsMatchByte
+ beq wsBndLo2Miss
+ cmp wsNewByte
+ beq wsBndLo2Miss
+ lda #1
+ bra wsBndLo2Store
+wsBndLo2Miss:
+ lda #0
+wsBndLo2Store:
+ sta wsScanCurHit
+ bne wsBndLo2NoFall
+ lda wsScanPrevHit
+ beq wsBndLo2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsBndLo2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ inc wsScanCurX
+ iny
+ brl wsBndPairLoop2
+
+wsBndTrailing2:
+ lda wsScanCurX
+ cmp wsRightX
+ bcc wsBndDoTrail2
+ beq wsBndDoTrail2
+ brl wsScanDone
+
+wsBndDoTrail2:
+ sep #0x20
+ .a8
+ lda [wsScanRow],y
+ lsr a
+ lsr a
+ lsr a
+ lsr a ; A = high nibble
+ cmp wsMatchByte
+ beq wsBndTr2Miss
+ cmp wsNewByte
+ beq wsBndTr2Miss
+ lda #1
+ bra wsBndTr2Store
+wsBndTr2Miss:
+ lda #0
+wsBndTr2Store:
+ sta wsScanCurHit
+ bne wsBndTr2NoFall
+ lda wsScanPrevHit
+ beq wsBndTr2NoFall
+ rep #0x20
+ .a16
+ lda wsScanCurX
+ dec a
+ sta wsPushX
+ jsr wsPushXY
+ sep #0x20
+ .a8
+wsBndTr2NoFall:
+ lda wsScanCurHit
+ sta wsScanPrevHit
+ rep #0x20
+ .a16
+ brl wsScanDone
+
+wsScanDone:
+; Trailing run: if prevHit, push (rightX, scanY).
+ sep #0x20
+ .a8
+ lda wsScanPrevHit
+ rep #0x20
+ .a16
+ beq wsScanReturn
+ lda wsRightX
+ sta wsPushX
+ jsr wsPushXY
+wsScanReturn:
+ rts
+
+
+; wsPushXY: push (wsPushX, wsScanY) at sp if sp> 1)];
- if (x & 1) {
- *byte = (uint8_t)((*byte & 0xF0u) | nibLo);
- } else {
- *byte = (uint8_t)((*byte & 0x0Fu) | nibHi);
- }
-}
-
-
-// Bresenham midpoint-circle outline. Plots all 8 octants per
-// iteration. Caller must verify cx +/- r and cy +/- r are all in
-// surface bounds (the halFastDrawCircle contract).
-static inline void draw68kCircleOutline(uint8_t *pixels, int16_t cx, int16_t cy,
- uint16_t r, uint8_t colorIndex) {
- int16_t x;
- int16_t y;
- int16_t err;
- uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
- uint8_t nibHi = (uint8_t)(nibLo << 4);
-
- if (r == 0u) {
- draw68kPlotPixel(pixels, cx, cy, nibLo, nibHi);
- return;
- }
-
- x = (int16_t)r;
- y = 0;
- err = (int16_t)(1 - x);
- while (x >= y) {
- draw68kPlotPixel(pixels, (int16_t)(cx + x), (int16_t)(cy + y), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx - x), (int16_t)(cy + y), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx + x), (int16_t)(cy - y), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx - x), (int16_t)(cy - y), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx + y), (int16_t)(cy + x), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx - y), (int16_t)(cy + x), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx + y), (int16_t)(cy - x), nibLo, nibHi);
- draw68kPlotPixel(pixels, (int16_t)(cx - y), (int16_t)(cy - x), nibLo, nibHi);
- y++;
- if (err <= 0) {
- err = (int16_t)(err + y + y + 1);
- } else {
- x--;
- err = (int16_t)(err + y + y - x - x + 1);
- }
- }
-}
-
-
-// Filled circle: for each y from 0..r, find the largest x with
-// x*x + y*y <= r*r and emit the symmetric horizontal span. Caller
-// must verify the bounding box is on-surface.
-//
-// Each span is a horizontal run on a single row, so we go straight
-// to the byte-fill path used by halFastFillRect-style code rather
-// than calling per-pixel helpers.
-static inline void draw68kCircleFill(uint8_t *pixels, int16_t cx, int16_t cy,
- uint16_t r, uint8_t colorIndex) {
- int16_t y;
- int16_t x;
- uint16_t xx;
- uint16_t yy;
- uint16_t r2;
- uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
- uint8_t nibHi = (uint8_t)(nibLo << 4);
- uint8_t doubled = (uint8_t)(nibHi | nibLo);
-
- if (r == 0u) {
- draw68kPlotPixel(pixels, cx, cy, nibLo, nibHi);
- return;
- }
-
- xx = (uint16_t)(r * r);
- r2 = xx;
- yy = 0;
- x = (int16_t)r;
-
- for (y = 0; y <= (int16_t)r; y++) {
- int16_t spanLeft;
- int16_t spanRight;
- int16_t rowsRemaining;
- int16_t rowYDelta;
-
- while (xx + yy > r2) {
- xx = (uint16_t)(xx - (uint16_t)((uint16_t)x + (uint16_t)x - 1u));
- x--;
- }
-
- spanLeft = (int16_t)(cx - x);
- spanRight = (int16_t)(cx + x);
- rowsRemaining = (y == 0) ? 1 : 2;
- for (rowYDelta = 0; rowYDelta < rowsRemaining; rowYDelta++) {
- int16_t rowY = (rowYDelta == 0) ? (int16_t)(cy + y) : (int16_t)(cy - y);
- uint8_t *rowBase = &pixels[(uint16_t)rowY * (uint16_t)SURFACE_BYTES_PER_ROW];
- int16_t px = spanLeft;
-
- /* Leading partial nibble. */
- if (px & 1) {
- uint8_t *byte = &rowBase[(uint16_t)px >> 1];
- *byte = (uint8_t)((*byte & 0xF0u) | nibLo);
- px++;
- }
- /* Middle whole bytes. */
- {
- int16_t midBytes = (int16_t)((spanRight + 1 - px) >> 1);
- if (midBytes > 0) {
- uint8_t *p = &rowBase[(uint16_t)px >> 1];
- int16_t i;
- for (i = 0; i < midBytes; i++) {
- p[i] = doubled;
- }
- px = (int16_t)(px + (midBytes << 1));
- }
- }
- /* Trailing partial nibble. */
- if (px <= spanRight) {
- uint8_t *byte = &rowBase[(uint16_t)px >> 1];
- *byte = (uint8_t)((*byte & 0x0Fu) | nibHi);
- }
- }
-
- yy = (uint16_t)(yy + (uint16_t)((uint16_t)y + (uint16_t)y + 1u));
- }
-}
-
-
-// Bresenham line plot. Caller must verify both endpoints are on
-// surface (halFastDrawLine contract). Falls through to a tight
-// inner loop with no per-pixel function calls.
-static inline void draw68kLine(uint8_t *pixels, int16_t x0, int16_t y0,
- int16_t x1, int16_t y1, uint8_t colorIndex) {
- int16_t dx;
- int16_t dy;
- int16_t sx;
- int16_t sy;
- int16_t err;
- int16_t e2;
- uint8_t nibLo = (uint8_t)(colorIndex & 0x0Fu);
- uint8_t nibHi = (uint8_t)(nibLo << 4);
-
- dx = (int16_t)(x1 - x0);
- if (dx < 0) {
- dx = (int16_t)(-dx);
- }
- dy = (int16_t)(y1 - y0);
- if (dy < 0) {
- dy = (int16_t)(-dy);
- }
- sx = (x0 < x1) ? 1 : -1;
- sy = (y0 < y1) ? 1 : -1;
- err = (int16_t)(dx - dy);
-
- for (;;) {
- draw68kPlotPixel(pixels, x0, y0, nibLo, nibHi);
- if (x0 == x1 && y0 == y1) {
- break;
- }
- e2 = (int16_t)(err + err);
- if (e2 > -dy) {
- err = (int16_t)(err - dy);
- x0 = (int16_t)(x0 + sx);
- }
- if (e2 < dx) {
- err = (int16_t)(err + dx);
- y0 = (int16_t)(y0 + sy);
- }
- }
-}
-
-
-#endif /* JOEYLIB_DRAW68K_INLINE_H */
diff --git a/src/shared68k/surface68k.s b/src/shared68k/surface68k.s
index 3537723..3de9840 100644
--- a/src/shared68k/surface68k.s
+++ b/src/shared68k/surface68k.s
@@ -1,10 +1,11 @@
-| Shared 68000 fast paths for the chunky surface buffer.
+| Shared 68000 fast path for the planar surface buffer.
|
-| Both Amiga and Atari ST keep the JoeyLib stage / surfaces in the
-| same 4bpp packed (2 px / byte, 160 bytes / row, 32000 bytes / surface)
-| format -- the c2p / blit-to-screen step that converts to native
-| pixel layout happens only at present time. So the per-byte buffer
-| operations (fill, clear) can be shared across both ports.
+| Holds surface68kFillSpan4Planes -- the per-row body of the 4-plane
+| planar rect/circle fill. Only the Amiga port calls it (ST is
+| word-interleaved and uses its own St* routines); it lives here
+| because the file is globbed into both ports' builds. The earlier
+| chunky clear/fill primitives that this file once also held were
+| removed once both ports went native-planar.
|
| Calling convention: cdecl (gcc-amigaos / gcc-atari-mint).
| d0/d1/a0/a1 are caller-save (scratch).
@@ -16,245 +17,6 @@
.text
-| ----------------------------------------------------------------
-| void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
-|
-| Fill 32000 bytes (SURFACE_PIXELS_SIZE) starting at `pixels` with
-| `fillByte`. Unrolled move.l with the byte expanded to a 4-byte
-| word so we can write 32 bytes per loop body instead of 1.
-| ----------------------------------------------------------------
- .globl _surface68kClearLong
-
- .equ SURF_CLEAR_BYTES, 32000
- .equ SURF_CLEAR_LONGS, (SURF_CLEAR_BYTES / 4)
- .equ SURF_CLEAR_PER_ITER, 8
- .equ SURF_CLEAR_ITERS, (SURF_CLEAR_LONGS / SURF_CLEAR_PER_ITER)
-
- | Save d2 (used as dbra counter). a0/a1/d0/d1 are
- | caller-save so we may clobber them freely. Stack
- | offset to args = 4 (saved d2) + 4 (return PC) = 8.
- .equ CLR_SAVED, 4
-
-_surface68kClearLong:
- move.l %d2,-(%sp)
-
- move.l 4+CLR_SAVED(%sp),%a0 | pixels
- | Zero d0 BEFORE the move.b -- m68k move.b only touches
- | the low byte of the destination, so d0[31..8] would
- | otherwise hold whatever garbage the caller left in d0
- | and pollute the OR-replicate chain below.
- moveq #0,%d0
- move.b 8+CLR_SAVED+3(%sp),%d0 | fillByte (low byte of int)
-
- | Build a long with fillByte replicated four times.
- | d0 = $000000FB -> $0000FBFB -> $FBFBFBFB
- move.l %d0,%d1
- lsl.l #8,%d1
- or.l %d1,%d0 | d0 = $0000FBFB
- move.l %d0,%d1
- swap %d1 | d1 = $FBFB0000
- or.l %d1,%d0 | d0 = $FBFBFBFB
-
- move.l %a0,%a1 | dest cursor
- move.w #(SURF_CLEAR_ITERS - 1),%d2 | dbra count
-
-.LclearLoop:
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- dbra %d2,.LclearLoop
-
- move.l (%sp)+,%d2
- rts
-
-
-| ----------------------------------------------------------------
-| void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h,
-| uint16_t fillByte);
-|
-| Full-width (320 px = 160 byte) rectangle fill. `pixels` is the
-| surface base; the row at `y` is `pixels + y * 160`. Fills `h`
-| consecutive rows using move.l writes.
-| ----------------------------------------------------------------
- .globl _surface68kFillRectFull
-
- .equ SURF_FRF_BYTES_PER_ROW, 160
-
- | Save d2/d3 (used for fillByte replicate scratch and
- | row counter). 8 bytes saved + 4 ret PC = 12 to args.
- .equ FRF_SAVED, 8
-
-_surface68kFillRectFull:
- movem.l %d2-%d3,-(%sp)
-
- move.l 4+FRF_SAVED(%sp),%a0 | pixels base
- | Zero d0/d1 BEFORE move.b/move.w -- those only update
- | sub-register portions and would otherwise carry the
- | caller's garbage upper bits into the replicate OR.
- moveq #0,%d0
- moveq #0,%d1
- moveq #0,%d2
- move.w 8+FRF_SAVED+2(%sp),%d1 | y (low word)
- move.w 12+FRF_SAVED+2(%sp),%d2 | h (low word)
- move.b 16+FRF_SAVED+3(%sp),%d0 | fillByte
-
- | Bail on degenerate height.
- tst.w %d2
- ble .Lfrf_done
-
- | Build replicated fillByte in d0.
- move.l %d0,%d3
- lsl.l #8,%d3
- or.l %d3,%d0
- move.l %d0,%d3
- swap %d3
- or.l %d3,%d0 | d0 = byte * $01010101
-
- | row pointer = pixels + y * 160 = pixels + y*128 + y*32
- ext.l %d1
- move.l %d1,%d3
- lsl.l #7,%d3 | y * 128
- lsl.l #5,%d1 | y * 32
- add.l %d3,%d1 | y * 160
- add.l %d1,%a0 | a0 = first row to fill
-
- subq.w #1,%d2 | row dbra count
-
-.Lfrf_rowLoop:
- move.l %a0,%a1
- | 40 longs per row, fully unrolled.
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- move.l %d0,(%a1)+
- lea SURF_FRF_BYTES_PER_ROW(%a0),%a0
- dbra %d2,.Lfrf_rowLoop
-
-.Lfrf_done:
- movem.l (%sp)+,%d2-%d3
- rts
-
-
-| ----------------------------------------------------------------
-| void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes,
-| uint16_t h, uint16_t fillByte);
-|
-| Per-row middle-bytes fill for a jlFillRect where the leading and
-| trailing nibble fixups have already been done (or none are
-| needed because x and w are both even). `rowFirst` points at the
-| first FULLY-INSIDE byte of the rect on row 0; subsequent rows
-| are at +160. `midBytes` is how many full bytes per row to fill.
-| ----------------------------------------------------------------
- .globl _surface68kFillRectByteAligned
-
- .equ SURF_FRB_STRIDE, 160
-
- | Save d2-d6 (5 regs = 20 bytes). Args at +20+4 = +24.
- .equ FRB_SAVED, 20
-
-_surface68kFillRectByteAligned:
- movem.l %d2-%d6,-(%sp)
-
- move.l 4+FRB_SAVED(%sp),%a0 | rowFirst
- | Zero scratch regs BEFORE the sub-register loads --
- | move.w / move.b only touch low portions, leaving
- | caller's garbage in the upper bits which would
- | otherwise pollute the replicate-OR chain below.
- moveq #0,%d0
- moveq #0,%d1
- moveq #0,%d2
- move.w 8+FRB_SAVED+2(%sp),%d1 | midBytes
- move.w 12+FRB_SAVED+2(%sp),%d2 | h
- move.b 16+FRB_SAVED+3(%sp),%d0 | fillByte
-
- tst.w %d1
- beq .Lfrb_done
- tst.w %d2
- ble .Lfrb_done
-
- | Build replicated fillByte in d0.
- move.l %d0,%d3
- lsl.l #8,%d3
- or.l %d3,%d0
- move.l %d0,%d3
- swap %d3
- or.l %d3,%d0
-
- | Pre-compute long-count and trailing-byte residual.
- move.w %d1,%d4
- lsr.w #2,%d4 | longCount
- move.w %d1,%d5
- and.w #3,%d5 | trailBytes
-
- subq.w #1,%d2 | row dbra count
-
-.Lfrb_rowLoop:
- move.l %a0,%a1
- move.w %d4,%d6 | longs left
- tst.w %d6
- beq .Lfrb_tail
- subq.w #1,%d6
-.Lfrb_longLoop:
- move.l %d0,(%a1)+
- dbra %d6,.Lfrb_longLoop
-.Lfrb_tail:
- move.w %d5,%d6 | trailing bytes
- tst.w %d6
- beq .Lfrb_rowDone
- subq.w #1,%d6
-.Lfrb_byteLoop:
- move.b %d0,(%a1)+
- dbra %d6,.Lfrb_byteLoop
-.Lfrb_rowDone:
- lea SURF_FRB_STRIDE(%a0),%a0
- dbra %d2,.Lfrb_rowLoop
-
-.Lfrb_done:
- movem.l (%sp)+,%d2-%d6
- rts
-
-
| ----------------------------------------------------------------
| void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1,
| uint8_t *p2, uint8_t *p3,
@@ -283,13 +45,15 @@ _surface68kFillRectByteAligned:
| -- branchless: the same arithmetic produces "set" or "clear" based
| on whether fbN is 0xFF or 0x00.
|
-| ABI: m68k cdecl. d2-d7/a2-a6 callee-save (movem'd here).
-| Stack offset to first arg after MOVEM: 11 regs * 4 = 44 bytes saved
-| + 4 ret PC = 48.
+| ABI: m68k cdecl. d2-d7/a2-a6 callee-save. Only a2/a3 are actually
+| used by the body (planes 2/3); a4-a6 are not touched, so only
+| d2-d7/a2-a3 are movem'd here.
+| Stack offset to first arg after MOVEM: 8 regs * 4 = 32 bytes saved
+| + 4 ret PC = 36.
| ----------------------------------------------------------------
.globl _surface68kFillSpan4Planes
- .equ SP_SAVED, 44
+ .equ SP_SAVED, 32
.equ SP_RPC, 4
.equ SP_OFF, (SP_SAVED + SP_RPC)
@@ -305,43 +69,56 @@ _surface68kFillRectByteAligned:
.equ SP_FB2, SP_OFF + 36 + 3
.equ SP_FB3, SP_OFF + 40 + 3
-| Macro: per-plane work fully inlined. Args:
-| plane_an = the address register holding this plane's pointer.
-| fb_off = the stack offset for this plane's fillByte.
-| Uses d6/d7 as scratch; d1=leftMask, d2=~leftMask, d3=rightMask,
-| d4=~rightMask; d0=numMid-1 (only valid if mid_count > 0). The mid
-| loop is skipped via .LfsSkipMid_ when numMid was 0 at entry --
-| the per-plane caller branches to the right tail label.
+| Per-plane work is fully inlined (hand-unrolled 4x rather than
+| bsr+rts, to dodge ~12 cyc per return + the per-plane re-test of
+| numMid that an earlier build paid). Register convention across all
+| four planes:
+| d1 = leftMask, d2 = ~leftMask, d3 = rightMask, d4 = ~rightMask
+| d5 = this plane's fb byte, d6/d0/d7 = per-plane scratch.
|
-| Hand-unrolled 4x rather than using bsr+rts to dodge ~12 cyc per
-| return + the per-plane re-test of numMid that the previous build
-| paid. The mid-loop label suffix is the plane index so all four
-| inline copies can coexist without label collisions.
+| The unmasked middle run is filled with long stores rather than
+| byte-at-a-time. fb is 0x00 or 0xFF, so the long is built with a
+| byte->word->long sign-extend (ext.w / ext.l). The mid pointer is
+| advanced to a long boundary by storing 0-3 fb bytes (align run),
+| then move.l (an)+ fills (midRem >> 2) longs, then a 0-3 byte tail
+| drains the remainder. The masked left/right edge bytes keep their
+| per-plane read-modify-write; only the unmasked middle changed.
|
| Plain text version of the per-plane body (translate to asm 4x with
| different a-regs and fb stack offsets):
|
-| move.b (an),%d6
+| move.b (an),%d6 | --- leading masked edge byte ---
| and.b %d2,%d6
-| move.b fb,%d7
+| move.b fb,%d5
+| move.b %d5,%d7
| and.b %d1,%d7
| or.b %d7,%d6
| move.b %d6,(an)+
| < if has-middle path: >
-| move.w %d0,%d7
-| .midN:
-| move.b fb,(an)+
-| dbra %d7,.midN
-| < trailing: >
+| move.b %d5,%d7 | build replicated fill long in d7
+| ext.w %d7
+| ext.l %d7
+| move.l an,%d6 | align = (-an) & 3, clamped to numMid
+| neg.l %d6
+| and.w #3,%d6
+| move.w numMid,%d0
+| < clamp d6 to d0; d0 = midRem = numMid - align >
+| < store d6 fb bytes to reach a long boundary >
+| < midLongs = midRem >> 2; tail = midRem & 3 >
+| .longN:
+| move.l %d7,(an)+
+| dbra midLongs,.longN
+| < store tail fb bytes >
+| < trailing masked edge byte: >
| move.b (an),%d6
| and.b %d4,%d6
-| move.b fb,%d7
+| move.b %d5,%d7
| and.b %d3,%d7
| or.b %d7,%d6
| move.b %d6,(an)
_surface68kFillSpan4Planes:
- movem.l %d2-%d7/%a2-%a6,-(%sp)
+ movem.l %d2-%d7/%a2-%a3,-(%sp)
move.b SP_LMASK(%sp),%d1
move.b %d1,%d2
@@ -356,12 +133,27 @@ _surface68kFillSpan4Planes:
move.l SP_P3(%sp),%a3
| One-time numMid test. d0.w = numMid; if 0 jump to
- | the no-middle entry, otherwise pre-decrement for dbra
- | and fall into the with-middle entry. Both paths
- | unroll all 4 planes.
+ | the no-middle entry. The with-middle path now fills
+ | the unmasked middle run with long stores (move.l
+ | (an)+) instead of byte-at-a-time, after aligning the
+ | mid pointer to a long boundary and draining a 0-3
+ | byte tail. The masked left/right edge bytes keep their
+ | per-plane read-modify-write exactly as before.
+ |
+ | The mid fill byte fbN is 0x00 or 0xFF, so the long
+ | replication is a byte->word->long sign-extend (ext.w
+ | then ext.l): 0xFF -> 0xFFFFFFFF, 0x00 -> 0x00000000.
+ |
+ | Per-plane register use during the mid run:
+ | d5 = fbN byte (held for tail + trailing edge)
+ | d7 = fbN replicated to a long (move.l fill value)
+ | d6 = scratch / align count / midLongs count
+ | d0 = numMid, then midRem, then tail-byte count
+ | Masks d1-d4 stay live across all four planes. Local
+ | numeric labels 1f-7f are reused per plane; each
+ | reference resolves to the nearest copy.
move.w SP_NMID(%sp),%d0
beq .LfsNoMid
- subq.w #1,%d0
| ---- WITH-MIDDLE PATH ----
| Plane 0
@@ -372,10 +164,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a0)+
- move.w %d0,%d7
-.LfsMid0: move.b %d5,(%a0)+
- dbra %d7,.LfsMid0
- move.b (%a0),%d6
+ move.b %d5,%d7
+ ext.w %d7
+ ext.l %d7
+ move.l %a0,%d6
+ neg.l %d6
+ and.w #3,%d6
+ move.w SP_NMID(%sp),%d0
+ cmp.w %d0,%d6
+ bls.s 1f
+ move.w %d0,%d6
+1: sub.w %d6,%d0
+ tst.w %d6
+ beq.s 2f
+3: move.b %d5,(%a0)+
+ subq.w #1,%d6
+ bne.s 3b
+2: move.w %d0,%d6
+ lsr.w #2,%d6
+ and.w #3,%d0
+ tst.w %d6
+ beq.s 5f
+ subq.w #1,%d6
+4: move.l %d7,(%a0)+
+ dbra %d6,4b
+5: tst.w %d0
+ beq.s 7f
+6: move.b %d5,(%a0)+
+ subq.w #1,%d0
+ bne.s 6b
+7: move.b (%a0),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@@ -390,10 +208,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a1)+
- move.w %d0,%d7
-.LfsMid1: move.b %d5,(%a1)+
- dbra %d7,.LfsMid1
- move.b (%a1),%d6
+ move.b %d5,%d7
+ ext.w %d7
+ ext.l %d7
+ move.l %a1,%d6
+ neg.l %d6
+ and.w #3,%d6
+ move.w SP_NMID(%sp),%d0
+ cmp.w %d0,%d6
+ bls.s 1f
+ move.w %d0,%d6
+1: sub.w %d6,%d0
+ tst.w %d6
+ beq.s 2f
+3: move.b %d5,(%a1)+
+ subq.w #1,%d6
+ bne.s 3b
+2: move.w %d0,%d6
+ lsr.w #2,%d6
+ and.w #3,%d0
+ tst.w %d6
+ beq.s 5f
+ subq.w #1,%d6
+4: move.l %d7,(%a1)+
+ dbra %d6,4b
+5: tst.w %d0
+ beq.s 7f
+6: move.b %d5,(%a1)+
+ subq.w #1,%d0
+ bne.s 6b
+7: move.b (%a1),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@@ -408,10 +252,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a2)+
- move.w %d0,%d7
-.LfsMid2: move.b %d5,(%a2)+
- dbra %d7,.LfsMid2
- move.b (%a2),%d6
+ move.b %d5,%d7
+ ext.w %d7
+ ext.l %d7
+ move.l %a2,%d6
+ neg.l %d6
+ and.w #3,%d6
+ move.w SP_NMID(%sp),%d0
+ cmp.w %d0,%d6
+ bls.s 1f
+ move.w %d0,%d6
+1: sub.w %d6,%d0
+ tst.w %d6
+ beq.s 2f
+3: move.b %d5,(%a2)+
+ subq.w #1,%d6
+ bne.s 3b
+2: move.w %d0,%d6
+ lsr.w #2,%d6
+ and.w #3,%d0
+ tst.w %d6
+ beq.s 5f
+ subq.w #1,%d6
+4: move.l %d7,(%a2)+
+ dbra %d6,4b
+5: tst.w %d0
+ beq.s 7f
+6: move.b %d5,(%a2)+
+ subq.w #1,%d0
+ bne.s 6b
+7: move.b (%a2),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@@ -426,17 +296,43 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a3)+
- move.w %d0,%d7
-.LfsMid3: move.b %d5,(%a3)+
- dbra %d7,.LfsMid3
- move.b (%a3),%d6
+ move.b %d5,%d7
+ ext.w %d7
+ ext.l %d7
+ move.l %a3,%d6
+ neg.l %d6
+ and.w #3,%d6
+ move.w SP_NMID(%sp),%d0
+ cmp.w %d0,%d6
+ bls.s 1f
+ move.w %d0,%d6
+1: sub.w %d6,%d0
+ tst.w %d6
+ beq.s 2f
+3: move.b %d5,(%a3)+
+ subq.w #1,%d6
+ bne.s 3b
+2: move.w %d0,%d6
+ lsr.w #2,%d6
+ and.w #3,%d0
+ tst.w %d6
+ beq.s 5f
+ subq.w #1,%d6
+4: move.l %d7,(%a3)+
+ dbra %d6,4b
+5: tst.w %d0
+ beq.s 7f
+6: move.b %d5,(%a3)+
+ subq.w #1,%d0
+ bne.s 6b
+7: move.b (%a3),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
or.b %d7,%d6
move.b %d6,(%a3)
- movem.l (%sp)+,%d2-%d7/%a2-%a6
+ movem.l (%sp)+,%d2-%d7/%a2-%a3
rts
.LfsNoMid:
@@ -501,5 +397,5 @@ _surface68kFillSpan4Planes:
or.b %d7,%d6
move.b %d6,(%a3)
- movem.l (%sp)+,%d2-%d7/%a2-%a6
+ movem.l (%sp)+,%d2-%d7/%a2-%a3
rts
diff --git a/toolchains/env.sh b/toolchains/env.sh
index ca1998a..76a161a 100644
--- a/toolchains/env.sh
+++ b/toolchains/env.sh
@@ -11,29 +11,22 @@ JOEY_TOOLCHAINS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export JOEY_TOOLCHAINS
# ------------------------------------------------------------------------
-# IIgs
+# IIgs (llvm-mos w65816: clang + llvm-mc + link816)
#
-# Two separate trees:
-# - toolchains/iigs/gg-tools/bin/ Unix host binaries (iix, dumpobj, ...)
-# built from the GoldenGate source repo.
-# - toolchains/iigs/goldengate/ The IIgs filesystem iix emulates.
-# $GOLDEN_GATE points here.
-#
-# iix is the host-side command shell that runs ORCA tools (cc, Linker,
-# Asm65816, ...) via 65816 emulation against the $GOLDEN_GATE tree.
+# LLVM816_ROOT is the toolchain root (clang / llvm-mc / link816 plus the
+# prebuilt runtime). clang-build.sh drives compile/assemble/link. It can
+# be overridden to point at a vendored copy; it currently defaults to the
+# build scratchpad where the toolchain was bootstrapped.
+# TODO: vendor the toolchain under toolchains/iigs.
# ------------------------------------------------------------------------
-export GOLDEN_GATE="${JOEY_TOOLCHAINS}/iigs/goldengate"
-export ORCA_ROOT="${GOLDEN_GATE}"
-
+: "${LLVM816_ROOT:=/tmp/claude-1000/-home-scott-claude-joeylib/bf471cbc-f6e2-4287-86b6-58e16cc489e6/scratchpad/65816-llvm-mos}"
+export LLVM816_ROOT
+export IIGS_CLANG_BUILD="${JOEY_TOOLCHAINS}/iigs/clang-build.sh"
+# merlin32 assembles the NinjaTrackerPlus audio replayer (third-party asm).
export IIGS_AS="${JOEY_TOOLCHAINS}/iigs/merlin32/bin/merlin32"
-export IIGS_IIX="${JOEY_TOOLCHAINS}/iigs/gg-tools/bin/iix"
-export IIGS_BUILD="${JOEY_TOOLCHAINS}/iigs/iix-build.sh"
-export IIGS_INCLUDE_SHIM="${JOEY_TOOLCHAINS}/iigs/include-shim"
-# Kept as an alias for IIGS_IIX so the top-level Makefile's HAVE_IIGS
-# availability probe (`[ -x $IIGS_CC ]`) works with the common pattern
-# used for the other platforms.
-export IIGS_CC="${IIGS_IIX}"
+# Alias used by the top-level Makefile's HAVE_IIGS probe (`[ -x $IIGS_CC ]`).
+export IIGS_CC="${IIGS_CLANG_BUILD}"
# ------------------------------------------------------------------------
# Amiga
diff --git a/toolchains/install.sh b/toolchains/install.sh
index 75fd43f..7e7d94c 100755
--- a/toolchains/install.sh
+++ b/toolchains/install.sh
@@ -5,7 +5,7 @@
# Installs all cross-compilers, assemblers, and libraries needed to build
# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
# toolchains/ folder. Nothing is installed system-wide. Free tools are
-# fetched and built; non-free tools (ORCA/C, GoldenGate) print clear
+# fetched and built; tools that must be placed manually print clear
# placement instructions and the script verifies they were placed
# correctly on a subsequent run.
#
@@ -177,7 +177,8 @@ should_install() {
}
# ------------------------------------------------------------------------
-# IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual)
+# IIgs: Merlin32 (free) + NinjaTrackerPlus audio source. The w65816
+# clang/llvm-mos toolchain is referenced via LLVM816_ROOT (see env.sh).
# ------------------------------------------------------------------------
install_audio_libxmp() {
@@ -352,188 +353,6 @@ EOF
fi
fi
- # GoldenGate Unix-side host tools (iix, dumpobj, profuse, ...).
- # These are built from the GoldenGate source repo at stuff/GoldenGate/.
- # They must NOT live inside the IIgs filesystem tree that
- # $GOLDEN_GATE points at, because that tree represents the emulated
- # IIgs volume (case-insensitive, with its own bin/ of IIgs utilities).
- # Install layout:
- # toolchains/iigs/gg-tools/bin/ Unix binaries on host PATH
- # toolchains/iigs/goldengate/ $GOLDEN_GATE root (IIgs filesystem)
- local gg_tools_dir="${base}/gg-tools"
- local gg_root="${base}/goldengate"
- local stuff_dir="${REPO_DIR}/stuff"
-
- # Migrate any Unix tools the user may have placed at goldengate/bin/
- # over to gg-tools/bin/ so the goldengate directory can be repopulated
- # with the IIgs filesystem tree from gg-linux.tgz. iix's siblings gno
- # and orca are typically symlinks to iix -- use -e (not -f) so broken
- # symlinks from a prior partial migration also match.
- if [ -x "${gg_root}/bin/iix" ] && [ ! -x "${gg_tools_dir}/bin/iix" ]; then
- info "Relocating Unix host tools from goldengate/bin to gg-tools/bin"
- mkdir -p "${gg_tools_dir}/bin"
- # Move iix last so the symlinks that point at it resolve during mv.
- for f in dumpobj profuse mkfs-profuse opus-extractor orca gno iix; do
- if [ -e "${gg_root}/bin/${f}" ] || [ -L "${gg_root}/bin/${f}" ]; then
- mv "${gg_root}/bin/${f}" "${gg_tools_dir}/bin/"
- fi
- done
- rmdir "${gg_root}/bin" 2>/dev/null || true
- fi
-
- # Clean up any dangling symlinks that reference a now-moved iix.
- if [ -d "${gg_root}/bin" ]; then
- find "${gg_root}/bin" -maxdepth 1 -type l -xtype l -delete 2>/dev/null || true
- fi
-
- # Recreate gno/orca symlinks next to iix in gg-tools if they're
- # missing (iix behaves differently when invoked as 'gno' or 'orca').
- if [ -x "${gg_tools_dir}/bin/iix" ]; then
- for alias in gno orca; do
- if [ ! -e "${gg_tools_dir}/bin/${alias}" ]; then
- ln -s iix "${gg_tools_dir}/bin/${alias}"
- fi
- done
- fi
-
- # Verify Unix tools
- if [ -x "${gg_tools_dir}/bin/iix" ]; then
- ok "GoldenGate host tools present at ${gg_tools_dir}/bin/"
- STATUS[iigs_gg_tools]="ok"
- else
- STATUS[iigs_gg_tools]="missing"
- INSTRUCTIONS[iigs_gg_tools]=$(cat </dev/null || {
- STATUS[iigs_goldengate]="failed"
- INSTRUCTIONS[iigs_goldengate]="Extract of ${gg_linux_tgz} failed"
- }
-
- # Extract fix-filetypes.sh separately (it sits at gg-linux/ root).
- if [ ! -f "${CACHE_DIR}/fix-filetypes.sh" ]; then
- tar -xzf "${gg_linux_tgz}" -C "${CACHE_DIR}" --strip-components=1 \
- gg-linux/fix-filetypes.sh 2>/dev/null || true
- fi
-
- # FST tarball: merge fst/orca/ subtree into goldengate/.
- if [ -f "${fst_tgz}" ]; then
- info "Merging fst.tgz into goldengate/System/"
- tar -xzf "${fst_tgz}" -C "${gg_root}" --strip-components=2 \
- fst/orca/ 2>/dev/null || warn "fst.tgz extract reported issues"
- else
- warn "${fst_tgz} not found; FSTs will be missing"
- fi
-
- if [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then
- mark_done "iigs_goldengate"
- ok "GoldenGate IIgs filesystem populated"
- STATUS[iigs_goldengate]="ok"
- else
- STATUS[iigs_goldengate]="failed"
- INSTRUCTIONS[iigs_goldengate]="goldengate/ did not end up with System/rom and bin/; check ${gg_linux_tgz}"
- fi
- fi
- fi
-
- # Merge ORCA (from Opus II) directories into goldengate/.
- if is_done "iigs_orca" && [ -f "${gg_root}/Languages/cc" ]; then
- ok "ORCA/C merged into goldengate/"
- STATUS[iigs_orca]="ok"
- else
- if [ "${STATUS[iigs_goldengate]}" != "ok" ]; then
- STATUS[iigs_orca]="missing"
- INSTRUCTIONS[iigs_orca]="Install GoldenGate IIgs filesystem first."
- elif [ ! -d "${opus_dir}" ]; then
- STATUS[iigs_orca]="missing"
- INSTRUCTIONS[iigs_orca]=$(cat </dev/null || orca_ok=0
- else
- warn "${opus_dir}/${sub} missing"
- orca_ok=0
- fi
- done
-
- # Run fix-filetypes.sh which sets IIgs filetype xattrs via chtyp.
- # chtyp is an IIgs binary inside goldengate/bin and is invoked
- # by fix-filetypes through iix, so we need iix on PATH and
- # GOLDEN_GATE exported first.
- if [ "${orca_ok}" -eq 1 ] && [ -f "${CACHE_DIR}/fix-filetypes.sh" ] \
- && [ -x "${gg_tools_dir}/bin/iix" ]; then
- info "Running fix-filetypes.sh"
- (
- cd "${gg_root}"
- export GOLDEN_GATE="${gg_root}"
- export PATH="${gg_tools_dir}/bin:${PATH}"
- bash "${CACHE_DIR}/fix-filetypes.sh" >/dev/null 2>&1
- ) || warn "fix-filetypes.sh reported issues; proceeding"
- fi
-
- if [ "${orca_ok}" -eq 1 ] && [ -f "${gg_root}/Languages/cc" ]; then
- mark_done "iigs_orca"
- ok "ORCA/C merged and filetypes fixed"
- STATUS[iigs_orca]="ok"
- else
- STATUS[iigs_orca]="failed"
- INSTRUCTIONS[iigs_orca]="ORCA merge did not produce ${gg_root}/Languages/cc; check ${opus_dir}/"
- fi
- fi
- fi
-
# NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
# source (ZIP) plus binary tool disk images on ninjaforce.com.
# Redistribution is granted by the copyright holder per the user's
diff --git a/tools/uber-perf-table b/tools/uber-perf-table
index 2492e5d..6561c9c 100755
--- a/tools/uber-perf-table
+++ b/tools/uber-perf-table
@@ -45,6 +45,24 @@ PORTS = [
]
+# Appended below the table when IIgs is the reference port. The IIgs is
+# a 2.8 MHz 65816 moving a 32000-byte SHR screen, so any full-screen
+# byte-move op has a hard throughput ceiling; numbers above it are a
+# measurement artifact, not real speed.
+IIGS_CEILING_NOTE = (
+ "\n"
+ "> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves\n"
+ "> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both\n"
+ "> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So\n"
+ "> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`\n"
+ "> cannot legitimately exceed ~29; a higher reading means the benchmark\n"
+ "> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-\n"
+ "> driven GetTick, so an op that holds `SEI` while it runs can over-\n"
+ "> report its ops/sec. Treat full-screen IIgs numbers above ~29 as\n"
+ "> inflated and cross-check against the cycle model.\n"
+)
+
+
def parse_log(path):
"""Return {op: ops/sec} from a UBER log. Last entry per op wins."""
perf = {}
@@ -144,6 +162,8 @@ def main(argv):
ordered = ref_ops + extras
table = render_table(port_data, ordered, args.ref)
+ if args.ref == "iigs":
+ table += "\n" + IIGS_CEILING_NOTE
if args.out:
with open(args.out, "w") as f:
f.write(table + "\n")