Migrating from ORCA/C to Clang for the IIgs.

This commit is contained in:
Scott Duensing 2026-06-25 03:23:34 -05:00
parent 087bc77a6b
commit de280b7a32
85 changed files with 7646 additions and 7410 deletions

View file

@ -189,7 +189,7 @@ The user should pick which to prioritize.
The repo's auto-memory at The repo's auto-memory at
`~/.claude/projects/-home-scott-claude-joeylib/memory/` includes `~/.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 constraints) plus `project_joeylib_agi_plan.md` which describes the
overall AGI port plan. All persist into the next session overall AGI port plan. All persist into the next session
automatically; no need to reload. automatically; no need to reload.
@ -197,9 +197,8 @@ automatically; no need to reload.
Specifically relevant to the AGI port: Specifically relevant to the AGI port:
- `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate - `project_joeylib_agi_plan.md` — overall plan, 3-4 month estimate
- `feedback_orca_c_atoi_segment.md` — don't use atoi in AGI code - AGI code uses a manual `parseArgU8` instead of atoi to keep the
(it pushes the IIgs ORCA-C stdio cluster past 64 KB; we use a IIgs link budget small.
manual parseArgU8 instead)
- `project_perf_directive.md` — IIgs is the perf floor; every - `project_perf_directive.md` — IIgs is the perf floor; every
other target must match or beat it other target must match or beat it
- `feedback_iigs_speed.md` — IIgs must be absolute fastest - `feedback_iigs_speed.md` — IIgs must be absolute fastest
@ -213,6 +212,6 @@ Specifically relevant to the AGI port:
`feedback_git.md`). `feedback_git.md`).
- Always `source toolchains/env.sh` before any `make` -- the user - Always `source toolchains/env.sh` before any `make` -- the user
has added that to .claude/settings.local.json allow list. 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. - The user does NOT need hand-holding -- they're an expert C dev.
Skip excessive explanation; describe what was done and move on. Skip excessive explanation; describe what was done and move on.

294
IIGS_PERF_PLAN.md Normal file
View file

@ -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.

View file

@ -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 JoeyLib for each target. They are not redistributed inside the
produced game binaries. produced game binaries.
### Golden Gate (host tools: iix, profuse, mkfs-profuse, dumpobj) ### llvm-mos (clang / llvm-mc / link816 w65816 toolchain)
- **Used on:** Apple IIgs - **Used on:** Apple IIgs
- **URL:** <https://golden-gate.ksherlock.com/> - **URL:** <https://github.com/llvm-mos/llvm-mos>
(publisher / store: <https://juiced.gs/store/golden-gate/>) - **License:** Apache License 2.0 with LLVM Exceptions (the upstream
- **License:** **Commercial** software by Kelvin Sherlock, LLVM license).
distributed through Juiced.GS / Gamebits. End-user purchase - **Role:** clang C compiler targeting the 65816, plus llvm-mc for
required; redistribution **not** permitted. assembling hand-written `.s` files and link816 for producing OMF
- **Role:** Cross-host harness that runs ORCA-style IIgs tools on a S16 executables. Driven by `toolchains/iigs/clang-build.sh`; the
modern OS. Provides the `iix` shell, the OMF linker, `profuse` toolchain root is located via the `LLVM816_ROOT` environment
(ProDOS FUSE mount), `mkfs-profuse`, `dumpobj`, and the IIgs-side variable.
filesystem image (`$GOLDEN_GATE`) that the harness emulates. - **Vendored at:** `toolchains/iigs/llvm-mos/`
- **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:** <https://github.com/byteworksinc/ORCA-C>
(commercial distribution: <https://juiced.gs/store/opus-ii/>)
- **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.
### Merlin32 ### Merlin32

12
PERF.md
View file

@ -34,3 +34,15 @@
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x | | jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x | | surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
| c2p full screen | - | - | - | - | | 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.)

View file

@ -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 /* This single-file example can blow the 65816's 64 KB code-bank
* places everything from one TU into one code bank by default. This * limit, so on the IIgs we split code across segments at section
* single-file example easily blows the 64 KB code-bank limit, so we * boundaries to spread the load across multiple banks. */
* 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. */
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVART";
#endif #endif
@ -1103,7 +1098,6 @@ static bool buildAllSprites(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVROOM";
#endif #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 #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVMOVE";
#endif #endif
@ -1869,7 +1862,6 @@ static void drawInventoryBar(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVUI";
#endif #endif
@ -1935,7 +1927,6 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVVERB";
#endif #endif
@ -2213,7 +2204,6 @@ static bool buildBackgrounds(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "";
#endif #endif
@ -2231,7 +2221,6 @@ int main(void) {
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports * ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
* need ~16 KB. */ * need ~16 KB. */
config.codegenBytes = 256UL * 1024; config.codegenBytes = 256UL * 1024;
config.maxSurfaces = 6;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -235,12 +235,10 @@ static bool isTrunkChar(char c) {
} }
/* ORCA-C's auto-segmenter splits at function boundaries; one TU's /* One TU's code can overflow the 65816's 64 KB code bank if it lands
* code can still overflow the 65816's 64 KB code bank if it lands * in a single segment. Split the heavier sections across segments to
* in a single segment. Sprinkle ORCA-only segment directives at the * spread the load across multiple banks. */
* heavier sections to spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADV2ART";
#endif #endif
@ -723,7 +721,6 @@ static void programScbForView(DebugViewE view) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADV2PAINT";
#endif #endif
@ -930,7 +927,6 @@ static void drawEgoMasked(int16_t x, int16_t y, DirE dir, uint8_t frame) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADV2PATH";
#endif #endif
@ -1214,7 +1210,6 @@ static void drawInventoryBar(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "ADV2COMP";
#endif #endif
@ -1328,7 +1323,6 @@ static void handleClick(int16_t mx, int16_t my, bool leftClick, bool rightClick)
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "";
#endif #endif
@ -1346,7 +1340,6 @@ int main(void) {
* be small. We need 4 surfaces (stage + visual + priority + * be small. We need 4 surfaces (stage + visual + priority +
* control). */ * control). */
config.codegenBytes = 4UL * 1024; config.codegenBytes = 4UL * 1024;
config.maxSurfaces = 8;
config.audioBytes = 0; config.audioBytes = 0;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -307,7 +307,6 @@ static uint32_t gAgiV11SubTickAccum; // counts AGI ticks toward next
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIMAIN";
#endif #endif
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
@ -1687,7 +1686,6 @@ static void updateEgoFromInput(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "";
#endif #endif
// ----- Entry point ----- // ----- Entry point -----
@ -1703,7 +1701,6 @@ int main(int argc, char **argv) {
bool vmReady; bool vmReady;
cfg.codegenBytes = 0u; // no sprites needed -- backdrop-restore path is direct memcpy 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 // AGI sound is software-synthesized PSG squarewaves pushed
// through the Sound Blaster auto-init DMA path (see agiSynthFill // through the Sound Blaster auto-init DMA path (see agiSynthFill
// and src/port/dos/audio.c). The audio pool covers the 4 KB // and src/port/dos/audio.c). The audio pool covers the 4 KB

View file

@ -24,7 +24,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIOBJ";
#endif #endif

View file

@ -81,7 +81,6 @@ static void plot(AgiPicT *pic, const PicStateT *state, int16_t x, int16_t y)
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIPIC";
#endif #endif
// ----- Internal helpers (alphabetical) ----- // ----- 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- // 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 // 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 // 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) { 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 dx;
int16_t dy; int16_t dy;

View file

@ -48,9 +48,8 @@
// IIgs 64 KB code-bank rule: park this TU in its own load segment // 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 // 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 #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIRES";
#endif #endif
@ -154,7 +153,7 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
} }
volNibble = (uint8_t)(buf[0] >> 4); 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. // 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]; offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];

View file

@ -23,7 +23,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGITXT";
#endif #endif

View file

@ -40,7 +40,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIVIEW";
#endif #endif
@ -143,7 +142,6 @@ uint8_t agiActorPriorityForY(int16_t y) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIDRAW";
#endif #endif
void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx, void agiViewDraw(const AgiViewT *view, uint8_t loopIdx, uint8_t celIdx,

View file

@ -273,7 +273,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIVM";
#endif #endif

View file

@ -118,7 +118,6 @@ int main(void) {
uint32_t sfxLen; uint32_t sfxLen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -243,7 +243,6 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -10,7 +10,6 @@ int main(void) {
jlConfigT config; jlConfigT config;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -218,7 +218,6 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -218,7 +218,6 @@ int main(void) {
int16_t cursorRow; int16_t cursorRow;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -106,7 +106,6 @@ int main(void) {
jlSurfaceT *screen; jlSurfaceT *screen;
config.codegenBytes = 8 * 1024; config.codegenBytes = 8 * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -111,7 +111,6 @@ int main(void) {
// halSpriteSavePlanes / halSpriteRestorePlanes as static lib code. // halSpriteSavePlanes / halSpriteRestorePlanes as static lib code.
// 32 KB is plenty for whatever other codegen needs are around. // 32 KB is plenty for whatever other codegen needs are around.
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.maxSurfaces = 4; // stage + work
config.audioBytes = 32UL * 1024; // music + SFX config.audioBytes = 32UL * 1024; // music + SFX
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -24,7 +24,6 @@
#include "spacetaxi.h" #include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
#define ST_SFX_VOICE 2u #define ST_SFX_VOICE 2u
#define ST_THRUST_VOICE 1u #define ST_THRUST_VOICE 1u

View file

@ -27,7 +27,6 @@
#include "spacetaxi.h" #include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
// Velocity safety clamp. With ST_SUBPIXEL = 256, ST_MAX_VX = 512 caps // 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 // the cab at ~2 px/frame which is in line with the C64 max effective

View file

@ -11,7 +11,6 @@
#include "spacetaxi.h" #include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
void stHudDraw(jlSurfaceT *stage, const StGameT *game) { void stHudDraw(jlSurfaceT *stage, const StGameT *game) {

View file

@ -13,7 +13,6 @@
#include "spacetaxi.h" #include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
// STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav) // STL2 adds the per-level physics templates (xAccel/yAccel/xGrav/yGrav)
// and the full VIC color block ($7D00-$7D08). Drops the patience byte // and the full VIC color block ($7D00-$7D08). Drops the patience byte

View file

@ -20,7 +20,6 @@
#include "spacetaxi.h" #include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
#define ST_PASSENGER_W_PX 16 #define ST_PASSENGER_W_PX 16
#define ST_PASSENGER_H_PX 16 #define ST_PASSENGER_H_PX 16

View file

@ -22,7 +22,6 @@
// All STAXI example sources share the STAXI load segment so the // 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. // 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 // Tile bank files are per-level: each level's .dat references a
// numeric bank id, and the host loads tiles/tbankN.tbk on demand // numeric bank id, and the host loads tiles/tbankN.tbk on demand

View file

@ -107,7 +107,6 @@ int main(void) {
* the chunky two-shift case asks for. 32 KB fits a 16x16 ball * the chunky two-shift case asks for. 32 KB fits a 16x16 ball
* with all variants. */ * with all variants. */
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {

View file

@ -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 ----- // ----- Main -----
static void runAllTests(void) { static void runAllTests(void) {
@ -296,9 +485,8 @@ int main(void) {
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar /* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
* compiled sprite emitter generates. UL on the multiply because * 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.codegenBytes = 32UL * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) { if (!jlInit(&config)) {
@ -373,10 +561,11 @@ int main(void) {
jlLogF("UBER: audioInit failed (skipping audio)\n"); jlLogF("UBER: audioInit failed (skipping audio)\n");
} }
// Reset stage + run all per-frame timed tests. // Visual showcase: render one of each primitive into its own grid
jlSurfaceClear(gStage, 0); // cell so the run shows something legible (the timed benchmark below
jlFillRect(gStage, 0, 0, 320, 8, 1); // never presents, so this stays on screen throughout it). The first
jlStagePresent(); // timed op is jlSurfaceClear, so this leaves the benchmark untouched.
drawShowcase();
runAllTests(); runAllTests();
@ -391,6 +580,10 @@ int main(void) {
jlStagePresent(); jlStagePresent();
jlLogF("UBER: press any key to exit\n"); 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(); jlWaitForAnyKey();
jlSpriteDestroy(gSprite); jlSpriteDestroy(gSprite);

View file

@ -24,6 +24,11 @@
#define JOEY_AUDIO_SFX_SLOTS 4 #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 // Initialize audio. Returns true if the platform has a working audio
// engine and was able to start it. Returns false silently otherwise; // engine and was able to start it. Returns false silently otherwise;
// the rest of the API stays callable but produces no sound. // the rest of the API stays callable but produces no sound.

View file

@ -8,8 +8,7 @@
typedef struct { typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size uint32_t codegenBytes; // runtime compiled-sprite cache size
uint16_t maxSurfaces; // maximum concurrent surfaces uint32_t audioBytes; // reserved; not yet consulted by any audio engine
uint32_t audioBytes; // audio sample and module RAM pool
} jlConfigT; } jlConfigT;
// Initialize the library. Returns true on success. // Initialize the library. Returns true on success.
@ -35,11 +34,10 @@ const char *jlVersionString(void);
// always a hardware-level wait, not a software timer. // always a hardware-level wait, not a software timer.
void jlWaitVBL(void); void jlWaitVBL(void);
// Monotonic 16-bit frame counter. Polled by callers; ports detect // Monotonic 16-bit frame counter. Caller must poll faster than
// the rising edge inside this call (IIgs $C019, DOS $3DA, Amiga // 2 * jlFrameHz() so no edge is missed. Used by benchmarks and
// VPOSR) or expose a counter maintained by a VBL ISR (Atari ST). // frame-rate-independent animation. (Per-port edge-detect / VBL-ISR
// Caller must poll faster than 2 * jlFrameHz() so no edge is // mechanism is documented at halFrameCount in src/core/hal.h.)
// missed. Used by benchmarks and frame-rate-independent animation.
uint16_t jlFrameCount(void); uint16_t jlFrameCount(void);
// Nominal display frame rate in Hz: 50 (Amiga PAL), 60 (IIgs / ST // 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 // Monotonic wall-clock millisecond counter since jlInit. Decoupled
// from frame rate -- two consecutive calls separated by a long // from frame rate -- two consecutive calls separated by a long
// render will report the true elapsed time, not the number of VBLs // render report the true elapsed time, not the number of VBLs that
// that fired. Per-port implementation: DOS uses uclock() (PIT // fired. (Per-port millisecond timer sources are documented at
// counter 0, ~1 us resolution); ST reads the 200 Hz Timer C // halMillisElapsed in src/core/hal.h.)
// 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.
uint32_t jlMillisElapsed(void); uint32_t jlMillisElapsed(void);
// Deterministic, seedable pseudo-random generator. Bit-identical // Deterministic, seedable pseudo-random generator. Bit-identical
// output across every JoeyLib port for a given seed, since stdlib // 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, // The underlying algorithm is xorshift32 (Marsaglia): period 2^32-1,
// no multiply, suitable for games but not for cryptography. // no multiply, suitable for games but not for cryptography.
uint32_t jlRandom(void); uint32_t jlRandom(void);

View file

@ -20,7 +20,7 @@
#define JOEYLIB_PLATFORM_AMIGA #define JOEYLIB_PLATFORM_AMIGA
#elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__) #elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__)
#define JOEYLIB_PLATFORM_ATARIST #define JOEYLIB_PLATFORM_ATARIST
#elif defined(__ORCAC__) || defined(__APPLE2GS__) || defined(__GNO__) #elif defined(__APPLE2GS__) || defined(__GNO__)
#define JOEYLIB_PLATFORM_IIGS #define JOEYLIB_PLATFORM_IIGS
#else #else
#error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_<TARGET> explicitly via -D" #error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_<TARGET> explicitly via -D"
@ -63,20 +63,6 @@
#define JOEYLIB_PLATFORM_NAME "MS-DOS" #define JOEYLIB_PLATFORM_NAME "MS-DOS"
#endif #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 ----- // ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1 #define JOEYLIB_VERSION_MAJOR 1

View file

@ -15,10 +15,11 @@
#include "types.h" #include "types.h"
// Flip the dirty regions of the stage to the display, then clear the // Flip the dirty regions of the stage to the display, then clear the
// dirty state. Cheap when nothing has changed since the last call // dirty state. Drawing primitives mark dirty as a side effect, so
// (gStageAnyDirty short-circuit). Drawing primitives mark dirty as // callers only need to call jlStagePresent at the end of a frame --
// a side effect, so callers only need to call jlStagePresent at the // everything they drew shows up. Rows that nothing touched since the
// end of a frame -- everything they drew shows up. // 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 // To present a region you didn't draw with the standard primitives
// (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on // (e.g. direct framebuffer poking), call surfaceMarkDirtyRect on

View file

@ -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 // Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least // (x,y) would write to. backup->bytes must have at least
// (widthTiles*4) * (heightTiles*8) bytes of capacity for fully // (widthTiles*4) * (heightTiles*8) bytes of capacity for fully
// in-bounds draws; for clipped draws only the visible bytes are // in-bounds draws on chunky ports (DOS, IIgs); for clipped draws only
// stored. The captured region's exact size is reported in // the visible bytes are stored. The captured region's exact size is
// backup->sizeBytes. // 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); 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 // 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 // captures the destination bytes about to be overwritten, then draws
// the sprite. Both ops share validation, the destination ptr is // the sprite. Both ops share validation, the destination ptr is
// computed once, and a single dirty-rect mark covers both. Saves // 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. // frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
// //
// Identical semantics to: // Identical semantics to:

View file

@ -6,10 +6,7 @@
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
typedef enum { // This header is the shared include point for stdint / stdbool across
VIDEO_REGION_NTSC, // every public JoeyLib header (pulled in transitively via joey.h).
VIDEO_REGION_PAL,
VIDEO_REGION_VGA
} jlVideoRegionE;
#endif #endif

View file

@ -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/clang-build.sh drives compile (clang) / assemble
# toolchains/iigs/iix-build.sh wrapper handles ORCA's case-sensitivity # (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
# quirks (lowercase .a/.root/.sym vs linker demands for uppercase), # Hand-rolled asm lives in src/port/iigs/*.s (llvm-mc GAS syntax);
# multi-source compile/link, and pragma injection for include paths # bank placement is handled by -ffunction-sections + link816
# and the stdint/stdbool shim headers. # 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 include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
PLATFORM := iigs PLATFORM := iigs
BUILD := $(REPO_DIR)/build/$(PLATFORM) BUILD := $(REPO_DIR)/build/$(PLATFORM)
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
OBJDIR := $(BUILD)/obj
PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c) CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang
# Hand-rolled .asm sources go through ORCA's macro assembler via RT_INC := $(LLVM816_ROOT)/runtime/include
# iix-build.sh's `assemble` dispatch. Each .asm declares its target CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections
# load segment in the START operand (e.g. peislam.asm -> PEISLAMS) INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen
# 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)
# audio_full.c declares its functions in the AUDIOIMPL load segment PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c)
# (`segment "AUDIOIMPL"` at file scope, see ORCA/C ch. 30) so the # Hand-rolled asm, migrated to llvm-mc GAS .s.
# implementation code lives in its own bank, not _ROOT. That lets PORT_ASM_SRCS := $(wildcard $(SRC_PORT)/iigs/*.s)
# 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)
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that # IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter # DOS and ST share, so src/core/audioSfxMix.c is unused here.
# it out so the ORCA Linker doesn't pull its dead code into every
# IIgs binary (the monolithic-link budget is tight).
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS)) CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: 65816 emitter + cross-platform compile dispatch. # Sprite codegen: 65816 emitter + cross-platform compile dispatch.
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \ CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \
$(REPO_DIR)/src/codegen/spriteCompile.c $(REPO_DIR)/src/codegen/spriteCompile.c
# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS)
# 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_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(PORT_ASM_SRCS_ALL) $(NTP_ASM) $(CODEGEN_SRCS) # Example sources (single-TU unless noted).
# 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.
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN
DRAW_SRC := $(EXAMPLES)/draw/draw.c DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/DRAW
KEYS_SRC := $(EXAMPLES)/keys/keys.c KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS
JOY_SRC := $(EXAMPLES)/joy/joy.c JOY_SRC := $(EXAMPLES)/joy/joy.c
JOY_BIN := $(BINDIR)/JOY
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
SPRITE_BIN := $(BINDIR)/SPRITE
UBER_SRC := $(EXAMPLES)/uber/uber.c UBER_SRC := $(EXAMPLES)/uber/uber.c
UBER_BIN := $(BINDIR)/UBER
ADV_SRC := $(EXAMPLES)/adventure/adventure.c ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV_BIN := $(BINDIR)/ADV
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c 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_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 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 := \ # Default: compile-check the library + run the end-to-end smoke test.
-I $(IIGS_INCLUDE_SHIM) \ all iigs: iigs-lib iigs-clang-smoke
-I $(INCLUDE_DIR) \
-I $(INCLUDE_DIR)/joey \
-I $(SRC_CORE) \
-I $(REPO_DIR)/src/codegen
.PHONY: all iigs iigs-disk clean-iigs # Compile-check every library and example translation unit with clang,
# Building the disk implicitly builds every binary it depends on, so # and assemble every .s with llvm-mc. This is the canonical "does the
# `make iigs` ends with a fresh joey.2mg on every change. Without this, # IIgs source still build" gate while the launchable pipeline is wired.
# stale disk images would silently mask binary updates -- a surprise iigs-lib:
# when the run script always boots from joey.2mg. @mkdir -p $(OBJDIR)
all iigs: $(DISK_IMG) @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) # End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating
@mkdir -p $(dir $@) # the ported asm computes correctly on emulated hardware.
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s iigs-clang-smoke:
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s $(REPO_DIR)/toolchains/iigs/clang-smoke.sh
mv $(BUILD)/audio/ntpplayer $@
# Bake the NTP replayer bytes into an ORCA-M asm file. The asm declares # Per-example binaries via clang-build.sh. clang-build.sh mirrors the
# the bytes in a `data NTPDATA` segment; ORCA's linker groups same- # old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These
# name object segments into one load segment, and the GS/OS loader # link single-segment today; examples larger than one bank need the
# places it in its own bank. Net effect: the 34 KB of NTP bytes don't # multi-segment OMF wiring still in progress.
# 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.
DEP_DIR := $(BUILD)/dep 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) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(PATTERN_DEP) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/PATTERN.d $(INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(DRAW_BIN): $(DRAW_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD) $(BINDIR)/DRAW: $(DRAW_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(DRAW_DEP) $(IIX_INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/DRAW.d $(INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD) $(BINDIR)/KEYS: $(KEYS_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(KEYS_DEP) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/KEYS.d $(INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD) $(BINDIR)/JOY: $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(JOY_DEP) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD) $(BINDIR)/SPRITE: $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(SPRITE_DEP) $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SPRITE.d $(INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# UBER bumps user stack to 16 KB. ORCA-C's default user stack is small $(BINDIR)/UBER: $(UBER_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
# (~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)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(UBER_DEP) $(IIX_INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/UBER.d $(INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# Adventure has a lot of static const sprite-row arrays and walk-mask $(BINDIR)/ADV: $(ADV_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
# 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)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(ADV_DEP) $(IIX_INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV.d $(INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(ADV2_BIN): $(ADV2_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD) $(BINDIR)/ADV2: $(ADV2_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(ADV2_DEP) $(IIX_INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# AGI binary spans multiple .c files (agi.c is main; agiRes.c is the $(BINDIR)/AGI: $(AGI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
# 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)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(AGI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# Space Taxi: multi-source like AGI; 16 KB stack for headroom on the $(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
# 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)
@mkdir -p $(dir $@) $(DEP_DIR) @mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(STAXI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# Convert the cross-platform .MOD asset to NinjaTrackerPlus runtime # NOTE: the audio example is omitted until the NinjaTrackerPlus replayer
# format via joeymod (which shells out to ntpconverter.php). Without # is wired into the clang build (a path for baking the replayer bytes
# php-cli the conversion is skipped; in that case the IIgs disk just # into a data segment via llvm-mc is pending).
# 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)
clean-iigs: clean-iigs:
rm -rf $(BUILD) rm -rf $(BUILD)
# Pull in per-binary header-dependency files generated by iix-build.sh -M. -include $(wildcard $(DEP_DIR)/*.d)
# 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)

View file

@ -13,7 +13,6 @@
# Outputs of a run land in /tmp/mame-iigs/: # Outputs of a run land in /tmp/mame-iigs/:
# - debug.log MAME's debugger console output (-debuglog) # - debug.log MAME's debugger console output (-debuglog)
# - crash.txt Lua-hook crash dump (if anything halts the CPU) # - crash.txt Lua-hook crash dump (if anything halts the CPU)
# - joeylog.txt Extracted from the post-run disk image
set -euo pipefail set -euo pipefail
@ -232,7 +231,6 @@ On crash the MAME debugger halts and Lua dumps state to:
$out/crash.txt $out/crash.txt
After exit: After exit:
$out/joeylog.txt extracted from disk image
$out/debug.log MAME debugger console output $out/debug.log MAME debugger console output
EOF EOF
@ -248,23 +246,6 @@ cleanup() {
rm -rf "$out/snap" rm -rf "$out/snap"
mv "$work/snap" "$out/snap" mv "$work/snap" "$out/snap"
fi 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" rm -rf "$work"
} }
trap cleanup EXIT trap cleanup EXIT

View file

@ -7,9 +7,7 @@
# #
# No argument: GSplus has no way to dispatch a specific binary on boot # No argument: GSplus has no way to dispatch a specific binary on boot
# (Finder is interactive), so this script just stages the disk and # (Finder is interactive), so this script just stages the disk and
# launches the emulator. The post-run trap below extracts joeylog.txt # launches the emulator.
# from the data disk so demos that left a breadcrumb file are visible
# from the host shell after the emulator exits.
set -euo pipefail set -euo pipefail
@ -21,12 +19,6 @@ fi
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) 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 gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom rom=$repo/toolchains/emulators/support/apple-iigs.rom
sys_disk=$repo/toolchains/emulators/support/gsos-system.po sys_disk=$repo/toolchains/emulators/support/gsos-system.po
@ -49,46 +41,15 @@ done
# copies so repeated runs do not mutate the originals. # copies so repeated runs do not mutate the originals.
work=$(mktemp -d -t joeylib-iigs.XXXXXX) work=$(mktemp -d -t joeylib-iigs.XXXXXX)
# After GSplus exits, mount the work copy of joey.2mg via profuse and cleanup() {
# 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
# Stash the post-run disk image for manual inspection. # Stash the post-run disk image for manual inspection.
if [[ -f $work/joey.2mg ]]; then if [[ -f $work/joey.2mg ]]; then
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg" 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 echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
fi 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" rm -rf "$work"
} }
trap extract_log EXIT trap cleanup EXIT
cp "$sys_disk" "$work/boot.po" cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg" cp "$data_disk" "$work/joey.2mg"

View file

@ -20,26 +20,35 @@
#include "surfaceInternal.h" #include "surfaceInternal.h"
// Largest scratch buffer needed for any single emit call. 16 KB // Scratch buffer for a single emit call. 16 KB comfortably covers any
// covers a 32x32 sprite even on 68k (the biggest mixed-RMW byte- // sprite up to a 32x32 worst-case all-mixed DRAW (16 rows*17 dest
// emit at 16 bytes/byte * (16*17 dest bytes per shift) ~= 4.5 KB, // bytes/shift * SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE ~= 4.5 KB on 68k,
// times shift count 2). Round up generously. // the largest per-destbyte target). It is NOT trusted to bound every
#define SPRITE_EMIT_SCRATCH_BYTES (16u * 1024u) // 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/ // Compile-time selection of the per-CPU emitter. One src/codegen/
// spriteEmit*.c file is built per platform, but the dispatch lives // spriteEmit*.c file is built per platform, but the dispatch lives
// in this file so jlSpriteCompile + spriteCompiledDraw aren't // in this file so jlSpriteCompile + spriteCompiledDraw aren't
// duplicated three times. // 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) #if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitDrawX86(out, sp, shift); return spriteEmitDrawX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitDrawPlanar68k(out, sp, shift); return spriteEmitDrawPlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitDrawInterleaved68k(out, sp, shift); return spriteEmitDrawInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS) #elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitDrawIigs(out, sp, shift); return spriteEmitDrawIigs(out, cap, sp, shift);
#else #else
# error "jlSpriteCompile: no emitter selected for this platform" # error "jlSpriteCompile: no emitter selected for this platform"
#endif #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] // src/core/sprite.c gates on routineOffsets[shift][SPRITE_OP_SAVE]
// != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy // != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy
// path otherwise. // 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) #if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitSaveX86(out, sp, shift); return spriteEmitSaveX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitSavePlanar68k(out, sp, shift); return spriteEmitSavePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitSaveInterleaved68k(out, sp, shift); return spriteEmitSaveInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS) #elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitSaveIigs(out, sp, shift); return spriteEmitSaveIigs(out, cap, sp, shift);
#else #else
(void)out; (void)sp; (void)shift; (void)out; (void)cap; (void)sp; (void)shift;
return 0; return 0;
#endif #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) #if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitRestoreX86(out, sp, shift); return spriteEmitRestoreX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitRestorePlanar68k(out, sp, shift); return spriteEmitRestorePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitRestoreInterleaved68k(out, sp, shift); return spriteEmitRestoreInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS) #elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitRestoreIigs(out, sp, shift); return spriteEmitRestoreIigs(out, cap, sp, shift);
#else #else
(void)out; (void)sp; (void)shift; (void)out; (void)cap; (void)sp; (void)shift;
return 0; return 0;
#endif #endif
} }
@ -90,13 +99,24 @@ static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t
// land in the arena. // land in the arena.
static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) { static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
uint32_t total; uint32_t total;
uint32_t written;
uint8_t shift; uint8_t shift;
uint8_t op;
total = 0; total = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) { for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
total += emitDrawForTarget(scratch, sp, shift); for (op = 0; op < SPRITE_OP_COUNT; op++) {
total += emitSaveForTarget(scratch, sp, shift); switch (op) {
total += emitRestoreForTarget(scratch, sp, shift); 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; return total;
} }
@ -109,8 +129,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
uint8_t op; uint8_t op;
ArenaSlotT *slot; ArenaSlotT *slot;
uint8_t *dst; uint8_t *dst;
uint16_t written; uint32_t written;
uint16_t offset; uint32_t offset;
if (sp == NULL) { if (sp == NULL) {
return false; return false;
@ -135,6 +155,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
} }
totalSize = emitTotalSize(scratch, 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) { if (totalSize > 0xFFFFu) {
free(scratch); free(scratch);
return false; return false;
@ -159,17 +181,32 @@ bool jlSpriteCompile(jlSpriteT *sp) {
offset = 0; offset = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) { for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) { for (op = 0; op < SPRITE_OP_COUNT; op++) {
uint32_t capLeft;
capLeft = (uint32_t)(totalSize - offset);
switch (op) { switch (op) {
case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, sp, shift); break; case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, capLeft, sp, shift); break;
case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, sp, shift); break; case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, capLeft, sp, shift); break;
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, sp, shift); break; case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, capLeft, sp, shift); break;
default: written = 0; 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; sp->routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
} else { } else {
sp->routineOffsets[shift][op] = offset; sp->routineOffsets[shift][op] = (uint16_t)offset;
offset = (uint16_t)(offset + written); 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) #if defined(JOEYLIB_PLATFORM_IIGS)
// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs; // SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs;
// declared in surfaceInternal.h. Replaces ORCA-C's __mul16 JSL with a // declared in surfaceInternal.h. Uses a single indexed long-mode read
// single indexed long-mode read. // for the multiply.
// IIgs uses inline asm + a self-modifying call stub instead of a C // IIgs uses inline asm + a self-modifying call stub instead of a C
// function-pointer cast. The build uses ORCA-C large memory model // function-pointer cast. The build uses a large memory model so
// (-b for sprite demos) so pointers are 24-bit and JSL works // pointers are 24-bit and JSL works cross-bank.
// cross-bank.
// //
// `sta abs,Y` on 65816 uses the data bank register (DBR) for the // `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 // 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) { void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
uint8_t shift; uint8_t shift;
uint32_t destAddr;
uint16_t destOffset; uint16_t destOffset;
uint8_t destBank; uint8_t destBank;
uint32_t fnAddr; uint32_t fnAddr;
{ {
uint8_t *destPtr; uint8_t *destPtr;
uint32_t destAddr;
const uint8_t *destB;
shift = (uint8_t)(x & 1); shift = (uint8_t)(x & 1);
destPtr = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_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; destAddr = (uint32_t)destPtr;
destOffset = (uint16_t)(destAddr & 0xFFFFu); destB = (const uint8_t *)&destAddr;
destBank = (uint8_t)((destAddr >> 16) & 0xFFu); destOffset = (uint16_t)(destB[0] | ((uint16_t)destB[1] << 8));
destBank = destB[2];
fnAddr = codegenArenaBaseAddr() fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset + sp->slot->offset
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW]; + (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
} }
(void)destAddr;
if (!gDrawStubInited) { if (!gDrawStubInited) {
gSpriteCallStub[ 0] = 0x8B; gSpriteCallStub[ 0] = 0x8B;
@ -284,8 +345,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
gDrawStubLastFnAddr = fnAddr; gDrawStubLastFnAddr = fnAddr;
} }
// ORCA-C compiles this function under `longa on / longi on` // This function is compiled under `longa on / longi on`
// (M=16, X=16) and emits the function epilogue assuming those // (M=16, X=16) and the function epilogue assumes those
// widths at exit -- the deallocation ADC takes a 2-byte immediate // widths at exit -- the deallocation ADC takes a 2-byte immediate
// and any LDX/LDY use 2-byte immediates. The byte writes to // and any LDX/LDY use 2-byte immediates. The byte writes to
// gSpriteCallStub above leave M=8, and an earlier PHP/PLP-only // 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 // swallowed the TCS, S was never adjusted, RTL popped wrong
// bytes, control fell into BSS, and the IIgs hit BRK on a zero // bytes, control fell into BSS, and the IIgs hit BRK on a zero
// byte. Force M=16/X=16 before returning to compiled C. // byte. Force M=16/X=16 before returning to compiled C.
asm { __asm__ volatile (
rep #0x30 "rep #0x30\n\t"
sep #0x20 "sep #0x20\n\t"
jsl gSpriteCallStub "jsl gSpriteCallStub\n\t"
rep #0x30 "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 // Split a 24-bit pointer into its low 16 bits + bank byte. To avoid
// (uint32_t) cast works correctly in ORCA/C 2.2.1 (the 2.1.0 lossy- // invoking a 32-bit-shift helper for the `>> 16` to extract the bank
// bank-byte bug is fixed). To avoid invoking the ~LSHR4 32-bit-shift // byte, we cast to uint32_t and then byte-alias the storage -- gets
// helper for the `>> 16` to extract the bank byte, we cast to // the same bytes with simple loads.
// uint32_t and then byte-alias the storage -- gets the same bytes
// with simple loads.
#define SPLIT_POINTER(_ptr, _outLo, _outBank) \ #define SPLIT_POINTER(_ptr, _outLo, _outBank) \
do { \ do { \
uint32_t spAddr_ = (uint32_t)(_ptr); \ 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); clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8); widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 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)]; screenPtr = (uint8_t *)&src->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)clippedX >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank); 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 /* Compute the 1D index into the cached* / routineOffsets 2D arrays
* once. ORCA-C 2.2.1 lowers `shift * SPRITE_OP_COUNT` (where * once. `shift * SPRITE_OP_COUNT` (where SPRITE_OP_COUNT==3) can
* SPRITE_OP_COUNT==3) to a ~MUL4 helper call; (shift<<1)+shift * lower to a multiply helper call; (shift<<1)+shift compiles to
* compiles to two ASLs and an ADC, no helper. */ * two ASLs and an ADC, no helper. */
cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE); cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx; cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + 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 // MVN-based routine: needs M=16 / X=16; restore M=16 on exit
// matches ORCA-C `longa on` epilogue expectations. // matches the `longa on` epilogue expectations.
asm { __asm__ volatile (
rep #0x30 "rep #0x30\n\t"
jsl gSpriteSaveStub "jsl gSpriteSaveStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
} }
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) { void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
uint8_t shift; uint8_t shift;
uint16_t heightPx; uint16_t heightPx;
uint16_t copyBytes;
uint16_t spriteBytesPerRow;
uint16_t screenLo; uint16_t screenLo;
uint16_t backupLo; uint16_t backupLo;
uint8_t screenBank; uint8_t screenBank;
@ -524,9 +583,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
sp = backup->sprite; sp = backup->sprite;
heightPx = backup->height; heightPx = backup->height;
copyBytes = (uint16_t)(backup->width >> 1); shift = spriteChunkyRestoreShift(sp, backup);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)]; screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank); SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
@ -580,11 +637,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
*cachedSrc = backupBank; *cachedSrc = backupBank;
} }
asm { __asm__ volatile (
rep #0x30 "rep #0x30\n\t"
jsl gSpriteRestoreStub "jsl gSpriteRestoreStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
} }
#elif defined(JOEYLIB_PLATFORM_AMIGA) #elif defined(JOEYLIB_PLATFORM_AMIGA)
@ -602,7 +660,10 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* interpreter, which handles arbitrary x via halSpriteDrawPlanes / * interpreter, which handles arbitrary x via halSpriteDrawPlanes /
* halSpriteSavePlanes / halSpriteRestorePlanes. */ * 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) { 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); 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; DrawFn fn;
shift = (uint8_t)(x & 7); 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; p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1); p1 = halSurfacePlanePtr(dst, 1);
p2 = halSurfacePlanePtr(dst, 2); p2 = halSurfacePlanePtr(dst, 2);
@ -646,7 +707,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
if (shift != 0u) { if (shift != 0u) {
widthPx = (uint16_t)(widthPx + 8u); 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->sprite = sp;
backup->x = clippedX; 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 * Shifted slots 1..7 all alias to the same restore bytes, so
* slot 1 stands in for any non-zero shift. */ * slot 1 stands in for any non-zero shift. */
shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u); 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; p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1); 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 * 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher * per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
* falls back to halSpriteDrawPlanes. */ * 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) { void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
typedef void (*DrawFn)(uint8_t *groupBase); typedef void (*DrawFn)(uint8_t *groupBase);
uint8_t shift; uint8_t shift;
@ -734,8 +803,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
return; return;
} }
groupBase = base groupBase = base
+ (uint16_t)y * 160u + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * 8u; + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(groupBase); fn(groupBase);
} }
@ -788,7 +857,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
clippedX = (int16_t)(x & ~1); clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8); widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 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)]; 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); typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
jlSpriteT *sp; jlSpriteT *sp;
uint8_t shift; uint8_t shift;
uint16_t copyBytes;
uint16_t spriteBytesPerRow;
uint8_t *screenPtr; uint8_t *screenPtr;
CopyFn fn; CopyFn fn;
sp = backup->sprite; sp = backup->sprite;
copyBytes = (uint16_t)(backup->width >> 1); shift = spriteChunkyRestoreShift(sp, backup);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)]; screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];

