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")