View file

@ -35,12 +35,25 @@
#define TILE_BYTES_PER_ROW 4 #define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0 #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 ----- // ----- 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 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 uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
static uint16_t writeBE16(uint8_t *out, uint16_t value); 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=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE) // 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 row;
uint16_t col; uint16_t col;
uint16_t advance; uint16_t advance;
uint32_t rowBytes;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes); advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) { 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. // Unrolled: move.b (a0)+, (a1)+ -- 0x12D8.
for (col = 0; col < copyBytes; col++) { for (col = 0; col < copyBytes; col++) {
cursor += writeBE16(out + cursor, 0x12D8u); 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. // 68k draw emit. Returns bytes written, or SPRITE_EMIT_OVERFLOW if the
uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { // routine would exceed `cap`.
uint16_t cursor; uint32_t spriteEmitDraw68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t row; uint16_t row;
uint16_t col; uint16_t col;
uint16_t heightPx; uint16_t heightPx;
@ -176,15 +196,21 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); 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 // Prologue: movea.l 4(sp), a0
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x206Fu); cursor += writeBE16(out + cursor, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u); cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
for (row = 0; row < heightPx; row++) { for (row = 0; row < heightPx; row++) {
if (row > 0) { if (row > 0) {
// adda.w #SURFACE_BYTES_PER_ROW, a0 // 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, 0xD0FCu);
cursor += writeBE16(out + cursor, (uint16_t)SURFACE_BYTES_PER_ROW); 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) { if (opaqueMask == 0x00) {
continue; 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) { if (opaqueMask == 0xFFu) {
// move.b #imm, d16(a0) // move.b #imm, d16(a0)
// Opcode 0x117C: bits 11-9 = dst reg (0=a0), bits 8-6 = // 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 // Epilogue: rts
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x4E75u); cursor += writeBE16(out + cursor, 0x4E75u);
return cursor; return cursor;
} }
// RESTORE: copy backup -> screen. Destination has the screen stride. // RESTORE: copy backup -> screen. Destination has the screen stride.
uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitRestore68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t heightPx; uint16_t heightPx;
uint16_t copyBytes; uint16_t copyBytes;
@ -238,24 +272,33 @@ uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); 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). // 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, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u); cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu); 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); cursor += writeBE16(out + cursor, 0x4E75u);
return cursor; return cursor;
} }
// SAVE: copy screen -> backup. Source has the screen stride. // SAVE: copy screen -> backup. Source has the screen stride.
uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitSave68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t heightPx; uint16_t heightPx;
uint16_t copyBytes; uint16_t copyBytes;
@ -265,15 +308,24 @@ uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); 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, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u); cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu); 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); cursor += writeBE16(out + cursor, 0x4E75u);
return cursor; return cursor;
} }

View file

@ -1,7 +1,7 @@
// IIgs (65816) sprite codegen. Emits PIC draw routines that write // IIgs (65816) sprite codegen. Emits PIC draw routines that write
// 4bpp packed surface bytes via abs,Y indexed addressing. // 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, // runtime never calls these routines via a C cast -- instead,
// spriteCompiledDraw (in jlSpriteCompile.c, gated on // spriteCompiledDraw (in jlSpriteCompile.c, gated on
// JOEYLIB_PLATFORM_IIGS) builds a self-modifying JSL stub that // JOEYLIB_PLATFORM_IIGS) builds a self-modifying JSL stub that
@ -9,7 +9,7 @@
// - M = 8-bit (set by stub before JSL) // - M = 8-bit (set by stub before JSL)
// - X = 16-bit (set by stub) // - X = 16-bit (set by stub)
// - Y = destRow (loaded by stub from immediate) // - Y = destRow (loaded by stub from immediate)
// - DBR = program bank (ORCA-C default) // - DBR = program bank
// No stack arg, no prologue. Body executes directly. // No stack arg, no prologue. Body executes directly.
// //
// Routine shape (per-byte emit, no PEA optimization yet): // Routine shape (per-byte emit, no PEA optimization yet):
@ -39,12 +39,10 @@
#define TILE_BYTES_PER_ROW 4 #define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0 #define TRANSPARENT_NIBBLE 0
#define MAX_ROUTINE_BYTES 8192
// ----- Prototypes ----- // ----- 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 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 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 // writeLE16 was inlined at every call site. Inlining cuts a JSL/RTL
// per emitted 16-bit immediate (4 instructions per byte * 12 sites) // per emitted 16-bit immediate (4 instructions per byte * 12 sites).
// and avoids ORCA-Linker bank-fragility around tiny-helper resolution.
// Common backbone for save and restore. Both ops copy a byte-aligned // Common backbone for save and restore. Both ops copy a byte-aligned
// rectangle row-by-row using MVN; only the operand banks (which buffer // 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 // The MVN at row R has its dstbk at routine offset (12*R + 4) and
// srcbk at (12*R + 5). // srcbk at (12*R + 5).
static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX) { // Worst-case bytes emitted per MVN copy row: row 0 = 6 bytes, any
uint16_t cursor; // 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 advance;
uint16_t row; 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; cursor = 0;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes); advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) { 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) { if (row > 0) {
out[cursor++] = advanceX ? 0x8A : 0x98; // TXA / TYA out[cursor++] = advanceX ? 0x8A : 0x98; // TXA / TYA
out[cursor++] = 0x18; // CLC 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; // dstbk -- patched per call
out[cursor++] = 0x00; // srcbk -- patched per call out[cursor++] = 0x00; // srcbk -- patched per call
} }
if (cursor + 1u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x6B; // RTL out[cursor++] = 0x6B; // RTL
return cursor; 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 // 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 // already correct for the next row; screen rows are SURFACE_BYTES_PER_ROW
// apart so X needs an explicit ADC between rows. // 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 heightPx;
uint16_t spriteBytesPerRow; uint16_t spriteBytesPerRow;
uint16_t copyBytes; 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); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); 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, // RESTORE (backup -> screen). Stub passes X = backup low offset,
// Y = screen low offset. Backup is contiguous so X advances correctly // Y = screen low offset. Backup is contiguous so X advances correctly
// via MVN; screen needs explicit advance, so Y is the one we ADC. // 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 heightPx;
uint16_t spriteBytesPerRow; uint16_t spriteBytesPerRow;
uint16_t copyBytes; 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); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); 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 // Mixed bytes always run on the M=8 path because the AND/ORA in
// M=16 would clobber the adjacent byte. // M=16 would clobber the adjacent byte.
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t row; uint16_t row;
uint16_t col; uint16_t col;
uint16_t heightPx; uint16_t heightPx;
@ -273,7 +294,7 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); 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; wide = false;
// No prologue: caller (the inline-asm stub in jlSpriteCompile.c) // 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) { if (opaqueMask == 0x00) {
continue; 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) { if (opaqueMask == 0xFFu && (col + 1) < destBytesPerRow) {
// Look ahead: if (col, col+1) are both fully opaque // 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 // 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 // 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 // asm wrapper after the JSL forces M=16 again, but be defensive

View file

@ -1,34 +1,15 @@
// 68k sprite codegen for ST word-interleaved planar layout. Emits a // 68k sprite codegen for ST word-interleaved planar layout.
// cdecl-callable routine `void draw(uint8_t *groupBase)` that walks
// the sprite's tile data and writes plane bytes via `d16(a0)` chains.
// //
// ST planar layout reminder (doc/atarist_planar.md): one buffer; per // Phase 11 (shared-walker): the per-shift unrolled emitter was
// scanline 20 groups of 8 bytes; per group, 4 plane words back-to- // retired. halSpriteDrawPlanes in src/port/atarist/hal.c now handles
// back. groupBase points at the FIRST group the sprite touches: // all shifts byte-wide at runtime, including the formerly fast shifts
// pd->base + y * 160 + (x >> 4) * 8 // 0 and 1 -- per-cel arena cost drops to zero. The byte-aligned fast
// // path stSpriteDrawByteAligned is still called from halSpriteDrawPlanes
// Shift index for ST is bit 3 of x (whether the sprite starts in the // for shift==0 sprites that are fully on surface, so byte-aligned UBER
// high half or low half of the first group). x mod 8 != 0 falls back // perf does not regress. The entry points below are kept so the
// to the interpreter (returns 0 from this emitter so sp->slot stays // dispatcher API stays uniform across ports; they emit zero bytes, so
// NULL for those alignments). // jlSpriteCompile takes the "totalSize == 0 -> bail" path and sp->slot
// // stays NULL.
// 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.
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -36,194 +17,21 @@
#include "spriteInternal.h" #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 ----- // ----- Emit API -----
uint16_t spriteEmitDrawInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
/* Phase 11 (shared-walker): bypass the per-shift unrolled emitter. (void)out; (void)cap; (void)sp; (void)shift;
* 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;
return 0u; return 0u;
} }
uint16_t spriteEmitDrawInterleaved68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; (void)out; (void)cap; (void)sp; (void)shift;
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;
return 0u; return 0u;
} }
uint16_t spriteEmitRestoreInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift; (void)out; (void)cap; (void)sp; (void)shift;
return 0u; return 0u;
} }

View file

@ -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 // Phase 11 (shared-walker): the Amiga port no longer emits per-cel
// address-register pointers (a0..a3 = plane[0..3] base + byteOff, // unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
// where byteOff = y*40 + x/8 -- the dispatcher pre-computes this). // 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): // The entry points below are kept so the dispatcher API stays uniform
// draw(p0, p1, p2, p3): // across ports. Returning 0 tells emitTotalSize() the emitter has no
// args at 4(sp), 8(sp), 12(sp), 16(sp) -- one ULONG per plane. // bytes for this op, so jlSpriteCompile takes the "totalSize == 0 ->
// loaded into a0..a3 by the prologue. // bail" path and sp->slot stays NULL.
// 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<mask<0xFF): move.b d16(an), d0;
// andi.b #~mask, d0;
// ori.b #imm, d0;
// move.b d0, d16(an) (4+6+6+4 = 20 bytes)
//
// Per row advance: 4 plane pointers each get adda.w #SURFACE_WIDTH/8
// = adda.w #40, an (4 bytes encoded each, 16 bytes total per row).
// We omit the advance after the last row.
//
// Shift handling: shifts 0..7 are pre-baked. The dispatcher selects
// the variant via x % 8 and pre-computes byteOff = y*40 + (x & ~7)/8
// (i.e. round x DOWN to 8-pixel boundary). The variant for shift s
// then emits to (widthTiles + 1) plane bytes per row when s != 0
// (the rightmost shift bits spill into one extra plane byte) and to
// widthTiles plane bytes per row when s == 0.
//
// The emitter assumes sprite width is a multiple of 8 (= a multiple
// of one tile = a multiple of 8 pixels) so plane bytes per row are
// integer. JoeyLib sprites are always tile-multiple by API contract.
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -42,499 +22,21 @@
#include "spriteInternal.h" #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 <d16,SP>, 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 ----- // ----- Public API -----
/* Phase 11 (shared-walker): the Amiga port no longer emits per-cel uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
* unrolled draw/save/restore code. Each cel was ~17-23 KB compiled -- (void)out; (void)cap; (void)sp; (void)shift;
* 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;
return 0u; return 0u;
} }
uint16_t spriteEmitSavePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift; (void)out; (void)cap; (void)sp; (void)shift;
return 0u; return 0u;
} }
uint16_t spriteEmitRestorePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift; (void)out; (void)cap; (void)sp; (void)shift;
return 0u; return 0u;
} }

View file

@ -38,17 +38,35 @@
#define TILE_BYTES_PER_ROW 4 #define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0 #define TRANSPARENT_NIBBLE 0
// Worst-case bytes per emitted routine, used to size the scratch // Stack-arg displacements for the emitted cdecl routines, after the
// buffer. A 32x32 sprite is 16 rows * (16 dest bytes + 1 for shift1) // prologue saves esi (+esi/edi for save/restore):
// = 272 dest-byte slots, each up to 10 bytes mixed = 2720; plus // draw(dst) -> dst at [esp+8] (1 push)
// per-row prologues 32*6=192; plus prologue/epilogue 8. Round up // save/restore(src, dst) -> src at [esp+12], dst at [esp+16] (2 pushes)
// generously. #define DRAW_ARG_DST_DISP 0x08u
#define MAX_ROUTINE_BYTES 8192 #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 ----- // ----- 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 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 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=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE) // 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 row;
uint16_t dwords; uint16_t dwords;
uint16_t tail; 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; advance = (int32_t)SURFACE_BYTES_PER_ROW - (int32_t)copyBytes;
for (row = 0; row < heightPx; row++) { 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) { if (dwords > 0) {
// mov ecx, dwords (B9 imm32); rep movsd (F3 A5) // mov ecx, dwords (B9 imm32); rep movsd (F3 A5)
out[cursor++] = 0xB9; 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 // Decompose a destination byte's contribution from the sprite into
// (value, opaqueMask) for shift in {0, 1}. opaqueMask high nibble // (value, opaqueMask) for shift in {0, 1}. opaqueMask high nibble
// 0xF0 means high dest nibble is opaque; 0x0F means low is opaque; // 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). // Routine signature: void f(uint8_t *dst).
uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t row; uint16_t row;
uint16_t col; uint16_t col;
uint16_t runEnd; uint16_t runEnd;
@ -207,17 +258,23 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); 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] // Prologue: push esi; mov esi, [esp+8]
if (cursor + 5u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x56; // push esi out[cursor++] = 0x56; // push esi
out[cursor++] = 0x8B; out[cursor++] = 0x74; 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. // Body: per row, scan dest bytes coalescing fully-opaque runs.
for (row = 0; row < heightPx; row++) { for (row = 0; row < heightPx; row++) {
if (row > 0) { if (row > 0) {
// add esi, SURFACE_BYTES_PER_ROW (32-bit imm) // 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++] = 0x81; out[cursor++] = 0xC6;
out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu); out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu);
out[cursor++] = (uint8_t)((SURFACE_BYTES_PER_ROW >> 8) & 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++; col++;
continue; 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) { if (opaqueMask != 0xFFu) {
// Mixed: read-modify-write. // Mixed: read-modify-write.
// mov al, [esi+col] (8A 46 cc) // mov al, [esi+col] (8A /r)
// and al, ~opaqueMask (24 mm) // and al, ~opaqueMask (24 mm)
// or al, value (0C vv) // or al, value (0C vv)
// mov [esi+col], al (88 46 cc) // mov [esi+col], al (88 /r)
out[cursor++] = 0x8A; out[cursor++] = 0x46; out[cursor++] = 0x8A;
out[cursor++] = (uint8_t)(col & 0xFFu); cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = 0x24; out[cursor++] = 0x24;
out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu); out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu);
out[cursor++] = 0x0C; out[cursor++] = 0x0C;
out[cursor++] = value; out[cursor++] = value;
out[cursor++] = 0x88; out[cursor++] = 0x46; out[cursor++] = 0x88;
out[cursor++] = (uint8_t)(col & 0xFFu); cursor = emitEsiOperand(out, cursor, col);
col++; col++;
continue; 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 // enough that re-reading per chunk beats threading a
// fixed-size buffer through. // fixed-size buffer through.
while (runLen >= 4) { 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 + 1), shift, spriteBytesPerRow, &v1, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m); shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 3), shift, spriteBytesPerRow, &v3, &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++] = 0xC7; out[cursor++] = 0x46; cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = (uint8_t)(col & 0xFFu);
out[cursor++] = value; out[cursor++] = value;
out[cursor++] = v1; out[cursor++] = v1;
out[cursor++] = v2; out[cursor++] = v2;
@ -281,11 +346,14 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
} }
} }
if (runLen >= 2) { 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); 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++] = 0x66;
out[cursor++] = 0xC7; out[cursor++] = 0x46; out[cursor++] = 0xC7;
out[cursor++] = (uint8_t)(col & 0xFFu); cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value; out[cursor++] = value;
out[cursor++] = v1; out[cursor++] = v1;
col = (uint16_t)(col + 2); col = (uint16_t)(col + 2);
@ -295,9 +363,12 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
} }
} }
if (runLen == 1) { if (runLen == 1) {
// mov byte [esi+col], imm8 (C6 46 cc ii) // mov byte [esi+col], imm8 (C6 /0 ... ii)
out[cursor++] = 0xC6; out[cursor++] = 0x46; if (cursor + 1u + esiOperandLen(col) + 1u + EPILOGUE_BYTES > cap) {
out[cursor++] = (uint8_t)(col & 0xFFu); return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0xC6;
cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value; out[cursor++] = value;
col++; col++;
} }
@ -305,6 +376,9 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
} }
// Epilogue: pop esi; ret // Epilogue: pop esi; ret
if (cursor + 2u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x5E; out[cursor++] = 0x5E;
out[cursor++] = 0xC3; out[cursor++] = 0xC3;
return cursor; 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. // RESTORE: copy backup -> screen. Destination has the screen stride.
uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t heightPx; uint16_t heightPx;
uint16_t copyBytes; uint16_t copyBytes;
@ -323,17 +397,26 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); 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] // 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++] = 0x56;
out[cursor++] = 0x57; out[cursor++] = 0x57;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C; 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++] = 0x10; 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 // Epilogue: pop edi; pop esi; ret
if (cursor + 3u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x5F; out[cursor++] = 0x5F;
out[cursor++] = 0x5E; out[cursor++] = 0x5E;
out[cursor++] = 0xC3; 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. // SAVE: copy screen -> backup. Source has the screen stride.
uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor; uint32_t cursor;
uint16_t heightPx; uint16_t heightPx;
uint16_t copyBytes; uint16_t copyBytes;
@ -353,15 +436,24 @@ uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0; cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); 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++] = 0x56;
out[cursor++] = 0x57; out[cursor++] = 0x57;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C; 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++] = 0x10; 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++] = 0x5F;
out[cursor++] = 0x5E; out[cursor++] = 0x5E;
out[cursor++] = 0xC3; out[cursor++] = 0xC3;

View file

@ -8,12 +8,25 @@
// plane bytes at draw time -- but the entry points are kept so the // plane bytes at draw time -- but the entry points are kept so the
// dispatcher API stays uniform across ports. // dispatcher API stays uniform across ports.
// //
// Each emit function takes the sprite + shift variant and writes // Each emit function takes the sprite + shift variant and a capacity
// position-independent draw-routine bytes into `out`. Returns the // `cap` (bytes available at `out`), and writes position-independent
// number of bytes written. The output bytes follow the target CPU's // draw-routine bytes into `out`. It returns the number of bytes
// calling convention as documented in the per-CPU file's header // written (uint32_t -- a single pathological large routine can exceed
// comment, so the bytes can be called via a C function pointer cast // 64 KB, so a 16-bit count would truncate and mis-size the arena
// once placed in executable memory. // 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 #ifndef JOEYLIB_SPRITE_EMITTER_H
#define JOEYLIB_SPRITE_EMITTER_H #define JOEYLIB_SPRITE_EMITTER_H
@ -21,9 +34,35 @@
#include "joey/sprite.h" #include "joey/sprite.h"
#include "spriteInternal.h" #include "spriteInternal.h"
uint16_t spriteEmitDrawX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift); // Overflow sentinel returned by an emitter whose output would exceed
uint16_t spriteEmitDraw68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); // the supplied capacity. Distinct from a real byte count and from the
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift); // 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 // Save-under and restore-under emitters. Both copy a byte-aligned
// rectangle between the destination surface and a backup buffer. The // 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)` // 68k use a plain cdecl `void copy(const uint8_t *src, uint8_t *dst)`
// where the caller swaps args between SAVE (screen->backup) and // where the caller swaps args between SAVE (screen->backup) and
// RESTORE (backup->screen). // RESTORE (backup->screen).
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 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 spriteEmitSaveX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestoreX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSave68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitSave68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestore68k (uint8_t *out, 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 // Planar 68k emitters (Amiga). Distinct from the chunky 68k emitters
// above because the destination addressing is across 4 separate // 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 // dispatcher). Returns 0 for shifts not yet implemented (today only
// shift 0 == byte-aligned x is emitted; shifts 1..7 fall back to the // shift 0 == byte-aligned x is emitted; shifts 1..7 fall back to the
// cross-platform interpreter). // cross-platform interpreter).
uint16_t spriteEmitDrawPlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSavePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestorePlanar68k (uint8_t *out, 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 // Word-interleaved planar 68k emitter (ST). Calling convention for
// the emitted bytes: // 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); // 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 // other shifts return 0 so the cross-platform dispatcher falls back
// to halSpriteDrawPlanes. // to halSpriteDrawPlanes.
uint16_t spriteEmitDrawInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSaveInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestoreInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift); uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
#endif #endif

View file

@ -26,13 +26,12 @@
#include "joey/tile.h" #include "joey/tile.h"
#include "spriteInternal.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 // fopen / fread / fclose call drags a chunk of the stdio cluster
// in; concentrating them in one named segment keeps the root bank // in; concentrating them in one named segment keeps the root bank
// from overflowing past 64 KB (the failure mode is the linker's // from overflowing past 64 KB (the failure mode is the linker's
// "Code exceeds code bank size" / "Address is not in current bank" // "Code exceeds code bank size" / "Address is not in current bank"
// errors documented in the feedback memory). // errors documented in the feedback memory).
JOEYLIB_SEGMENT("ASSETLD")
// ----- Constants ----- // ----- Constants -----
@ -41,6 +40,14 @@ JOEYLIB_SEGMENT("ASSETLD")
#define ASSET_PALETTE_ENTRIES 16 #define ASSET_PALETTE_ENTRIES 16
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows #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 // Per-platform expected target byte for .tbk files. .spr files carry
// target=0 (cross-platform chunky) and pass on every build. // target=0 (cross-platform chunky) and pass on every build.
#if defined(JOEYLIB_PLATFORM_AMIGA) #if defined(JOEYLIB_PLATFORM_AMIGA)
@ -57,10 +64,46 @@ JOEYLIB_SEGMENT("ASSETLD")
// ----- Prototypes ----- // ----- 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); static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
// ----- Internal helpers (alphabetical) ----- // ----- 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) { static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
uint8_t i; uint8_t i;
@ -72,6 +115,10 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
// ----- Public API (alphabetical) ----- // ----- 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 jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
uint16_t *outPalette) { uint16_t *outPalette) {
FILE *fp; FILE *fp;
@ -87,26 +134,24 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
if (path == NULL || outCels == NULL || maxCels == 0u) { if (path == NULL || outCels == NULL || maxCels == 0u) {
return 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 // Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
// else means the file was either baked wrong or is a stale .spr // else means the file was either baked wrong or is a stale .spr
// from the old compiled-codegen pipeline. // from the old compiled-codegen pipeline.
if (memcmp(header, "JSP1", 4) != 0 || header[4] != 0u) { if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
fclose(fp);
return 0u; return 0u;
} }
widthTiles = header[6]; widthTiles = header[6];
heightTiles = header[7]; heightTiles = header[7];
cellCount = (uint16_t)(header[8] | (header[9] << 8)); 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; 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->heightTiles = heightTiles;
sp->ownsTileData = true; sp->ownsTileData = true;
sp->slot = NULL; 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->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank)); memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes)); 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, uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette) { bool *outValid, uint16_t *outPalette) {
FILE *fp; FILE *fp;
@ -159,28 +211,22 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
if (path == NULL || outTiles == NULL || maxTiles == 0u) { if (path == NULL || outTiles == NULL || maxTiles == 0u) {
return 0u; return 0u;
} }
fp = fopen(path, "rb"); // The target byte for .tbk must match the build platform. .spr
if (fp == NULL) { // files carry target=0; tiles carry the per-platform value. A
return 0u; // mismatch is a build-system bug -- the per-target make rule
} // should have produced a .tbk with the matching target byte.
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) { if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
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);
return 0u; return 0u;
} }
tileCount = (uint16_t)(header[6] | (header[7] << 8)); 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; toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;

View file

@ -88,8 +88,8 @@ void jlAudioTone(uint16_t freqHz) {
void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) { void jlAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
// Same reasoning as jlAudioTone: direct hardware-register // Same reasoning as jlAudioTone: direct hardware-register
// programming, no DSP/DMA dependency. Range-check voice here so // programming, no DSP/DMA dependency. Range-check voice here so
// the per-port HAL can assume voice < 3. // the per-port HAL can assume voice < JOEY_AUDIO_VOICES.
if (voice >= 3u) { if (voice >= JOEY_AUDIO_VOICES) {
return; return;
} }
halAudioVoice(voice, freqHz, atten); halAudioVoice(voice, freqHz, atten);

View file

@ -1,27 +1,52 @@
// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract. // Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.
#include <limits.h>
#include <stddef.h> #include <stddef.h>
#include "audioSfxMixInternal.h" #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 // Refill a streaming slot's prefetch buffer. Preserves the very last
// sample of the previous chunk at streamBuf[0] so linear interp can // sample of the previous chunk at streamBuf[0] so linear interp can
// step across the chunk boundary without a discontinuity click. The // step across the chunk boundary without a discontinuity click. The
// new chunk fills streamBuf[1..]; pos resets to "after byte 0" so // new chunk fills streamBuf[1..]. On a real refill (a previous chunk
// the next fetch is byte 1 (= first sample of the new 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 // Returns true if more samples are available, false if the stream
// has ended (caller should mark the slot inactive). // has ended (caller should mark the slot inactive).
static bool streamRefill(AudioSfxSlotT *slot) { static bool streamRefill(AudioSfxSlotT *slot) {
int8_t lastSample; int8_t lastSample;
uint32_t got; uint32_t got;
bool haveCarry;
if (slot->streamEof) { if (slot->streamEof) {
return false; 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); 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) { if (got == 0u) {
slot->streamEof = true; slot->streamEof = true;
slot->streamLen = 0u; slot->streamLen = 0u;
@ -29,8 +54,17 @@ static bool streamRefill(AudioSfxSlotT *slot) {
} }
slot->streamBuf[0] = lastSample; slot->streamBuf[0] = lastSample;
slot->streamLen = (uint16_t)(got + 1u); slot->streamLen = (uint16_t)(got + 1u);
// Reset pos to (1 sample, 0 frac) so the next read is at index 1. if (haveCarry) {
slot->posFp = (uint64_t)1u << 16; // 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; return true;
} }
@ -39,10 +73,14 @@ static bool streamRefill(AudioSfxSlotT *slot) {
// `frac` is the 16-bit fractional part of pos; 0 = exactly at s0, // `frac` is the 16-bit fractional part of pos; 0 = exactly at s0,
// 0xFFFF = nearly s1. Result is a signed int in [-128, +127]. // 0xFFFF = nearly s1. Result is a signed int in [-128, +127].
static int interp(int8_t s0, int8_t s1, uint32_t frac) { static int interp(int8_t s0, int8_t s1, uint32_t frac) {
int delta; int32_t delta;
delta = (int)s1 - (int)s0; // delta (up to +/-255) * frac (up to 65535) is ~16.7M -- needs the
return (int)s0 + ((delta * (int)frac) >> 16); // 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);
} }

View file

@ -5,17 +5,20 @@
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
// On the IIgs the arena holds 65816 machine code that callers JSL // 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, // into. A normal C heap allocation returns memory from bank 0 (system
// which is system RAM) so JSL'ing into it lands on whatever happens // RAM), so JSL'ing into it lands on whatever happens to live there ->
// to live there -> instant crash. Memory Manager NewHandle with // instant crash. Memory Manager NewHandle with
// attrFixed | attrLocked | attrPage | attrNoCross gives us a fixed // attrFixed | attrLocked | attrPage | attrNoCross gives us a fixed
// page-aligned region in a single bank we can safely jump into. // page-aligned region in a single bank we can safely jump into.
// // The IIgs toolbox bindings live in <iigs/toolbox.h>. Handles are typed
// types.h must be included before our stdbool shim because ORCA's // as void* and the owner ID comes from MMStartUp(); the Memory Manager
// types.h defines true/false as #define ... without #ifndef guards // attribute bits we use are defined here (values from Apple's Memory
// and would re-#define our shim's macros. // Manager reference).
#include <types.h> #include <iigs/toolbox.h>
#include <memory.h> #define attrPage 0x0004
#define attrNoCross 0x0010
#define attrFixed 0x4000
#define attrLocked 0x8000
#endif #endif
#include <stdlib.h> #include <stdlib.h>
@ -34,16 +37,15 @@
// Memory Manager handle is locked-in-place on IIgs). Callers MUST // Memory Manager handle is locked-in-place on IIgs). Callers MUST
// treat them as read-only. // treat them as read-only.
uint8_t *gCodegenArenaBase = NULL; uint8_t *gCodegenArenaBase = NULL;
// gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit // gCodegenArenaBaseAddr mirrors gCodegenArenaBase as a 24-bit absolute
// absolute address. ORCA-C's (uint32_t)pointer cast on the IIgs // address that JSL targets read directly (captured by copying the
// zeros the bank byte for some pointer expressions, so JSL targets // handle pointer's raw bytes -- see codegenArenaInit).
// read this field directly.
uint32_t gCodegenArenaBaseAddr = 0; uint32_t gCodegenArenaBaseAddr = 0;
static uint32_t gTotalBytes = 0; static uint32_t gTotalBytes = 0;
static uint32_t gUsedBytes = 0; static uint32_t gUsedBytes = 0;
static ArenaSlotT *gFirstSlot = NULL; static ArenaSlotT *gFirstSlot = NULL;
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
static Handle gCodegenArenaBaseHandle = NULL; static void *gCodegenArenaBaseHandle = NULL;
#endif #endif
@ -224,24 +226,29 @@ bool codegenArenaInit(uint32_t totalBytes) {
return false; return false;
} }
#if defined(JOEYLIB_PLATFORM_IIGS) #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, attrFixed | attrLocked | attrPage | attrNoCross,
NULL); (void *)0);
if (gCodegenArenaBaseHandle == NULL || _toolErr != 0) { }
gCodegenArenaBaseHandle = NULL; if (gCodegenArenaBaseHandle == NULL) {
return false; return false;
} }
HLock(gCodegenArenaBaseHandle); HLock(gCodegenArenaBaseHandle);
// Capture the 24-bit absolute address by copying the Pointer's // Capture the 24-bit absolute address by copying the master
// raw bytes -- (uint32_t)pointer through a chain of expressions // pointer's raw bytes; bytes[3] (bank-high/flags) is masked off.
// 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.
{ {
Pointer p; void *p;
uint8_t bytes[4]; uint8_t bytes[4];
p = *gCodegenArenaBaseHandle;
gCodegenArenaBase = (uint8_t *)p; p = *(void **)gCodegenArenaBaseHandle;
gCodegenArenaBase = (uint8_t *)p;
memcpy(bytes, &p, 4); memcpy(bytes, &p, 4);
gCodegenArenaBaseAddr = (uint32_t)bytes[0] gCodegenArenaBaseAddr = (uint32_t)bytes[0]
| ((uint32_t)bytes[1] << 8) | ((uint32_t)bytes[1] << 8)

View file

@ -9,7 +9,7 @@
// compaction that moves a slot's bytes is transparent to callers. // compaction that moves a slot's bytes is transparent to callers.
// //
// Allocations go through plain malloc; on every supported port // 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 // readable, writable, AND executable. We are not running in a W^X
// environment. // environment.
// //

View file

@ -1,18 +1,19 @@
// Cross-platform "where did it hang?" logger. Holds joeylog.txt open // Cross-platform "where did it hang?" logger. Holds joeylog.txt open
// across calls; libc's stdio buffer absorbs writes (~4 KB) and the // across calls and FLUSHES AFTER EVERY LINE: safe logs are more
// final fclose at program exit (via atexit) gets the buffer to disk. // 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 // The cost is one host FST WRITE per line (seconds each through the
// guaranteed on disk if we hang"); that cost ~1 second per call // emulator's ProDOS FST emulation; that host-IO time is not tracked
// through GoldenGate's ProDOS FST emulation -- a 50-line UBER run // by the IIgs VBL counter, so wall-time totals computed from the frame
// burned ~5 minutes in IO. Even per-line fflush is too expensive // counter slightly underreport). The 16 KB full buffer below still
// because every fflush forces an FST WRITE, and host-OS file IO time // coalesces each line's individual vfprintf writes into ONE FST write,
// isn't tracked by the IIgs VBL counter so wall-time logs underreport. // so the flush is per-line, not per-fragment.
// //
// Tradeoff: if the program crashes mid-run, buffered log lines may // (Earlier revs opened+closed per call, then buffered the whole run and
// not reach disk. For UBER and similar batch demos that's acceptable; // only flushed at the atexit fclose -- fast, but a mid-run crash lost
// for hang-debugging where durability matters, call jlLogFlush() // the buffered tail, exactly when you need it most. Safety wins.)
// at the suspected hang points.
#include <stdio.h> #include <stdio.h>
#include <stdarg.h> #include <stdarg.h>
@ -23,12 +24,10 @@
static const char *kLogPath = "joeylog.txt"; static const char *kLogPath = "joeylog.txt";
static FILE *gLogFp = NULL; static FILE *gLogFp = NULL;
/* 16 KB is enough for UBER's full log (~5 KB) plus generous headroom, /* 16 KB full buffer so a single log line's many small vfprintf writes
* so the file never auto-flushes mid-run. ORCA-C / libnix default * coalesce into one host FST WRITE; jlLog/jlLogF then fflush once per
* buffers are only ~512 bytes; with that, a 50-line log triggers ~10 * line. Default stdio buffers are only ~512 bytes, which would split a
* ProDOS / AmigaDOS WRITEs through the host FST, each of which is * long line across multiple FST writes. */
* untracked-host-time (seconds). Buffer the whole thing in memory and
* let the atexit fclose flush once. */
#define JOEY_LOG_BUF_BYTES 16384 #define JOEY_LOG_BUF_BYTES 16384
static char gLogBuf[JOEY_LOG_BUF_BYTES]; static char gLogBuf[JOEY_LOG_BUF_BYTES];
@ -56,6 +55,7 @@ void jlLog(const char *msg) {
} }
fputs(msg, fp); fputs(msg, fp);
fputc('\n', fp); fputc('\n', fp);
fflush(fp);
} }
@ -73,6 +73,7 @@ void jlLogF(const char *fmt, ...) {
vfprintf(fp, fmt, args); vfprintf(fp, fmt, args);
va_end(args); va_end(args);
fputc('\n', fp); fputc('\n', fp);
fflush(fp);
} }

View file

@ -21,43 +21,22 @@
// On overflow the fill silently truncates rather than crashing. // On overflow the fill silently truncates rather than crashing.
#define FLOOD_STACK_SIZE 512 #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 ----- // ----- 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 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 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) ----- // ----- 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) { 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 nibble = colorIndex & 0x0F;
uint8_t doubled = (uint8_t)((nibble << 4) | nibble); 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; uint16_t midBytes;
uint8_t *line; 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) /* px* and midBytes are uint16_t (clipped values are non-negative)
* so `>>1` lowers to a single LSR instead of ORCA-C's * so `>>1` lowers to a single LSR instead of a signed-shift
* ~SSHIFTRIGHT helper. Same with `<<1` for midBytes. */ * helper. Same with `<<1` for midBytes. */
for (row = 0; row < h; row++) { for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)]; line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x; 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 // Smith's scanline flood fill. Implements both the unbounded and the
// boundary-stopped variants in one pass: the matching predicate is // boundary-stopped variants in one pass: the matching predicate is
// (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill, // (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, stackX, stackY,
&sp, FLOOD_STACK_SIZE, &sp, FLOOD_STACK_SIZE,
&seedMatched, &leftX, &rightX)) { &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; 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 // Fill the span. walk-out already guaranteed leftX/rightX are in
// already guaranteed leftX/rightX are in [0..SURFACE_WIDTH-1] // [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same
// and the seed-pop bounds check did the same for y. We DO // for y, so the span is provably on-surface -- route it through
// need the planar dual-write (which jlFillRect's wrapper would // the shared on-surface tail (chunky fill + planar dual-write +
// call), so invoke halFillRectPlanes explicitly after the // dirty mark) rather than re-clipping through jlFillRect. The
// chunky span fill -- otherwise PLANAR_PRESENT builds (and, // planar dual-write here is load-bearing: without it
// post-Phase-9, every build) display flood-filled regions // PLANAR_PRESENT builds (and, post-Phase-9, every build) display
// as the unfilled background. // flood-filled regions as the unfilled background.
{ {
int16_t spanW = (int16_t)(rightX - leftX + 1); int16_t spanW = (int16_t)(rightX - leftX + 1);
if (!halFastFillRect(s, leftX, y, (uint16_t)spanW, 1, newNibble)) { fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
fillRectClipped(s, leftX, y, spanW, 1, newNibble);
}
halFillRectPlanes(s, leftX, y, (uint16_t)spanW, 1, newNibble);
} }
// Scan rows above and below for run boundaries. The hot // 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) ----- // ----- Public API (alphabetical) -----
void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { 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 y;
int16_t err; int16_t err;
int16_t ir; int16_t ir;
int16_t minX;
int16_t minY;
int16_t maxX;
int16_t maxY;
if (s == NULL) { if (s == NULL) {
return; 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 // 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 // hand off to the port asm (no per-pixel bounds check needed in
// the inner loop) and mark the bounding box dirty once. // the inner loop) and mark the bounding box dirty once. A circle
ir = (int16_t)r; // with r >= SURFACE_HEIGHT can never fit on-surface, and casting
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH && // such an r to int16_t can wrap negative (r > 32767), so reject it
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT && // here before the cast feeds the fast path / dirty-rect extents.
halFastDrawCircle(s, cx, cy, r, colorIndex)) { if (r < SURFACE_HEIGHT) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), ir = (int16_t)r;
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1)); if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
return; 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- // Bresenham midpoint: maintain (x, y) on the perimeter, eight-
// octant symmetry plots all 8 reflections each iteration. Routes // octant symmetry plots all 8 reflections each iteration. Plots
// through jlDrawPixel so off-surface pixels clip individually. // 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; x = (int16_t)r;
y = 0; y = 0;
err = (int16_t)(1 - x); err = (int16_t)(1 - x);
while (x >= y) { while (x >= y) {
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex); // The 8 octants span columns cx +/- x and cx +/- y, and rows
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex); // cy +/- y and cy +/- x. Fold all reflections into the box in
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex); // one shot, then clamp once after the loop.
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex); if (cx + x > maxX) { maxX = (int16_t)(cx + x); }
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex); if (cx - x < minX) { minX = (int16_t)(cx - x); }
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex); if (cx + y > maxX) { maxX = (int16_t)(cx + y); }
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex); if (cx - y < minX) { minX = (int16_t)(cx - y); }
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex); 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++; y++;
/* Use `+ + 1` instead of `2 * y + 1` so ORCA-C never emits /* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits
* the ~SMUL2 helper -- two ADDs are unconditionally cheaper. */ * a multiply helper -- two ADDs are unconditionally cheaper. */
if (err <= 0) { if (err <= 0) {
err = (int16_t)(err + y + y + 1); err = (int16_t)(err + y + y + 1);
} else { } 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); 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 err;
int16_t e2; int16_t e2;
int16_t tmp; int16_t tmp;
int16_t minX;
int16_t minY;
int16_t maxX;
int16_t maxY;
int32_t span;
if (s == NULL) { if (s == NULL) {
return; return;
} }
// Horizontal and vertical fast paths use jlFillRect; the general // 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. // off-surface clipping just works.
if (y0 == y1) { if (y0 == y1) {
if (x0 > x1) { 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; x0 = x1;
x1 = tmp; 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; return;
} }
if (x0 == x1) { 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; y0 = y1;
y1 = tmp; 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; return;
} }
@ -426,13 +532,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
return; 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)); dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1))); dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
sx = (int16_t)((x0 < x1) ? 1 : -1); sx = (int16_t)((x0 < x1) ? 1 : -1);
sy = (int16_t)((y0 < y1) ? 1 : -1); sy = (int16_t)((y0 < y1) ? 1 : -1);
err = (int16_t)(dx + dy); err = (int16_t)(dx + dy);
while (1) { 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) { if (x0 == x1 && y0 == y1) {
break; 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); 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) { void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
uint8_t *byte;
uint8_t nibble;
if (s == NULL) { if (s == NULL) {
return; 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; 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)) { if (word < gStageMinWord[y]) {
/* Cast to uint16_t before shift -- already validated x >= 0, gStageMinWord[y] = word;
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */ }
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)]; if (word > gStageMaxWord[y]) {
nibble = colorIndex & 0x0F; gStageMaxWord[y] = word;
if (x & 1) {
*byte = (uint8_t)((*byte & 0xF0) | nibble);
} else {
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
} }
} }
#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); 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); jlFillRect(s, x, y, w, h, colorIndex);
return; 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. // Top edge.
jlFillRect(s, x, y, w, 1, colorIndex); jlFillRect(s, x, y, w, 1, colorIndex);
// Bottom edge. // 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 y;
int16_t x; int16_t x;
int16_t ir; int16_t ir;
uint16_t xx; uint32_t xx;
uint16_t yy; uint32_t yy;
uint16_t r2; uint32_t r2;
bool onSurface;
if (s == NULL) { if (s == NULL) {
return; return;
@ -516,26 +678,39 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
return; return;
} }
ir = (int16_t)r; // Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH && // on-surface, and casting such an r to int16_t can wrap negative
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT && // (r > 32767), spuriously passing the bounds test and feeding the
halFastFillCircle(s, cx, cy, r, colorIndex)) { // asm / dirty-rect garbage. Reject it here before the cast.
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), onSurface = false;
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1)); if (r < SURFACE_HEIGHT) {
return; 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 // 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 // <= 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 -- // incrementally so the hot loop never does a 32-bit multiply --
// critical on 65816 / 68000 / 286 where mul is slow or absent. // 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 // (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are
// so xx, yy, r2 fit in uint16_t for any r where x*x+y*y can equal // uint32_t so r*r does not wrap for r >= 256 (a uint16_t product
// r2 (i.e. r <= 255 -> r2 <= 65025). // overflows at r == 256 -> r2 == 0).
/* Same `+ +` pattern as jlDrawCircle so ORCA-C doesn't emit ~SMUL2 / /* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit
* ~CUMUL2 helpers for the `2 * ...` constants. spanWidth is hoisted * multiply helpers for the `2 * ...` constants. spanWidth is hoisted
* because both jlFillRect calls in the body need it. */ * because both jlFillRect calls in the body need it. */
xx = (uint16_t)(r * r); xx = (uint32_t)r * (uint32_t)r;
r2 = xx; r2 = xx;
yy = 0; yy = 0;
x = (int16_t)r; 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; uint16_t spanWidth;
while (xx + yy > r2) { 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--; x--;
} }
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u); spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex); if (onSurface) {
if (y > 0) { // 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex); // 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 sy;
int16_t sw; int16_t sw;
int16_t sh; int16_t sh;
bool visible; int32_t left;
int32_t top;
int32_t right;
int32_t bottom;
if (s == NULL) { if (s == NULL) {
return; return;
} }
sx = x; // Clip in 32-bit so any int16_t x/y combined with any uint16_t w/h
sy = y; // is handled without overflow (a uint16_t w > 32767 would wrap
sw = (int16_t)w; // negative if narrowed to int16_t before clipping, and a negative x
sh = (int16_t)h; // with a large w must still fill out to the right surface edge).
clipRect(&sx, &sy, &sw, &sh, &visible); // The surface is only 320x200, so the clipped result always fits
if (!visible) { // 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; return;
} }
if (!halFastFillRect(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex)) { sx = (int16_t)left;
fillRectClipped(s, sx, sy, sw, sh, colorIndex); sy = (int16_t)top;
} sw = (int16_t)(right - left);
halFillRectPlanes(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex); sh = (int16_t)(bottom - top);
surfaceMarkDirtyRect(s, sx, sy, sw, sh); // 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; nibble = colorIndex & 0x0F;
doubled = (uint8_t)((nibble << 4) | nibble); doubled = (uint8_t)((nibble << 4) | nibble);
if (!halFastSurfaceClear(s, doubled)) { if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) {
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE); memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
} }
surfaceMarkDirtyAll(s); surfaceMarkDirtyAll(s);

View file

@ -191,8 +191,12 @@ uint16_t halFrameHz(void);
// Wall-clock millisecond counter since halInit. Source of truth // Wall-clock millisecond counter since halInit. Source of truth
// for time-based scheduling (sound event timing, animation cadence // for time-based scheduling (sound event timing, animation cadence
// on platforms where rendering can outpace VBL). Per-port impl // on platforms where rendering can outpace VBL). Per-port impl uses
// uses the cheapest sub-frame timer available on that hardware. // 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); uint32_t halMillisElapsed(void);
// Audio: per-port engine setup, module + SFX playback, teardown. // 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. // caller should fall back to the platform-agnostic C implementation.
// //
// Funneling all asm dispatches through hal.c (one TU per port) avoids // 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 // hits when multiple cross-platform TUs each call into a named load
// segment full of asm primitives. Cross-platform code in src/core/ // segment full of asm primitives. Cross-platform code in src/core/
// only ever calls into HAL, so the link-time expression cost is paid // 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 // Cross-platform code in src/core/*.c is unchanged -- it still calls
// halFastDrawPixel(...) etc. On IIgs the preprocessor swaps that for // 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 // halFast* C definitions in src/port/iigs/hal.c are deleted, since
// nothing references them once the macros take effect. // 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 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 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 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 iigsTileFillInner (uint8_t *dstRow0, uint16_t fillWord);
extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0); extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent); extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
@ -416,7 +421,7 @@ extern uint16_t gFloodRightX;
#undef halFastSurfaceClear #undef halFastSurfaceClear
#define halFastSurfaceClear(_s, _d) \ #define halFastSurfaceClear(_s, _d) \
((_s) == jlStageGet() \ ((_s) == jlStageGet() \
? (iigsSurfaceClearInner((_s)->pixels, \ ? (iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \ (uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \
true) \ true) \
: false) : false)
@ -438,7 +443,7 @@ extern uint16_t gFloodRightX;
// Tile primitives operate on caller-computed row pointers; just // Tile primitives operate on caller-computed row pointers; just
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte // 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 // 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 #undef halFastTileFill
#define halFastTileFill(_s, _bx, _by, _fw) \ #define halFastTileFill(_s, _bx, _by, _fw) \
(iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \ (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 // Tier 2/3 flood fallbacks always returned false on IIgs (the asm
// impls were deleted as unreachable). Macros to constant false so // impls were deleted as unreachable). Macros to constant false so
// ORCA-C dead-code-eliminates the never-taken fallback branches in // the compiler dead-code-eliminates the never-taken fallback branches
// floodFillInternal. // in floodFillInternal.
#undef halFastFloodWalk #undef halFastFloodWalk
#define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false) #define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false)
@ -494,6 +499,63 @@ extern uint16_t gFloodRightX;
*(_rxOut) = (int16_t)gFloodRightX, \ *(_rxOut) = (int16_t)gFloodRightX, \
true) 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 /* JOEYLIB_PLATFORM_IIGS */
#endif #endif

View file

@ -15,13 +15,16 @@
// 8 KB fits the largest typical sprite working set (~3-4 KB per // 8 KB fits the largest typical sprite working set (~3-4 KB per
// 32x32 sprite at all opaque) and keeps malloc requests small enough // 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) #define DEFAULT_CODEGEN_BYTES (8u * 1024u)
// ----- Prototypes ----- // ----- Prototypes -----
static void clearError(void); 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 ----- // ----- Module state -----
@ -36,7 +39,7 @@ static void clearError(void) {
} }
static void setError(const char *message) { void coreSetError(const char *message) {
gLastError = message; gLastError = message;
} }
@ -47,12 +50,12 @@ bool jlInit(const jlConfigT *config) {
clearError(); clearError();
if (gInitialized) { if (gInitialized) {
setError("jlInit called while already initialized"); coreSetError("jlInit called while already initialized");
return false; return false;
} }
if (config == NULL) { if (config == NULL) {
setError("jlInit called with NULL config"); coreSetError("jlInit called with NULL config");
return false; return false;
} }
@ -63,19 +66,19 @@ bool jlInit(const jlConfigT *config) {
// SHR setup that halInit performs. // SHR setup that halInit performs.
if (!halInit(&gConfig)) { if (!halInit(&gConfig)) {
const char *halMsg = halLastError(); const char *halMsg = halLastError();
setError(halMsg != NULL ? halMsg : "halInit failed"); coreSetError(halMsg != NULL ? halMsg : "halInit failed");
return false; return false;
} }
if (!stageAlloc()) { if (!stageAlloc()) {
setError("failed to allocate stage surface"); coreSetError("failed to allocate stage surface");
halShutdown(); halShutdown();
return false; return false;
} }
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes
: DEFAULT_CODEGEN_BYTES)) { : DEFAULT_CODEGEN_BYTES)) {
setError("failed to allocate codegen arena"); coreSetError("failed to allocate codegen arena");
stageFree(); stageFree();
halShutdown(); halShutdown();
return false; return false;

View file

@ -39,7 +39,7 @@ extern void iigsInputSnapshot(void);
// and the small button counts, and walks every array one byte per // and the small button counts, and walks every array one byte per
// element. If a future change adds/removes keys or buttons the asm // 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 // 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 // the wrong bytes. Either condition declares a zero-size array
// below, which is a compile error. // below, which is a compile error.
typedef int joey_keycount_check [(KEY_COUNT == 60) ? 1 : -1]; 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) { void jlInputPoll(void) {
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
// One asm pass for: TTL decrement + key snapshot + mouse/joy // 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. // that used to live in halInputPoll. ~0.6 ms saved per frame.
iigsInputSnapshot(); iigsInputSnapshot();
#else #else
@ -88,8 +88,7 @@ void jlWaitForAnyKey(void) {
* and upper-bound check (`>= COUNT`) into a single unsigned compare. * and upper-bound check (`>= COUNT`) into a single unsigned compare.
* Index 0 (KEY_NONE / MOUSE_BUTTON_NONE) is a sentinel that no HAL * Index 0 (KEY_NONE / MOUSE_BUTTON_NONE) is a sentinel that no HAL
* ever writes, so reading gKeyState[0] / gMouseButtonState[0] is * ever writes, so reading gKeyState[0] / gMouseButtonState[0] is
* always 0 -- the predicate result is unchanged but ORCA-C drops the * always 0 -- the predicate result is unchanged. */
* compound `||` into one branch each. */
bool jlKeyDown(jlKeyE key) { bool jlKeyDown(jlKeyE key) {
if ((uint16_t)key >= (uint16_t)KEY_COUNT) { if ((uint16_t)key >= (uint16_t)KEY_COUNT) {
return false; return false;
@ -172,9 +171,9 @@ int8_t jlJoystickY(jlJoystickE js) {
} }
/* Joystick button predicates: ORCA-C 2.2.1 lowers `gJoyButtonState[js][button]` /* Joystick button predicates: compute the 1D byte index once and read
* to a ~MUL4 helper per access. Compute the 1D byte index once and read * via an explicit (uint8_t *) cast so the 2D-array indexing avoids a
* via an explicit (uint8_t *) cast -- no helpers. */ * per-access multiply helper. */
bool jlJoyDown(jlJoystickE js, jlJoyButtonE button) { bool jlJoyDown(jlJoystickE js, jlJoyButtonE button) {
uint16_t idx; uint16_t idx;
if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) { if ((uint16_t)js >= (uint16_t)JOYSTICK_COUNT) {

View file

@ -12,13 +12,13 @@
#include "joey/input.h" #include "joey/input.h"
#include "joey/types.h" #include "joey/types.h"
// Stored as uint8_t (not bool) because ORCA-C compiles _Bool as a // Stored as uint8_t (not bool) because a _Bool wider than one byte
// 2-byte word (Symbol.pas: size := cgWordSize). The IIgs asm fast // would break the IIgs asm fast path (iigsInputSnapshot), which walks
// path (iigsInputSnapshot) walks these arrays one byte per element; // these arrays one byte per element; with a 2-byte bool element k
// a 2-byte bool would put element k at byte offset 2*k and the asm's // would sit at byte offset 2*k and the asm's per-byte clear would
// per-byte clear would never reach the live half. uint8_t pins the // never reach the live half. uint8_t pins the storage to one byte per
// storage to one byte per element on every port. Public predicates // element on every port. Public predicates still return bool via
// still return bool via implicit coercion. // implicit coercion.
extern uint8_t gKeyState[KEY_COUNT]; extern uint8_t gKeyState[KEY_COUNT];
extern uint8_t gKeyPrev [KEY_COUNT]; extern uint8_t gKeyPrev [KEY_COUNT];

View file

@ -10,6 +10,14 @@
#include "joey/palette.h" #include "joey/palette.h"
#include "surfaceInternal.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 // Standard 16-color EGA palette in IIgs $0RGB format. Used as the
// per-surface default at allocation time (paletteInitDefault) so a // per-surface default at allocation time (paletteInitDefault) so a
@ -45,8 +53,14 @@ void paletteInitDefault(jlSurfaceT *s) {
if (s == NULL) { if (s == NULL) {
return; 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++) { 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) { if (paletteIndex >= SURFACE_PALETTE_COUNT) {
return; 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. */ * jlPaletteSet for the reasoning. */
row = (const uint16_t *)((const uint8_t *)s->palette + ((uint16_t)paletteIndex << 5)); row = (const uint16_t *)((const uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
memcpy(out16, row, SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t)); 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 /* 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 * (16 entries * 2 bytes = 32 = 1 << 5) so the 2D-array indexing
* ~MUL4 helper for the 2D-array indexing. Then walk both arrays * avoids a multiply helper. Then walk both arrays with
* with post-increment pointers so the inner loop avoids ~MUL4 * post-increment pointers so the inner loop avoids the index
* for every `row[i]` / `colors16[i]` index multiply too. */ * multiply for every `row[i]` / `colors16[i]` too. */
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5)); row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
src = colors16; src = colors16;

View file

@ -1,8 +1,8 @@
// Deterministic, seedable PRNG (xorshift32 / Marsaglia). // Deterministic, seedable PRNG (xorshift32 / Marsaglia).
// //
// stdlib rand() is unsuitable: DJGPP, ORCA-C, libnix, and mintlib all // stdlib rand() is unsuitable: the per-platform C libraries ship
// ship different LCG constants, and ORCA-C's rand() returns a 16-bit // different LCG constants and differing result widths (some return a
// value while the others return 31 bits. Any game logic relying on // 16-bit value, others 31 bits). Any game logic relying on
// reproducible sequences (replay, demo records, level seeds, deck // reproducible sequences (replay, demo records, level seeds, deck
// shuffles) would diverge silently. xorshift32 has no multiply, so // shuffles) would diverge silently. xorshift32 has no multiply, so
// the 65816 perf floor isn't penalized by a software mul routine. // 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) { if (bound == 0u) {
return 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);
} }

View file

@ -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) { void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t paletteIndex) {
uint16_t line;
uint16_t last; uint16_t last;
if (s == NULL) { if (s == NULL) {
@ -56,12 +55,11 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
return; return;
} }
// memset is far cheaper than the per-iter loop on ORCA-C with -b // memset is far cheaper than the per-iter loop (scb is uint8_t,
// (scb is uint8_t, sizeof(uint8_t)==1, so the call form below is // sizeof(uint8_t)==1, so the call form below is exact). On IIgs
// exact). On IIgs ORCA-C lowers small fixed-size memsets to MVP / // small fixed-size memsets lower to MVP / PEI tricks; on
// PEI tricks; on Amiga/ST/DOS it uses libc memset which is // Amiga/ST/DOS libc memset is already vectorized. Either way, much
// already vectorized. Either way, much tighter than the C loop. // tighter than the C loop.
(void)line;
memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1)); memset(&s->scb[firstLine], paletteIndex, (size_t)(last - firstLine + 1));
if (s == jlStageGet()) { if (s == jlStageGet()) {
gStageScbDirty = true; gStageScbDirty = true;

View file

@ -8,16 +8,15 @@
#include <string.h> #include <string.h>
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/tile.h"
#include "codegenArenaInternal.h" #include "codegenArenaInternal.h"
#include "hal.h" #include "hal.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
// 8x8 tiles, 4bpp packed = 4 bytes/row * 8 rows = 32 bytes/tile. // 8x8 tile geometry constants come from joey/tile.h -- one source of
#define TILE_BYTES 32 // truth for the 4bpp packed layout (32 bytes/tile, 4 bytes/row, 8 px/side).
#define TILE_PIXELS 8
#define TILE_BYTES_PER_ROW 4
// Color 0 is always transparent for sprites (DESIGN.md contract). // Color 0 is always transparent for sprites (DESIGN.md contract).
#define TRANSPARENT_NIBBLE 0 #define TRANSPARENT_NIBBLE 0
@ -43,10 +42,10 @@
// ----- Prototypes ----- // ----- 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 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 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 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); 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) { static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble) {
uint8_t *byte; 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 // Pixel-by-pixel draw. Used directly when the sprite has no compiled
// slot, and as the clip-edge fallback when a compiled draw would // slot, and as the clip-edge fallback when a compiled draw would
// extend off the surface. // 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) { static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
int16_t dx; int16_t dx;
int16_t dy; int16_t dy;
@ -150,17 +146,16 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
int16_t w; int16_t w;
int16_t h; int16_t h;
int16_t row; int16_t row;
int16_t col; uint8_t wTiles;
uint8_t nibble;
uint8_t *dstRow;
dx = x; dx = x;
dy = y; dy = y;
w = (int16_t)(sp->widthTiles * TILE_PIXELS); w = (int16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
h = (int16_t)(sp->heightTiles * TILE_PIXELS); h = (int16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) { if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) {
return; return;
} }
wTiles = sp->widthTiles;
/* Skip the chunky write loop on planar ports (s->pixels == NULL). /* Skip the chunky write loop on planar ports (s->pixels == NULL).
* halSpriteDrawPlanes is called by the jlSpriteDraw caller and does * 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. */ * happen there. Phase 9 dropped the chunky shadow on Amiga. */
if (s->pixels != NULL) { if (s->pixels != NULL) {
for (row = 0; row < h; row++) { 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++) { for (col = 0; col < w; col++) {
nibble = srcNibble(sp, (int16_t)(sx + col), (int16_t)(sy + row)); uint8_t byte;
if (nibble == TRANSPARENT_NIBBLE) { uint8_t nibble;
continue;
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) { if (sp == NULL) {
return NULL; return NULL;
} }
sp->tileData = tileData; spriteInitFields(sp, tileData, widthTiles, heightTiles, false);
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));
return sp; 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 // 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 // on whole bytes -- mid-byte snapshots would lose half a pixel at
// the left edge. // 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; return NULL;
} }
widthPx = (uint16_t)(widthTiles * TILE_PIXELS); widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(heightTiles * TILE_PIXELS); heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
if (x < 0 || y < 0 || if (x < 0 || y < 0 ||
x + (int16_t)widthPx > SURFACE_WIDTH || x + (int16_t)widthPx > SURFACE_WIDTH ||
y + (int16_t)heightPx > SURFACE_HEIGHT) { 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 (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) { for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES]; dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
for (row = 0; row < TILE_PIXELS; row++) { for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS + row) * SURFACE_BYTES_PER_ROW]; srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row) * SURFACE_BYTES_PER_ROW];
memcpy(&dstTile[row * TILE_BYTES_PER_ROW], memcpy(&dstTile[row * TILE_BYTES_PER_ROW],
&srcRow[((uint16_t)x >> 1) + tx * TILE_BYTES_PER_ROW], &srcRow[((uint16_t)x >> 1) + tx * TILE_BYTES_PER_ROW],
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 (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) { for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES]; dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
for (row = 0; row < TILE_PIXELS; row++) { for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS + row); py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row);
for (col = 0; col < TILE_PIXELS; col += 2) { for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) {
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS + col); px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col);
hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu); 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); loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu);
dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] = 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); free(buf);
return NULL; return NULL;
} }
sp->tileData = buf; spriteInitFields(sp, buf, widthTiles, heightTiles, true);
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));
return sp; return sp;
} }
@ -329,8 +335,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
if (s == NULL || sp == NULL) { if (s == NULL || sp == NULL) {
return; return;
} }
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS); widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes + fully on surface. Off-surface draws // Fast path: compiled bytes + fully on surface. Off-surface draws
// fall back to the interpreter so the compiled routines never // 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; hTiles = sp->heightTiles;
slot = sp->slot; slot = sp->slot;
widthPx = (uint16_t)(wTiles * TILE_PIXELS); widthPx = (uint16_t)(wTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(hTiles * TILE_PIXELS); heightPx = (uint16_t)(hTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes available, fully on surface, backup // Fast path: compiled bytes available, fully on surface, backup
// buffer supplied. Save fills out backup->{x,y,width,height, // 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: // Defragment the codegen arena (compiled sprite slots). The live .spr
// offset bytes field // on-disk format is documented beside the loader in core/assetLoad.c.
// ------ ----- --------------------------------------------
// 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)
//
void jlSpriteCompact(void) { void jlSpriteCompact(void) {
codegenArenaCompact(); codegenArenaCompact();
} }
@ -471,7 +467,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
/* Validate. Note: SURFACE_WIDTH - bx and SURFACE_HEIGHT - by stay /* Validate. Note: SURFACE_WIDTH - bx and SURFACE_HEIGHT - by stay
* in uint16_t range once bx >= 0 / by >= 0 has been checked, so * 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 * 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 || if (backup->bytes == NULL ||
bw == 0 || bh == 0 || bw == 0 || bh == 0 ||
bx < 0 || by < 0 || bx < 0 || by < 0 ||
@ -483,22 +479,32 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
} }
sp = backup->sprite; 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 routeIdx;
uint16_t routeOffset; uint16_t routeOffset;
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW); spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (int16_t)(bw >> 1); copyBytes = (int16_t)(bw >> 1);
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1; // The compiled restore routine walks a FIXED per-row byte count
/* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */ // derived purely from the chosen shift (spriteBytesPerRow for
routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE); // shift 0, spriteBytesPerRow+1 for shift 1). A horizontally
routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1)); // clipped save records a narrower backup->width / copyBytes that
if (routeOffset != SPRITE_NOT_COMPILED) { // matches neither, so only take the fast path when copyBytes is
spriteCompiledRestoreUnder(s, backup); // exactly one of those two compiled widths; any other width must
if (!COMPILED_SPRITE_WRITES_PLANES) { // fall through to the interpreted memcpy that honors copyBytes.
halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes); 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) { 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 /* 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. */ * stack frame for them on every fast-path call. */
uint16_t widthPx; uint16_t widthPx;
uint16_t heightPx; uint16_t heightPx;
@ -540,8 +546,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
backup->sizeBytes = 0; backup->sizeBytes = 0;
slot = sp->slot; slot = sp->slot;
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS); widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS); heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Compiled fast path: fully on surface and the platform emitted // Compiled fast path: fully on surface and the platform emitted
// bytes for SAVE at this shift. The compiled routine assumes a // 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. // through to the interpreted memcpy loop below.
// //
// The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as // The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as
// explicit byte-pointer arithmetic to dodge ORCA-C 2.2.1's ~MUL4 // explicit byte-pointer arithmetic to dodge the multiply helper
// helper that gets emitted for `uint16_t arr[N][M]` indexing. // that gets emitted for `uint16_t arr[N][M]` indexing.
if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) { if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
uint16_t routeIdx; uint16_t routeIdx;
uint16_t routeOffset; uint16_t routeOffset;

View file

@ -61,8 +61,8 @@ struct jlSpriteT {
uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT]; uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
// Cached `copyBytes * heightPx` per shift for spriteCompiledSaveUnder's // Cached `copyBytes * heightPx` per shift for spriteCompiledSaveUnder's
// `backup->sizeBytes` field. uint16_t * uint16_t goes through ORCA-C // `backup->sizeBytes` field. uint16_t * uint16_t goes through a
// 2.2.1's ~CUMUL2 helper (~30-50 cyc); cache hit dodges it. Filled // software multiply helper (~30-50 cyc); cache hit dodges it. Filled
// lazily on first call (0 sentinel = uncached). // lazily on first call (0 sentinel = uncached).
uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT]; uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT];
}; };

View file

@ -30,20 +30,20 @@ uint8_t gStageMinWord[SURFACE_HEIGHT];
uint8_t gStageMaxWord[SURFACE_HEIGHT]; uint8_t gStageMaxWord[SURFACE_HEIGHT];
// "Stage SCB / palette has changed since last present-side upload." // "Stage SCB / palette has changed since last present-side upload."
// Cheap flag check at present time replaces the 200+512 byte memcmps // A cheap flag check at present time lets a port skip re-uploading the
// the IIgs port used to run every frame in halPresentRect's // 200-byte SCB and 512-byte palette when neither changed -- ~7 ms /
// uploadScbAndPaletteIfNeeded -- ~7 ms / frame saved on demos that // frame saved on demos that don't churn palette/SCB (i.e., almost all
// don't churn palette/SCB (i.e., almost all demos). // demos).
// //
// Initially true so the first present uploads. jlScbSet*/jlPaletteSet // Initially true so the first present uploads. jlScbSet*, jlPaletteSet,
// re-mark dirty when the stage's data changes; per-port present code // and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile)
// clears the flag after consuming. // 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 gStageScbDirty = true;
bool gStagePaletteDirty = true; bool gStagePaletteDirty = true;
// ----- Internal helpers (alphabetical) ----- // ----- Internal helpers (alphabetical) -----
#ifndef JOEYLIB_PLATFORM_IIGS
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) { static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
if (minWord < gStageMinWord[y]) { if (minWord < gStageMinWord[y]) {
gStageMinWord[y] = minWord; gStageMinWord[y] = minWord;
@ -52,7 +52,6 @@ static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
gStageMaxWord[y] = maxWord; gStageMaxWord[y] = maxWord;
} }
} }
#endif
// ----- Public API (alphabetical) ----- // ----- Public API (alphabetical) -----
@ -70,6 +69,13 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
memcpy(dst->palette, src->palette, sizeof(src->palette)); memcpy(dst->palette, src->palette, sizeof(src->palette));
halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */ halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */
surfaceMarkDirtyAll(dst); 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)); s = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
if (s == NULL) { if (s == NULL) {
coreSetError("out of memory allocating surface");
return NULL; return NULL;
} }
/* halSurfaceAllocPixels returns NULL on planar ports (Amiga); the // halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
* primary storage is the port-allocated planes via portData below. */ // 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->pixels = halSurfaceAllocPixels();
s->portData = halSurfaceAllocPortData(s, false); 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); paletteInitDefault(s);
return s; return s;
} }
@ -102,30 +127,11 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
} }
// Cheapest deterministic hash that still detects per-byte changes: // Cross-port hash of the surface's logical pixel content (plus SCB and
// (hash << 1) ^ byte, a single 16-bit accumulator. ORCA-C / 65816 // palette), delegated to the port HAL so each port reads its native
// compiles to ASL + EOR -- about 35 cyc per byte. A 32-bit multiply // pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while
// FNV-style hash takes ~200 cyc per byte via ~UMUL4, which adds // producing the same logical-pixel hash. Used by the UBER validation
// 80+ seconds to a UBER run on IIgs. Discrimination is weaker than // harness to pixel-compare ports against an IIgs golden reference.
// 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. */
uint32_t jlSurfaceHash(const jlSurfaceT *s) { uint32_t jlSurfaceHash(const jlSurfaceT *s) {
if (s == NULL) { if (s == NULL) {
return 0u; return 0u;
@ -172,6 +178,12 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
} }
fclose(fp); fclose(fp);
surfaceMarkDirtyAll(dst); 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; return true;
} }
@ -204,15 +216,14 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
void surfaceMarkDirtyAll(const jlSurfaceT *s) { void surfaceMarkDirtyAll(const jlSurfaceT *s) {
int16_t row;
if (s != gStage) { if (s != gStage) {
return; return;
} }
for (row = 0; row < SURFACE_HEIGHT; row++) { // Whole-stage mark is a constant fill of both band arrays; memset
gStageMinWord[row] = 0; // lowers to a tight fill (MVN-seeded on IIgs) and drops the 200-iter
gStageMaxWord[row] = (uint8_t)(SURFACE_WORDS_PER_ROW - 1); // 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) { if (w <= 0 || h <= 0) {
return; return;
} }
/* Clipped x/w are non-negative; cast to uint16_t before `>> 2` so /* Clipped x/w are non-negative; SURFACE_WORD_INDEX casts to uint16_t
* ORCA-C lowers to a pair of LSRs instead of the ~SSHIFTRIGHT * before `>> 2` so the shift lowers to a pair of LSRs instead of a
* helper signed shifts emit. */ * signed-shift helper. */
minWord = (uint8_t)((uint16_t)x >> 2); minWord = SURFACE_WORD_INDEX(x);
maxWord = (uint8_t)((uint16_t)(x + w - 1) >> 2); maxWord = SURFACE_WORD_INDEX(x + w - 1);
yEnd = y + h; yEnd = y + h;
#ifdef JOEYLIB_PLATFORM_IIGS #ifdef JOEYLIB_PLATFORM_IIGS
iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd, // Single-row marks (every jlDrawPixel, every horizontal span) widen
(uint16_t)minWord, (uint16_t)maxWord); // 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 #else
for (row = y; row < yEnd; row++) { for (row = y; row < yEnd; row++) {
widenRow(row, minWord, maxWord); widenRow(row, minWord, maxWord);
@ -261,14 +280,31 @@ bool stageAlloc(void) {
if (gStage == NULL) { if (gStage == NULL) {
return false; return false;
} }
/* halStageAllocPixels returns NULL on planar ports (Amiga) where // halStageAllocPixels returns NULL on planar ports (Amiga) where
* the chunky shadow doesn't exist; the planes from portData are // the chunky shadow doesn't exist; the planes from portData are
* the source of truth. NULL pixels is no longer a failure. */ // 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(); gStage->pixels = halStageAllocPixels();
if (gStage->pixels != NULL) { if (gStage->pixels != NULL) {
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE); memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
} }
gStage->portData = halSurfaceAllocPortData(gStage, true); 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(); stageDirtyClearAll();
paletteInitDefault(gStage); paletteInitDefault(gStage);
return true; return true;

View file

@ -33,13 +33,32 @@ struct jlSurfaceT {
// 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words. // 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 // 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. // 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) #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 // 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. // dirty range, so the present loop tests `min > max` to skip a row.
#define STAGE_DIRTY_CLEAN_MIN 0xFFu #define STAGE_DIRTY_CLEAN_MIN 0xFFu
#define STAGE_DIRTY_CLEAN_MAX 0x00u #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 // Per-row dirty word bands for the stage. gStageMinWord[y] is the
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y] // leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
// is the rightmost (inclusive). Both default to the CLEAN sentinels // 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]; extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b. // 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 // Strength-reduced to shifts so the compiler doesn't emit a software
// per call); 32 KB hashed twice -> ~5 minutes per UBER run. The // 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 // shift form is 16-bit-equivalent (mod 2^16) so hash values are
// identical to the original `* 31u` / `* 251u` form. // identical to the original `* 31u` / `* 251u` form.
// lo *= 31 == (lo << 5) - lo // lo *= 31 == (lo << 5) - lo
@ -94,8 +114,8 @@ void stageDirtyClearAll(void);
// On IIgs this expands to a single indexed long-mode read against // On IIgs this expands to a single indexed long-mode read against
// gRowOffsetLut (built once at halInit). // gRowOffsetLut (built once at halInit).
// //
// The explicit (y << 1) byte-pointer arithmetic dodges ORCA-C 2.2.1's // The explicit (y << 1) byte-pointer arithmetic dodges the multiply
// `~MUL4` helper that gets emitted for `uint16_t arr[N]` indexing // helper that gets emitted for `uint16_t arr[N]` indexing
// (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the // (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the
// compiler emits a single ASL + indexed long-mode read. // 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)) #define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
#endif #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 // Allocate and free the library-owned stage (the back-buffer surface
// that jlStagePresent flips to the display). Called from init.c during // that jlStagePresent flips to the display). Called from init.c during
// jlInit / jlShutdown. The stage's pixel storage is supplied by // jlInit / jlShutdown. The stage's pixel storage is supplied by

View file

@ -16,11 +16,8 @@
// (No <string.h> -- the 4-byte-per-row inner copies are spelled out // (No <string.h> -- the 4-byte-per-row inner copies are spelled out
// inline below. Avoiding memcpy / memset from the DRAWPRIMS load // inline below. Avoiding memcpy / memset from the DRAWPRIMS load
// segment keeps cross-bank relocation references out of 13/SysLib; // segment keeps cross-bank relocation references out of 13/SysLib.)
// without that the ORCA Linker hits "Expression too complex" on
// the small-binary builds.)
JOEYLIB_SEGMENT("DRAWPRIMS")
// ----- Prototypes ----- // ----- Prototypes -----

View file

@ -40,6 +40,7 @@ extern UBYTE mt_E8Trigger;
#define PAULA_CLOCK_PAL 3546895UL #define PAULA_CLOCK_PAL 3546895UL
#define PAULA_PERIOD_MIN 124 // Paula HW limit #define PAULA_PERIOD_MIN 124 // Paula HW limit
#define PAULA_PERIOD_MAX 65535 #define PAULA_PERIOD_MAX 65535
#define PAULA_CHANNELS 4 // hardware audio channel count
#define SFX_VOLUME_MAX 64 #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 // PTPlayer addresses SFX by Paula channel (0..3), but mt_playfx
// picks a channel dynamically and does not return which one. We // picks a channel dynamically and does not return which one. We
// can't reliably map slot -> channel after the fact, so stop all // can't reliably map slot -> channel after the fact, so stop all
// four channels' SFX state. mt_stopfx is idempotent on idle // PAULA_CHANNELS channels' SFX state. mt_stopfx is idempotent on
// channels, so stopping a quiet one is harmless. // idle channels, so stopping a quiet one is harmless.
for (i = 0; i < 4; i++) { for (i = 0; i < PAULA_CHANNELS; i++) {
mt_stopfx((void *)&custom, i); mt_stopfx((void *)&custom, i);
} }
} }

View file

@ -40,6 +40,7 @@
#include <graphics/view.h> #include <graphics/view.h>
#include <hardware/custom.h> #include <hardware/custom.h>
#include <hardware/blit.h>
#include <proto/exec.h> #include <proto/exec.h>
#include <proto/intuition.h> #include <proto/intuition.h>
@ -48,7 +49,6 @@
#include "hal.h" #include "hal.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "draw68k_inline.h"
extern struct Custom custom; extern struct Custom custom;
@ -86,6 +86,8 @@ typedef struct {
// ----- Prototypes ----- // ----- 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 amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color);
static void buildCopperList(const jlSurfaceT *src); static void buildCopperList(const jlSurfaceT *src);
static void installCopperList(void); 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 gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[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 // Cached SCB + palettes from the last present. halPresent* only needs
// to rebuild/install the copper list when SCB assignments or palette // to rebuild/install the copper list when SCB assignments or palette
// RGB values differ from what is already on screen; pure pixel updates // 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) ----- // ----- 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). // Build a user copper list for per-scanline palette (SCB emulation).
// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is // One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is
// stored in gNewUCL until installCopperList swaps it onto the screen. // stored in gNewUCL until installCopperList swaps it onto the screen.
@ -647,6 +739,31 @@ void halPresent(const jlSurfaceT *src) {
return; 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: // Phase 10 page-flip:
// * Write to the OFF-screen plane set so the beam never crosses // * Write to the OFF-screen plane set so the beam never crosses
// in-flight pixels (no tearing regardless of copy speed). // in-flight pixels (no tearing regardless of copy speed).
@ -687,6 +804,16 @@ void halPresent(const jlSurfaceT *src) {
} }
if (firstRow >= 0) { 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. // Each planar byte covers 8 px = 2 chunky words.
byteStart = (uint16_t)(unionMinWord >> 1); byteStart = (uint16_t)(unionMinWord >> 1);
bytesPerRow = (uint16_t)(((unionMaxWord >> 1) + 1u) - byteStart); 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. // Snapshot current dirty bands as "previous" for next present.
// jlStagePresent clears the live bands after halPresent returns, // 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; uint16_t i;
bool anyDirty = false;
for (i = 0; i < SURFACE_HEIGHT; i++) { for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = gStageMinWord[i]; gPrevStageMinWord[i] = gStageMinWord[i];
gPrevStageMaxWord[i] = gStageMaxWord[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 = // Server protocol: called by the interrupt dispatcher with A1 =
// is_Data, A6 = ExecBase. We compile without fbaserel so the small- // is_Data, A6 = ExecBase. We compile without fbaserel so the small-
// data ABI is not in use; gFrameCount is a plain absolute global and // 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 // no __saveds is needed to reach it. Return 0 (D0 zero, Z flag set)
// keep the chain going so other VBL servers further down the priority // so the interrupt dispatcher treats this server as "not claimed" and
// list still fire. // 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) { static ULONG vblServer(void) {
gFrameCount = (uint16_t)(gFrameCount + 1u); gFrameCount = (uint16_t)(gFrameCount + 1u);
return 0; return 0;
@ -837,19 +974,19 @@ void halShutdown(void) {
FreeMem(gBitMap, (ULONG)sizeof(struct BitMap)); FreeMem(gBitMap, (ULONG)sizeof(struct BitMap));
gBitMap = NULL; gBitMap = NULL;
} }
if (gNewUCL != NULL) { // gNewUCL is never live at shutdown: buildCopperList and
FreeMem(gNewUCL, sizeof(struct UCopList)); // installCopperList run back-to-back inside updateCopperIfNeeded,
gNewUCL = NULL; // 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/ // Shared 68k fast paths from src/shared68k/surface68k.s. Only the
// surface68k.s). Same primitives used by the Atari ST port -- the // planar span fill and the planar circle outline apply on Amiga (pure
// stage / surfaces are an identical 4bpp packed layout on both. // planar after Phase 9); the chunky-layout primitives that file also
extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte); // exports are unused here.
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);
extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
uint16_t numMid, uint8_t leftMask, uint8_t rightMask, uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3); 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 3 planar dual-write: write the bitplanes alongside the
// Phase 9: Amiga is pure planar. Every halFast* below returns true // Phase 9: Amiga is pure planar. Every halFast* below returns true
// to suppress the cross-platform chunky fallback path -- there is no // for any surface that has planar storage (s->portData != NULL) to
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The actual // suppress the cross-platform chunky fallback path -- there is no
// planar work happens in dedicated halXxxPlanes hooks called by // chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate
// cross-platform code AFTER each halFast*. // 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). // halFastSurfaceClear inlines the planar fill (no separate hook).
// All other halFast* are short-circuit stubs that return true. // All other halFast* are short-circuit stubs that return true.
@ -870,19 +1011,29 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t colorIndex; uint8_t colorIndex;
uint16_t i; uint16_t i;
uint8_t planeByte; bool setBits;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
colorIndex = (uint8_t)(doubled & 0x0Fu); colorIndex = (uint8_t)(doubled & 0x0Fu);
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
for (i = 0; i < AMIGA_BITPLANES; i++) { // Blitter DEST-only block fill of every whole plane to $00 (clear)
planeByte = ((colorIndex >> i) & 1u) ? 0xFFu : 0x00u; // or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
memset(pd->planes[i], planeByte, AMIGA_PLANE_SIZE); // 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; 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 * partial-byte mask handling. We just claim ownership so the C
* chunky fallback never runs. */ * chunky fallback never runs. */
(void)x; (void)y; (void)w; (void)h; (void)colorIndex; (void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
return true; 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; yEnd = y + (int16_t)h;
/* Full-row fast path: no partial-byte RMW on either edge, so each /* 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 * plane row is a whole 40-byte (20-word) store. jlFillRect 320x200
* is the dominant case and lands here; saves 200 rows of leading * (clear-to-color, full-screen wipes) is the dominant case and
* byte RMW + 200 of trailing byte RMW per plane = 1600 chip-bus * lands here. With the shadow planes now in CHIP RAM the blitter
* read+write cycles per plane on top of the actual data write. */ * 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)) { if (byteFirst == 0u && byteLast == (uint16_t)(AMIGA_BYTES_PER_ROW - 1u)) {
uint16_t totalLongs; uint8_t *rowBase;
uint16_t groups;
uint16_t tail;
uint32_t fillLong;
uint32_t *p32;
/* AMIGA_BYTES_PER_ROW = 40 = 10 longs / row. 8x-unrolled inner OwnBlitter();
* 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);
for (plane = 0; plane < AMIGA_BITPLANES; plane++) { for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u); bitVal = (uint8_t)((colorIndex >> plane) & 1u);
fullByte = bitVal ? 0xFFu : 0x00u; rowBase = pd->planes[plane] + (uint16_t)y * AMIGA_BYTES_PER_ROW;
fillLong = (uint32_t)fullByte * 0x01010101UL; amigaBlitterFillOne(rowBase, AMIGA_BLT_WORDS_PER_ROW, h, 0u, bitVal != 0u);
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--;
}
}
} }
WaitBlit();
DisownBlitter();
return; 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 * bytes (leftMask == rightMask == 0xFF) every byte in the row is
* a full overwrite -- no RMW needed. UBER jlFillRect 80x80 at x=120 * a full overwrite -- no RMW needed. UBER jlFillRect 80x80 at x=120
* lands here (byteFirst=15, byteLast=24). Plane bases are * lands here (byteFirst=15, byteLast=24). Plane bases are
* MEMF_FAST-allocated long-aligned, and y*40 is also a multiple * AllocMem(MEMF_CHIP)-returned and thus long-aligned, and y*40 is
* of 4, so rowP alignment is determined by byteFirst alone -- * also a multiple of 4, so rowP alignment is determined by
* computed once, not per-row. */ * byteFirst alone -- computed once, not per-row. */
if (leftMask == 0xFFu && rightMask == 0xFFu) { if (leftMask == 0xFFu && rightMask == 0xFFu) {
uint16_t nBytes = (uint16_t)(byteLast - byteFirst + 1u); uint16_t nBytes = (uint16_t)(byteLast - byteFirst + 1u);
uint8_t alignBytes = (uint8_t)((4u - (byteFirst & 3u)) & 3u); 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 /* Hoist middle-region alignment outside both per-plane and
* per-row loops. midStart = planeBase + y*40 + byteFirst + 1. * per-row loops. midStart = planeBase + y*40 + byteFirst + 1.
* Plane bases are MEMF_FAST long-aligned and y*40 is a * Plane bases are AllocMem(MEMF_CHIP) long-aligned and y*40 is
* multiple of 4, so midStart's alignment is determined by * a multiple of 4, so midStart's alignment is determined by
* (byteFirst + 1) & 3 alone -- constant across planes/rows. */ * (byteFirst + 1) & 3 alone -- constant across planes/rows. */
uint8_t midAlignBytes = (uint8_t)((4u - ((byteFirst + 1u) & 3u)) & 3u); uint8_t midAlignBytes = (uint8_t)((4u - ((byteFirst + 1u) & 3u)) & 3u);
uint16_t midRem; 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. // Phase 9 sprite save/restore plane-backup hooks.
// Sprite save at (x, y, w, h) writes 4 plane stripes into backup // Sprite save at (x, y, w, h) records the destination region into the
// buffer; restore reads them back. x and w are 2-pixel aligned by // caller's backup buffer; restore writes it back. x and w are 2-pixel
// cross-platform code; we round x DOWN and w UP to 8-pixel boundaries // aligned by cross-platform code.
// 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.
// //
// If the rect's rounded width is wider than the chunky-sized backup // Backup layout matches the CHUNKY-packed format the cross-platform
// would hold (h * (w/2) bytes), we silently truncate -- the planar // contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see
// stripes for partial-byte-aligned sprites won't fit. This case is // include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte,
// rare for tile-aligned sprites; document if it bites. // 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
static void amigaSpriteRoundRect(int16_t *xp, uint16_t *wp, uint16_t *bprp) { // the Atari ST port uses and never overruns the contract buffer --
int16_t xIn = *xp; // unlike a plane-major layout, which would round x down to an 8-pixel
uint16_t wIn = *wp; // boundary and width up and then write (4*roundedW/8)*h = up to
int16_t xOut = (int16_t)(xIn & ~7); /* round down to 8-pixel */ // (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each
uint16_t span = (uint16_t)(((uint16_t)xIn + wIn) - (uint16_t)xOut); // pixel's 4bpp color is assembled from / written to the 4 plane bits
uint16_t wOut = (uint16_t)((span + 7u) & ~7u); // at byte x>>3, bit 0x80 >> (x & 7).
*xp = xOut;
*wp = wOut;
*bprp = (uint16_t)(wOut >> 3);
}
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) { void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const AmigaPlanarT *pd; const AmigaPlanarT *pd;
uint16_t bpr; uint16_t pairs;
uint16_t planeStripe;
uint16_t i;
uint16_t row; uint16_t row;
uint16_t pair;
uint8_t *dst; uint8_t *dst;
const uint8_t *src;
pd = (const AmigaPlanarT *)s->portData; pd = (const AmigaPlanarT *)s->portData;
if (pd == NULL || dstPlaneBytes == NULL) { if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return; return;
} }
amigaSpriteRoundRect(&x, &w, &bpr); pairs = (uint16_t)(w >> 1);
planeStripe = (uint16_t)((uint16_t)h * bpr); for (row = 0; row < (uint16_t)h; row++) {
for (i = 0; i < AMIGA_BITPLANES; i++) { const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
dst = dstPlaneBytes + i * planeStripe; const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
for (row = 0; row < (uint16_t)h; row++) { const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
src = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3); const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
memcpy(dst, src, bpr); dst = dstPlaneBytes + (uint16_t)(row * pairs);
dst += bpr; 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) { void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint16_t bpr; uint16_t pairs;
uint16_t planeStripe;
uint16_t i;
uint16_t row; uint16_t row;
uint8_t *dst; uint16_t pair;
const uint8_t *src; const uint8_t *src;
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd == NULL || srcPlaneBytes == NULL) { if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return; return;
} }
amigaSpriteRoundRect(&x, &w, &bpr); pairs = (uint16_t)(w >> 1);
planeStripe = (uint16_t)((uint16_t)h * bpr); for (row = 0; row < (uint16_t)h; row++) {
for (i = 0; i < AMIGA_BITPLANES; i++) { uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
src = srcPlaneBytes + i * planeStripe; uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
for (row = 0; row < (uint16_t)h; row++) { uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
dst = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3); uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
memcpy(dst, src, bpr); src = srcPlaneBytes + (uint16_t)(row * pairs);
src += bpr; 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) * 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 * 2. converts to 4 plane bytes + a shape mask (1 bit per pixel
* where the chunky nibble is non-transparent) * where the chunky nibble is non-transparent)
* 3. shifts the per-plane bytes by (dx & 7) so the sprite can land * 3. shifts the per-plane bytes by (x & 7) so the sprite can land
* at any pixel x, not just 8-px boundaries * 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 * 4. writes the left byte (and the spilled right byte for shifted
* sprites) via AND-mask + OR to all 4 plane buffers * 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. */ * code per cel that the old emitter generated. */
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) { void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
int16_t dx;
int16_t dy; int16_t dy;
int16_t sx;
int16_t sy; int16_t sy;
int16_t w; int16_t w;
int16_t h; int16_t h;
@ -1445,14 +1590,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
int16_t tileY; int16_t tileY;
int16_t inTileY; int16_t inTileY;
int16_t startByteX; int16_t startByteX;
int16_t dstByteIdx;
uint16_t wTiles; uint16_t wTiles;
uint16_t rowEnd;
uint8_t shift; uint8_t shift;
uint8_t bit; uint8_t bit;
uint8_t p0, p1, p2, p3, mask; uint8_t p0, p1, p2, p3, mask;
uint8_t leftMask, notMask; uint8_t leftMask, notMask;
uint8_t rshift, rightMask; uint8_t rshift, rightMask;
uint16_t dstByteOff;
uint16_t dstRowBase; uint16_t dstRowBase;
const uint8_t *tile; const uint8_t *tile;
const uint8_t *chunky; const uint8_t *chunky;
@ -1465,37 +1609,34 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
wTiles = sp->widthTiles; wTiles = sp->widthTiles;
w = (int16_t)(wTiles * 8u); w = (int16_t)(wTiles * 8u);
h = (int16_t)(sp->heightTiles * 8u); h = (int16_t)(sp->heightTiles * 8u);
dx = x;
dy = y; dy = y;
sx = 0;
sy = 0; sy = 0;
/* Clip dst rect against surface (sx/sy track offset INTO sprite). */ /* Vertical clip only (sy tracks offset INTO sprite); horizontal
if (dx < 0) { sx = (int16_t)(-dx); w = (int16_t)(w - sx); dx = 0; } * 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 (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; return;
} }
if (dx + w > SURFACE_WIDTH) { w = (int16_t)(SURFACE_WIDTH - dx); }
if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); } if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); }
shift = (uint8_t)(dx & 7u); /* Derive shift / startByteX from the ORIGINAL signed dest x so a
startByteX = (int16_t)(dx >> 3); * 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++) { for (row = 0; row < h; row++) {
spritePy = (int16_t)(sy + row); spritePy = (int16_t)(sy + row);
tileY = (int16_t)(spritePy >> 3); tileY = (int16_t)(spritePy >> 3);
inTileY = (int16_t)(spritePy & 7); inTileY = (int16_t)(spritePy & 7);
dstRowBase = (uint16_t)((uint16_t)(dy + row) * AMIGA_BYTES_PER_ROW); 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++) { for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol); tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
if (spriteBx >= (int16_t)wTiles) {
break;
}
tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
chunky = tile + inTileY * 4u; chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */ /* 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; 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); 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); notMask = (uint8_t)(~leftMask);
pd->planes[0][dstByteOff] = (uint8_t)((pd->planes[0][dstByteOff] & notMask) | (uint8_t)(p0 >> shift)); 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)); 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) { if (shift != 0u) {
int16_t rIdx = (int16_t)(dstByteIdx + 1);
rshift = (uint8_t)(8u - shift); rshift = (uint8_t)(8u - shift);
rightMask = (uint8_t)(mask << rshift); rightMask = (uint8_t)(mask << rshift);
if (rightMask != 0u) { if (rightMask != 0u && rIdx >= 0 && rIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
uint16_t rOff = (uint16_t)(dstByteOff + 1u); uint16_t rOff = (uint16_t)(dstRowBase + (uint16_t)rIdx);
if (rOff < rowEnd) { notMask = (uint8_t)(~rightMask);
notMask = (uint8_t)(~rightMask); pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
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[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[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));
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) { void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
AmigaPlanarT *dstPd; AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd; AmigaPlanarT *srcPd;
uint16_t i;
dstPd = (AmigaPlanarT *)dst->portData; dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData; srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) { if (dstPd == NULL || srcPd == NULL) {
return; return;
} }
for (i = 0; i < AMIGA_BITPLANES; i++) { // Straight blitter A->D copy of every whole plane (200 rows x 20
memcpy(dstPd->planes[i], srcPd->planes[i], AMIGA_PLANE_SIZE); // 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) { bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
uint8_t nibLo; uint8_t nibLo;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
nibLo = (uint8_t)(colorIndex & 0x0Fu); nibLo = (uint8_t)(colorIndex & 0x0Fu);
pd = (AmigaPlanarT *)s->portData; 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; return true;
} }
// Bresenham's diagonal line, planar-only walk. Same algorithm as // Bresenham's diagonal line, planar-only walk. Same algorithm as
// cross-platform jlDrawLine's fallback so the pixel set matches the // cross-platform jlDrawLine's fallback so the pixel set matches it
// chunky walker (draw68kLine) bit-for-bit. // bit-for-bit.
static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) { 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 dx;
int16_t dy; 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) { bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (AmigaPlanarT *)s->portData; 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; return true;
} }
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) { bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
AmigaPlanarT *pd; AmigaPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) { surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1], pd->planes[2], pd->planes[3],
pd->planes[2], pd->planes[3], (uint16_t)cx, (uint16_t)cy, r,
(uint16_t)cx, (uint16_t)cy, r, (uint8_t)(colorIndex & 0x0Fu));
(uint8_t)(colorIndex & 0x0Fu));
}
return true; return true;
} }
@ -1904,32 +2046,30 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
int16_t spanX; int16_t spanX;
uint16_t spanW; uint16_t spanW;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) { /* Inline per-span plane fill -- avoids 4 halFillRectPlanes
/* Inline per-span plane fill -- avoids 4 halFillRectPlanes * dispatches per midpoint iter (~320 dispatches for r=40). */
* dispatches per midpoint iter (~320 dispatches for r=40). */ x = (int16_t)r;
x = (int16_t)r; y = 0;
y = 0; err = (int16_t)(1 - x);
err = (int16_t)(1 - x); while (x >= y) {
while (x >= y) { spanX = (int16_t)(cx - x);
spanX = (int16_t)(cx - x); spanW = (uint16_t)(2 * x + 1);
spanW = (uint16_t)(2 * x + 1); amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex); amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex); spanX = (int16_t)(cx - y);
spanX = (int16_t)(cx - y); spanW = (uint16_t)(2 * y + 1);
spanW = (uint16_t)(2 * y + 1); amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex); amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex); y++;
y++; if (err <= 0) {
if (err <= 0) { err = (int16_t)(err + y + y + 1);
err = (int16_t)(err + y + y + 1); } else {
} else { x--;
x--; err = (int16_t)(err + y + y - x - x + 1);
err = (int16_t)(err + y + y - x - x + 1);
}
} }
} }
return true; return true;
@ -2375,26 +2515,20 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
* is gPlanes[], CHIP, updated only by halPresent). Non-stage uses * is gPlanes[], CHIP, updated only by halPresent). Non-stage uses
* these as the surface's only planes. * these as the surface's only planes.
* *
* MEMF_FAST (no MEMF_CHIP) explicitly demands fast RAM. The * MEMF_CHIP because the bulk plane ops (clear, full-row fill,
* shadow planes are CPU-only (no blitter/copper access); freeing * surface copy) now run on the BLITTER, which can only reach chip
* them from chip RAM cuts halPresent's chip-bus contention in * RAM. This is a deliberate performance tradeoff: chip RMW is
* half (read FAST + write CHIP instead of read CHIP + write CHIP) * slower than fast RAM for per-pixel CPU draws (line / circle /
* and gives RMW drawing primitives 2-3x speedup. If fast RAM is * single pixel), but the blitter wins big on the full-plane
* unavailable (bare A500, no expansion), we fall back to chip * clear/fill/copy paths and runs in parallel with the CPU. Net is
* via the loop below. */ * 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++) { for (i = 0; i < AMIGA_BITPLANES; i++) {
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE, pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
(ULONG)(MEMF_FAST | MEMF_CLEAR)); (ULONG)(MEMF_CHIP | MEMF_CLEAR));
if (pd->planes[i] == NULL) { jlLogF("amiga: shadow plane %u in CHIP (blitter, addr=$%08lX)",
/* No fast RAM available; fall back to chip. */ (unsigned)i, (unsigned long)pd->planes[i]);
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]);
}
if (pd->planes[i] == NULL) { if (pd->planes[i] == NULL) {
/* Roll back any planes already allocated. */ /* Roll back any planes already allocated. */
while (i > 0u) { while (i > 0u) {

View file

@ -46,6 +46,12 @@
#define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L) #define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L)
#define ST_YM_DATA ((volatile uint8_t *)0xFFFF8802L) #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_TA_BIT 0x20
#define MFP_TACR_STOP 0x00 #define MFP_TACR_STOP 0x00
#define MFP_TACR_DIV200 0x07 #define MFP_TACR_DIV200 0x07
@ -81,8 +87,23 @@ static void (*gOldTimerAVec)(void) = NULL;
// Mixed in over libxmp's MOD output during halAudioFrameTick. // Mixed in over libxmp's MOD output during halAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS]; 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 ----- // ----- 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) { static long installTimerA(void) {
uint16_t i; uint16_t i;
@ -114,8 +135,8 @@ static long installTimerA(void) {
// disable bits over an assumed-safe TOS-default mask. Bit 6 set // disable bits over an assumed-safe TOS-default mask. Bit 6 set
// (port A output) matches stock TOS; bit 7 set (port B output) // (port A output) matches stock TOS; bit 7 set (port B output)
// matches the centronics-printer direction TOS configures. // matches the centronics-printer direction TOS configures.
*ST_YM_SELECT = 7; gYmToneMask = 0x3Fu; // all tones + noise disabled (PWM-DAC default)
*ST_YM_DATA = 0xFF; // all tones + noises off; I/O ports A+B output (TOS default) ymWriteMixer(); // -> 0xFF: tones+noise off, I/O ports A+B output
*ST_YM_SELECT = 8; *ST_YM_SELECT = 8;
*ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start *ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start
*ST_YM_SELECT = 9; *ST_YM_SELECT = 9;
@ -271,7 +292,7 @@ void halAudioTone(uint16_t freqHz) {
static volatile uint8_t gYmVoice; static volatile uint8_t gYmVoice;
static volatile uint16_t gYmFreqHz; static volatile uint16_t gYmFreqHz;
static volatile uint8_t gYmAtten; 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) { static long applyYmVoice(void) {
@ -287,8 +308,7 @@ static long applyYmVoice(void) {
// Silence: disable tone on this channel (mixer bit 0/1/2 set), // Silence: disable tone on this channel (mixer bit 0/1/2 set),
// zero volume. Don't touch other channels' bits. // zero volume. Don't touch other channels' bits.
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice)); gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
*ST_YM_SELECT = 7; ymWriteMixer();
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask); // preserve I/O port-direction high bits
*ST_YM_SELECT = volReg; *ST_YM_SELECT = volReg;
*ST_YM_DATA = 0; *ST_YM_DATA = 0;
return 0; return 0;
@ -313,8 +333,7 @@ static long applyYmVoice(void) {
// Enable tone on this channel (clear its bit in mixer). // Enable tone on this channel (clear its bit in mixer).
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice)); gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
*ST_YM_SELECT = 7; ymWriteMixer();
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask);
return 0; return 0;
} }
@ -331,7 +350,10 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
void halAudioCriticalEnter(void) { 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) { void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= 3u) { if (voice >= JOEY_AUDIO_VOICES) {
return; return;
} }
// The PWM-via-volume engine in halAudioInit hammers YM reg 8 // 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 // Drains ISR-raised refill flags by re-running libxmp's mixer into
// the consumed half then overlaying any active SFX. Called from the // the consumed half then overlaying any active SFX. Called from the
// game loop, never from IRQ. // 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) { void halAudioFrameTick(void) {
if (!gReady) { if (!gReady) {
return; 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 (gNeedRefill[0]) {
if (gXmpStarted) { if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount); xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);

View file

@ -24,19 +24,17 @@
| d5.w = cx (cached) | d5.w = cx (cached)
| a4 = cy (cached, sign-extended) | a4 = cy (cached, sign-extended)
| a3 = base | a3 = base
| a5 = bitMaskWordLut | a5 = _gStBitMaskWordLut
| d0,d1,d6,d7 = scratch | d0,d1,d6,d7 = scratch
| |
| Scratch block (24 bytes) at sp+0..23: | Scratch block (40 bytes) at sp+0..39. Each xp record is 8 bytes but
| sp+0..3: xp1 record [groupOff_w, bitMask_b, notMask_b] | only 2 words are actually stored: groupOff_word at +0 and
| groupOff = (x >> 4) * 8 (byte offset of group within row) | bitMask_word at +2 (+4/+6 are unused padding kept only for the
| bitMask = byte representation of 1 << (15 - (x & 15)) | 8-byte stride). notMask is NOT cached -- PLOT_FIXED derives it per
| ... wait, bitMask must be a WORD on ST not a byte. | 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.
| Actually layout differs from Amiga: ST needs a WORD bit mask, not | xp record: groupOff = (x >> 4) * 8 (byte offset of group in row)
| a byte. Per-record layout (8 bytes): | bitMask = 1 << (15 - (x & 15)) (word, from LUT)
| groupOff_word (2 bytes), bitMask_word (2 bytes), notMask_word
| (2 bytes), pad (2 bytes)
| |
| sp+0..7: xp1 record (cx + bx) | sp+0..7: xp1 record (cx + bx)
| sp+8..15: xp2 record (cx - bx) | sp+8..15: xp2 record (cx - bx)
@ -205,9 +203,11 @@ _surface68kStCircleOutline:
movem.l %d2-%d7/%a2-%a6,-(%sp) movem.l %d2-%d7/%a2-%a6,-(%sp)
lea -SP_LOCAL(%sp),%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 move.l SP_BASE(%sp),%a3
lea bitMaskWordLut(%pc),%a5 lea _gStBitMaskWordLut,%a5
| Cache cx in d5, cy (sign-extended) in a4. | Cache cx in d5, cy (sign-extended) in a4.
move.w SP_CX(%sp),%d5 move.w SP_CX(%sp),%d5
@ -278,11 +278,5 @@ _surface68kStCircleOutline:
rts rts
.align 2 | (bitMaskWordLut and yLut both live in lineSpan.s as the shared
| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15. | _gStBitMaskWordLut / _gStRowOffsetLut.)
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)

View file

@ -51,13 +51,13 @@
lsr.w #4,%d0 | groupLast lsr.w #4,%d0 | groupLast
sub.w %d1,%d0 | numGroups sub.w %d1,%d0 | numGroups
move.w %d0,4(%sp) move.w %d0,4(%sp)
| leftMask via LUT[bitFirst]; a5 = leftMaskLut base | leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base
move.w 0(%sp),%d0 move.w 0(%sp),%d0
and.w #15,%d0 and.w #15,%d0
add.w %d0,%d0 add.w %d0,%d0
move.w (%a5,%d0.w),%d1 move.w (%a5,%d0.w),%d1
move.w %d1,0(%sp) move.w %d1,0(%sp)
| rightMask via LUT[bitLast]; a6 = rightMaskLut base | rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base
move.w 2(%sp),%d0 move.w 2(%sp),%d0
and.w #15,%d0 and.w #15,%d0
add.w %d0,%d0 add.w %d0,%d0
@ -216,8 +216,10 @@ _surface68kStFillCircle:
| LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS). | LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS).
| a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160). | a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160).
lea leftMaskLut(%pc),%a5 | All three LUTs are the shared definitions in lineSpan.s;
lea rightMaskLut(%pc),%a6 | reference them absolute (single source of truth).
lea _gStLeftMaskLut,%a5
lea _gStRightMaskLut,%a6
lea _gStRowOffsetLut,%a2 lea _gStRowOffsetLut,%a2
| loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0) | loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0)
@ -299,17 +301,5 @@ _surface68kStFillCircle:
rts rts
.align 2 | (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared
| leftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst (0..15) | _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.)
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

View file

@ -38,7 +38,6 @@
#include "surfaceInternal.h" #include "surfaceInternal.h"
#include "spriteInternal.h" #include "spriteInternal.h"
#include "joey/tile.h" #include "joey/tile.h"
#include "draw68k_inline.h"
// ----- Constants ----- // ----- Constants -----
@ -63,10 +62,12 @@
// Amiga's 4 separate plane buffers -- same total bytes, very different // Amiga's 4 separate plane buffers -- same total bytes, very different
// access pattern. // access pattern.
// //
// The stage gets its own SHADOW planar buffer (NOT aliased to // The stage gets its own SHADOW planar buffer (NOT aliased to a
// gScreenBase) so drawing primitives don't appear until jlStagePresent // displayed page) so drawing primitives don't appear until
// memcpy's shadow -> screen. Same rationale as Amiga's per-stage // jlStagePresent copies the union(prev,current) dirty rows from the
// shadow planes. // 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 { typedef struct {
uint8_t *base; // 32000-byte interleaved planar buffer uint8_t *base; // 32000-byte interleaved planar buffer
uint8_t *raw; // unaligned malloc result for free() uint8_t *raw; // unaligned malloc result for free()
@ -76,6 +77,17 @@ typedef struct {
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F. // Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L) #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. // MFP hardware addresses.
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control #define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data #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 uint16_t quantizeColorToSt(uint16_t orgb);
static void flattenScbPalettes(const jlSurfaceT *src); static void flattenScbPalettes(const jlSurfaceT *src);
static void setShifterVideoBase(const uint8_t *page);
static void writeDiagnostics(void); static void writeDiagnostics(void);
static long writePrevPaletteRegs(void); static long writePrevPaletteRegs(void);
@ -147,19 +160,51 @@ static void refreshPaletteStateIfNeeded(const
// ----- Module state ----- // ----- Module state -----
// Screen buffer: enough for 320x200x4bpp planar plus padding for // Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
// runtime 256-byte alignment. TOS .PRG format only supports 2-byte // padding for runtime 256-byte alignment. TOS .PRG format only supports
// object-file alignment, so we overallocate and align the pointer // 2-byte object-file alignment, so we overallocate and align the first
// manually in halInit. // page pointer manually in halInit; the second page sits exactly one
static uint8_t gScreenBuffer[SURFACE_PIXELS_SIZE + ST_SCREEN_ALIGN]; // 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 *gPrevPhysbase = NULL;
static void *gPrevLogbase = NULL; static void *gPrevLogbase = NULL;
static int16_t gPrevRez = 0; static int16_t gPrevRez = 0;
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE]; static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
static bool gModeSet = false; 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; // Per-scanline pre-quantized palette table. Indexed by display line;
// each row is a 16-word palette ready to be copied straight into the // 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 // 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 // auto-reload keep counting for the next fire. We deliberately do
// NOT stop/reload/restart the timer here: that sequence would cost // NOT stop/reload/restart the timer here: that sequence would cost
// 1-2 HBL edges each fire, and those losses compound across 7+ // 1-2 HBL edges each fire, and those losses compound across 7+
// transitions into a visible "last band short" drift. Updating // transitions into a visible "last band short" drift.
// TBDR in place is enough for variable-length bands -- the new //
// value takes effect on the fire AFTER next, which is acceptable // MFP event-count auto-reload latches the CURRENT TBDR at each
// when adjacent bands have similar lengths; uniform bands (like // underflow, so a value written here does not take effect on the
// pattern.c) don't need TBDR updates at all so stay perfectly // NEXT fire (that interval was already reloaded at the underflow
// aligned with no drift. // 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) { static void timerBIsr(void) {
uint16_t band; uint16_t band;
uint8_t palIdx; uint8_t palIdx;
@ -344,14 +395,19 @@ static void timerBIsr(void) {
dst[15] = src[15]; dst[15] = src[15];
if (band + 1 < gBandCount) { if (band + 1 < gBandCount) {
// Update TBDR for the fire-after-next (auto-reload at // A transition to band+1 is still pending; keep Timer B
// the NEXT fire still uses the old value). Don't stop // running. The band+1 underflow has already had its reload
// the timer. // value latched (set one fire earlier), so program TBDR
nextDelta = gBandStart[band + 1] - gBandStart[band]; // now with the band+1 -> band+2 delta, which the band+1
if (nextDelta == 0 || nextDelta > 255) { // underflow will reload and use to time band+2. Don't
nextDelta = 1; // 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; *ST_MFP_ISRA = MFP_TB_CLEAR;
return; return;
} }
@ -410,6 +466,19 @@ static void vblIsr(void) {
*ST_MFP_TBDR = (uint8_t)delta; *ST_MFP_TBDR = (uint8_t)delta;
*ST_MFP_ISRA = MFP_TB_CLEAR; *ST_MFP_ISRA = MFP_TB_CLEAR;
*ST_MFP_TBCR = MFP_TBCR_EVENT; *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 { } else {
*ST_MFP_TBCR = MFP_TBCR_STOP; *ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_ISRA = MFP_TB_CLEAR; *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) { static long writePrevPaletteRegs(void) {
uint16_t i; uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) { for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
@ -448,30 +534,45 @@ static void writeDiagnostics(void) {
bool halInit(const jlConfigT *config) { bool halInit(const jlConfigT *config) {
uintptr_t addr; uintptr_t addr;
uint16_t i;
(void)config; (void)config;
// Align screen buffer to 256 bytes inside the static storage. // Align page 0 to 256 bytes inside the static storage; page 1
addr = (uintptr_t)gScreenBuffer; // follows exactly one 32000-byte (256-aligned) page later, so it
addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1); // is 256-aligned too. The shifter only displays 256-byte-aligned
gScreenBase = (uint8_t *)addr; // 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(); gPrevPhysbase = Physbase();
gPrevLogbase = Logbase(); gPrevLogbase = Logbase();
gPrevRez = Getrez(); gPrevRez = Getrez();
// Capture current palette so we can restore exactly on shutdown. // Capture current palette so we can restore exactly on shutdown.
{ for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
uint16_t 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. // Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
Setscreen((long)gScreenBase, (long)gScreenBase, 0); // 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; gModeSet = true;
// Force hardware palette entry 0 to black so the overscan border // Force hardware palette entry 0 to black so the overscan border
@ -502,13 +603,23 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) { void halPresent(const jlSurfaceT *src) {
StPlanarT *pd; StPlanarT *pd;
uint8_t *backPage;
uint8_t backIdx;
int16_t y; 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 minWord;
uint8_t maxWord; uint8_t maxWord;
uint16_t groupStart; uint16_t groupStart;
uint16_t groupEnd; uint16_t groupEnd;
uint16_t byteStart; uint16_t byteStart;
uint16_t byteLen; uint16_t byteLen;
bool fullWidth;
if (src == NULL || !gModeSet) { if (src == NULL || !gModeSet) {
return; return;
@ -519,23 +630,168 @@ void halPresent(const jlSurfaceT *src) {
} }
refreshPaletteStateIfNeeded(src); refreshPaletteStateIfNeeded(src);
// Planar buffer -> screen RAM. Each dirty word covers 4 pixels // Idle-frame early-out. When the previous frame's snapshot recorded
// (a quarter of an 8-byte group). Round to whole groups for a // no dirty rows (gPrevAnyDirty == false) AND the current frame
// simple aligned memcpy, since planar groups are the natural // touched no row, union(prev,current) is empty: both pages already
// copy unit. // match the shadow (the one settling flip after the last activity
for (y = 0; y < SURFACE_HEIGHT; y++) { // ran on the first idle frame), so there is nothing to copy, no
minWord = gStageMinWord[y]; // page to flip, and the snapshot below would only copy clean->clean.
maxWord = gStageMaxWord[y]; // Skip the whole 200-row union scan and the 200-row snapshot copy.
if (minWord > maxWord) { // A row is dirty iff its min word stayed at or below its max word;
continue; // 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); if (!anyDirty) {
groupEnd = (uint16_t)((maxWord >> 2) + 1); return;
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], // PAGE FLIP: draw into the stage's shadow (pd->base), bring the
byteLen); // 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 // fault on user-mode access. Resolution is 5 ms, plenty for sound
// scheduling at 60 Hz AGI ticks. // scheduling at 60 Hz AGI ticks.
static volatile uint32_t gMillisBase200 = 0; static volatile uint32_t gMillisBase200 = 0;
static volatile bool gMillisBaseSet = false;
static long readHz200(void) { static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL); return (long)*((volatile uint32_t *)0x4BAUL);
@ -585,11 +842,15 @@ static long readHz200(void) {
uint32_t halMillisElapsed(void) { uint32_t halMillisElapsed(void) {
uint32_t ticks; uint32_t ticks;
ticks = (uint32_t)Supexec(readHz200); 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; gMillisBase200 = ticks;
gMillisBaseSet = true;
return 0u; 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); return (uint32_t)((ticks - gMillisBase200) * 5u);
} }
@ -626,12 +887,9 @@ void halShutdown(void) {
} }
// Shared 68k fast paths for the chunky surface buffer (src/shared68k/ // ST word-interleaved planar 68k fast paths. The surface68kSt*
// surface68k.s). Same primitives used by the Amiga port -- the stage / // primitives are ST-specific (plane-major group layout); they live in
// surfaces are identical 4bpp packed layout on both 68k targets. // src/shared68k/surface68k.s alongside the Amiga-only planar helpers.
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);
extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color); 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 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); 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; uint8_t color;
uint32_t loLong; uint32_t loLong;
uint32_t hiLong; uint32_t hiLong;
uint32_t *p32;
uint16_t groups;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
color = (uint8_t)(doubled & 0x0Fu); color = (uint8_t)(doubled & 0x0Fu);
/* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs. /* 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. */ * 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); | ((color & 2u) ? 0x0000FFFFul : 0ul);
hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul) hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul)
| ((color & 8u) ? 0x0000FFFFul : 0ul); | ((color & 8u) ? 0x0000FFFFul : 0ul);
(void)p32;
(void)groups;
surface68kStLongFill(pd->base, surface68kStLongFill(pd->base,
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP), (uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
loLong, hiLong); loLong, hiLong);
@ -686,7 +937,7 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// never runs. // never runs.
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { 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; (void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
return true; 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) { bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (StPlanarT *)s->portData; 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; 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) { bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
if (pd != NULL) { // Asm walker assumes fully on-surface; partial-clip lines fall
// Asm walker assumes fully on-surface; partial-clip lines fall // back to the C walker which clips per-pixel.
// back to the C walker which clips per-pixel. if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT && x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
&& x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) { surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu)); } else {
} else { stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
} }
return true; 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) { bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
/* Off-surface circles fall back to the per-pixel C walker which /* 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 * does the clip per plot; the asm assumes fully-on-surface so it
* can drop the clip check from the inner loop. */ * 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) { bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd; StPlanarT *pd;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
// Off-surface bounding box falls back to the C span walker, which // Off-surface bounding box falls back to the C span walker, which
// clips each span; the asm assumes the whole circle is on-surface. // clips each span; the asm assumes the whole circle is on-surface.
if ((int32_t)cx - (int32_t)r < 0 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. // false would dereference NULL s->pixels.
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) { bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)bx; (void)by; (void)fillWord; (void)bx; (void)by; (void)fillWord;
if (s != jlStageGet()) { if (s->portData == NULL) {
return false; return false;
} }
return true; 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 // Phase 2: allocate a shadow word-interleaved planar buffer per
// surface. Both stage and non-stage get their own buffer (gScreenBase // surface. Both stage and non-stage get their own buffer; the two
// remains the single display target). // 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 // 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 // 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 * (where half_byte = dst_byte_col & 1). This replaces the old
* per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and * per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and
* murdered the frame rate on shifted sprites (passenger walk, * 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); uint8_t shift = (uint8_t)(x & 7);
int16_t startByteX = (int16_t)(spritePxStart >> 3); int16_t startByteX = (int16_t)(x >> 3);
int16_t byteCol; int16_t byteCol;
rowBase = pd->base + (uint16_t)spritePyStart * ST_BYTES_PER_ROW; 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); int16_t inTileY = (int16_t)(spritePy & 7);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) { for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol); const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
if (spriteBx >= (int16_t)wTiles) {
break;
}
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
const uint8_t *chunky = tile + inTileY * 4u; const uint8_t *chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */ /* 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. /* Compute ST address for the dst byte at byte_col_dst.
* Plane bytes are interleaved word-wise inside groups of * 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); 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); uint8_t leftMask = (uint8_t)(mask >> shift);
if (leftMask != 0u) { if (leftMask != 0u
uint8_t notM = (uint8_t)(~leftMask); && dstByteCol >= 0
uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half; && 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[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 >> shift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 >> shift)); p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 >> shift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 >> 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) { if (shift != 0u) {
uint8_t rshift = (uint8_t)(8u - shift); uint8_t rshift = (uint8_t)(8u - shift);
uint8_t rightMask = (uint8_t)(mask << rshift); uint8_t rightMask = (uint8_t)(mask << rshift);
if (rightMask != 0u) { int16_t rdc = (int16_t)(dstByteCol + 1);
int16_t rdc = (int16_t)(dstByteCol + 1); if (rightMask != 0u
if (rdc < (int16_t)(SURFACE_WIDTH / 8)) { && rdc >= 0
uint16_t rgroup = (uint16_t)(rdc >> 1); && rdc < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t rhalf = (uint16_t)(rdc & 1); uint16_t rgroup = (uint16_t)(rdc >> 1);
uint8_t notM = (uint8_t)(~rightMask); uint16_t rhalf = (uint16_t)(rdc & 1);
uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf; uint8_t notM = (uint8_t)(~rightMask);
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift)); uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift)); p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift)); p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift)); p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
} p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
} }
} }
} }

View file

@ -24,7 +24,7 @@
| d7 = sx (long; moveq #1 or #-1, low word used for .w add) | d7 = sx (long; moveq #1 or #-1, low word used for .w add)
| a4 = sy (long; sign-extended) | a4 = sy (long; sign-extended)
| a3 = base | a3 = base
| a5 = bitMaskWordLut | a5 = _gStBitMaskWordLut
| a2 = scratch (per-pixel: base + byteOff) | a2 = scratch (per-pixel: base + byteOff)
| d0,d1 = scratch | d0,d1 = scratch
| |
@ -46,7 +46,7 @@
| ---- DL_PLOT: 4-plane word RMW for hardcoded color ---- | ---- 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 | Trashes: d0, d1, a2
.macro DL_PLOT color .macro DL_PLOT color
@ -127,7 +127,7 @@ _surface68kStDrawLine:
| Load base & luts. | Load base & luts.
move.l SP_BASE(%sp),%a3 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. | a6 = yLut base (yp -> yp*160) for use in DL_PLOT.
lea _gStRowOffsetLut(%pc),%a6 lea _gStRowOffsetLut(%pc),%a6
@ -224,8 +224,12 @@ _surface68kStDrawLine:
.align 2 .align 2
| 16 word entries: bitMaskWordLut[i] = 1 << (15 - i), for i in 0..15. | 16 word entries: _gStBitMaskWordLut[i] = 1 << (15 - i), for i in
bitMaskWordLut: | 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 0x8000, 0x4000, 0x2000, 0x1000
.word 0x0800, 0x0400, 0x0200, 0x0100 .word 0x0800, 0x0400, 0x0200, 0x0100
.word 0x0080, 0x0040, 0x0020, 0x0010 .word 0x0080, 0x0040, 0x0020, 0x0010
@ -795,7 +799,7 @@ _surface68kStFillRectMulti:
move.w %d0,%d2 move.w %d0,%d2
and.w #15,%d2 and.w #15,%d2
add.w %d2,%d2 add.w %d2,%d2
lea frmLeftMaskLut(%pc),%a2 lea _gStLeftMaskLut(%pc),%a2
move.w (%a2,%d2.w),%d2 | d2 = leftMask move.w (%a2,%d2.w),%d2 | d2 = leftMask
move.w %d2,%d4 move.w %d2,%d4
not.w %d4 | d4 = notLeftMask not.w %d4 | d4 = notLeftMask
@ -804,7 +808,7 @@ _surface68kStFillRectMulti:
move.w %d1,%d3 move.w %d1,%d3
and.w #15,%d3 and.w #15,%d3
add.w %d3,%d3 add.w %d3,%d3
lea frmRightMaskLut(%pc),%a2 lea _gStRightMaskLut(%pc),%a2
move.w (%a2,%d3.w),%d3 | d3 = rightMask move.w (%a2,%d3.w),%d3 | d3 = rightMask
move.w %d3,%d5 move.w %d3,%d5
not.w %d5 | d5 = notRightMask not.w %d5 | d5 = notRightMask
@ -890,15 +894,20 @@ _surface68kStFillRectMulti:
.align 2 .align 2
| Same LUTs as in jlFillCircle.s; duplicated locally so each .o file's | Leading/trailing edge-mask LUTs, exported (like _gStRowOffsetLut)
| PC-rel lea can reach them within its own .text segment. | as the single source of truth shared with jlFillCircle.s via
frmLeftMaskLut: | 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 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
.word 0x0FFF, 0x07FF, 0x03FF, 0x01FF .word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
.word 0x00FF, 0x007F, 0x003F, 0x001F .word 0x00FF, 0x007F, 0x003F, 0x001F
.word 0x000F, 0x0007, 0x0003, 0x0001 .word 0x000F, 0x0007, 0x0003, 0x0001
frmRightMaskLut: .globl _gStRightMaskLut
_gStRightMaskLut:
.word 0x8000, 0xC000, 0xE000, 0xF000 .word 0x8000, 0xC000, 0xE000, 0xF000
.word 0xF800, 0xFC00, 0xFE00, 0xFF00 .word 0xF800, 0xFC00, 0xFE00, 0xFF00
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0 .word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
@ -906,6 +915,14 @@ frmRightMaskLut:
.align 2 .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 | Shared y -> y*160 LUT. Used by jlDrawLine (DL_PLOT), jlDrawCircle
| (YP_REC), jlFillCircle (SPAN_BODY). 200 words = 400 bytes. | (YP_REC), jlFillCircle (SPAN_BODY). 200 words = 400 bytes.
| Replaces a 44-cyc lsl.w #5 + lsl.w #7 + add.w shift chain with | Replaces a 44-cyc lsl.w #5 + lsl.w #7 + add.w shift chain with
@ -915,7 +932,7 @@ frmRightMaskLut:
_gStRowOffsetLut: _gStRowOffsetLut:
.set li_y, 0 .set li_y, 0
.rept 200 .rept 200
.word li_y * 160 .word li_y * ST_BYTES_PER_ROW
.set li_y, li_y + 1 .set li_y, li_y + 1
.endr .endr
@ -924,19 +941,22 @@ _gStRowOffsetLut:
| |
| Bulk long-fill helper for full-row fills (jlSurfaceClear, jlFillRect | Bulk long-fill helper for full-row fills (jlSurfaceClear, jlFillRect
| 320x200). Writes numGroups groups of 8 bytes (loLong, hiLong) | 320x200). Writes numGroups groups of 8 bytes (loLong, hiLong)
| starting at dst. Uses movem.l d2-d7 (3 groups = 24 bytes per | starting at dst. Register-pumps movem.l d2-d7/a1-a6 (12 longs =
| batch) plus a tail pair to amortize loop overhead. | 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, | void surface68kStLongFill(uint8_t *dst,
| uint16_t numGroups, | uint16_t numGroups,
| uint32_t loLong, | uint32_t loLong,
| uint32_t hiLong); | uint32_t hiLong);
| |
| Per-batch cost: movem.l (56 cyc) + subq (8) + bne (10) = 74 cyc | STF has no blitter, so this CPU movem.l burst is the fast path; the
| for 24 bytes -- ~3 cyc/byte vs ~5 cyc/byte for the straight C | fill is memory-bandwidth bound, so the win is in cutting loop
| do-while of two move.l writes. | 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_OFF, (SP_LF_SAVED + 4)
.equ SP_LF_DST, SP_LF_OFF + 0 .equ SP_LF_DST, SP_LF_OFF + 0
.equ SP_LF_NGROUPS, SP_LF_OFF + 4 + 2 .equ SP_LF_NGROUPS, SP_LF_OFF + 4 + 2
@ -947,45 +967,51 @@ _gStRowOffsetLut:
.globl _surface68kStLongFill .globl _surface68kStLongFill
_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_DST(%sp),%a0
move.l SP_LF_LO(%sp),%d2 move.l SP_LF_LO(%sp),%d2
move.l SP_LF_HI(%sp),%d3 move.l SP_LF_HI(%sp),%d3
move.w SP_LF_NGROUPS(%sp),%d0 move.w SP_LF_NGROUPS(%sp),%d0
| Set up d2-d7 = lo, hi, lo, hi, lo, hi (movem writes | Spread the {lo,hi} pattern across 12 registers in the
| in d-reg order, so this gives the right alternation | exact order movem.l writes them (d2-d7 then a1-a6, low
| for 3 consecutive 8-byte groups). | 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,%d4
move.l %d2,%d6 move.l %d2,%d6
move.l %d2,%a1
move.l %d2,%a3
move.l %d2,%a5
move.l %d3,%d5 move.l %d3,%d5
move.l %d3,%d7 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 ext.l %d0
divu.w #3,%d0 divu.w #6,%d0
move.l %d0,%d1 move.l %d0,%d1
swap %d1 | d1.w = remainder swap %d1 | d1.w = remainder (0..5)
tst.w %d0 | quotient tst.w %d0 | quotient
beq.s .Llf_tail beq.s .Llf_tail
.Llf_loop: .Llf_loop:
movem.l %d2-%d7,(%a0) movem.l %d2-%d7/%a1-%a6,(%a0) | 48 bytes = 6 groups
lea 24(%a0),%a0 lea 48(%a0),%a0
subq.w #1,%d0 subq.w #1,%d0
bne.s .Llf_loop bne.s .Llf_loop
.Llf_tail: .Llf_tail:
| Remainder: 0, 1, or 2 groups of 8 bytes | Remainder: 0..5 groups of 8 bytes
tst.w %d1 tst.w %d1
beq.s .Llf_done beq.s .Llf_done
.Llf_tailLoop:
move.l %d2,(%a0)+ move.l %d2,(%a0)+
move.l %d3,(%a0)+ move.l %d3,(%a0)+
subq.w #1,%d1 subq.w #1,%d1
beq.s .Llf_done bne.s .Llf_tailLoop
move.l %d2,(%a0)+
move.l %d3,(%a0)+
.Llf_done: .Llf_done:
movem.l (%sp)+,%d2-%d7 movem.l (%sp)+,%d2-%d7/%a2-%a6
rts rts

View file

@ -94,9 +94,15 @@
// ----- Mix configuration ----- // ----- 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_RATE 22050
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves #define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
#define MIX_HALF (MIX_DMA_BUFFER / 2) #define MIX_HALF (MIX_DMA_BUFFER / 2)
#define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base
#define ISR_LOCK_SIZE 4096 #define ISR_LOCK_SIZE 4096
@ -109,6 +115,12 @@ static uint8_t gDspMajor = 0;
static uint8_t gDspMinor = 0; static uint8_t gDspMinor = 0;
static bool gDspPresent = false; 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. // Conventional-memory DMA buffer.
static int gDmaSelector = 0; static int gDmaSelector = 0;
static uint32_t gDmaLinear = 0; static uint32_t gDmaLinear = 0;
@ -153,6 +165,7 @@ static void dspWrite(uint8_t value);
static void freeDmaBuffer(void); static void freeDmaBuffer(void);
static void haltDspDma(void); static void haltDspDma(void);
static void parseBlasterEnv(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 picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq); static void picUnmaskIrq(uint8_t irq);
static int picVectorForIrq(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 // 8-bit DSP time constant: tc = 256 - SB_DSP_CLOCK_HZ / rate. SB plays
// 1000000 / (256 - tc) Hz, so we round to the nearest representable // at SB_DSP_CLOCK_HZ / (256 - tc) Hz, so we round to the nearest
// rate. For 22050 Hz: tc = 256 - 45 = 211. // representable rate. For 22050 Hz: tc = 256 - 45 = 211.
static uint8_t dspTimeConst(uint16_t rate) { static uint8_t dspTimeConst(uint16_t rate) {
uint32_t tc; uint32_t tc;
tc = 256u - (1000000u / rate); tc = 256u - (SB_DSP_CLOCK_HZ / rate);
if (tc < 4) { if (tc < 4) {
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 // 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 // can reach the CPU. Some environments (DOSBox in particular) leave the
// SB IRQ masked at boot; without this the hooked vector never fires // 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 bit;
uint8_t mask; uint8_t mask;
if (irq < 8) { picIrqPortBit(irq, &port, &bit);
port = PIC1_IMR;
bit = (uint8_t)(1u << irq);
} else {
port = PIC2_IMR;
bit = (uint8_t)(1u << (irq - 8));
}
mask = inportb(port); mask = inportb(port);
mask = (uint8_t)(mask & ~bit); mask = (uint8_t)(mask & ~bit);
outportb(port, mask); outportb(port, mask);
@ -398,13 +418,7 @@ static void picMaskIrq(uint8_t irq) {
uint8_t bit; uint8_t bit;
uint8_t mask; uint8_t mask;
if (irq < 8) { picIrqPortBit(irq, &port, &bit);
port = PIC1_IMR;
bit = (uint8_t)(1u << irq);
} else {
port = PIC2_IMR;
bit = (uint8_t)(1u << (irq - 8));
}
mask = inportb(port); mask = inportb(port);
mask = (uint8_t)(mask | bit); mask = (uint8_t)(mask | bit);
outportb(port, mask); outportb(port, mask);
@ -474,6 +488,11 @@ bool halAudioInit(void) {
gDspMajor = (uint8_t)major; gDspMajor = (uint8_t)major;
gDspMinor = (uint8_t)minor; 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()) { if (!allocDmaBuffer()) {
return false; return false;
} }
@ -484,6 +503,20 @@ bool halAudioInit(void) {
return false; 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 // Lock ISR code + the data sbIsr touches at IRQ time. The mixer
// refill no longer runs in interrupt context, so refillHalf and // refill no longer runs in interrupt context, so refillHalf and
// libxmp internals do not need locking. // 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 *)&gSbBase, sizeof(gSbBase));
_go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq)); _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); _go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
gIrqHooked = true; gIrqHooked = true;
@ -569,7 +592,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
return; return;
} }
gXmpLoaded = true; 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_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
xmp_release_module(gXmpCtx); xmp_release_module(gXmpCtx);
gXmpLoaded = false; 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) { if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; 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) { if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return; return;
} }
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate);
} }

View file

@ -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 // 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 // one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of
@ -17,6 +17,7 @@
#include <string.h> #include <string.h>
#include <time.h> // uclock / UCLOCKS_PER_SEC #include <time.h> // uclock / UCLOCKS_PER_SEC
#include <dos.h>
#include <dpmi.h> #include <dpmi.h>
#include <go32.h> #include <go32.h>
#include <pc.h> #include <pc.h>
@ -40,6 +41,7 @@
static void crashHandler(int sig); 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 expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
static void installCrashLog(void); static void installCrashLog(void);
static void rebuildExpandLut(uint8_t palBase);
static void uploadPalette(const jlSurfaceT *src); static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src); static void uploadPaletteIfNeeded(const jlSurfaceT *src);
@ -49,48 +51,74 @@ static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false; static bool gNearEnabled = false;
static FILE *gCrashLog = NULL; 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 // Cached palette from the last present. VGA DAC programming is ~768
// outportb calls; skip it when the source palette is unchanged. // outportb calls; skip it when the source palette is unchanged.
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE]; static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false; static bool gCacheValid = false;
// Scratch row buffer for halPresent. expandAndWriteLine builds the // Expand LUT for halPresent. Maps a 4bpp-packed source byte to the
// 8bpp form of a dirty band here, then a single memcpy lowers to REP // 2-byte little-endian mode-13h output pair for the current scanline's
// MOVS to push the whole band into VGA RAM in one bus-batched run. // SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
// The original per-pixel loop wrote one byte at a time, which stalled // the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
// the CPU on every ISA cycle (~125 ns at 8 MHz bus); REP MOVS pipelines // pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
// 16-bit ISA writes back-to-back, much better on slow CPUs. // write therefore lands the left pixel at the lower address, matching
static uint8_t gExpandRow[SURFACE_WIDTH]; // 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) ----- // ----- Internal helpers (alphabetical) -----
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) { static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
uint8_t palBase; uint8_t palBase;
const uint8_t *srcBytes; const uint8_t *srcBytes;
uint8_t *out; uint16_t *out;
uint16_t pairs; uint16_t pairs;
uint16_t i; 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 // Dirty bands are chunky-word aligned (halPresent computes
// pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source // pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source
// byte (= 2 nibbles) maps to 2 destination bytes -- no per-pixel // byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps
// odd/even shift branch needed. // &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)]; srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
out = gExpandRow; out = (uint16_t *)&dst[x];
pairs = (uint16_t)(w >> 1); pairs = (uint16_t)(w >> 1);
for (i = 0; i < pairs; i++) { for (i = 0; i < pairs; i++) {
byte = *srcBytes++; *out++ = gExpandLut[*srcBytes++];
*out++ = (uint8_t)(palBase | (byte >> 4));
*out++ = (uint8_t)(palBase | (byte & 0x0Fu));
} }
}
// Single bus-batched copy to VGA. memcpy in DJGPP libc lowers to
// REP MOVS, which queues 16-bit ISA write cycles back-to-back static void rebuildExpandLut(uint8_t palBase) {
// instead of stalling the CPU on every byte store. uint16_t byte;
memcpy(&dst[x], gExpandRow, w); 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 g;
uint8_t b; 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); outportb(DAC_INDEX_PORT, 0);
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) { for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) { 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); r = (uint8_t)((rgb >> 8) & 0x0F);
g = (uint8_t)((rgb >> 4) & 0x0F); g = (uint8_t)((rgb >> 4) & 0x0F);
b = (uint8_t)(rgb & 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)((r << 2) | (r >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2))); outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2))); outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2)));
} }
} }
enable();
} }
@ -256,6 +293,13 @@ void halPresent(const jlSurfaceT *src) {
if (minWord > maxWord) { if (minWord > maxWord) {
continue; 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); pixelX = (int16_t)((uint16_t)minWord << 2);
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2); pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]); 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) { uint16_t halFrameCount(void) {
uint8_t now; uint8_t now;
@ -322,9 +360,12 @@ extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz; extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive; extern volatile bool gAudioIsrActive;
static uclock_t gMillisBase = 0; static uclock_t gMillisBase = 0;
static bool gMillisBaseSet = false;
uint32_t halMillisElapsed(void) { uint32_t halMillisElapsed(void) {
uclock_t delta;
if (gAudioIsrActive && gAudioIsrHz > 0u) { if (gAudioIsrActive && gAudioIsrHz > 0u) {
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so // Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
// wall-clock-correct and monotonic. uint64 promotion in // wall-clock-correct and monotonic. uint64 promotion in
@ -333,11 +374,20 @@ uint32_t halMillisElapsed(void) {
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL) return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
/ (uint32_t)gAudioIsrHz); / (uint32_t)gAudioIsrHz);
} }
if (gMillisBase == 0) { // Use an explicit one-shot flag rather than gMillisBase == 0, so a
gMillisBase = uclock(); // 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 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);
} }

View file

@ -269,20 +269,30 @@ static void joystickPoll(void) {
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0; gJoyButtonState[JOYSTICK_1][JOY_BUTTON_0] = (btn & BIOS_BTN_B1) == 0;
gJoyButtonState[JOYSTICK_1][JOY_BUTTON_1] = (btn & BIOS_BTN_B2) == 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)); memset(&r, 0, sizeof(r));
r.h.ah = BIOS_FN_GAMEPORT; r.h.ah = BIOS_FN_GAMEPORT;
r.x.dx = BIOS_GAMEPORT_DX_POS; r.x.dx = BIOS_GAMEPORT_DX_POS;
__dpmi_int(BIOS_INT, &r); __dpmi_int(BIOS_INT, &r);
if (r.x.flags & BIOS_FLAGS_CARRY) { 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; return;
} }
gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax); gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax);
gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx); gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx);
gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx); gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx);
gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx); 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)); memset(&r, 0, sizeof(r));
r.x.ax = MOUSE_FN_GET_STATE; r.x.ax = MOUSE_FN_GET_STATE;
__dpmi_int(MOUSE_INT, &r); __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; gMouseX = (int16_t)r.x.cx;
gMouseY = (int16_t)r.x.dx; gMouseY = (int16_t)r.x.dx;
btn = r.x.bx; btn = r.x.bx;

View file

@ -1,13 +1,8 @@
// Apple IIgs audio HAL -- single source linked into every IIgs demo. // 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 // Memory Manager + the 34 KB NTP replayer only need to be present in
// real implementation, filtering audio_full.c out of non-AUDIO source // AUDIO binaries; link-time dead-stripping drops this implementation
// sets, because pulling Memory Manager + the 34 KB NTP replayer into // code from non-AUDIO binaries, which pay only for the data
// every binary blew the ORCA Linker's blank-segment / "Expression too // references they actually use.
// 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.
// //
// The NinjaTrackerPlus replayer is Merlin32-assembled at build time // The NinjaTrackerPlus replayer is Merlin32-assembled at build time
// to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the // to ntpplayer.bin and baked into this TU as gNtpPlayerBytes via the
@ -18,25 +13,27 @@
#include <string.h> #include <string.h>
#include <types.h> // Memory Manager via <iigs/toolbox.h>. Handles/pointers are void* and the
#include <memory.h> // 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 <iigs/toolbox.h>
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 "hal.h"
#include "joey/audio.h" #include "joey/audio.h"
// Place every function defined below in the shared DRAWPRIMS overflow // The 34 KB NTP replayer bytes live in their own data segment
// load segment so the linker keeps the implementation code out of // (build/iigs/audio/ntpdata.asm, auto-generated from ntpplayer.bin by
// _ROOT in every binary that includes this TU. (See ORCA/C ch. 30 // make/iigs.mk). We just extern the symbols here.
// "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";
extern const unsigned char gNtpPlayerBytes[]; extern const unsigned char gNtpPlayerBytes[];
extern const unsigned long gNtpPlayerBytes_len; 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-slot config cache. halAudioPlaySfx's biggest cost is the
// per-byte XOR-with-$80 loop over the entire sample (signed -> DOC's // 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 // unsigned format), which on a 4 KB sample is ~120 k cyc / ~43 ms.
// ORCA-C. Most callers play the same SFX repeatedly into the same // Most callers play the same SFX repeatedly into the same
// slot. Cache (sample ptr, length, rate) per slot; on cache hit // slot. Cache (sample ptr, length, rate) per slot; on cache hit
// (same sample re-triggered) skip the byte copy AND the struct // (same sample re-triggered) skip the byte copy AND the struct
// rebuild, just re-fire NTPstreamsound. // 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 // `jsl gCallStub` followed by `rep #0x30` -- no global-operand
// references in the asm block. // references in the asm block.
// //
// The trailing REP is required to satisfy ORCA-C's `longa on / // The trailing REP restores M=16/X=16 for the function epilogue: the
// longi on` exit contract: the byte writes the call sites do to // byte writes the call sites do to patch this stub leave M=8, and
// patch this stub leave M=8, and NTP's RTL leaves M/X in whatever // NTP's RTL leaves M/X in whatever state NTP chose. Without REP, the
// state NTP chose. Without REP, the C epilogue's ADC/LDX widths // C epilogue's ADC/LDX widths decode wrong and the function returns
// decode wrong and the function returns to garbage. See // to garbage. See project_iigs_inline_asm_mode.md.
// project_iigs_inline_asm_mode.md.
// //
// (We tried the obvious `lda gAsmGlobal / jsl gJslStub` shape first; // Folding the X/Y/A loads and the JSL target into the stub keeps the
// ORCA's inline assembler accepts the first absolute-global operand // inline asm block free of absolute-global operands entirely.
// in a block but rejects the second and third with "invalid operand".
// Folding the loads into the stub side-steps the issue entirely.)
// //
// Layout (15 bytes): // Layout (15 bytes):
// 00: C2 30 REP #$30 ; 16-bit M/X // 00: C2 30 REP #$30 ; 16-bit M/X
@ -183,7 +177,7 @@ static bool loadNTP(void) {
return false; return false;
} }
HLock(h); HLock(h);
p = *h; p = *(void **)h;
if (p == NULL) { if (p == NULL) {
DisposeHandle(h); DisposeHandle(h);
return false; return false;
@ -222,7 +216,7 @@ bool halAudioInit(void) {
} }
HLock(modHandle); HLock(modHandle);
gModuleHandle = modHandle; gModuleHandle = modHandle;
gModuleBase = (uint32_t)*modHandle; gModuleBase = (uint32_t)*(void **)modHandle;
// Third fixed-bank handle for SFX sample storage. Same idea as // Third fixed-bank handle for SFX sample storage. Same idea as
// the module handle: NTPstreamsound wants a 24-bit pointer, and // the module handle: NTPstreamsound wants a 24-bit pointer, and
@ -239,7 +233,7 @@ bool halAudioInit(void) {
} }
HLock(sfxHandle); HLock(sfxHandle);
gSfxHandle = sfxHandle; gSfxHandle = sfxHandle;
gSfxBase = (uint32_t)*sfxHandle; gSfxBase = (uint32_t)*(void **)sfxHandle;
gNTPReady = true; gNTPReady = true;
return 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 & 0xFFFFu),
(uint16_t)((gModuleBase >> 16) & 0xFFu), (uint16_t)((gModuleBase >> 16) & 0xFFu),
0); 0);
asm { __asm__ volatile (
jsl gCallStub "jsl gCallStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
// NTPplay(loopFlag in A). 0 = loop forever, 1 = play once. // NTPplay(loopFlag in A). 0 = loop forever, 1 = play once.
buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1); buildCallStub(gNTPBase + 3, 0, 0, loop ? 0 : 1);
asm { __asm__ volatile (
jsl gCallStub "jsl gCallStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
gNTPPlaying = true; 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 // for this slot? Then the slot's DOC sample bytes and stream
// struct are already correct -- skip the 4 KB byte-XOR loop and // struct are already correct -- skip the 4 KB byte-XOR loop and
// the 15-byte struct rebuild, both of which together can run // 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] && if (sample == gSfxSlotSample[slot] &&
length == gSfxSlotLength[slot] && length == gSfxSlotLength[slot] &&
rateHz == gSfxSlotRateHz[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 & 0xFFFFu),
(uint16_t)((structAddr >> 16) & 0xFFu), (uint16_t)((structAddr >> 16) & 0xFFu),
0); 0);
asm { __asm__ volatile (
jsl gCallStub "jsl gCallStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
} }
@ -466,10 +463,11 @@ void halAudioStopMod(void) {
return; return;
} }
buildCallStub(gNTPBase + 6, 0, 0, 0); buildCallStub(gNTPBase + 6, 0, 0, 0);
asm { __asm__ volatile (
jsl gCallStub "jsl gCallStub\n\t"
rep #0x30 "rep #0x30"
} ::: "memory"
);
gNTPPlaying = false; gNTPPlaying = false;
} }
@ -487,17 +485,13 @@ void halAudioStopSfx(uint8_t slot) {
// NTP has no SFX-stop entry point. We poke DOC oscillator control // NTP has no SFX-stop entry point. We poke DOC oscillator control
// registers directly to halt the slot's osc pair. SEI/CLI keeps // registers directly to halt the slot's osc pair. SEI/CLI keeps
// NTP's DOC IRQ from racing our sound_address writes. // NTP's DOC IRQ from racing our sound_address writes.
asm { __asm__ volatile ("sei" ::: "memory");
sei
}
*DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME; *DOC_SOUND_CONTROL = *DOC_IRQ_VOLUME;
for (i = 0; i < SFX_OSCS_PER_SLOT; i++) { for (i = 0; i < SFX_OSCS_PER_SLOT; i++) {
*DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i); *DOC_SOUND_ADDRESS = (uint8_t)(DOC_OSC_CTRL_BASE + firstOsc + i);
*DOC_SOUND_DATA = DOC_OSC_HALT_BITS; *DOC_SOUND_DATA = DOC_OSC_HALT_BITS;
} }
asm { __asm__ volatile ("cli" ::: "memory");
cli
}
} }

View file

@ -7,20 +7,19 @@
// $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB // $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB
// //
// NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on. // 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 // Long (24-bit) addresses are used so they resolve to bank $E1.
// that the long addresses resolve to bank $E1.
// //
// DIRTY-WALK + PEI-SLAM PRESENT // DIRTY-WALK + PEI-SLAM PRESENT
// ----------------------------- // -----------------------------
// halPresent walks the per-row dirty bands maintained by drawing // halPresent walks the per-row dirty bands maintained by drawing
// primitives in src/core/*.c. Fully-dirty rows go through the PEI // 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 // slam in src/port/iigs/peislam.s (~530 cyc/row, ~55% faster than
// memcpy/MVN); partial-dirty rows use memcpy, which ORCA-C lowers // memcpy/MVN); partial-dirty rows use memcpy, which lowers
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into // 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 // bank $E1 when the dirty band is too narrow to amortize the slam's
// per-call AUXWRITE/RAMRD/shadow toggle. // 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 // places it in its own bank, separate from AUDIO's _ROOT (where
// audio_full.c + Memory Manager + stdio + NTPstreamsound already // audio_full.c + Memory Manager + stdio + NTPstreamsound already
// crowd up against the 64 KB-per-bank limit). // crowd up against the 64 KB-per-bank limit).
@ -33,7 +32,7 @@
#include "hal.h" #include "hal.h"
#include "surfaceInternal.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 * ($2503) and returns the low 16 bits of the system's tick counter
* (firmware VBL ISR-driven). Polling $C019 from C user code missed * (firmware VBL ISR-driven). Polling $C019 from C user code missed
* transitions for any op over ~1 ms; the system's tick counter is * 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- // hal.c is the single TU that calls into joeyDraw.asm. Cross-
// platform draw.c / tile.c / etc. dispatch through halFast* // platform draw.c / tile.c / etc. dispatch through halFast*
// functions defined here; they never reference the asm symbols // functions defined here; they never reference the asm symbols
// directly. This avoids the cumulative ORCA-Linker-Expression- // directly. Keeping the asm externs in a single TU avoids the
// too-complex-in-13/SysLib failure that hit when each cross- // link-time fragility seen when each cross-platform TU brought its
// platform TU brought its own asm extern. // own asm extern.
JOEYLIB_SEGMENT("DRAWPRIMS")
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen. // 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord); 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); 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. // 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. // 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 // The asm performs the upload directly rather than a C-side memcpy,
// from halPresent (DBR-state quirk after prior asm primitives). // which is unreliable when called from halPresent (DBR-state quirk
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr); // after prior asm primitives).
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner, // jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by halFastFloodWalkAndScans. // read back by halFastFloodWalkAndScans.
extern uint16_t gFloodSeedMatch; extern uint16_t gFloodSeedMatch;
@ -102,7 +101,7 @@ extern void iigsInitRowLut(void);
// screen presents (halPresentRect). srcOffset is the byte offset // screen presents (halPresentRect). srcOffset is the byte offset
// within bank $01 of the FIRST byte to copy on the FIRST row; // within bank $01 of the FIRST byte to copy on the FIRST row;
// subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs // 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); 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 // 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 // ~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 / // SCB / palette upload skipping is now driven by gStageScbDirty /
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp- // 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; // PEI slam scratch. File-scope non-static so the asm can `ext` them;
// all accesses inside the slam use long-mode `>` addressing so they // 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 gPeiTempRowBase;
volatile uint16_t gPeiCurRow; // row counter saved across slam (stack is hijacked) 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 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 // the present pipeline now does its own PEI-slam loop inside
// iigsBlitStageToShr above (with dirty-row skip). // 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 // Upload SCB / palette into bank-$E1 SHR memory only when the
// matching dirty flag is set. Replaces a per-frame 712-byte memcmp // 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 // gStageScbDirty / gStagePaletteDirty live in core/surface.c; they
// start true (forces the very first present to upload), get set true // start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and // again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
@ -231,16 +232,22 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) { void halPresent(const jlSurfaceT *src) {
uint16_t uploadFlags;
if (src == NULL) { if (src == NULL) {
return; return;
} }
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette // iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores. // 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 // halPresent's calling context, so we route everything through
// the asm path. Future: re-introduce per-row dirty-band logic // the asm path. uploadFlags gates the optional SCB / palette
// for partial-screen updates (currently we always blit 32K). // uploads on the dirty bits so we don't re-push 712 unchanged
iigsBlitStageToShr(src->scb, &src->palette[0][0]); // 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 // halFastFillRect: macro-dispatched in core/hal.h, same as the other
// halFast* primitives. The C wrapper that used to live here was kept // 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. // 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. // IIgs is chunky-native: the hal*Planes hooks are compiled out by
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) { // no-op macros in core/hal.h (gated on JOEYLIB_PLATFORM_IIGS), so no
(void)s; // stub functions are defined here.
(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;
}
/* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like /* 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. // context where ToolBox interrupt setup would be intrusive.
// //
// gFrameCount uses an explicit lda+adc+sta read-modify-write rather // 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 |gFrameCount` (the only INC abs form on 65816 -- there is no
// INC long-abs). With this file in the DRAWPRIMS load segment but // INC long-abs). With this file in the DRAWPRIMS load segment but
// halFrameCount called from CORESYS via JSL, DBR isn't pointing at // 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 // IIgs: frame-counter approximation. time() is avoided here, so
// stdio cluster bank limit (see feedback_orca_c_time_segment), so
// we approximate wall-clock ms from the 16-bit frame counter at // we approximate wall-clock ms from the 16-bit frame counter at
// gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so // gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so
// callers tracking deltas should use this for short intervals only // callers tracking deltas should use this for short intervals only

View file

@ -36,8 +36,6 @@
#include <string.h> #include <string.h>
#include <types.h>
#include "hal.h" #include "hal.h"
#include "inputInternal.h" #include "inputInternal.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -118,9 +116,8 @@ static int8_t thresholdPaddle(uint8_t v);
// ----- Module state ----- // ----- Module state -----
// ASCII -> jlKeyE, filled once at halInputInit. ORCA/C is C89 and // ASCII -> jlKeyE, filled once at halInputInit. Runtime fill keeps
// does not accept designated initializers; runtime fill keeps lookup // lookup O(1) instead of a 40-plus-case switch.
// O(1) instead of a 40-plus-case switch.
static uint8_t gAsciiToKey[ASCII_TABLE_SIZE]; static uint8_t gAsciiToKey[ASCII_TABLE_SIZE];
static int16_t gMouseAbsX = SURFACE_WIDTH / 2; 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 // Each paddle read triggers an RC scan via $C070 and then polls the
// paddle softswitch until bit 7 clears; the iteration count // paddle softswitch until bit 7 clears; the iteration count
// approximates the paddle's 0..255 position (the Apple firmware // approximates the paddle's 0..255 position (the Apple firmware
// PREAD routine works the same way). The two reads are inlined here // PREAD routine works the same way). The paddle one-shot timer takes ~3 ms to
// 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
// charge at full deflection; if NO joystick is wired up, the BUSY bit // 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 // 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. // 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 // to the digital threshold mapping. gJoyRecalibrate is set by
// halJoystickReset and cleared on the next successful poll, which // halJoystickReset and cleared on the next successful poll, which
// captures the new center. // captures the new center.
// uint8_t (not bool) so the per-element stride is 1 byte. ORCA-C's // uint8_t (not bool) so the per-element stride is a known 1 byte.
// _Bool is 2 bytes, which forces a ~MUL4 helper for every index // Storage is still 0 or 1 either way.
// multiply -- even when the index is a constant the compiler doesn't
// fold. Storage is still 0 or 1 either way.
static uint8_t gJoyCenterX [JOYSTICK_COUNT]; static uint8_t gJoyCenterX [JOYSTICK_COUNT];
static uint8_t gJoyCenterY [JOYSTICK_COUNT]; static uint8_t gJoyCenterY [JOYSTICK_COUNT];
static uint8_t gJoyCenterValid [JOYSTICK_COUNT]; static uint8_t gJoyCenterValid [JOYSTICK_COUNT];
@ -290,10 +283,8 @@ static void pollJoystick(void) {
bool yResolved; bool yResolved;
// Buttons are I/O reads -- always cheap, do them every frame. // 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 // Indexing through a (uint8_t *) cast collapses each
// 2D arrays even when both indices are constants, so each // gJoyButtonState[i][j] write to a literal byte offset.
// gJoyButtonState[i][j] write emits a ~MUL4 helper. Indexing
// through a (uint8_t *) cast collapses to a literal byte offset.
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0] ((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_0]
= (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0; = (*IIGS_BTN0 & IIGS_BUTTON_BIT) != 0;
((uint8_t *)gJoyButtonState)[JOYSTICK_0 * JOY_BUTTON_COUNT + JOY_BUTTON_1] ((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. // stick. One resolves => stick is present, reset the counter.
// //
// gJoyConsecutiveTimeouts uses a local-var read-modify-write rather // gJoyConsecutiveTimeouts uses a local-var read-modify-write rather
// than `++`. ORCA-C lowers `++` to `inc abs` (the only INC abs form // than `++` so the update is DBR-independent: an `inc abs` on the
// on 65816), which depends on DBR pointing at this static's bank. // 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 // cross-segment JSL doesn't update DBR, so a caller in a different
// load segment would silently mutate the wrong byte. Long-mode // load segment could silently mutate the wrong byte. Long-mode
// lda+sta is DBR-independent. // lda+sta avoids that.
if (!xResolved && !yResolved) { if (!xResolved && !yResolved) {
uint16_t timeouts; uint16_t timeouts;

File diff suppressed because it is too large Load diff

4248
src/port/iigs/joeyDraw.s Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,66 +0,0 @@
* peislam.asm - originally a PEI-slam helper, now hosts the GetTick
* and ReadBParam trampolines. The PEI-slam logic was rolled into
* iigsBlitStageToShr in joeyDraw.asm.
keep PEISLAM
case on
* Stub kept so the PEISLAM load segment stays present (the build's
* PORT_ASM_SRCS_ALL wildcard pulls in this file by name).
peislamStub start IIGSASM
rtl
end
****************************************************************
* uint16_t iigsGetTickWord(void)
*
* Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of
* the 32-bit tick counter. The system increments this counter from
* the actual VBL hardware interrupt, so it stays accurate regardless
* of caller polling rate -- C-side polling of $C019 missed transitions
* for any op over ~1 ms.
*
* GetTick output convention: caller pushes 4 bytes of output space,
* tool dispatcher writes the LongWord into them. We pull the low 16
* bits into A (ORCA-C Word return convention -- A holds the result,
* not Y; verified against jIIgs.asm asmGetVbl) and discard the high
* 16 into X.
*
* ORCA-C cdecl ABI: caller has M=I=16. Word return in A.
****************************************************************
iigsGetTickWord start IIGSASM
pha ; output space high word
pha ; output space low word
ldx #$2503 ; _GetTick
jsl $E10000
pla ; A = low 16 bits (return value)
plx ; discard high 16 bits
rtl
end
****************************************************************
* uint16_t iigsReadHzParam(void)
*
* Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03)
* and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
*
* GetTick fires from the hardware VBL ISR, so its rate matches the
* video field rate -- 60 Hz on NTSC, 50 Hz on PAL. halFrameHz must
* report whichever this machine actually runs so wall-clock math
* (frames * 1000 / halFrameHz) is correct on both.
****************************************************************
iigsReadHzParam start IIGSASM
pha ; output space (Word)
pea $001D ; hrtz50or60 parameter ID
ldx #$0C03 ; _ReadBParam
jsl $E10000
pla ; A = result (ORCA-C Word return)
rtl
end

56
src/port/iigs/peislam.s Normal file
View file

@ -0,0 +1,56 @@
; peislam.s - GetTick and ReadBParam trampolines for the IIgs.
;
; GAS syntax for the llvm-mos w65816 toolchain. The old PEI-slam present
; logic was rolled into iigsBlitStageToShr in joeyDraw; only the two
; toolbox trampolines remain.
;
; C ABI (llvm-mos w65816): caller has M=I=16. A uint16_t return value
; is left in A, which matches the toolbox dispatcher's word result, so
; these wrappers need no register shuffling beyond the pop.
.text
; ----------------------------------------------------------------
; uint16_t iigsGetTickWord(void)
;
; Calls Misc Toolset GetTick ($2503) and returns the low 16 bits of
; the 32-bit tick counter. The system increments this counter from the
; actual VBL hardware interrupt, so it stays accurate regardless of
; caller polling rate -- C-side polling of $C019 missed transitions
; for any op over ~1 ms.
;
; GetTick output convention: caller pushes 4 bytes of output space, the
; tool dispatcher writes the LongWord into them. We pull the low 16
; bits into A (the return value) and discard the high 16 into X.
; ----------------------------------------------------------------
.section .text.iigsGetTickWord,"ax"
.globl iigsGetTickWord
iigsGetTickWord:
pha ; output space high word
pha ; output space low word
ldx #0x2503 ; _GetTick
jsl 0xe10000
pla ; A = low 16 bits (return value)
plx ; discard high 16 bits
rtl
; ----------------------------------------------------------------
; uint16_t iigsReadHzParam(void)
;
; Reads battery RAM parameter hrtz50or60 ($1D) via _ReadBParam ($0C03)
; and returns the raw value: 0 = NTSC (60 Hz), 1 = PAL (50 Hz).
;
; GetTick fires from the hardware VBL ISR, so its rate matches the
; video field rate -- 60 Hz on NTSC, 50 Hz on PAL. halFrameHz must
; report whichever this machine actually runs so wall-clock math
; (frames * 1000 / halFrameHz) is correct on both.
; ----------------------------------------------------------------
.section .text.iigsReadHzParam,"ax"
.globl iigsReadHzParam
iigsReadHzParam:
pha ; output space (Word)
pea 0x001d ; hrtz50or60 parameter ID
ldx #0x0c03 ; _ReadBParam
jsl 0xe10000
pla ; A = result (return value)
rtl

View file

@ -1,201 +0,0 @@
// Shared 68k C-side fast paths for drawing primitives.
//
// Header-only `static inline` so both Amiga and Atari ST hal.c can
// pull the same code without makefile changes -- the surface buffer
// layout is identical between the two ports (4bpp packed, 160 bytes
// per row, 32000 bytes per surface), so the per-pixel write logic
// is portable.
//
// All functions assume the caller has already verified the rect /
// circle bounding box is fully on-surface (which is what the
// halFast* contract guarantees). No clip checks inside the hot
// loops, no per-pixel surfaceMarkDirtyRect calls -- the caller marks
// the bounding box dirty once after the call.
//
// gcc-amigaos / gcc-atari-mint both inline these aggressively at -O2,
// so the wrapper functions in each port's hal.c compile to a single
// tight m68k loop with no JSR per pixel.
#ifndef JOEYLIB_DRAW68K_INLINE_H
#define JOEYLIB_DRAW68K_INLINE_H
#include "joey/types.h"
#include "joey/surface.h"
// Plot a single pixel at (x, y) without bounds checking. nibLo is
// the colorIndex's low nibble, nibHi is (low << 4) precomputed by
// the caller so the hot loop doesn't do the shift per call.
static inline void draw68kPlotPixel(uint8_t *pixels, int16_t x, int16_t y,
uint8_t nibLo, uint8_t nibHi) {
uint8_t *byte = &pixels[(uint16_t)y * (uint16_t)SURFACE_BYTES_PER_ROW
+ ((uint16_t)x >> 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 */

View file

@ -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 | Holds surface68kFillSpan4Planes -- the per-row body of the 4-plane
| same 4bpp packed (2 px / byte, 160 bytes / row, 32000 bytes / surface) | planar rect/circle fill. Only the Amiga port calls it (ST is
| format -- the c2p / blit-to-screen step that converts to native | word-interleaved and uses its own St* routines); it lives here
| pixel layout happens only at present time. So the per-byte buffer | because the file is globbed into both ports' builds. The earlier
| operations (fill, clear) can be shared across both ports. | 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). | Calling convention: cdecl (gcc-amigaos / gcc-atari-mint).
| d0/d1/a0/a1 are caller-save (scratch). | d0/d1/a0/a1 are caller-save (scratch).
@ -16,245 +17,6 @@
.text .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, | void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1,
| uint8_t *p2, uint8_t *p3, | uint8_t *p2, uint8_t *p3,
@ -283,13 +45,15 @@ _surface68kFillRectByteAligned:
| -- branchless: the same arithmetic produces "set" or "clear" based | -- branchless: the same arithmetic produces "set" or "clear" based
| on whether fbN is 0xFF or 0x00. | on whether fbN is 0xFF or 0x00.
| |
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save (movem'd here). | ABI: m68k cdecl. d2-d7/a2-a6 callee-save. Only a2/a3 are actually
| Stack offset to first arg after MOVEM: 11 regs * 4 = 44 bytes saved | used by the body (planes 2/3); a4-a6 are not touched, so only
| + 4 ret PC = 48. | 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 .globl _surface68kFillSpan4Planes
.equ SP_SAVED, 44 .equ SP_SAVED, 32
.equ SP_RPC, 4 .equ SP_RPC, 4
.equ SP_OFF, (SP_SAVED + SP_RPC) .equ SP_OFF, (SP_SAVED + SP_RPC)
@ -305,43 +69,56 @@ _surface68kFillRectByteAligned:
.equ SP_FB2, SP_OFF + 36 + 3 .equ SP_FB2, SP_OFF + 36 + 3
.equ SP_FB3, SP_OFF + 40 + 3 .equ SP_FB3, SP_OFF + 40 + 3
| Macro: per-plane work fully inlined. Args: | Per-plane work is fully inlined (hand-unrolled 4x rather than
| plane_an = the address register holding this plane's pointer. | bsr+rts, to dodge ~12 cyc per return + the per-plane re-test of
| fb_off = the stack offset for this plane's fillByte. | numMid that an earlier build paid). Register convention across all
| Uses d6/d7 as scratch; d1=leftMask, d2=~leftMask, d3=rightMask, | four planes:
| d4=~rightMask; d0=numMid-1 (only valid if mid_count > 0). The mid | d1 = leftMask, d2 = ~leftMask, d3 = rightMask, d4 = ~rightMask
| loop is skipped via .LfsSkipMid_<n> when numMid was 0 at entry -- | d5 = this plane's fb byte, d6/d0/d7 = per-plane scratch.
| the per-plane caller branches to the right tail label.
| |
| Hand-unrolled 4x rather than using bsr+rts to dodge ~12 cyc per | The unmasked middle run is filled with long stores rather than
| return + the per-plane re-test of numMid that the previous build | byte-at-a-time. fb is 0x00 or 0xFF, so the long is built with a
| paid. The mid-loop label suffix is the plane index so all four | byte->word->long sign-extend (ext.w / ext.l). The mid pointer is
| inline copies can coexist without label collisions. | 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 | Plain text version of the per-plane body (translate to asm 4x with
| different a-regs and fb stack offsets): | different a-regs and fb stack offsets):
| |
| move.b (an),%d6 | move.b (an),%d6 | --- leading masked edge byte ---
| and.b %d2,%d6 | and.b %d2,%d6
| move.b fb,%d7 | move.b fb,%d5
| move.b %d5,%d7
| and.b %d1,%d7 | and.b %d1,%d7
| or.b %d7,%d6 | or.b %d7,%d6
| move.b %d6,(an)+ | move.b %d6,(an)+
| < if has-middle path: > | < if has-middle path: >
| move.w %d0,%d7 | move.b %d5,%d7 | build replicated fill long in d7
| .midN: | ext.w %d7
| move.b fb,(an)+ | ext.l %d7
| dbra %d7,.midN | move.l an,%d6 | align = (-an) & 3, clamped to numMid
| < trailing: > | 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 | move.b (an),%d6
| and.b %d4,%d6 | and.b %d4,%d6
| move.b fb,%d7 | move.b %d5,%d7
| and.b %d3,%d7 | and.b %d3,%d7
| or.b %d7,%d6 | or.b %d7,%d6
| move.b %d6,(an) | move.b %d6,(an)
_surface68kFillSpan4Planes: _surface68kFillSpan4Planes:
movem.l %d2-%d7/%a2-%a6,-(%sp) movem.l %d2-%d7/%a2-%a3,-(%sp)
move.b SP_LMASK(%sp),%d1 move.b SP_LMASK(%sp),%d1
move.b %d1,%d2 move.b %d1,%d2
@ -356,12 +133,27 @@ _surface68kFillSpan4Planes:
move.l SP_P3(%sp),%a3 move.l SP_P3(%sp),%a3
| One-time numMid test. d0.w = numMid; if 0 jump to | One-time numMid test. d0.w = numMid; if 0 jump to
| the no-middle entry, otherwise pre-decrement for dbra | the no-middle entry. The with-middle path now fills
| and fall into the with-middle entry. Both paths | the unmasked middle run with long stores (move.l
| unroll all 4 planes. | (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 move.w SP_NMID(%sp),%d0
beq .LfsNoMid beq .LfsNoMid
subq.w #1,%d0
| ---- WITH-MIDDLE PATH ---- | ---- WITH-MIDDLE PATH ----
| Plane 0 | Plane 0
@ -372,10 +164,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7 and.b %d1,%d7
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a0)+ move.b %d6,(%a0)+
move.w %d0,%d7 move.b %d5,%d7
.LfsMid0: move.b %d5,(%a0)+ ext.w %d7
dbra %d7,.LfsMid0 ext.l %d7
move.b (%a0),%d6 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 and.b %d4,%d6
move.b %d5,%d7 move.b %d5,%d7
and.b %d3,%d7 and.b %d3,%d7
@ -390,10 +208,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7 and.b %d1,%d7
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a1)+ move.b %d6,(%a1)+
move.w %d0,%d7 move.b %d5,%d7
.LfsMid1: move.b %d5,(%a1)+ ext.w %d7
dbra %d7,.LfsMid1 ext.l %d7
move.b (%a1),%d6 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 and.b %d4,%d6
move.b %d5,%d7 move.b %d5,%d7
and.b %d3,%d7 and.b %d3,%d7
@ -408,10 +252,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7 and.b %d1,%d7
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a2)+ move.b %d6,(%a2)+
move.w %d0,%d7 move.b %d5,%d7
.LfsMid2: move.b %d5,(%a2)+ ext.w %d7
dbra %d7,.LfsMid2 ext.l %d7
move.b (%a2),%d6 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 and.b %d4,%d6
move.b %d5,%d7 move.b %d5,%d7
and.b %d3,%d7 and.b %d3,%d7
@ -426,17 +296,43 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7 and.b %d1,%d7
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a3)+ move.b %d6,(%a3)+
move.w %d0,%d7 move.b %d5,%d7
.LfsMid3: move.b %d5,(%a3)+ ext.w %d7
dbra %d7,.LfsMid3 ext.l %d7
move.b (%a3),%d6 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 and.b %d4,%d6
move.b %d5,%d7 move.b %d5,%d7
and.b %d3,%d7 and.b %d3,%d7
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a3) move.b %d6,(%a3)
movem.l (%sp)+,%d2-%d7/%a2-%a6 movem.l (%sp)+,%d2-%d7/%a2-%a3
rts rts
.LfsNoMid: .LfsNoMid:
@ -501,5 +397,5 @@ _surface68kFillSpan4Planes:
or.b %d7,%d6 or.b %d7,%d6
move.b %d6,(%a3) move.b %d6,(%a3)
movem.l (%sp)+,%d2-%d7/%a2-%a6 movem.l (%sp)+,%d2-%d7/%a2-%a3
rts rts

View file

@ -11,29 +11,22 @@ JOEY_TOOLCHAINS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export JOEY_TOOLCHAINS export JOEY_TOOLCHAINS
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
# IIgs # IIgs (llvm-mos w65816: clang + llvm-mc + link816)
# #
# Two separate trees: # LLVM816_ROOT is the toolchain root (clang / llvm-mc / link816 plus the
# - toolchains/iigs/gg-tools/bin/ Unix host binaries (iix, dumpobj, ...) # prebuilt runtime). clang-build.sh drives compile/assemble/link. It can
# built from the GoldenGate source repo. # be overridden to point at a vendored copy; it currently defaults to the
# - toolchains/iigs/goldengate/ The IIgs filesystem iix emulates. # build scratchpad where the toolchain was bootstrapped.
# $GOLDEN_GATE points here. # TODO: vendor the toolchain under toolchains/iigs.
#
# iix is the host-side command shell that runs ORCA tools (cc, Linker,
# Asm65816, ...) via 65816 emulation against the $GOLDEN_GATE tree.
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
export GOLDEN_GATE="${JOEY_TOOLCHAINS}/iigs/goldengate" : "${LLVM816_ROOT:=/tmp/claude-1000/-home-scott-claude-joeylib/bf471cbc-f6e2-4287-86b6-58e16cc489e6/scratchpad/65816-llvm-mos}"
export ORCA_ROOT="${GOLDEN_GATE}" 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_AS="${JOEY_TOOLCHAINS}/iigs/merlin32/bin/merlin32"
export IIGS_IIX="${JOEY_TOOLCHAINS}/iigs/gg-tools/bin/iix" # Alias used by the top-level Makefile's HAVE_IIGS probe (`[ -x $IIGS_CC ]`).
export IIGS_BUILD="${JOEY_TOOLCHAINS}/iigs/iix-build.sh" export IIGS_CC="${IIGS_CLANG_BUILD}"
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}"
# ------------------------------------------------------------------------ # ------------------------------------------------------------------------
# Amiga # Amiga

View file

@ -5,7 +5,7 @@
# Installs all cross-compilers, assemblers, and libraries needed to build # Installs all cross-compilers, assemblers, and libraries needed to build
# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local # JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
# toolchains/ folder. Nothing is installed system-wide. Free tools are # 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 # placement instructions and the script verifies they were placed
# correctly on a subsequent run. # 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() { install_audio_libxmp() {
@ -352,188 +353,6 @@ EOF
fi fi
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 <<EOF
GoldenGate Unix host tools not found at ${gg_tools_dir}/bin/.
Build from the GoldenGate source repo (e.g. in stuff/GoldenGate):
cd stuff/GoldenGate
git submodule update --init
mkdir -p build && cd build
cmake .. && make iix dumpobj profuse mkfs-profuse opus-extractor
Then copy the built binaries to ${gg_tools_dir}/bin/ and re-run
./toolchains/install.sh to verify.
EOF
)
fi
# Populate the IIgs filesystem at $GOLDEN_GATE. Sources expected in
# stuff/:
# gg-linux.tgz IIgs support tree (bin, etc, Home, lib, System, usr)
# fst.tgz File System Translators and fst-rom
# opus/ Opus II extract with Languages/Libraries/Shell/Utilities/
local gg_linux_tgz="${stuff_dir}/gg-linux.tgz"
local fst_tgz="${stuff_dir}/fst.tgz"
local opus_dir="${stuff_dir}/opus"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
clear_done "iigs_goldengate"
clear_done "iigs_orca"
rm -rf "${gg_root}"
fi
if is_done "iigs_goldengate" && [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then
ok "GoldenGate IIgs filesystem already populated"
STATUS[iigs_goldengate]="ok"
else
if [ ! -f "${gg_linux_tgz}" ]; then
STATUS[iigs_goldengate]="missing"
INSTRUCTIONS[iigs_goldengate]=$(cat <<EOF
Cannot populate the $GOLDEN_GATE tree at ${gg_root}/.
Expected archive: ${gg_linux_tgz}
Place the gg-linux.tgz archive (from the GoldenGate distribution) into
${stuff_dir}/ and re-run ./toolchains/install.sh.
EOF
)
else
info "Extracting gg-linux.tgz into goldengate/"
mkdir -p "${gg_root}"
# The archive roots at gg-linux/GoldenGate/; strip both levels.
tar -xzf "${gg_linux_tgz}" -C "${gg_root}" --strip-components=2 \
gg-linux/GoldenGate/ 2>/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 <<EOF
ORCA source directory not found at ${opus_dir}/.
Extract the Opus II ORCA distribution into ${opus_dir}/ so it contains:
Languages/ Libraries/ Shell/ Utilities/
Then re-run ./toolchains/install.sh.
EOF
)
else
info "Merging Opus II ORCA directories into goldengate/"
local orca_ok=1
for sub in Languages Libraries Shell Utilities; do
if [ -d "${opus_dir}/${sub}" ]; then
cp -rn "${opus_dir}/${sub}" "${gg_root}/" 2>/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 # NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
# source (ZIP) plus binary tool disk images on ninjaforce.com. # source (ZIP) plus binary tool disk images on ninjaforce.com.
# Redistribution is granted by the copyright holder per the user's # Redistribution is granted by the copyright holder per the user's

View file

@ -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): def parse_log(path):
"""Return {op: ops/sec} from a UBER log. Last entry per op wins.""" """Return {op: ops/sec} from a UBER log. Last entry per op wins."""
perf = {} perf = {}
@ -144,6 +162,8 @@ def main(argv):
ordered = ref_ops + extras ordered = ref_ops + extras
table = render_table(port_data, ordered, args.ref) table = render_table(port_data, ordered, args.ref)
if args.ref == "iigs":
table += "\n" + IIGS_CEILING_NOTE
if args.out: if args.out:
with open(args.out, "w") as f: with open(args.out, "w") as f:
f.write(table + "\n") f.write(table + "\n")