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

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
produced game binaries.
### Golden Gate (host tools: iix, profuse, mkfs-profuse, dumpobj)
### llvm-mos (clang / llvm-mc / link816 w65816 toolchain)
- **Used on:** Apple IIgs
- **URL:** <https://golden-gate.ksherlock.com/>
(publisher / store: <https://juiced.gs/store/golden-gate/>)
- **License:** **Commercial** software by Kelvin Sherlock,
distributed through Juiced.GS / Gamebits. End-user purchase
required; redistribution **not** permitted.
- **Role:** Cross-host harness that runs ORCA-style IIgs tools on a
modern OS. Provides the `iix` shell, the OMF linker, `profuse`
(ProDOS FUSE mount), `mkfs-profuse`, `dumpobj`, and the IIgs-side
filesystem image (`$GOLDEN_GATE`) that the harness emulates.
- **Distribution:** **Not redistributed by JoeyLib.** Each developer
must bring their own purchased copy. `toolchains/install.sh`
builds host tools from a developer-supplied source checkout under
`stuff/GoldenGate/` (the `stuff/` tree is gitignored) and stages
the IIgs filesystem from a developer-supplied `gg-linux.tgz`. The
resulting `toolchains/iigs/gg-tools/` and `toolchains/iigs/goldengate/`
trees are gitignored.
### ORCA/C compiler + ORCA libraries
- **Used on:** Apple IIgs
- **URL:** <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.
- **URL:** <https://github.com/llvm-mos/llvm-mos>
- **License:** Apache License 2.0 with LLVM Exceptions (the upstream
LLVM license).
- **Role:** clang C compiler targeting the 65816, plus llvm-mc for
assembling hand-written `.s` files and link816 for producing OMF
S16 executables. Driven by `toolchains/iigs/clang-build.sh`; the
toolchain root is located via the `LLVM816_ROOT` environment
variable.
- **Vendored at:** `toolchains/iigs/llvm-mos/`
### Merlin32

12
PERF.md
View file

@ -34,3 +34,15 @@
| jlAudioIsPlayingMod | 3536 | 9.28x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.38x | 1.26x | 58.95x |
| c2p full screen | - | - | - | - |
> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves
> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both
> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So
> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`
> cannot legitimately exceed ~29; a higher reading means the benchmark
> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-
> driven GetTick, so an op that holds `SEI` while it runs can over-
> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
> inflated and cross-check against the cycle model. (`jlFillRect 320x200`
> = 60 and `jlStagePresent full` = 42 in the table above predate the
> Phase 0 SEI removal and are inflated; expect ~25-28 after re-measure.)

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
* places everything from one TU into one code bank by default. This
* single-file example easily blows the 64 KB code-bank limit, so we
* sprinkle `segment "NAME";` directives at section boundaries to
* force the linker to place each region in its own code segment.
* The directive is ORCA-specific; gcc-targeted ports ignore it via
* the platform guard. */
/* This single-file example can blow the 65816's 64 KB code-bank
* limit, so on the IIgs we split code across segments at section
* boundaries to spread the load across multiple banks. */
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVART";
#endif
@ -1103,7 +1098,6 @@ static bool buildAllSprites(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVROOM";
#endif
@ -1603,7 +1597,6 @@ static bool findPath(RoomE room, int16_t sx, int16_t sy, int16_t tx, int16_t ty)
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVMOVE";
#endif
@ -1869,7 +1862,6 @@ static void drawInventoryBar(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVUI";
#endif
@ -1935,7 +1927,6 @@ static void composeFrame(int16_t cursorX, int16_t cursorY) {
#ifdef JOEYLIB_PLATFORM_IIGS
segment "ADVVERB";
#endif
@ -2213,7 +2204,6 @@ static bool buildBackgrounds(void) {
#ifdef JOEYLIB_PLATFORM_IIGS
segment "";
#endif
@ -2231,7 +2221,6 @@ int main(void) {
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
* need ~16 KB. */
config.codegenBytes = 256UL * 1024;
config.maxSurfaces = 6;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {

View file

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

View file

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

View file

@ -24,7 +24,6 @@
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIOBJ";
#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
segment "AGIPIC";
#endif
// ----- Internal helpers (alphabetical) -----
@ -122,7 +121,7 @@ static void drawCorner(AgiPicT *pic, const PicStateT *state, const uint8_t *data
// the minor axis is linearly interpolated with round-half-away-from-
// zero. All math stays in 16-bit -- AGI coordinates are 8-bit so the
// worst product (dy * i = 167 * 167 = 27889) fits in int16, and the
// 65816 build avoids ORCA-C's long-arithmetic helpers entirely.
// 65816 build avoids long-arithmetic helpers entirely.
static void drawLine(AgiPicT *pic, const PicStateT *state, int16_t x0, int16_t y0, int16_t x1, int16_t y1) {
int16_t dx;
int16_t dy;

View file

@ -48,9 +48,8 @@
// IIgs 64 KB code-bank rule: park this TU in its own load segment
// so it doesn't pile onto _ROOT (which still holds main() and the
// ORCA-C startup). Cross-platform builds ignore this pragma.
// runtime startup). Cross-platform builds ignore this pragma.
#ifdef JOEYLIB_PLATFORM_IIGS
segment "AGIRES";
#endif
@ -154,7 +153,7 @@ static bool loadDirectory(AgiGameT *game, AgiResTypeE type, const char *gameDir,
}
volNibble = (uint8_t)(buf[0] >> 4);
// 20-bit offset, cast to uint32_t before shift so ORCA-C's
// 20-bit offset, cast to uint32_t before shift so a
// 16-bit int doesn't truncate the (& 0x0F) << 16 term.
offset = ((uint32_t)(buf[0] & 0x0F) << 16) | ((uint32_t)buf[1] << 8) | (uint32_t)buf[2];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -20,7 +20,6 @@
#include "spacetaxi.h"
JOEYLIB_SEGMENT("STAXI")
#define ST_PASSENGER_W_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
// IIgs binary's _ROOT bank stays under 64 KB. No-op on other ports.
JOEYLIB_SEGMENT("STAXI")
// Tile bank files are per-level: each level's .dat references a
// numeric bank id, and the host loads tiles/tbankN.tbk on demand

View file

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

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 -----
static void runAllTests(void) {
@ -296,9 +485,8 @@ int main(void) {
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar
* compiled sprite emitter generates. UL on the multiply because
* ORCA-C's 16-bit int overflows on 32 * 1024. */
* a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024;
config.maxSurfaces = 4;
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {
@ -373,10 +561,11 @@ int main(void) {
jlLogF("UBER: audioInit failed (skipping audio)\n");
}
// Reset stage + run all per-frame timed tests.
jlSurfaceClear(gStage, 0);
jlFillRect(gStage, 0, 0, 320, 8, 1);
jlStagePresent();
// Visual showcase: render one of each primitive into its own grid
// cell so the run shows something legible (the timed benchmark below
// never presents, so this stays on screen throughout it). The first
// timed op is jlSurfaceClear, so this leaves the benchmark untouched.
drawShowcase();
runAllTests();
@ -391,6 +580,10 @@ int main(void) {
jlStagePresent();
jlLogF("UBER: press any key to exit\n");
// Flush the log to disk BEFORE the blocking key wait, so an automated
// (headless) run that kills the process at this prompt still captures
// the results -- joeyLog otherwise only flushes at the atexit fclose.
jlLogFlush();
jlWaitForAnyKey();
jlSpriteDestroy(gSprite);

View file

@ -24,6 +24,11 @@
#define JOEY_AUDIO_SFX_SLOTS 4
// Number of PSG-style polyphonic tone voices (jlAudioVoice selects
// voice 0/1/2). The core gate range-checks against this so each
// per-port HAL can assume voice < JOEY_AUDIO_VOICES.
#define JOEY_AUDIO_VOICES 3
// Initialize audio. Returns true if the platform has a working audio
// engine and was able to start it. Returns false silently otherwise;
// the rest of the API stays callable but produces no sound.

View file

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

View file

@ -20,7 +20,7 @@
#define JOEYLIB_PLATFORM_AMIGA
#elif defined(__atarist__) || defined(__MINT__) || defined(__TOS__)
#define JOEYLIB_PLATFORM_ATARIST
#elif defined(__ORCAC__) || defined(__APPLE2GS__) || defined(__GNO__)
#elif defined(__APPLE2GS__) || defined(__GNO__)
#define JOEYLIB_PLATFORM_IIGS
#else
#error "JoeyLib: unknown platform; define JOEYLIB_PLATFORM_<TARGET> explicitly via -D"
@ -63,20 +63,6 @@
#define JOEYLIB_PLATFORM_NAME "MS-DOS"
#endif
// ----- ORCA-C named load segments -----
//
// On the IIgs the ORCA Linker fits each load segment in its own bank,
// so spilling cross-platform .c files into named segments is the way
// to keep monolithic IIgs binaries under the 64 KB-per-bank _ROOT
// limit. The `segment "name";` statement is ORCA-C-specific syntax
// (see ORCA/C ch. 30); other ports' compilers don't recognize it, so
// the macro evaluates to nothing on Amiga/ST/DOS.
#ifdef JOEYLIB_PLATFORM_IIGS
#define JOEYLIB_SEGMENT(name) segment name;
#else
#define JOEYLIB_SEGMENT(name)
#endif
// ----- Library version -----
#define JOEYLIB_VERSION_MAJOR 1

View file

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

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
// (x,y) would write to. backup->bytes must have at least
// (widthTiles*4) * (heightTiles*8) bytes of capacity for fully
// in-bounds draws; for clipped draws only the visible bytes are
// stored. The captured region's exact size is reported in
// backup->sizeBytes.
// in-bounds draws on chunky ports (DOS, IIgs); for clipped draws only
// the visible bytes are stored. The captured region's exact size is
// reported in backup->sizeBytes.
//
// Planar ports (Amiga, Atari ST) round x DOWN to an 8-pixel plane
// boundary and width UP, so a sprite drawn at an odd-of-8 x captures
// one extra plane-byte column per row. Size backup->bytes for the
// planar worst case ((widthTiles*4) + 1) * (heightTiles*8) bytes so an
// odd-x save cannot overrun the buffer.
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// Repaint the destination region from a jlSpriteBackupT captured by a
@ -126,7 +132,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup);
// captures the destination bytes about to be overwritten, then draws
// the sprite. Both ops share validation, the destination ptr is
// computed once, and a single dirty-rect mark covers both. Saves
// roughly one full dispatcher chain (~150 cyc on IIgs ORCA-C) per
// roughly one full dispatcher chain (~150 cyc on IIgs) per
// frame versus calling jlSpriteSaveUnder + jlSpriteDraw separately.
//
// Identical semantics to:

View file

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

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/iix-build.sh wrapper handles ORCA's case-sensitivity
# quirks (lowercase .a/.root/.sym vs linker demands for uppercase),
# multi-source compile/link, and pragma injection for include paths
# and the stdint/stdbool shim headers.
# toolchains/iigs/clang-build.sh drives compile (clang) / assemble
# (llvm-mc) / link (link816) against the prebuilt w65816 runtime.
# Hand-rolled asm lives in src/port/iigs/*.s (llvm-mc GAS syntax);
# bank placement is handled by -ffunction-sections + link816
# dead-stripping.
#
# STATUS: the launchable-binary pipeline (multi-segment OMF emission +
# disk packaging + GS/OS launch) is still being brought up. `iigs-lib`
# compile-checks every library + example translation unit, and
# `iigs-clang-smoke` validates the full clang -> llvm-mc -> link816 ->
# MAME path end to end. Per-example binary targets build via
# clang-build.sh; large examples need the multi-segment OMF wiring still
# in progress.
include $(dir $(lastword $(MAKEFILE_LIST)))/common.mk
PLATFORM := iigs
BUILD := $(REPO_DIR)/build/$(PLATFORM)
BINDIR := $(BUILD)/bin
OBJDIR := $(BUILD)/obj
PORT_C_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.c)
# Hand-rolled .asm sources go through ORCA's macro assembler via
# iix-build.sh's `assemble` dispatch. Each .asm declares its target
# load segment in the START operand (e.g. peislam.asm -> PEISLAMS)
# so the linker places its bytes in a separate bank from _ROOT.
# See ORCA/M for IIgs ch. 6 "Load Segments" for the mechanism.
PORT_ASM_SRCS_ALL := $(wildcard $(SRC_PORT)/iigs/*.asm)
CLANG := $(LLVM816_ROOT)/tools/llvm-mos-build/bin/clang
RT_INC := $(LLVM816_ROOT)/runtime/include
CFLAGS := --target=w65816 -O2 -ffreestanding -ffunction-sections
INCLUDES := -I$(RT_INC) -I$(INCLUDE_DIR) -I$(INCLUDE_DIR)/joey -I$(SRC_CORE) -I$(REPO_DIR)/src/codegen
# audio_full.c declares its functions in the AUDIOIMPL load segment
# (`segment "AUDIOIMPL"` at file scope, see ORCA/C ch. 30) so the
# implementation code lives in its own bank, not _ROOT. That lets
# the same source link into every binary, replacing the earlier
# audio.c-stub vs audio_full.c-real split. The 34 KB NTP replayer
# bytes still ride along via the xxd-baked header.
PORT_C_SRCS := $(PORT_C_SRCS_ALL)
PORT_C_SRCS := $(wildcard $(SRC_PORT)/iigs/*.c)
# Hand-rolled asm, migrated to llvm-mc GAS .s.
PORT_ASM_SRCS := $(wildcard $(SRC_PORT)/iigs/*.s)
# IIgs uses NTPstreamsound for SFX, not the libxmp+overlay combo that
# DOS and ST share, so src/core/audioSfxMix.c is unused here. Filter
# it out so the ORCA Linker doesn't pull its dead code into every
# IIgs binary (the monolithic-link budget is tight).
# DOS and ST share, so src/core/audioSfxMix.c is unused here.
CORE_C_SRCS_IIGS := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: 65816 emitter + cross-platform compile dispatch.
CODEGEN_SRCS := $(REPO_DIR)/src/codegen/spriteEmitIigs.c \
$(REPO_DIR)/src/codegen/spriteCompile.c
# NinjaTrackerPlus replayer. Assembled with Merlin32 from the staged
# source at toolchains/iigs/ntp/ninjatrackerplus.s. Output is a 34 KB
# raw 65816 binary that the IIgs audio HAL loads at runtime via
# Memory Manager into a fixed-bank handle. NTP source is bank-internal
# (no JSL/JML cross-bank jumps) so the player works at any bank-start
# load address even though it was assembled with `org $0F0000`.
NTP_SRC := $(REPO_DIR)/toolchains/iigs/ntp/ninjatrackerplus.s
NTP_BIN := $(BUILD)/audio/ntpplayer.bin
NTP_ASM := $(BUILD)/audio/ntpdata.asm
IIGS_MERLIN := $(REPO_DIR)/toolchains/iigs/merlin32/bin/merlin32
LIB_C_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(CODEGEN_SRCS)
LIB_SRCS := $(LIB_C_SRCS) $(PORT_ASM_SRCS)
LIB_SRCS := $(CORE_C_SRCS_IIGS) $(PORT_C_SRCS) $(PORT_ASM_SRCS_ALL) $(NTP_ASM) $(CODEGEN_SRCS)
# HELLO is omitted from the disk because UBER exercises everything it
# does and the disk was tight. PATTERN is included as the SCB / palette
# golden-reference for cross-port debugging.
# Example sources (single-TU unless noted).
PATTERN_SRC := $(EXAMPLES)/pattern/pattern.c
PATTERN_BIN := $(BINDIR)/PATTERN
DRAW_SRC := $(EXAMPLES)/draw/draw.c
DRAW_BIN := $(BINDIR)/DRAW
KEYS_SRC := $(EXAMPLES)/keys/keys.c
KEYS_BIN := $(BINDIR)/KEYS
JOY_SRC := $(EXAMPLES)/joy/joy.c
JOY_BIN := $(BINDIR)/JOY
SPRITE_SRC := $(EXAMPLES)/sprite/sprite.c
SPRITE_BIN := $(BINDIR)/SPRITE
UBER_SRC := $(EXAMPLES)/uber/uber.c
UBER_BIN := $(BINDIR)/UBER
ADV_SRC := $(EXAMPLES)/adventure/adventure.c
ADV_BIN := $(BINDIR)/ADV
ADV2_SRC := $(EXAMPLES)/adventure2/adventure2.c
ADV2_BIN := $(BINDIR)/ADV2
AGI_SRCS := $(EXAMPLES)/agi/agi.c $(EXAMPLES)/agi/agiRes.c $(EXAMPLES)/agi/agiPic.c $(EXAMPLES)/agi/agiView.c $(EXAMPLES)/agi/agiVm.c $(EXAMPLES)/agi/agiObj.c $(EXAMPLES)/agi/agiText.c
AGI_BIN := $(BINDIR)/AGI
STAXI_SRCS := $(EXAMPLES)/spacetaxi/spacetaxi.c $(EXAMPLES)/spacetaxi/stLevel.c $(EXAMPLES)/spacetaxi/stRender.c $(EXAMPLES)/spacetaxi/stEngine.c $(EXAMPLES)/spacetaxi/stPassenger.c $(EXAMPLES)/spacetaxi/stHud.c $(EXAMPLES)/spacetaxi/stAudio.c
# IIgs convention follows the others: app dir under bin/ with a
# DATA/ subdir for runtime assets. The disk packager will need to
# carry both into the 2mg image (TODO).
STAXI_INSTALL_DIR := $(BINDIR)/STAXI
STAXI_BIN := $(STAXI_INSTALL_DIR)/STAXI
AUDIO_SRC := $(EXAMPLES)/audio/audio.c
AUDIO_BIN := $(BINDIR)/AUDIO
AUDIO_MOD := $(REPO_DIR)/assets/test.mod
AUDIO_SFX := $(REPO_DIR)/assets/test.sfx
AUDIO_NTP := $(BUILD)/audio/test.ntp
AUDIO_HEADER := $(BUILD)/audio/test_assets.h
JOEYMOD := $(REPO_DIR)/tools/joeymod/joeymod
DISK_IMG := $(BINDIR)/joey.2mg
IIGS_PACKAGE := $(REPO_DIR)/toolchains/iigs/package-disk.sh
.PHONY: all iigs iigs-lib iigs-clang-smoke clean-iigs
IIX_INCLUDES := \
-I $(IIGS_INCLUDE_SHIM) \
-I $(INCLUDE_DIR) \
-I $(INCLUDE_DIR)/joey \
-I $(SRC_CORE) \
-I $(REPO_DIR)/src/codegen
# Default: compile-check the library + run the end-to-end smoke test.
all iigs: iigs-lib iigs-clang-smoke
.PHONY: all iigs iigs-disk clean-iigs
# Building the disk implicitly builds every binary it depends on, so
# `make iigs` ends with a fresh joey.2mg on every change. Without this,
# stale disk images would silently mask binary updates -- a surprise
# when the run script always boots from joey.2mg.
all iigs: $(DISK_IMG)
# Compile-check every library and example translation unit with clang,
# and assemble every .s with llvm-mc. This is the canonical "does the
# IIgs source still build" gate while the launchable pipeline is wired.
iigs-lib:
@mkdir -p $(OBJDIR)
@set -e; \
for c in $(LIB_C_SRCS); do \
echo " CC $$c"; \
$(CLANG) $(CFLAGS) $(INCLUDES) -c $$c -o $(OBJDIR)/$$(echo $$c | sed 's#[/.]#_#g').o; \
done; \
for s in $(PORT_ASM_SRCS); do \
echo " AS $$s"; \
$(LLVM816_ROOT)/tools/llvm-mos-build/bin/llvm-mc -arch=w65816 -filetype=obj $$s -o $(OBJDIR)/$$(basename $$s .s).o; \
done
$(NTP_BIN): $(NTP_SRC) $(IIGS_MERLIN)
@mkdir -p $(dir $@)
@cp $(NTP_SRC) $(BUILD)/audio/ninjatrackerplus.s
cd $(BUILD)/audio && $(IIGS_MERLIN) . ninjatrackerplus.s
mv $(BUILD)/audio/ntpplayer $@
# End-to-end smoke test: clang -> llvm-mc -> link816 -> MAME, validating
# the ported asm computes correctly on emulated hardware.
iigs-clang-smoke:
$(REPO_DIR)/toolchains/iigs/clang-smoke.sh
# Bake the NTP replayer bytes into an ORCA-M asm file. The asm declares
# the bytes in a `data NTPDATA` segment; ORCA's linker groups same-
# name object segments into one load segment, and the GS/OS loader
# places it in its own bank. Net effect: the 34 KB of NTP bytes don't
# crowd _ROOT in any binary, so audio_full.c can link into every demo
# (vs the old audio.c-stub split). audio_full.c references
# gNtpPlayerBytes / gNtpPlayerBytes_len as externs (case-sensitive
# symbol match against the asm labels).
$(NTP_ASM): $(NTP_BIN) $(REPO_DIR)/toolchains/iigs/bin-to-asm.sh
@mkdir -p $(dir $@)
$(REPO_DIR)/toolchains/iigs/bin-to-asm.sh $(NTP_BIN) $@ NTPDATA gNtpPlayerBytes gNtpPlayerBytes_len
# iix-build.sh takes MAIN.c first, then EXTRA sources (compiled with
# #pragma noroot). The example source supplies main(); libjoey sources
# are the extras. The chtyp post-step tags the output as GS/OS S16
# ($B3) so GS/OS recognizes it as launchable; the file-type lives in
# a user.com.apple.FinderInfo xattr that iix and profuse preserve.
#
# All binaries use ORCA-C large memory model (-b). Cost: slightly
# larger / slower compiled C per the ORCA docs. Win: 32-bit pointers
# everywhere, so library asm can take SurfaceT* args via one
# consistent ABI (small-mm 16-bit pointers truncated bank bytes,
# which broke any asm that wanted to address bank-1 stage memory).
# Per-binary header dependency files. iix-build.sh -M emits one .d
# alongside each binary covering every header transitively included
# by the C sources in that binary's build. Pulled in via -include at
# the bottom of this file so editing a shared header (e.g.
# surfaceInternal.h) triggers a rebuild of every IIgs binary that
# transitively depends on it.
# Per-example binaries via clang-build.sh. clang-build.sh mirrors the
# old driver's CLI (-I / -o / -M; -s and -b are accepted no-ops). These
# link single-segment today; examples larger than one bank need the
# multi-segment OMF wiring still in progress.
DEP_DIR := $(BUILD)/dep
PATTERN_DEP := $(DEP_DIR)/PATTERN.d
DRAW_DEP := $(DEP_DIR)/DRAW.d
KEYS_DEP := $(DEP_DIR)/KEYS.d
JOY_DEP := $(DEP_DIR)/JOY.d
SPRITE_DEP := $(DEP_DIR)/SPRITE.d
UBER_DEP := $(DEP_DIR)/UBER.d
AUDIO_DEP := $(DEP_DIR)/AUDIO.d
ADV_DEP := $(DEP_DIR)/ADV.d
ADV2_DEP := $(DEP_DIR)/ADV2.d
AGI_DEP := $(DEP_DIR)/AGI.d
STAXI_DEP := $(DEP_DIR)/STAXI.d
$(PATTERN_BIN): $(PATTERN_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/PATTERN: $(PATTERN_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(PATTERN_DEP) $(IIX_INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/PATTERN.d $(INCLUDES) -o $@ $(PATTERN_SRC) $(LIB_SRCS)
$(DRAW_BIN): $(DRAW_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/DRAW: $(DRAW_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(DRAW_DEP) $(IIX_INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/DRAW.d $(INCLUDES) -o $@ $(DRAW_SRC) $(LIB_SRCS)
$(KEYS_BIN): $(KEYS_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/KEYS: $(KEYS_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(KEYS_DEP) $(IIX_INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/KEYS.d $(INCLUDES) -o $@ $(KEYS_SRC) $(LIB_SRCS)
$(JOY_BIN): $(JOY_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/JOY: $(JOY_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(JOY_DEP) $(IIX_INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/JOY.d $(INCLUDES) -o $@ $(JOY_SRC) $(LIB_SRCS)
$(SPRITE_BIN): $(SPRITE_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/SPRITE: $(SPRITE_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(SPRITE_DEP) $(IIX_INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/SPRITE.d $(INCLUDES) -o $@ $(SPRITE_SRC) $(LIB_SRCS)
# UBER bumps user stack to 16 KB. ORCA-C's default user stack is small
# (~1 KB) and vfprintf's parsing buffer + the demo's own stack-local
# format buffers were spilling past it -- the symptom was a crash to
# monitor on the second varargs-style joeyLogF call. The hand-rolled
# decimal formatter in uber.c also uses larger stack-local buffers
# (line[96], num[16]) than typical demos. 16 KB is plenty of headroom.
$(UBER_BIN): $(UBER_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/UBER: $(UBER_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(UBER_DEP) $(IIX_INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/UBER.d $(INCLUDES) -o $@ $(UBER_SRC) $(LIB_SRCS)
# Adventure has a lot of static const sprite-row arrays and walk-mask
# strings; the auto-segmenter sees a big main TU, so the 16 KB user
# stack matches UBER's bump for the same reason (vfprintf + format
# buffers stay within bounds).
$(ADV_BIN): $(ADV_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/ADV: $(ADV_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(ADV_DEP) $(IIX_INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV.d $(INCLUDES) -o $@ $(ADV_SRC) $(LIB_SRCS)
$(ADV2_BIN): $(ADV2_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/ADV2: $(ADV2_SRC) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(ADV2_DEP) $(IIX_INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/ADV2.d $(INCLUDES) -o $@ $(ADV2_SRC) $(LIB_SRCS)
# AGI binary spans multiple .c files (agi.c is main; agiRes.c is the
# resource loader). iix-build.sh takes the first .c as main and the
# rest as extras (compiled #pragma noroot); ordering matters.
$(AGI_BIN): $(AGI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/AGI: $(AGI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(AGI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/AGI.d $(INCLUDES) -I$(EXAMPLES)/agi -o $@ $(AGI_SRCS) $(LIB_SRCS)
# Space Taxi: multi-source like AGI; 16 KB stack for headroom on the
# placeholder render path (full tilemap repaint + snprintf buffers).
# Note: asset files (level *.dat, *.jas) are NOT yet packaged onto
# the disk image -- IIGS_PACKAGE only handles binaries today. The
# game will fail to find its assets when run; wiring data files into
# the packager is the next step for this port on IIgs (similar to
# the open AGI/KQ3 disk-packaging task).
$(STAXI_BIN): $(STAXI_SRCS) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
$(BINDIR)/STAXI: $(STAXI_SRCS) $(LIB_SRCS) $(IIGS_CLANG_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -s 16384 -M $(STAXI_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
$(IIGS_CLANG_BUILD) -M $(DEP_DIR)/STAXI.d $(INCLUDES) -I$(EXAMPLES)/spacetaxi -o $@ $(STAXI_SRCS) $(LIB_SRCS)
# Convert the cross-platform .MOD asset to NinjaTrackerPlus runtime
# format via joeymod (which shells out to ntpconverter.php). Without
# php-cli the conversion is skipped; in that case the IIgs disk just
# won't carry a TEST.NTP and the audio demo will report "missing data
# file" at runtime instead of crashing on bad MOD magic.
HAVE_PHP := $(shell command -v php >/dev/null 2>&1 && echo 1)
ifeq ($(HAVE_PHP),1)
$(AUDIO_NTP): $(AUDIO_MOD) $(JOEYMOD)
@mkdir -p $(dir $@)
$(JOEYMOD) $< $@
AUDIO_DATA_FILES := $(AUDIO_NTP) $(AUDIO_SFX)
else
$(info iigs: php-cli not installed -- AUDIO demo will ship without TEST.NTP; install php-cli for real IIgs audio playback)
AUDIO_DATA_FILES := $(AUDIO_SFX)
endif
$(AUDIO_BIN): $(AUDIO_SRC) $(LIB_SRCS) $(NTP_ASM) $(IIGS_BUILD)
@mkdir -p $(dir $@) $(DEP_DIR)
$(IIGS_BUILD) -b -M $(AUDIO_DEP) $(IIX_INCLUDES) -I $(EXAMPLES)/audio -o $@ $(AUDIO_SRC) $(LIB_SRCS)
$(IIGS_IIX) chtyp -t S16 $@
# Assemble a ProDOS 2img containing the examples, ready to mount in
# GSplus alongside a GS/OS boot volume.
iigs-disk: $(DISK_IMG)
$(DISK_IMG): $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) $(AUDIO_DATA_FILES) $(IIGS_PACKAGE)
@mkdir -p $(dir $@)
$(IIGS_PACKAGE) $@ $(PATTERN_BIN) $(DRAW_BIN) $(KEYS_BIN) $(JOY_BIN) $(SPRITE_BIN) $(AUDIO_BIN) $(UBER_BIN) $(ADV_BIN) $(ADV2_BIN) $(AGI_BIN) $(STAXI_BIN) -- $(AUDIO_DATA_FILES)
# NOTE: the audio example is omitted until the NinjaTrackerPlus replayer
# is wired into the clang build (a path for baking the replayer bytes
# into a data segment via llvm-mc is pending).
clean-iigs:
rm -rf $(BUILD)
# Pull in per-binary header-dependency files generated by iix-build.sh -M.
# Without this, editing a header (e.g. surfaceInternal.h) doesn't rebuild
# IIgs binaries that include it -- the IIgs's iix toolchain has no native
# -MMD analog, so iix-build.sh shells out to host gcc for the scan.
-include $(PATTERN_DEP) $(DRAW_DEP) $(KEYS_DEP) $(JOY_DEP) $(SPRITE_DEP) $(UBER_DEP) $(AUDIO_DEP) $(ADV_DEP) $(ADV2_DEP) $(AGI_DEP) $(STAXI_DEP)
-include $(wildcard $(DEP_DIR)/*.d)

View file

@ -13,7 +13,6 @@
# Outputs of a run land in /tmp/mame-iigs/:
# - debug.log MAME's debugger console output (-debuglog)
# - crash.txt Lua-hook crash dump (if anything halts the CPU)
# - joeylog.txt Extracted from the post-run disk image
set -euo pipefail
@ -232,7 +231,6 @@ On crash the MAME debugger halts and Lua dumps state to:
$out/crash.txt
After exit:
$out/joeylog.txt extracted from disk image
$out/debug.log MAME debugger console output
EOF
@ -248,23 +246,6 @@ cleanup() {
rm -rf "$out/snap"
mv "$work/snap" "$out/snap"
fi
local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
local mnt=$work/_mnt
if [[ -x $profuse && -f $work/joey.2mg ]]; then
export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
export ORCA_ROOT="$GOLDEN_GATE"
mkdir -p "$mnt"
if "$profuse" -oro "$work/joey.2mg" "$mnt" 2>/dev/null; then
for name in JOEYLOG.TXT joeylog.txt; do
if [[ -f $mnt/$name ]]; then
cp "$mnt/$name" "$out/joeylog.txt"
echo "extracted joeylog.txt -> $out/joeylog.txt" >&2
break
fi
done
fusermount -u "$mnt" 2>/dev/null || true
fi
fi
rm -rf "$work"
}
trap cleanup EXIT

View file

@ -7,9 +7,7 @@
#
# No argument: GSplus has no way to dispatch a specific binary on boot
# (Finder is interactive), so this script just stages the disk and
# launches the emulator. The post-run trap below extracts joeylog.txt
# from the data disk so demos that left a breadcrumb file are visible
# from the host shell after the emulator exits.
# launches the emulator.
set -euo pipefail
@ -21,12 +19,6 @@ fi
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
# profuse looks up its FST helpers under $GOLDEN_GATE / $ORCA_ROOT and
# falls back to /usr/share/orca which we don't install. Point both at
# our staged GoldenGate tree.
export GOLDEN_GATE="$repo/toolchains/iigs/goldengate"
export ORCA_ROOT="$GOLDEN_GATE"
gsplus=$repo/toolchains/emulators/gsplus/bin/gsplus
rom=$repo/toolchains/emulators/support/apple-iigs.rom
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
@ -49,46 +41,15 @@ done
# copies so repeated runs do not mutate the originals.
work=$(mktemp -d -t joeylib-iigs.XXXXXX)
# After GSplus exits, mount the work copy of joey.2mg via profuse and
# pull joeylog.txt out (if the demo wrote one) before tearing down the
# scratch dir. This is a debug aid: the demos use joeyLog* to leave a
# breadcrumb file on the JOEYLIB volume, but the volume only lives
# inside the scratch dir while GSplus is running.
log_out=$repo/build/iigs/bin/joeylog.txt
extract_log() {
local profuse=$repo/toolchains/iigs/gg-tools/bin/profuse
local mnt=$work/_log_mnt
cleanup() {
# Stash the post-run disk image for manual inspection.
if [[ -f $work/joey.2mg ]]; then
cp "$work/joey.2mg" "$repo/build/iigs/bin/joey-after-run.2mg"
echo "run-iigs: saved post-run disk -> $repo/build/iigs/bin/joey-after-run.2mg" >&2
fi
if [[ ! -x $profuse ]]; then
echo "run-iigs: profuse not found at $profuse" >&2
elif [[ ! -f $work/joey.2mg ]]; then
echo "run-iigs: $work/joey.2mg missing" >&2
else
mkdir -p "$mnt"
if "$profuse" -oro "$work/joey.2mg" "$mnt"; then
echo "run-iigs: mounted $mnt; contents:" >&2
ls -la "$mnt" >&2 || true
if [[ -f $mnt/JOEYLOG.TXT ]]; then
cp "$mnt/JOEYLOG.TXT" "$log_out"
echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
elif [[ -f $mnt/joeylog.txt ]]; then
cp "$mnt/joeylog.txt" "$log_out"
echo "run-iigs: extracted joeylog.txt -> $log_out" >&2
else
echo "run-iigs: no JOEYLOG.TXT on disk" >&2
fi
fusermount -u "$mnt" 2>/dev/null || true
else
echo "run-iigs: profuse mount FAILED" >&2
fi
fi
rm -rf "$work"
}
trap extract_log EXIT
trap cleanup EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"

View file

@ -20,26 +20,35 @@
#include "surfaceInternal.h"
// Largest scratch buffer needed for any single emit call. 16 KB
// covers a 32x32 sprite even on 68k (the biggest mixed-RMW byte-
// emit at 16 bytes/byte * (16*17 dest bytes per shift) ~= 4.5 KB,
// times shift count 2). Round up generously.
#define SPRITE_EMIT_SCRATCH_BYTES (16u * 1024u)
// Scratch buffer for a single emit call. 16 KB comfortably covers any
// sprite up to a 32x32 worst-case all-mixed DRAW (16 rows*17 dest
// bytes/shift * SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE ~= 4.5 KB on 68k,
// the largest per-destbyte target). It is NOT trusted to bound every
// legal input: a sprite near the create-time dimension cap
// (widthTiles <= 40, heightTiles <= 25) can emit far more than 16 KB,
// so every emitter ALSO bound-checks its write cursor against the
// `cap` argument and returns SPRITE_EMIT_OVERFLOW when a routine would
// not fit. jlSpriteCompile then bails cleanly (no arena alloc,
// sp->slot stays NULL, the dispatcher falls back to the interpreted
// halSpriteXxxPlanes path) instead of corrupting the heap. The
// constant is a long literal so it never wraps the 16-bit int the
// IIgs uses for plain int arithmetic.
#define SPRITE_EMIT_SCRATCH_BYTES (16384ul)
// Compile-time selection of the per-CPU emitter. One src/codegen/
// spriteEmit*.c file is built per platform, but the dispatch lives
// in this file so jlSpriteCompile + spriteCompiledDraw aren't
// duplicated three times.
static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitDrawX86(out, sp, shift);
return spriteEmitDrawX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitDrawPlanar68k(out, sp, shift);
return spriteEmitDrawPlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitDrawInterleaved68k(out, sp, shift);
return spriteEmitDrawInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitDrawIigs(out, sp, shift);
return spriteEmitDrawIigs(out, cap, sp, shift);
#else
# error "jlSpriteCompile: no emitter selected for this platform"
#endif
@ -51,33 +60,33 @@ static uint16_t emitDrawForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shi
// src/core/sprite.c gates on routineOffsets[shift][SPRITE_OP_SAVE]
// != SPRITE_NOT_COMPILED and falls back to the interpreted memcpy
// path otherwise.
static uint16_t emitSaveForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
static uint32_t emitSaveForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitSaveX86(out, sp, shift);
return spriteEmitSaveX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitSavePlanar68k(out, sp, shift);
return spriteEmitSavePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitSaveInterleaved68k(out, sp, shift);
return spriteEmitSaveInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitSaveIigs(out, sp, shift);
return spriteEmitSaveIigs(out, cap, sp, shift);
#else
(void)out; (void)sp; (void)shift;
(void)out; (void)cap; (void)sp; (void)shift;
return 0;
#endif
}
static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitRestoreX86(out, sp, shift);
return spriteEmitRestoreX86(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_AMIGA)
return spriteEmitRestorePlanar68k(out, sp, shift);
return spriteEmitRestorePlanar68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_ATARIST)
return spriteEmitRestoreInterleaved68k(out, sp, shift);
return spriteEmitRestoreInterleaved68k(out, cap, sp, shift);
#elif defined(JOEYLIB_PLATFORM_IIGS)
return spriteEmitRestoreIigs(out, sp, shift);
return spriteEmitRestoreIigs(out, cap, sp, shift);
#else
(void)out; (void)sp; (void)shift;
(void)out; (void)cap; (void)sp; (void)shift;
return 0;
#endif
}
@ -90,13 +99,24 @@ static uint16_t emitRestoreForTarget(uint8_t *out, const jlSpriteT *sp, uint8_t
// land in the arena.
static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
uint32_t total;
uint32_t written;
uint8_t shift;
uint8_t op;
total = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
total += emitDrawForTarget(scratch, sp, shift);
total += emitSaveForTarget(scratch, sp, shift);
total += emitRestoreForTarget(scratch, sp, shift);
for (op = 0; op < SPRITE_OP_COUNT; op++) {
switch (op) {
case SPRITE_OP_DRAW: written = emitDrawForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
case SPRITE_OP_SAVE: written = emitSaveForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
default: written = 0; break;
}
if (written == SPRITE_EMIT_OVERFLOW) {
return SPRITE_EMIT_OVERFLOW;
}
total += written;
}
}
return total;
}
@ -109,8 +129,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
uint8_t op;
ArenaSlotT *slot;
uint8_t *dst;
uint16_t written;
uint16_t offset;
uint32_t written;
uint32_t offset;
if (sp == NULL) {
return false;
@ -135,6 +155,8 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
totalSize = emitTotalSize(scratch, sp);
/* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single
* routine that did not fit the scratch is rejected here as well. */
if (totalSize > 0xFFFFu) {
free(scratch);
return false;
@ -159,17 +181,32 @@ bool jlSpriteCompile(jlSpriteT *sp) {
offset = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
uint32_t capLeft;
capLeft = (uint32_t)(totalSize - offset);
switch (op) {
case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, sp, shift); break;
case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, sp, shift); break;
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, sp, shift); break;
default: written = 0; break;
case SPRITE_OP_DRAW: written = emitDrawForTarget (dst + offset, capLeft, sp, shift); break;
case SPRITE_OP_SAVE: written = emitSaveForTarget (dst + offset, capLeft, sp, shift); break;
case SPRITE_OP_RESTORE: written = emitRestoreForTarget(dst + offset, capLeft, sp, shift); break;
default: written = 0; break;
}
if (written == 0) {
if (written == SPRITE_EMIT_OVERFLOW) {
/* The second pass disagreed with the sizing pass (must
* not happen, but never write past the slot). Roll back. */
codegenArenaFree(slot);
free(scratch);
return false;
}
/* routineOffsets is uint16_t with 0xFFFF (SPRITE_NOT_COMPILED)
* as the "absent" sentinel, so a real offset must stay below
* it. totalSize <= 0xFFFF already guarantees this, but guard
* explicitly so a future change can't store an offset that
* collides with the sentinel. */
if (written == 0 || offset >= SPRITE_NOT_COMPILED) {
sp->routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
} else {
sp->routineOffsets[shift][op] = offset;
offset = (uint16_t)(offset + written);
sp->routineOffsets[shift][op] = (uint16_t)offset;
offset = offset + written;
}
}
}
@ -189,16 +226,37 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
#if defined(JOEYLIB_PLATFORM_IIGS) || (!defined(JOEYLIB_PLATFORM_AMIGA) && !defined(JOEYLIB_PLATFORM_ATARIST))
// Re-derive the shift a chunky SAVE used from its backup. The shift is
// a property of the save (x & 1) and is NOT stored in the backup, so
// every chunky restore path must re-infer it. Single source of truth
// for the two chunky restore dispatchers (IIgs + x86/68k) so the
// formula cannot drift between them: copyBytes == spriteBytesPerRow
// means shift 0 (even x), otherwise the +1-byte shift-1 routine.
#define SPRITE_CHUNKY_BYTES_PER_TILE_ROW 4u
static uint8_t spriteChunkyRestoreShift(const jlSpriteT *sp, const jlSpriteBackupT *backup) {
uint16_t copyBytes;
uint16_t spriteBytesPerRow;
copyBytes = (uint16_t)(backup->width >> 1);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * SPRITE_CHUNKY_BYTES_PER_TILE_ROW);
return (uint8_t)((copyBytes == spriteBytesPerRow) ? 0u : 1u);
}
#endif
#if defined(JOEYLIB_PLATFORM_IIGS)
// SURFACE_ROW_OFFSET dispatches to the gRowOffsetLut lookup on IIgs;
// declared in surfaceInternal.h. Replaces ORCA-C's __mul16 JSL with a
// single indexed long-mode read.
// declared in surfaceInternal.h. Uses a single indexed long-mode read
// for the multiply.
// IIgs uses inline asm + a self-modifying call stub instead of a C
// function-pointer cast. The build uses ORCA-C large memory model
// (-b for sprite demos) so pointers are 24-bit and JSL works
// cross-bank.
// function-pointer cast. The build uses a large memory model so
// pointers are 24-bit and JSL works cross-bank.
//
// `sta abs,Y` on 65816 uses the data bank register (DBR) for the
// high byte of the effective address, so we need DBR = dst's bank
@ -235,23 +293,26 @@ static uint32_t gDrawStubLastFnAddr = 0xFFFFFFFFul;
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
uint8_t shift;
uint32_t destAddr;
uint16_t destOffset;
uint8_t destBank;
uint32_t fnAddr;
{
uint8_t *destPtr;
uint8_t *destPtr;
uint32_t destAddr;
const uint8_t *destB;
shift = (uint8_t)(x & 1);
destPtr = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
/* Byte-alias the 24-bit pointer to grab lo16 + bank without the
* ~LSHR4 32-bit-shift helper, matching SPLIT_POINTER below. */
destAddr = (uint32_t)destPtr;
destOffset = (uint16_t)(destAddr & 0xFFFFu);
destBank = (uint8_t)((destAddr >> 16) & 0xFFu);
destB = (const uint8_t *)&destAddr;
destOffset = (uint16_t)(destB[0] | ((uint16_t)destB[1] << 8));
destBank = destB[2];
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
}
(void)destAddr;
if (!gDrawStubInited) {
gSpriteCallStub[ 0] = 0x8B;
@ -284,8 +345,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
gDrawStubLastFnAddr = fnAddr;
}
// ORCA-C compiles this function under `longa on / longi on`
// (M=16, X=16) and emits the function epilogue assuming those
// This function is compiled under `longa on / longi on`
// (M=16, X=16) and the function epilogue assumes those
// widths at exit -- the deallocation ADC takes a 2-byte immediate
// and any LDX/LDY use 2-byte immediates. The byte writes to
// gSpriteCallStub above leave M=8, and an earlier PHP/PLP-only
@ -294,12 +355,13 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
// swallowed the TCS, S was never adjusted, RTL popped wrong
// bytes, control fell into BSS, and the IIgs hit BRK on a zero
// byte. Force M=16/X=16 before returning to compiled C.
asm {
rep #0x30
sep #0x20
jsl gSpriteCallStub
rep #0x30
}
__asm__ volatile (
"rep #0x30\n\t"
"sep #0x20\n\t"
"jsl gSpriteCallStub\n\t"
"rep #0x30"
::: "memory"
);
}
@ -360,12 +422,10 @@ static void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank,
// Split a 24-bit pointer into its low 16 bits + bank byte. The
// (uint32_t) cast works correctly in ORCA/C 2.2.1 (the 2.1.0 lossy-
// bank-byte bug is fixed). To avoid invoking the ~LSHR4 32-bit-shift
// helper for the `>> 16` to extract the bank byte, we cast to
// uint32_t and then byte-alias the storage -- gets the same bytes
// with simple loads.
// Split a 24-bit pointer into its low 16 bits + bank byte. To avoid
// invoking a 32-bit-shift helper for the `>> 16` to extract the bank
// byte, we cast to uint32_t and then byte-alias the storage -- gets
// the same bytes with simple loads.
#define SPLIT_POINTER(_ptr, _outLo, _outBank) \
do { \
uint32_t spAddr_ = (uint32_t)(_ptr); \
@ -418,7 +478,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
screenPtr = (uint8_t *)&src->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)clippedX >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
@ -442,9 +502,9 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
}
/* Compute the 1D index into the cached* / routineOffsets 2D arrays
* once. ORCA-C 2.2.1 lowers `shift * SPRITE_OP_COUNT` (where
* SPRITE_OP_COUNT==3) to a ~MUL4 helper call; (shift<<1)+shift
* compiles to two ASLs and an ADC, no helper. */
* once. `shift * SPRITE_OP_COUNT` (where SPRITE_OP_COUNT==3) can
* lower to a multiply helper call; (shift<<1)+shift compiles to
* two ASLs and an ADC, no helper. */
cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
@ -495,20 +555,19 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
}
// MVN-based routine: needs M=16 / X=16; restore M=16 on exit
// matches ORCA-C `longa on` epilogue expectations.
asm {
rep #0x30
jsl gSpriteSaveStub
rep #0x30
}
// matches the `longa on` epilogue expectations.
__asm__ volatile (
"rep #0x30\n\t"
"jsl gSpriteSaveStub\n\t"
"rep #0x30"
::: "memory"
);
}
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
uint8_t shift;
uint16_t heightPx;
uint16_t copyBytes;
uint16_t spriteBytesPerRow;
uint16_t screenLo;
uint16_t backupLo;
uint8_t screenBank;
@ -524,9 +583,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
sp = backup->sprite;
heightPx = backup->height;
copyBytes = (uint16_t)(backup->width >> 1);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
shift = spriteChunkyRestoreShift(sp, backup);
screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
@ -580,11 +637,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
*cachedSrc = backupBank;
}
asm {
rep #0x30
jsl gSpriteRestoreStub
rep #0x30
}
__asm__ volatile (
"rep #0x30\n\t"
"jsl gSpriteRestoreStub\n\t"
"rep #0x30"
::: "memory"
);
}
#elif defined(JOEYLIB_PLATFORM_AMIGA)
@ -602,7 +660,10 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* interpreter, which handles arbitrary x via halSpriteDrawPlanes /
* halSpriteSavePlanes / halSpriteRestorePlanes. */
#define AMIGA_BYTES_PER_ROW_LOCAL 40
// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40.
// Derived from surface geometry so it cannot drift if the fixed
// surface width ever changes.
#define AMIGA_PLANE_BYTES_PER_ROW (SURFACE_WIDTH / 8)
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
@ -615,7 +676,7 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
DrawFn fn;
shift = (uint8_t)(x & 7);
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)x >> 3));
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1);
p2 = halSurfacePlanePtr(dst, 2);
@ -646,7 +707,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
if (shift != 0u) {
widthPx = (uint16_t)(widthPx + 8u);
}
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)clippedX >> 3));
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)clippedX >> 3));
backup->sprite = sp;
backup->x = clippedX;
@ -683,7 +744,7 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* Shifted slots 1..7 all alias to the same restore bytes, so
* slot 1 stands in for any non-zero shift. */
shift = (uint8_t)(backup->width > (uint16_t)(sp->widthTiles * 8) ? 1u : 0u);
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_BYTES_PER_ROW_LOCAL + ((uint16_t)backup->x >> 3));
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
p0 = halSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = halSurfacePlanePtr(dst, 1);
@ -708,6 +769,14 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
* 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
* falls back to halSpriteDrawPlanes. */
// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
// fact (group-interleaved layout, not chunky), so it gets its own
// name. Each 16-pixel group is 4 plane words = 8 bytes.
#define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8)
#define ST_BYTES_PER_PLANE_GROUP 8u
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
typedef void (*DrawFn)(uint8_t *groupBase);
uint8_t shift;
@ -734,8 +803,8 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
return;
}
groupBase = base
+ (uint16_t)y * 160u
+ (uint16_t)((uint16_t)x >> 4) * 8u;
+ (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(groupBase);
}
@ -788,7 +857,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
copyBytes = (uint16_t)((widthPx >> 1) + (shift == 1 ? 1 : 0));
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
screenPtr = (uint8_t *)&src->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)clippedX >> 1)];
@ -812,15 +881,11 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
jlSpriteT *sp;
uint8_t shift;
uint16_t copyBytes;
uint16_t spriteBytesPerRow;
uint8_t *screenPtr;
CopyFn fn;
sp = backup->sprite;
copyBytes = (uint16_t)(backup->width >> 1);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * 4);
shift = (copyBytes == spriteBytesPerRow) ? 0 : 1;
shift = spriteChunkyRestoreShift(sp, backup);
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];

View file

@ -35,12 +35,25 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
#define MAX_ROUTINE_BYTES 16384
// Stack-arg displacements for the m68k cdecl calling convention used by
// the emitted routines (args pushed right-to-left, return PC at 0(sp)):
// draw(dst) -> dst at 4(sp)
// save/restore(src, dst) -> src at 4(sp), dst at 8(sp)
#define STACK_ARG0_DISP 0x0004u
#define STACK_ARG1_DISP 0x0008u
// Worst-case emitted bytes for one draw dest byte (mixed RMW: move.b
// d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0) = 4 instrs * 4 bytes).
#define DRAW_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
// Bytes for the per-row adda.w stride advance.
#define ROW_STRIDE_ADVANCE_BYTES 4u
// Bytes for the rts epilogue.
#define EPILOGUE_BYTES 2u
// ----- Prototypes -----
static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
static uint16_t writeBE16(uint8_t *out, uint16_t value);
@ -58,14 +71,20 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value);
//
// strideOnSrc=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE)
static uint16_t emitCopyBody68k(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
static uint32_t emitCopyBody68k(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
uint16_t row;
uint16_t col;
uint16_t advance;
uint32_t rowBytes;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) {
// One row: copyBytes * 2 (move.b) + the per-row adda stride.
rowBytes = (uint32_t)copyBytes * 2u + ROW_STRIDE_ADVANCE_BYTES;
if (cursor + rowBytes > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// Unrolled: move.b (a0)+, (a1)+ -- 0x12D8.
for (col = 0; col < copyBytes; col++) {
cursor += writeBE16(out + cursor, 0x12D8u);
@ -155,9 +174,10 @@ static uint16_t writeBE16(uint8_t *out, uint16_t value) {
}
// 68k draw emit. Returns bytes written.
uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
// 68k draw emit. Returns bytes written, or SPRITE_EMIT_OVERFLOW if the
// routine would exceed `cap`.
uint32_t spriteEmitDraw68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
@ -176,15 +196,21 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
// Prologue: movea.l 4(sp), a0
if (cursor + 4u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u);
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
for (row = 0; row < heightPx; row++) {
if (row > 0) {
// adda.w #SURFACE_BYTES_PER_ROW, a0
if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0xD0FCu);
cursor += writeBE16(out + cursor, (uint16_t)SURFACE_BYTES_PER_ROW);
}
@ -193,6 +219,11 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
if (opaqueMask == 0x00) {
continue;
}
// Reserve the worst case for one dest byte (16-byte mixed
// RMW) plus the rts epilogue.
if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
if (opaqueMask == 0xFFu) {
// move.b #imm, d16(a0)
// Opcode 0x117C: bits 11-9 = dst reg (0=a0), bits 8-6 =
@ -221,14 +252,17 @@ uint16_t spriteEmitDraw68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
// Epilogue: rts
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
// RESTORE: copy backup -> screen. Destination has the screen stride.
uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitRestore68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@ -238,24 +272,33 @@ uint16_t spriteEmitRestore68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
// Prologue: movea.l 4(sp), a0 (src); movea.l 8(sp), a1 (dst).
if (cursor + 8u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u);
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
cursor += writeBE16(out + cursor, 0x0008u);
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, false);
cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, false);
if (cursor == SPRITE_EMIT_OVERFLOW) {
return SPRITE_EMIT_OVERFLOW;
}
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
// SAVE: copy screen -> backup. Source has the screen stride.
uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitSave68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@ -265,15 +308,24 @@ uint16_t spriteEmitSave68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
if (cursor + 8u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u);
cursor += writeBE16(out + cursor, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
cursor += writeBE16(out + cursor, 0x0008u);
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
cursor = emitCopyBody68k(out, cursor, heightPx, copyBytes, true);
cursor = emitCopyBody68k(out, cap, cursor, heightPx, copyBytes, true);
if (cursor == SPRITE_EMIT_OVERFLOW) {
return SPRITE_EMIT_OVERFLOW;
}
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}

View file

@ -1,7 +1,7 @@
// IIgs (65816) sprite codegen. Emits PIC draw routines that write
// 4bpp packed surface bytes via abs,Y indexed addressing.
//
// CALLING CONVENTION: NOT the ORCA-C fn-pointer convention. The
// CALLING CONVENTION: NOT a C fn-pointer convention. The
// runtime never calls these routines via a C cast -- instead,
// spriteCompiledDraw (in jlSpriteCompile.c, gated on
// JOEYLIB_PLATFORM_IIGS) builds a self-modifying JSL stub that
@ -9,7 +9,7 @@
// - M = 8-bit (set by stub before JSL)
// - X = 16-bit (set by stub)
// - Y = destRow (loaded by stub from immediate)
// - DBR = program bank (ORCA-C default)
// - DBR = program bank
// No stack arg, no prologue. Body executes directly.
//
// Routine shape (per-byte emit, no PEA optimization yet):
@ -39,12 +39,10 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
#define MAX_ROUTINE_BYTES 8192
// ----- Prototypes -----
static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
@ -116,8 +114,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
// writeLE16 was inlined at every call site. Inlining cuts a JSL/RTL
// per emitted 16-bit immediate (4 instructions per byte * 12 sites)
// and avoids ORCA-Linker bank-fragility around tiny-helper resolution.
// per emitted 16-bit immediate (4 instructions per byte * 12 sites).
// Common backbone for save and restore. Both ops copy a byte-aligned
// rectangle row-by-row using MVN; only the operand banks (which buffer
@ -150,15 +147,36 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
//
// The MVN at row R has its dstbk at routine offset (12*R + 4) and
// srcbk at (12*R + 5).
static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
uint16_t cursor;
// Worst-case bytes emitted per MVN copy row: row 0 = 6 bytes, any
// later row = 12. Plus the trailing RTL.
#define IIGS_MVN_MAX_BYTES_PER_ROW 12u
static uint32_t emitMvnCopyRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx, uint16_t copyBytes, bool advanceX) {
uint32_t cursor;
uint16_t advance;
uint16_t row;
// MVN copies (count + 1) bytes from the LDA #(copyBytes-1) operand.
// A zero copyBytes would emit LDA #0xFFFF and MVN a full 64 KB --
// a screen-trashing blit. widthTiles==0 is rejected at sprite
// creation, so copyBytes>=4 in practice, but guard the helper so a
// future caller cannot weaponize it: emit a bare RTL no-op instead.
if (copyBytes == 0u) {
if (cap < 1u) {
return SPRITE_EMIT_OVERFLOW;
}
out[0] = 0x6B;
return 1u;
}
cursor = 0;
advance = (uint16_t)(SURFACE_BYTES_PER_ROW - copyBytes);
for (row = 0; row < heightPx; row++) {
// Reserve the row's worst case plus the trailing RTL.
if (cursor + IIGS_MVN_MAX_BYTES_PER_ROW + 1u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
if (row > 0) {
out[cursor++] = advanceX ? 0x8A : 0x98; // TXA / TYA
out[cursor++] = 0x18; // CLC
@ -174,6 +192,9 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
out[cursor++] = 0x00; // dstbk -- patched per call
out[cursor++] = 0x00; // srcbk -- patched per call
}
if (cursor + 1u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x6B; // RTL
return cursor;
}
@ -184,7 +205,7 @@ static uint16_t emitMvnCopyRoutine(uint8_t *out, uint16_t heightPx, uint16_t cop
// by copyBytes per row. Backup rows are contiguous in memory so Y is
// already correct for the next row; screen rows are SURFACE_BYTES_PER_ROW
// apart so X needs an explicit ADC between rows.
uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
uint16_t copyBytes;
@ -195,16 +216,16 @@ uint16_t spriteEmitSaveIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/true);
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/true);
}
// RESTORE (backup -> screen). Stub passes X = backup low offset,
// Y = screen low offset. Backup is contiguous so X advances correctly
// via MVN; screen needs explicit advance, so Y is the one we ADC.
uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
uint16_t copyBytes;
@ -215,9 +236,9 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
return emitMvnCopyRoutine(out, heightPx, copyBytes, /*advanceX*/false);
return emitMvnCopyRoutine(out, cap, heightPx, copyBytes, /*advanceX*/false);
}
@ -252,8 +273,8 @@ uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
//
// Mixed bytes always run on the M=8 path because the AND/ORA in
// M=16 would clobber the adjacent byte.
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
@ -273,7 +294,7 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
wide = false;
// No prologue: caller (the inline-asm stub in jlSpriteCompile.c)
@ -285,7 +306,19 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
if (opaqueMask == 0x00) {
continue;
}
absOffset = (uint16_t)(row * SURFACE_BYTES_PER_ROW + col);
// Reserve the worst case for one dest byte: the 9-byte
// mixed RMW, plus a possible REP/SEP mode transition (2),
// plus the trailing SEP+RTL (3) at routine end.
if (cursor + SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS + 2u + 3u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// Compute the dest-byte offset in a 32-bit intermediate so
// row*160 cannot wrap a 16-bit int used for plain
// int arithmetic (row up to ~199, 199*160 = 31840 is in
// range for legal sprites, but a clamp-bypassing caller
// must not silently wrap). The result still fits the 16-bit
// abs,Y operand for any on-surface sprite.
absOffset = (uint16_t)((uint32_t)row * SURFACE_BYTES_PER_ROW + col);
if (opaqueMask == 0xFFu && (col + 1) < destBytesPerRow) {
// Look ahead: if (col, col+1) are both fully opaque
@ -341,6 +374,14 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
// Trailing SEP (2 bytes if still wide) + RTL (1 byte). The per-byte
// reserve above accounts for these whenever a byte was emitted, but
// an all-transparent sprite never entered the inner write, so guard
// the epilogue explicitly.
if (cursor + (wide ? 2u : 0u) + 1u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// Routine exits in M=8: the JSL stub assumes M=8 throughout (the
// stub itself only ever ran with M=8 and doesn't restore M). The
// asm wrapper after the JSL forces M=16 again, but be defensive

View file

@ -1,34 +1,15 @@
// 68k sprite codegen for ST word-interleaved planar layout. Emits a
// cdecl-callable routine `void draw(uint8_t *groupBase)` that walks
// the sprite's tile data and writes plane bytes via `d16(a0)` chains.
// 68k sprite codegen for ST word-interleaved planar layout.
//
// ST planar layout reminder (doc/atarist_planar.md): one buffer; per
// scanline 20 groups of 8 bytes; per group, 4 plane words back-to-
// back. groupBase points at the FIRST group the sprite touches:
// pd->base + y * 160 + (x >> 4) * 8
//
// Shift index for ST is bit 3 of x (whether the sprite starts in the
// high half or low half of the first group). x mod 8 != 0 falls back
// to the interpreter (returns 0 from this emitter so sp->slot stays
// NULL for those alignments).
//
// Per (row, tile_col, plane) we emit one of:
// * nothing (op byte = 0, all transparent)
// * move.b #pbN, d16(a0) (op = 0xFF, full replace, 6 bytes)
// * clr.b d16(a0) (op = 0xFF AND pbN = 0, 4 bytes)
// * andi.b #~op, d16(a0) (op partial, pbN = 0, 6 bytes)
// * ori.b #pbN, d16(a0) (op partial, pbN == op, 6 bytes)
// * andi.b #~op + ori.b #pbN (mixed, 12 bytes)
//
// d16 is the byte offset from groupBase to the target plane byte.
// Layout of the byte offset:
// shift 0: byteOff = (col >> 1) * 8 + plane*2 + (col & 1)
// shift 1: byteOff = ((col + 1) >> 1) * 8 + plane*2 + (1 - (col & 1))
// Each tile column is 8 sprite pixels = exactly half a 16-pixel
// group, alternating high (offset 0) and low (offset 1) bytes of
// each plane word.
//
// Per row we adda.w #160, a0 to advance to the next scanline.
// Phase 11 (shared-walker): the per-shift unrolled emitter was
// retired. halSpriteDrawPlanes in src/port/atarist/hal.c now handles
// all shifts byte-wide at runtime, including the formerly fast shifts
// 0 and 1 -- per-cel arena cost drops to zero. The byte-aligned fast
// path stSpriteDrawByteAligned is still called from halSpriteDrawPlanes
// for shift==0 sprites that are fully on surface, so byte-aligned UBER
// perf does not regress. The entry points below are kept so the
// dispatcher API stays uniform across ports; they emit zero bytes, so
// jlSpriteCompile takes the "totalSize == 0 -> bail" path and sp->slot
// stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
@ -36,194 +17,21 @@
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_PIXELS 8
#define TILE_BYTES 32
#define TILE_BYTES_PER_ROW 4
#define ST_BYTES_PER_ROW 160
// ----- Helpers -----
static uint16_t writeBE16(uint8_t *out, uint16_t value) {
out[0] = (uint8_t)(value >> 8);
out[1] = (uint8_t)(value & 0xFFu);
return 2;
}
// Build the 4 plane bytes + opacity byte for one (row, tileCol)
// pair. pbN bit 7 is sprite pixel 0 (leftmost), bit 0 is pixel 7.
// op bit N is set iff that pixel's color != 0.
static void buildPlaneBytes(const jlSpriteT *sp, uint16_t row, uint16_t tileCol,
uint8_t *outPb0, uint8_t *outPb1,
uint8_t *outPb2, uint8_t *outPb3,
uint8_t *outOp) {
uint16_t tileY = (uint16_t)(row >> 3);
uint16_t inTileY = (uint16_t)(row & 7u);
uint16_t wTiles = sp->widthTiles;
const uint8_t *tileBytes = sp->tileData + (uint32_t)(tileY * wTiles + tileCol) * 32u;
const uint8_t *tileRow = tileBytes + (uint32_t)inTileY * TILE_BYTES_PER_ROW;
uint8_t pb0 = 0u;
uint8_t pb1 = 0u;
uint8_t pb2 = 0u;
uint8_t pb3 = 0u;
uint8_t op = 0u;
uint8_t p;
uint8_t b;
uint8_t color;
uint8_t bit;
for (p = 0; p < 8u; p++) {
b = tileRow[p >> 1];
color = (p & 1u) ? (uint8_t)(b & 0x0Fu) : (uint8_t)(b >> 4);
bit = (uint8_t)(0x80u >> p);
if (color != 0u) {
op = (uint8_t)(op | bit);
if (color & 1u) pb0 = (uint8_t)(pb0 | bit);
if (color & 2u) pb1 = (uint8_t)(pb1 | bit);
if (color & 4u) pb2 = (uint8_t)(pb2 | bit);
if (color & 8u) pb3 = (uint8_t)(pb3 | bit);
}
}
*outPb0 = pb0;
*outPb1 = pb1;
*outPb2 = pb2;
*outPb3 = pb3;
*outOp = op;
}
// Emit code for one plane byte at d16(a0). Returns bytes written.
// op=opacity byte, pb=plane byte (subset of op).
static uint16_t emitPlaneByte(uint8_t *out, uint16_t cursor, uint16_t d16, uint8_t op, uint8_t pb) {
uint16_t start = cursor;
if (op == 0u) {
return 0u; /* nothing to emit */
}
if (op == 0xFFu) {
/* All 8 pixels opaque: replace the byte. */
if (pb == 0u) {
/* clr.b d16(a0). Opcode 0x4228 + d16. 4 bytes. */
cursor += writeBE16(out + cursor, 0x4228u);
cursor += writeBE16(out + cursor, d16);
} else {
/* move.b #pb, d16(a0). Opcode 0x117C + #imm word + d16. 6 bytes. */
cursor += writeBE16(out + cursor, 0x117Cu);
cursor += writeBE16(out + cursor, (uint16_t)pb);
cursor += writeBE16(out + cursor, d16);
}
return (uint16_t)(cursor - start);
}
/* Partial opacity. pb is a subset of op. */
if (pb == 0u) {
/* All opaque pixels have plane bit 0: just clear those bits. */
/* andi.b #~op, d16(a0). Opcode 0x0228 + #imm word + d16. 6 bytes. */
cursor += writeBE16(out + cursor, 0x0228u);
cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
cursor += writeBE16(out + cursor, d16);
return (uint16_t)(cursor - start);
}
if (pb == op) {
/* All opaque pixels have plane bit 1: just set those bits. */
/* ori.b #op, d16(a0). Opcode 0x0028 + #imm word + d16. 6 bytes. */
cursor += writeBE16(out + cursor, 0x0028u);
cursor += writeBE16(out + cursor, (uint16_t)op);
cursor += writeBE16(out + cursor, d16);
return (uint16_t)(cursor - start);
}
/* Mixed: clear opaque bits, then set the plane bits. */
cursor += writeBE16(out + cursor, 0x0228u);
cursor += writeBE16(out + cursor, (uint16_t)(~op & 0xFFu));
cursor += writeBE16(out + cursor, d16);
cursor += writeBE16(out + cursor, 0x0028u);
cursor += writeBE16(out + cursor, (uint16_t)pb);
cursor += writeBE16(out + cursor, d16);
return (uint16_t)(cursor - start);
}
// ----- Emit API -----
uint16_t spriteEmitDrawInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
/* Phase 11 (shared-walker): bypass the per-shift unrolled emitter.
* halSpriteDrawPlanes in src/port/atarist/hal.c now handles all
* shifts byte-wide at runtime, including the formerly fast
* shifts 0 and 1 -- per-cel arena cost drops to zero. The byte-
* aligned fast path stSpriteDrawByteAligned is still called from
* halSpriteDrawPlanes for shift==0 sprites that are fully on
* surface, so byte-aligned UBER perf doesn't regress. */
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint16_t spriteEmitDrawInterleaved68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint16_t row;
uint16_t col;
uint16_t plane;
uint16_t heightPx;
uint16_t wTiles;
uint8_t pb[4];
uint8_t op;
if (shift > 1u) {
return 0u;
}
cursor = 0u;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
wTiles = sp->widthTiles;
/* Prologue: movea.l 4(sp), a0. Opcode 0x206F + d16=4. 4 bytes. */
cursor += writeBE16(out + cursor, 0x206Fu);
cursor += writeBE16(out + cursor, 0x0004u);
for (row = 0; row < heightPx; row++) {
if (row > 0u) {
/* adda.w #160, a0. Opcode 0xD0FC + imm word. 4 bytes. */
cursor += writeBE16(out + cursor, 0xD0FCu);
cursor += writeBE16(out + cursor, (uint16_t)ST_BYTES_PER_ROW);
}
for (col = 0; col < wTiles; col++) {
buildPlaneBytes(sp, row, col, &pb[0], &pb[1], &pb[2], &pb[3], &op);
if (op == 0u) {
continue; /* whole tile column row is transparent */
}
for (plane = 0; plane < 4u; plane++) {
uint16_t d16;
if (shift == 0u) {
/* col 0 (high) -> +0, col 1 (low) -> +1, col 2
* (high group 1) -> +8, ... */
d16 = (uint16_t)((col >> 1) * 8 + plane * 2 + (col & 1u));
} else {
/* col 0 (low) -> +1, col 1 (high group 1) -> +8, ... */
d16 = (uint16_t)(((col + 1u) >> 1) * 8 + plane * 2 + (1u - (col & 1u)));
}
cursor += emitPlaneByte(out, cursor, d16, op, pb[plane]);
}
}
}
/* Epilogue: rts. */
cursor += writeBE16(out + cursor, 0x4E75u);
return cursor;
}
/* Save / restore aren't implemented yet -- returning 0 so they fall
* through to the C interpreter (halSpriteSavePlanes / halSpriteRestorePlanes
* fast paths cover the byte-aligned case). */
uint16_t spriteEmitSaveInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint16_t spriteEmitRestoreInterleaved68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitRestoreInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

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
// address-register pointers (a0..a3 = plane[0..3] base + byteOff,
// where byteOff = y*40 + x/8 -- the dispatcher pre-computes this).
// Phase 11 (shared-walker): the Amiga port no longer emits per-cel
// unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
// 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
// src/core/sprite.c routes through halSpriteXxxPlanes in
// src/port/amiga/hal.c when sp->slot is NULL, and those HAL hooks are
// byte-wide walkers that read sp->tileData on the fly and synthesize
// plane bytes at draw time. Same per-byte work, shared as one static
// lib routine instead of 21 inlined copies; the pre-shift plane-byte
// decomposition (and its shift/transparency math) now lives ONLY in
// halSpriteDrawPlanes, so there is one source of truth.
//
// Calling convention (cdecl on m68k-amigaos-gcc):
// draw(p0, p1, p2, p3):
// args at 4(sp), 8(sp), 12(sp), 16(sp) -- one ULONG per plane.
// loaded into a0..a3 by the prologue.
// save(p0, p1, p2, p3, backup):
// 5 args; backup at 20(sp), loaded into a4.
// restore(p0, p1, p2, p3, backup):
// same as save but reads backup, writes planes.
//
// Per-byte plane write encoding decisions:
// - all-transparent (mask=0): skip the byte entirely
// - all-opaque (mask=0xFF): move.b #imm, d16(an) (6 bytes)
// - mixed (0<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.
// The entry points below are kept so the dispatcher API stays uniform
// across ports. Returning 0 tells emitTotalSize() the emitter has no
// bytes for this op, so jlSpriteCompile takes the "totalSize == 0 ->
// bail" path and sp->slot stays NULL.
#include "joey/sprite.h"
#include "joey/surface.h"
@ -42,499 +22,21 @@
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_PIXELS 8
#define TILE_BYTES 32
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
#define AMIGA_BITPLANES 4
#define AMIGA_BYTES_PER_ROW 40
// ----- Instruction encoding helpers -----
static uint16_t writeBE16(uint8_t *out, uint16_t value) {
out[0] = (uint8_t)((value >> 8) & 0xFFu);
out[1] = (uint8_t)(value & 0xFFu);
return 2u;
}
// movea.l <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 -----
/* Phase 11 (shared-walker): the Amiga port no longer emits per-cel
* unrolled draw/save/restore code. Each cel was ~17-23 KB compiled --
* 21 cels burned ~340 KB of arena. The cross-platform dispatcher in
* src/core/sprite.c routes through halSpriteXxxPlanes in src/port/amiga
* /hal.c when sp->slot is NULL, and those HAL hooks are now byte-wide
* walkers that read sp->tileData on the fly. Same per-byte work, just
* shared as one static lib routine instead of 21 inlined copies.
*
* Returning 0 here tells emitTotalSize() the emitter has no bytes for
* this op, so jlSpriteCompile takes the "totalSize == 0 -> bail" path
* and sp->slot stays NULL. */
uint16_t spriteEmitDrawPlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
uint16_t widthTiles;
uint16_t bytesPerRow; /* per plane, per row */
uint8_t planeBytes[AMIGA_BITPLANES];
uint8_t maskByte;
uint8_t i;
if (shift > 7u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
widthTiles = (uint16_t)sp->widthTiles;
bytesPerRow = (uint16_t)(widthTiles + (shift == 0u ? 0u : 1u));
/* Prologue: m68k cdecl callee-saves a2-a6; we clobber a2 and a3
* loading plane pointers, so push them first. After the push, all
* stack arg displacements shift by +8 (two longs). */
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3);
for (i = 0; i < AMIGA_BITPLANES; i++) {
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)(8u + 4u + i * 4u));
}
for (row = 0; row < heightPx; row++) {
for (col = 0; col < bytesPerRow; col++) {
if (shift == 0u) {
planeByteAndMaskAt(sp, row, col, planeBytes, &maskByte);
} else {
planeByteAndMaskShifted(sp, row, col, shift, widthTiles, planeBytes, &maskByte);
}
for (i = 0; i < AMIGA_BITPLANES; i++) {
cursor = emitMergeByteToD16An(out, cursor, i, (uint8_t)col,
maskByte, planeBytes[i]);
}
}
if (row + 1u < heightPx) {
for (i = 0; i < AMIGA_BITPLANES; i++) {
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
}
}
}
/* Epilogue: restore a2-a3, rts. */
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3);
cursor += writeBE16(out + cursor, OPCODE_RTS);
return cursor;
}
// SAVE: planes -> backup. backup is one contiguous 4*H*W/8 byte buffer
// laid out as 4 plane stripes, matching halSpriteSavePlanes format
// (so cross-platform save buffer is interchangeable).
//
// Per row: for each plane, copy bytesPerRow bytes from d16(an) to
// (a4)+. After the row's reads, the planes need to advance by 40,
// while a4 advances naturally via post-increment.
//
// Plane stripes are sequential in backup. We could either (a) do all
// rows of plane 0, then plane 1, etc. (matches halSpriteSavePlanes
// layout), or (b) interleave rows of all 4 planes (different layout).
// halSpriteSavePlanes does (a) -- 4 separate plane stripes. The
// emitted code below matches that layout for compat.
uint16_t spriteEmitSavePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
uint16_t bytesPerRow;
uint8_t i;
/* Shifts 2..7 reuse shift 1's bytes (identical memcpy). The
* jlSpriteCompile post-emit pass aliases their routineOffsets to
* slot 1 so this routine is emitted once. */
if (shift > 1u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
/* Prologue: callee-save a2/a3/a4 (m68k cdecl), then load 4 plane
* pointers + backup pointer. After the push, all stack arg disps
* shift by +12 (three longs). */
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
for (i = 0; i < AMIGA_BITPLANES; i++) {
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
}
/* a4 = backup. */
cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
/* Plane-major: for each plane, walk all rows. After this routine,
* each An has advanced by H*40 (one frame full); we don't need to
* unwind because the function returns. We DO need to reset An
* back to start before walking the NEXT plane though.
*
* Simpler alternative: row-major (interleaved). Per row, copy
* bytesPerRow bytes from each plane to (a4)+, then advance all
* 4 planes by 40. Net: a4 advances by 4*H*bytesPerRow; planes
* advance by H*40. Backup layout becomes interleaved (plane0_row0,
* plane1_row0, plane2_row0, plane3_row0, plane0_row1, ...).
*
* That doesn't match halSpriteSavePlanes' plane-major layout. Need
* to either (a) match it -- emit per-plane outer loop with a4
* stride between planes -- or (b) change halSpriteSavePlanes to
* interleaved. Picking (b) is simpler in emitted code, but ALSO
* requires updating halSpriteRestorePlanes and halSpriteRestoreUnder
* fallback math.
*
* For now: use plane-major matching halSpriteSavePlanes. Per
* plane: walk rows, copy bytes from d16(an) to (a4)+, advance an
* by 40 after each row except the last; reset an back to start
* before next plane. */
for (i = 0; i < AMIGA_BITPLANES; i++) {
for (row = 0; row < heightPx; row++) {
for (col = 0; col < bytesPerRow; col++) {
cursor += writeBE16(out + cursor, kMoveBD16AnToA4Postinc[i]);
cursor += writeBE16(out + cursor, (uint16_t)col);
}
if (row + 1u < heightPx) {
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
}
}
/* Reset An back to the plane base for next iteration. The
* total advance was (heightPx - 1) * 40. Subtract that. */
if (i + 1u < AMIGA_BITPLANES) {
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
}
}
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
cursor += writeBE16(out + cursor, OPCODE_RTS);
return cursor;
}
// RESTORE: backup -> planes. Mirror of save. Uses MOVE.B (a4)+, d16(an).
uint16_t spriteEmitRestorePlanar68kOld(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint16_t row;
uint16_t col;
uint16_t heightPx;
uint16_t bytesPerRow;
uint8_t i;
if (shift > 1u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
bytesPerRow = (uint16_t)(sp->widthTiles + (shift == 0u ? 0u : 1u));
/* Callee-save a2/a3/a4; arg disps shift by +12. */
cursor += writeBE16(out + cursor, MOVEM_L_PUSH_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_A2_A3_A4);
for (i = 0; i < AMIGA_BITPLANES; i++) {
cursor += writeBE16(out + cursor, kMoveaSpToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + i * 4u));
}
cursor += writeBE16(out + cursor, kMoveaSpToAn[4]);
cursor += writeBE16(out + cursor, (uint16_t)(12u + 4u + 4u * 4u));
for (i = 0; i < AMIGA_BITPLANES; i++) {
for (row = 0; row < heightPx; row++) {
for (col = 0; col < bytesPerRow; col++) {
cursor += writeBE16(out + cursor, kMoveBA4PostincToD16An[i]);
cursor += writeBE16(out + cursor, (uint16_t)col);
}
if (row + 1u < heightPx) {
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)AMIGA_BYTES_PER_ROW);
}
}
if (i + 1u < AMIGA_BITPLANES) {
cursor += writeBE16(out + cursor, kAddaWImmToAn[i]);
cursor += writeBE16(out + cursor, (uint16_t)(0u - ((heightPx - 1u) * AMIGA_BYTES_PER_ROW)));
}
}
cursor += writeBE16(out + cursor, MOVEM_L_POP_OPCODE);
cursor += writeBE16(out + cursor, MOVEM_L_MASK_POP_A2_A3_A4);
cursor += writeBE16(out + cursor, OPCODE_RTS);
return cursor;
}
// ---- Phase 11 entry points: zero-byte stubs that force the dispatcher
// ---- to the shared walker in halSpriteDrawPlanes / halSpriteSavePlanes
// ---- / halSpriteRestorePlanes. The old emit-unrolled-bytes path is
// ---- kept above (renamed *Old) for reference until the walker is
// ---- thoroughly battle-tested.
uint16_t spriteEmitDrawPlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint16_t spriteEmitSavePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}
uint16_t spriteEmitRestorePlanar68k(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)sp; (void)shift;
uint32_t spriteEmitRestorePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
(void)out; (void)cap; (void)sp; (void)shift;
return 0u;
}

View file

@ -38,17 +38,35 @@
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
// Worst-case bytes per emitted routine, used to size the scratch
// buffer. A 32x32 sprite is 16 rows * (16 dest bytes + 1 for shift1)
// = 272 dest-byte slots, each up to 10 bytes mixed = 2720; plus
// per-row prologues 32*6=192; plus prologue/epilogue 8. Round up
// generously.
#define MAX_ROUTINE_BYTES 8192
// Stack-arg displacements for the emitted cdecl routines, after the
// prologue saves esi (+esi/edi for save/restore):
// draw(dst) -> dst at [esp+8] (1 push)
// save/restore(src, dst) -> src at [esp+12], dst at [esp+16] (2 pushes)
#define DRAW_ARG_DST_DISP 0x08u
#define COPY_ARG_SRC_DISP 0x0Cu
#define COPY_ARG_DST_DISP 0x10u
// Largest disp8 [esi+disp8] can encode as a non-negative offset. col
// values above this need the disp32 ModRM form (sign-extended disp8
// would address a negative offset).
#define DISP8_MAX 127u
// Worst-case emitted bytes for one draw dest byte. The mixed RMW path
// is mov al,[esi+d]; and al; or al; mov [esi+d],al = 3+2+2+3 = 10 with
// disp8; the disp32 form adds 3 bytes per [esi+disp32] operand (two of
// them), so up to 16 for a wide mixed byte.
#define DRAW_MAX_BYTES_PER_DESTBYTE 16u
// Bytes for the per-row `add esi, imm32` advance.
#define ROW_STRIDE_ADVANCE_BYTES 6u
// Bytes for the `pop esi[/edi]; ret` epilogue (worst case save/restore).
#define EPILOGUE_BYTES 3u
// ----- Prototypes -----
static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc);
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col);
static uint32_t esiOperandLen(uint16_t col);
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask);
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col);
@ -65,7 +83,7 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col
//
// strideOnSrc=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE)
static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
static uint32_t emitCopyBodyX86(uint8_t *out, uint32_t cap, uint32_t cursor, uint16_t heightPx, uint16_t copyBytes, bool strideOnSrc) {
uint16_t row;
uint16_t dwords;
uint16_t tail;
@ -76,6 +94,10 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
advance = (int32_t)SURFACE_BYTES_PER_ROW - (int32_t)copyBytes;
for (row = 0; row < heightPx; row++) {
// Worst case per row: rep movsd (7) + rep movsb (7) + add (6).
if (cursor + 7u + 7u + 6u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
if (dwords > 0) {
// mov ecx, dwords (B9 imm32); rep movsd (F3 A5)
out[cursor++] = 0xB9;
@ -111,6 +133,34 @@ static uint16_t emitCopyBodyX86(uint8_t *out, uint16_t cursor, uint16_t heightPx
}
// Emit the `[esi+col]` ModRM + displacement that follows an opcode in
// the draw stores. reg field is 000 (al / opcode-extension 0), so the
// ModRM is 0x46 for the disp8 form (col <= 127) and 0x86 for the disp32
// form. disp8 is sign-extended by the CPU, so any col in 128..255 would
// address a NEGATIVE offset and corrupt pixels; wide sprites
// (widthTiles > 31) hit this, so emit the disp32 form there. Returns
// the new cursor.
static uint32_t emitEsiOperand(uint8_t *out, uint32_t cursor, uint16_t col) {
if (col <= DISP8_MAX) {
out[cursor++] = 0x46; // ModRM mod=01 [esi+disp8]
out[cursor++] = (uint8_t)(col & 0xFFu);
} else {
out[cursor++] = 0x86; // ModRM mod=10 [esi+disp32]
out[cursor++] = (uint8_t)(col & 0xFFu);
out[cursor++] = (uint8_t)((col >> 8) & 0xFFu);
out[cursor++] = 0x00;
out[cursor++] = 0x00;
}
return cursor;
}
// Bytes emitEsiOperand will write for `col` (ModRM + disp).
static uint32_t esiOperandLen(uint16_t col) {
return (col <= DISP8_MAX) ? 2u : 5u;
}
// Decompose a destination byte's contribution from the sprite into
// (value, opaqueMask) for shift in {0, 1}. opaqueMask high nibble
// 0xF0 means high dest nibble is opaque; 0x0F means low is opaque;
@ -182,10 +232,11 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
}
// Emit a draw routine for one shift variant. Returns bytes written.
// Emit a draw routine for one shift variant. Returns bytes written, or
// SPRITE_EMIT_OVERFLOW if the routine would exceed `cap`.
// Routine signature: void f(uint8_t *dst).
uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t row;
uint16_t col;
uint16_t runEnd;
@ -207,17 +258,23 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
destBytesPerRow = (uint16_t)(spriteBytesPerRow + (shift == 1 ? 1 : 0));
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
// Prologue: push esi; mov esi, [esp+8]
if (cursor + 5u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x56; // push esi
out[cursor++] = 0x8B; out[cursor++] = 0x74;
out[cursor++] = 0x24; out[cursor++] = 0x08;
out[cursor++] = 0x24; out[cursor++] = DRAW_ARG_DST_DISP;
// Body: per row, scan dest bytes coalescing fully-opaque runs.
for (row = 0; row < heightPx; row++) {
if (row > 0) {
// add esi, SURFACE_BYTES_PER_ROW (32-bit imm)
if (cursor + ROW_STRIDE_ADVANCE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x81; out[cursor++] = 0xC6;
out[cursor++] = (uint8_t)(SURFACE_BYTES_PER_ROW & 0xFFu);
out[cursor++] = (uint8_t)((SURFACE_BYTES_PER_ROW >> 8) & 0xFFu);
@ -231,20 +288,25 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
col++;
continue;
}
// Reserve the worst case for one dest byte (wide mixed RMW
// = 16 bytes) plus the epilogue.
if (cursor + DRAW_MAX_BYTES_PER_DESTBYTE + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
if (opaqueMask != 0xFFu) {
// Mixed: read-modify-write.
// mov al, [esi+col] (8A 46 cc)
// mov al, [esi+col] (8A /r)
// and al, ~opaqueMask (24 mm)
// or al, value (0C vv)
// mov [esi+col], al (88 46 cc)
out[cursor++] = 0x8A; out[cursor++] = 0x46;
out[cursor++] = (uint8_t)(col & 0xFFu);
// mov [esi+col], al (88 /r)
out[cursor++] = 0x8A;
cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = 0x24;
out[cursor++] = (uint8_t)(~opaqueMask & 0xFFu);
out[cursor++] = 0x0C;
out[cursor++] = value;
out[cursor++] = 0x88; out[cursor++] = 0x46;
out[cursor++] = (uint8_t)(col & 0xFFu);
out[cursor++] = 0x88;
cursor = emitEsiOperand(out, cursor, col);
col++;
continue;
}
@ -264,12 +326,15 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
// enough that re-reading per chunk beats threading a
// fixed-size buffer through.
while (runLen >= 4) {
// mov dword [esi+col], imm32 (C7 /0 ... ii ii ii ii)
if (cursor + 1u + esiOperandLen(col) + 4u + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 2), shift, spriteBytesPerRow, &v2, &m);
shiftedByteAt(sp, row, (uint16_t)(col + 3), shift, spriteBytesPerRow, &v3, &m);
// mov dword [esi+col], imm32 (C7 46 cc ii ii ii ii)
out[cursor++] = 0xC7; out[cursor++] = 0x46;
out[cursor++] = (uint8_t)(col & 0xFFu);
out[cursor++] = 0xC7;
cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
out[cursor++] = v1;
out[cursor++] = v2;
@ -281,11 +346,14 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
if (runLen >= 2) {
// mov word [esi+col], imm16 (66 C7 /0 ... ii ii)
if (cursor + 2u + esiOperandLen(col) + 2u + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
shiftedByteAt(sp, row, (uint16_t)(col + 1), shift, spriteBytesPerRow, &v1, &m);
// mov word [esi+col], imm16 (66 C7 46 cc ii ii)
out[cursor++] = 0x66;
out[cursor++] = 0xC7; out[cursor++] = 0x46;
out[cursor++] = (uint8_t)(col & 0xFFu);
out[cursor++] = 0xC7;
cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
out[cursor++] = v1;
col = (uint16_t)(col + 2);
@ -295,9 +363,12 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
}
if (runLen == 1) {
// mov byte [esi+col], imm8 (C6 46 cc ii)
out[cursor++] = 0xC6; out[cursor++] = 0x46;
out[cursor++] = (uint8_t)(col & 0xFFu);
// mov byte [esi+col], imm8 (C6 /0 ... ii)
if (cursor + 1u + esiOperandLen(col) + 1u + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0xC6;
cursor = emitEsiOperand(out, cursor, col);
out[cursor++] = value;
col++;
}
@ -305,6 +376,9 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
}
// Epilogue: pop esi; ret
if (cursor + 2u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;
return cursor;
@ -312,8 +386,8 @@ uint16_t spriteEmitDrawX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
// RESTORE: copy backup -> screen. Destination has the screen stride.
uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@ -323,17 +397,26 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
// Prologue: push esi; push edi; mov esi,[esp+12]; mov edi,[esp+16]
if (cursor + 10u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x56;
out[cursor++] = 0x57;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, false);
cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, false);
if (cursor == SPRITE_EMIT_OVERFLOW) {
return SPRITE_EMIT_OVERFLOW;
}
// Epilogue: pop edi; pop esi; ret
if (cursor + 3u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x5F;
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;
@ -342,8 +425,8 @@ uint16_t spriteEmitRestoreX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift)
// SAVE: copy screen -> backup. Source has the screen stride.
uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
uint16_t cursor;
uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
@ -353,15 +436,24 @@ uint16_t spriteEmitSaveX86(uint8_t *out, const jlSpriteT *sp, uint8_t shift) {
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + (shift == 1u ? 1u : 0u));
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
if (cursor + 10u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x56;
out[cursor++] = 0x57;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = 0x0C;
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = 0x10;
out[cursor++] = 0x8B; out[cursor++] = 0x74; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_SRC_DISP;
out[cursor++] = 0x8B; out[cursor++] = 0x7C; out[cursor++] = 0x24; out[cursor++] = COPY_ARG_DST_DISP;
cursor = emitCopyBodyX86(out, cursor, heightPx, copyBytes, true);
cursor = emitCopyBodyX86(out, cap, cursor, heightPx, copyBytes, true);
if (cursor == SPRITE_EMIT_OVERFLOW) {
return SPRITE_EMIT_OVERFLOW;
}
if (cursor + 3u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
out[cursor++] = 0x5F;
out[cursor++] = 0x5E;
out[cursor++] = 0xC3;

View file

@ -8,12 +8,25 @@
// plane bytes at draw time -- but the entry points are kept so the
// dispatcher API stays uniform across ports.
//
// Each emit function takes the sprite + shift variant and writes
// position-independent draw-routine bytes into `out`. Returns the
// number of bytes written. The output bytes follow the target CPU's
// calling convention as documented in the per-CPU file's header
// comment, so the bytes can be called via a C function pointer cast
// once placed in executable memory.
// Each emit function takes the sprite + shift variant and a capacity
// `cap` (bytes available at `out`), and writes position-independent
// draw-routine bytes into `out`. It returns the number of bytes
// written (uint32_t -- a single pathological large routine can exceed
// 64 KB, so a 16-bit count would truncate and mis-size the arena
// slot). The output bytes follow the target CPU's calling convention
// as documented in the per-CPU file's header comment, so the bytes
// can be called via a C function pointer cast once placed in
// executable memory.
//
// Return-value contract:
// 0 -- op not implemented for this (shift, op)
// on this platform; dispatcher falls back
// to the interpreted path.
// SPRITE_EMIT_OVERFLOW -- the routine would not fit in `cap`. The
// sizing pass and the real emit pass both
// propagate this and jlSpriteCompile bails
// cleanly (no arena alloc, sp->slot NULL).
// any other value -- bytes written.
#ifndef JOEYLIB_SPRITE_EMITTER_H
#define JOEYLIB_SPRITE_EMITTER_H
@ -21,9 +34,35 @@
#include "joey/sprite.h"
#include "spriteInternal.h"
uint16_t spriteEmitDrawX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitDraw68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
// Overflow sentinel returned by an emitter whose output would exceed
// the supplied capacity. Distinct from a real byte count and from the
// 0 "not implemented" return.
#define SPRITE_EMIT_OVERFLOW 0xFFFFFFFFul
// Worst-case emitted bytes per destination byte, per target. Used to
// size the emit scratch and to derive the create-time dimension cap so
// the scratch, the cap, and the per-emitter bound all share one fact:
// 68k mixed RMW : move.b d16(a0),d0; andi.b; ori.b; move.b d0,d16(a0)
// = 4 instructions * 4 bytes = 16
// x86 mixed RMW : mov al,[esi+d]; and al; or al; mov [esi+d],al = 10
// IIgs mixed RMW: lda abs,Y; and #; ora #; sta abs,Y = 9
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K 16u
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86 10u
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS 9u
#if defined(JOEYLIB_PLATFORM_DOS)
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_X86
#elif defined(JOEYLIB_PLATFORM_IIGS)
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_IIGS
#else
// 68k chunky (unused) / planar / interleaved walkers emit 0 bytes;
// the 68k chunky worst case is the safe upper bound for sizing.
#define SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE SPRITE_EMIT_MAX_BYTES_PER_DESTBYTE_68K
#endif
uint32_t spriteEmitDrawX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitDraw68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Save-under and restore-under emitters. Both copy a byte-aligned
// rectangle between the destination surface and a backup buffer. The
@ -37,12 +76,12 @@ uint16_t spriteEmitDrawIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
// 68k use a plain cdecl `void copy(const uint8_t *src, uint8_t *dst)`
// where the caller swaps args between SAVE (screen->backup) and
// RESTORE (backup->screen).
uint16_t spriteEmitSaveIigs (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestoreIigs(uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSaveX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestoreX86 (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSave68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitSaveIigs (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitSaveX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitRestoreX86 (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitSave68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitRestore68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Planar 68k emitters (Amiga). Distinct from the chunky 68k emitters
// above because the destination addressing is across 4 separate
@ -55,9 +94,9 @@ uint16_t spriteEmitRestore68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift)
// dispatcher). Returns 0 for shifts not yet implemented (today only
// shift 0 == byte-aligned x is emitted; shifts 1..7 fall back to the
// cross-platform interpreter).
uint16_t spriteEmitDrawPlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSavePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitDrawPlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitSavePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
// Word-interleaved planar 68k emitter (ST). Calling convention for
// the emitted bytes:
@ -66,8 +105,8 @@ uint16_t spriteEmitRestorePlanar68k (uint8_t *out, const jlSpriteT *sp, uint8_t
// real bytes (x mod 16 == 0 for shift 0, x mod 16 == 8 for shift 1);
// other shifts return 0 so the cross-platform dispatcher falls back
// to halSpriteDrawPlanes.
uint16_t spriteEmitDrawInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitSaveInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint16_t spriteEmitRestoreInterleaved68k (uint8_t *out, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitDrawInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitSaveInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
uint32_t spriteEmitRestoreInterleaved68k (uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift);
#endif

View file

@ -26,13 +26,12 @@
#include "joey/tile.h"
#include "spriteInternal.h"
// Push the file-IO out of the ORCA-C _ROOT segment on the IIgs. Every
// Push the file-IO out of the root segment on the IIgs. Every
// fopen / fread / fclose call drags a chunk of the stdio cluster
// in; concentrating them in one named segment keeps the root bank
// from overflowing past 64 KB (the failure mode is the linker's
// "Code exceeds code bank size" / "Address is not in current bank"
// errors documented in the feedback memory).
JOEYLIB_SEGMENT("ASSETLD")
// ----- Constants -----
@ -41,6 +40,14 @@ JOEYLIB_SEGMENT("ASSETLD")
#define ASSET_PALETTE_ENTRIES 16
#define ASSET_TILE_DATA_BYTES 32 // 4bpp packed: 4 bytes/row * 8 rows
// Largest legitimately drawable sprite cel: the surface is 40x25 tiles
// (320/8 x 200/8), so any cel larger than that can never be fully
// on-screen. Reject crafted/truncated headers claiming more, which
// would otherwise force an unbounded per-cel malloc (DoS) and feed the
// downstream emitters/draw walkers out-of-range dimensions.
#define ASSET_MAX_TILES_W 40
#define ASSET_MAX_TILES_H 25
// Per-platform expected target byte for .tbk files. .spr files carry
// target=0 (cross-platform chunky) and pass on every build.
#if defined(JOEYLIB_PLATFORM_AMIGA)
@ -57,10 +64,46 @@ JOEYLIB_SEGMENT("ASSETLD")
// ----- Prototypes -----
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp);
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette);
// ----- Internal helpers (alphabetical) -----
// Open `path`, read the shared 44-byte header, and validate the magic
// + target byte common to both .spr and .tbk. On success leaves *outFp
// open (caller must fclose) and returns true; on any failure closes
// the file (if opened) and returns false. When outPalette is non-NULL
// and the header advertises a palette (header[5] != 0), the palette is
// decoded into outPalette. Shared by jlSpriteBankLoad and
// jlTileBankLoad so the open/read/validate/parse-palette boilerplate
// (and its fclose-on-error pattern) lives in one place.
static bool openReadValidateHeader(const char *path, const char *magic, uint8_t expectedTarget, uint8_t *header, uint16_t *outPalette, FILE **outFp) {
FILE *fp;
fp = fopen(path, "rb");
if (fp == NULL) {
return false;
}
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
fclose(fp);
return false;
}
if (memcmp(header, magic, 4) != 0) {
fclose(fp);
return false;
}
if (header[4] != expectedTarget) {
fclose(fp);
return false;
}
if (outPalette != NULL && header[5] != 0u) {
readPaletteFromHeader(header, outPalette);
}
*outFp = fp;
return true;
}
static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
uint8_t i;
@ -72,6 +115,10 @@ static void readPaletteFromHeader(const uint8_t *header, uint16_t *outPalette) {
// ----- Public API (alphabetical) -----
// Returns the number of cels successfully loaded into outCels[0..N).
// A return value strictly less than min(headerCellCount, maxCels)
// indicates the file was truncated/corrupt mid-stream relative to its
// own header count; the cels that were read are still valid.
uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCels,
uint16_t *outPalette) {
FILE *fp;
@ -87,26 +134,24 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
if (path == NULL || outCels == NULL || maxCels == 0u) {
return 0u;
}
fp = fopen(path, "rb");
if (fp == NULL) {
return 0u;
}
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
fclose(fp);
return 0u;
}
// Magic "JSP1" + target byte = 0 (cross-platform chunky). Anything
// else means the file was either baked wrong or is a stale .spr
// from the old compiled-codegen pipeline.
if (memcmp(header, "JSP1", 4) != 0 || header[4] != 0u) {
fclose(fp);
if (!openReadValidateHeader(path, "JSP1", 0u, header, outPalette, &fp)) {
return 0u;
}
widthTiles = header[6];
heightTiles = header[7];
cellCount = (uint16_t)(header[8] | (header[9] << 8));
if (outPalette != NULL && header[5] != 0u) {
readPaletteFromHeader(header, outPalette);
// Reject degenerate (0 in either dimension -> tileBytes 0, an
// implementation-defined malloc(0)) and oversized (a crafted header
// could force a multi-MB per-cel malloc) sprites. Matches
// jlSpriteCreate's invariant and the 40x25-tile surface ceiling.
if (widthTiles == 0u || heightTiles == 0u ||
widthTiles > ASSET_MAX_TILES_W || heightTiles > ASSET_MAX_TILES_H) {
fclose(fp);
return 0u;
}
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
@ -135,7 +180,9 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
sp->heightTiles = heightTiles;
sp->ownsTileData = true;
sp->slot = NULL;
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
// 0xFF == SPRITE_NOT_COMPILED for every routine offset (0 is a
// valid offset, so zero-fill would alias a real entry).
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
@ -147,6 +194,11 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
}
// Returns the number of tiles successfully loaded. outValid[0..maxTiles)
// is always fully written: true for each tile read, false for the rest
// (including slots past a truncated/short file), so the caller need not
// pre-zero it. A return value strictly less than min(headerTileCount,
// maxTiles) indicates the file was truncated/corrupt mid-stream.
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette) {
FILE *fp;
@ -159,28 +211,22 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
return 0u;
}
fp = fopen(path, "rb");
if (fp == NULL) {
return 0u;
}
if (fread(header, 1, ASSET_HEADER_BYTES, fp) != ASSET_HEADER_BYTES) {
fclose(fp);
return 0u;
}
if (memcmp(header, "JTB1", 4) != 0) {
fclose(fp);
return 0u;
}
if (header[4] != EXPECTED_TILE_TARGET) {
// File was baked for a different target. Build-system bug
// -- the per-target make rule should have produced a .tbk
// with the matching target byte.
fclose(fp);
// The target byte for .tbk must match the build platform. .spr
// files carry target=0; tiles carry the per-platform value. A
// mismatch is a build-system bug -- the per-target make rule
// should have produced a .tbk with the matching target byte.
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
return 0u;
}
tileCount = (uint16_t)(header[6] | (header[7] << 8));
if (outPalette != NULL && header[5] != 0u) {
readPaletteFromHeader(header, outPalette);
// Define the whole outValid array up front so leftover slots read
// false exactly as the tile.h contract promises, regardless of
// caller pre-zeroing.
if (outValid != NULL) {
for (i = 0; i < maxTiles; i++) {
outValid[i] = false;
}
}
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;

View file

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

View file

@ -1,27 +1,52 @@
// Shared SFX overlay mixer. See audioSfxMixInternal.h for the contract.
#include <limits.h>
#include <stddef.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
// sample of the previous chunk at streamBuf[0] so linear interp can
// step across the chunk boundary without a discontinuity click. The
// new chunk fills streamBuf[1..]; pos resets to "after byte 0" so
// the next fetch is byte 1 (= first sample of the new chunk).
// new chunk fills streamBuf[1..]. On a real refill (a previous chunk
// existed) pos resets to 0 so the first read interpolates the carried
// old-last sample at streamBuf[0] into the new chunk's first sample at
// streamBuf[1], bridging the boundary. On the very first prime there
// is no real previous sample, so pos resets to "after byte 0" and the
// first read starts at the new chunk's first sample (index 1).
//
// Returns true if more samples are available, false if the stream
// has ended (caller should mark the slot inactive).
static bool streamRefill(AudioSfxSlotT *slot) {
int8_t lastSample;
uint32_t got;
bool haveCarry;
if (slot->streamEof) {
return false;
}
lastSample = (slot->streamLen > 0u) ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
haveCarry = (slot->streamLen > 0u);
lastSample = haveCarry ? slot->streamBuf[slot->streamLen - 1u] : (int8_t)0;
// Clamp the callback's reported count to the request size so a
// non-conforming fill() that returns more than asked cannot make
// the mixer read past streamBuf. (Memory safety still requires
// fill() to honor count -- an over-returning callback has already
// over-written streamBuf+1; this caps only the subsequent reads.)
got = slot->fill(slot->fillCtx, slot->streamBuf + 1, AUDIO_SFX_STREAM_BUF - 1u);
if (got > (uint32_t)(AUDIO_SFX_STREAM_BUF - 1u)) {
got = AUDIO_SFX_STREAM_BUF - 1u;
}
if (got == 0u) {
slot->streamEof = true;
slot->streamLen = 0u;
@ -29,8 +54,17 @@ static bool streamRefill(AudioSfxSlotT *slot) {
}
slot->streamBuf[0] = lastSample;
slot->streamLen = (uint16_t)(got + 1u);
// Reset pos to (1 sample, 0 frac) so the next read is at index 1.
slot->posFp = (uint64_t)1u << 16;
if (haveCarry) {
// Bridge the chunk boundary: start at index 0 so the first
// read interpolates streamBuf[0] (old last sample) -> [1]
// (new first sample), replaying one sample with no audible
// discontinuity click.
slot->posFp = 0;
} else {
// First prime: streamBuf[0] is a synthetic zero, not a real
// sample. Skip it so it isn't played as a leading sample.
slot->posFp = (uint64_t)1u << 16;
}
return true;
}
@ -39,10 +73,14 @@ static bool streamRefill(AudioSfxSlotT *slot) {
// `frac` is the 16-bit fractional part of pos; 0 = exactly at s0,
// 0xFFFF = nearly s1. Result is a signed int in [-128, +127].
static int interp(int8_t s0, int8_t s1, uint32_t frac) {
int delta;
int32_t delta;
delta = (int)s1 - (int)s0;
return (int)s0 + ((delta * (int)frac) >> 16);
// delta (up to +/-255) * frac (up to 65535) is ~16.7M -- needs the
// explicit 32-bit width so it can't overflow a 16-bit int. The
// >> 16 of a possibly-negative int32_t relies on arithmetic shift,
// which holds on every target this file builds for (DOS/ST).
delta = (int32_t)s1 - (int32_t)s0;
return (int)s0 + (int)((delta * (int32_t)frac) >> 16);
}

View file

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

View file

@ -9,7 +9,7 @@
// compaction that moves a slot's bytes is transparent to callers.
//
// Allocations go through plain malloc; on every supported port
// (IIgs ORCA, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
// (IIgs, Amiga libnix, ST mintlib, DJGPP+CWSDPMI) the heap is
// readable, writable, AND executable. We are not running in a W^X
// environment.
//

View file

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

View file

@ -21,43 +21,22 @@
// On overflow the fill silently truncates rather than crashing.
#define FLOOD_STACK_SIZE 512
// Sentinel extents for an empty accumulated dirty bounding box. min* seed
// above any valid coordinate and max* below any valid coordinate so the
// first plotted point sets every edge, and a box that never receives an
// on-surface point collapses to a no-op after clamping (min > max).
#define DIRTY_BOX_MIN_INIT ((int16_t)0x7FFF)
#define DIRTY_BOX_MAX_INIT ((int16_t)0x8000)
// ----- Prototypes -----
static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible);
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex);
static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uint8_t newColor, uint8_t matchColor, bool matchEqual);
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex);
// ----- Internal helpers (alphabetical) -----
// Clip a rectangle to the surface bounds. Outputs *outVisible = false
// if the rect is entirely off-surface.
static void clipRect(int16_t *x, int16_t *y, int16_t *w, int16_t *h, bool *outVisible) {
if (*w <= 0 || *h <= 0) {
*outVisible = false;
return;
}
if (*x < 0) {
*w += *x;
*x = 0;
}
if (*y < 0) {
*h += *y;
*y = 0;
}
if (*x >= SURFACE_WIDTH || *y >= SURFACE_HEIGHT) {
*outVisible = false;
return;
}
if (*x + *w > SURFACE_WIDTH) {
*w = SURFACE_WIDTH - *x;
}
if (*y + *h > SURFACE_HEIGHT) {
*h = SURFACE_HEIGHT - *y;
}
*outVisible = (*w > 0 && *h > 0);
}
static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
uint8_t nibble = colorIndex & 0x0F;
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
@ -67,9 +46,16 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
uint16_t midBytes;
uint8_t *line;
// Planar ports have NULL s->pixels; the planar dual-write hook
// handles the actual plane fill. Guarding here keeps the chunky
// fallback from dereferencing NULL when a port has no chunky store.
if (s->pixels == NULL) {
return;
}
/* px* and midBytes are uint16_t (clipped values are non-negative)
* so `>>1` lowers to a single LSR instead of ORCA-C's
* ~SSHIFTRIGHT helper. Same with `<<1` for midBytes. */
* so `>>1` lowers to a single LSR instead of a signed-shift
* helper. Same with `<<1` for midBytes. */
for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x;
@ -93,6 +79,23 @@ static void fillRectClipped(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int1
}
// Fill an axis-aligned span that the CALLER has already proven to be
// fully on-surface (x >= 0, y >= 0, x + w <= SURFACE_WIDTH, y + h <=
// SURFACE_HEIGHT, w > 0, h > 0). Identical tail to jlFillRect but with
// the 32-bit clip elided -- on an on-surface rect that clip is a
// no-op, so callers that already hold the on-surface guarantee (rect
// outlines, circle scanlines, axis-aligned lines) avoid re-deriving
// it per span. Pixel output and dirty-mark word bands are bit-for-bit
// identical to routing the same span through jlFillRect.
static void fillRectOnSurface(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
if (!halFastFillRect(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex)) {
fillRectClipped(s, x, y, w, h, colorIndex);
}
halFillRectPlanes(s, x, y, (uint16_t)w, (uint16_t)h, colorIndex);
surfaceMarkDirtyRect(s, x, y, w, h);
}
// Smith's scanline flood fill. Implements both the unbounded and the
// boundary-stopped variants in one pass: the matching predicate is
// (pixel == matchColor) when matchEqual is true (unbounded jlFloodFill,
@ -164,6 +167,12 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
stackX, stackY,
&sp, FLOOD_STACK_SIZE,
&seedMatched, &leftX, &rightX)) {
// The asm filled the span internally but does not mark
// dirty -- that is the C wrapper's job. Mark the just-
// filled run so jlStagePresent blits it.
if (seedMatched) {
surfaceMarkDirtyRect(s, leftX, y, (int16_t)(rightX - leftX + 1), 1);
}
continue;
}
}
@ -224,20 +233,17 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
}
}
// Fill the span. Bypass jlFillRect's clipping wrapper: walk-out
// already guaranteed leftX/rightX are in [0..SURFACE_WIDTH-1]
// and the seed-pop bounds check did the same for y. We DO
// need the planar dual-write (which jlFillRect's wrapper would
// call), so invoke halFillRectPlanes explicitly after the
// chunky span fill -- otherwise PLANAR_PRESENT builds (and,
// post-Phase-9, every build) display flood-filled regions
// as the unfilled background.
// Fill the span. walk-out already guaranteed leftX/rightX are in
// [0..SURFACE_WIDTH-1] and the seed-pop bounds check did the same
// for y, so the span is provably on-surface -- route it through
// the shared on-surface tail (chunky fill + planar dual-write +
// dirty mark) rather than re-clipping through jlFillRect. The
// planar dual-write here is load-bearing: without it
// PLANAR_PRESENT builds (and, post-Phase-9, every build) display
// flood-filled regions as the unfilled background.
{
int16_t spanW = (int16_t)(rightX - leftX + 1);
if (!halFastFillRect(s, leftX, y, (uint16_t)spanW, 1, newNibble)) {
fillRectClipped(s, leftX, y, spanW, 1, newNibble);
}
halFillRectPlanes(s, leftX, y, (uint16_t)spanW, 1, newNibble);
fillRectOnSurface(s, leftX, y, spanW, 1, newNibble);
}
// Scan rows above and below for run boundaries. The hot
@ -320,6 +326,36 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
}
// Plot a single clipped pixel WITHOUT marking the dirty rect. Shared by
// the public jlDrawPixel (which marks per call afterward) and the
// Bresenham line/circle fallbacks (which accumulate one bounding box and
// mark it once after their loop). Mirrors jlDrawPixel's NULL + bounds
// check, halFastDrawPixel dispatch, and chunky nibble RMW fallback.
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
uint8_t *byte;
uint8_t nibble;
if (s == NULL) {
return;
}
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return;
}
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex) && s->pixels != NULL) {
/* Cast to uint16_t before shift -- already validated x >= 0,
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
nibble = colorIndex & 0x0F;
if (x & 1) {
*byte = (uint8_t)((*byte & 0xF0) | nibble);
} else {
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
}
}
}
// ----- Public API (alphabetical) -----
void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
@ -327,6 +363,10 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t y;
int16_t err;
int16_t ir;
int16_t minX;
int16_t minY;
int16_t maxX;
int16_t maxY;
if (s == NULL) {
return;
@ -338,34 +378,57 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// Fast path: when the bounding circle is fully on-surface we can
// hand off to the port asm (no per-pixel bounds check needed in
// the inner loop) and mark the bounding box dirty once.
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
halFastDrawCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
return;
// the inner loop) and mark the bounding box dirty once. A circle
// with r >= SURFACE_HEIGHT can never fit on-surface, and casting
// such an r to int16_t can wrap negative (r > 32767), so reject it
// here before the cast feeds the fast path / dirty-rect extents.
if (r < SURFACE_HEIGHT) {
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
halFastDrawCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
return;
}
}
// Bresenham midpoint: maintain (x, y) on the perimeter, eight-
// octant symmetry plots all 8 reflections each iteration. Routes
// through jlDrawPixel so off-surface pixels clip individually.
// octant symmetry plots all 8 reflections each iteration. Plots
// through plotPixelNoMark so off-surface pixels clip individually
// (same per-pixel clip as jlDrawPixel) without paying the
// cross-segment dirty mark per pixel; one accumulated bounding box
// is marked after the loop instead.
minX = DIRTY_BOX_MIN_INIT;
minY = DIRTY_BOX_MIN_INIT;
maxX = DIRTY_BOX_MAX_INIT;
maxY = DIRTY_BOX_MAX_INIT;
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
jlDrawPixel(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
jlDrawPixel(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
jlDrawPixel(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
jlDrawPixel(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
// The 8 octants span columns cx +/- x and cx +/- y, and rows
// cy +/- y and cy +/- x. Fold all reflections into the box in
// one shot, then clamp once after the loop.
if (cx + x > maxX) { maxX = (int16_t)(cx + x); }
if (cx - x < minX) { minX = (int16_t)(cx - x); }
if (cx + y > maxX) { maxX = (int16_t)(cx + y); }
if (cx - y < minX) { minX = (int16_t)(cx - y); }
if (cy + x > maxY) { maxY = (int16_t)(cy + x); }
if (cy - x < minY) { minY = (int16_t)(cy - x); }
if (cy + y > maxY) { maxY = (int16_t)(cy + y); }
if (cy - y < minY) { minY = (int16_t)(cy - y); }
plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy + y), colorIndex);
plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy + y), colorIndex);
plotPixelNoMark(s, (int16_t)(cx + x), (int16_t)(cy - y), colorIndex);
plotPixelNoMark(s, (int16_t)(cx - x), (int16_t)(cy - y), colorIndex);
plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy + x), colorIndex);
plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy + x), colorIndex);
plotPixelNoMark(s, (int16_t)(cx + y), (int16_t)(cy - x), colorIndex);
plotPixelNoMark(s, (int16_t)(cx - y), (int16_t)(cy - x), colorIndex);
y++;
/* Use `+ + 1` instead of `2 * y + 1` so ORCA-C never emits
* the ~SMUL2 helper -- two ADDs are unconditionally cheaper. */
/* Use `+ + 1` instead of `2 * y + 1` so the compiler never emits
* a multiply helper -- two ADDs are unconditionally cheaper. */
if (err <= 0) {
err = (int16_t)(err + y + y + 1);
} else {
@ -373,6 +436,17 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
err = (int16_t)(err + y + y - x - x + 1);
}
}
// Clamp the accumulated box to the surface and mark it once. If the
// circle was entirely off-surface no pixel was plotted, so minX/minY
// stay above maxX/maxY (after clamping) and nothing is marked.
if (minX < 0) { minX = 0; }
if (minY < 0) { minY = 0; }
if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
if (minX <= maxX && minY <= maxY) {
surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
}
}
@ -384,13 +458,18 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
int16_t err;
int16_t e2;
int16_t tmp;
int16_t minX;
int16_t minY;
int16_t maxX;
int16_t maxY;
int32_t span;
if (s == NULL) {
return;
}
// Horizontal and vertical fast paths use jlFillRect; the general
// case Bresenham routes per-pixel through jlDrawPixel so per-pixel
// case Bresenham plots per-pixel through plotPixelNoMark so per-pixel
// off-surface clipping just works.
if (y0 == y1) {
if (x0 > x1) {
@ -398,7 +477,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
x0 = x1;
x1 = tmp;
}
jlFillRect(s, x0, y0, (uint16_t)(x1 - x0 + 1), 1, colorIndex);
// Compute the span in 32-bit so far-apart off-surface endpoints
// (e.g. x0=-20000, x1=20000) do not overflow the 16-bit int the
// subtraction would otherwise promote to on a 16-bit-int target;
// clamp to the surface width so the downstream uint16_t narrowing
// is safe.
span = (int32_t)x1 - (int32_t)x0 + 1;
if (span > SURFACE_WIDTH) {
span = SURFACE_WIDTH;
}
// On-surface (the common case) skips jlFillRect's 32-bit clip:
// span is already <= SURFACE_WIDTH and x0 <= x1, so the row is
// fully on-surface exactly when its left edge and y are. Off-
// surface still routes through jlFillRect for correct clipping.
if (x0 >= 0 && y0 >= 0 && y0 < SURFACE_HEIGHT &&
(int32_t)x0 + span <= SURFACE_WIDTH) {
fillRectOnSurface(s, x0, y0, (int16_t)span, 1, colorIndex);
} else {
jlFillRect(s, x0, y0, (uint16_t)span, 1, colorIndex);
}
return;
}
if (x0 == x1) {
@ -407,7 +504,16 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
y0 = y1;
y1 = tmp;
}
jlFillRect(s, x0, y0, 1, (uint16_t)(y1 - y0 + 1), colorIndex);
span = (int32_t)y1 - (int32_t)y0 + 1;
if (span > SURFACE_HEIGHT) {
span = SURFACE_HEIGHT;
}
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 &&
(int32_t)y0 + span <= SURFACE_HEIGHT) {
fillRectOnSurface(s, x0, y0, 1, (int16_t)span, colorIndex);
} else {
jlFillRect(s, x0, y0, 1, (uint16_t)span, colorIndex);
}
return;
}
@ -426,13 +532,25 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
return;
}
// Diagonal fallback: plot through plotPixelNoMark so each pixel still
// clips off-surface individually (matching jlDrawPixel), but skip the
// per-pixel cross-segment dirty mark. Accumulate one bounding box over
// the plotted points and mark it once after the loop.
minX = DIRTY_BOX_MIN_INIT;
minY = DIRTY_BOX_MIN_INIT;
maxX = DIRTY_BOX_MAX_INIT;
maxY = DIRTY_BOX_MAX_INIT;
dx = (int16_t)((x1 > x0) ? (x1 - x0) : (x0 - x1));
dy = (int16_t)(-((y1 > y0) ? (y1 - y0) : (y0 - y1)));
sx = (int16_t)((x0 < x1) ? 1 : -1);
sy = (int16_t)((y0 < y1) ? 1 : -1);
err = (int16_t)(dx + dy);
while (1) {
jlDrawPixel(s, x0, y0, colorIndex);
if (x0 < minX) { minX = x0; }
if (x0 > maxX) { maxX = x0; }
if (y0 < minY) { minY = y0; }
if (y0 > maxY) { maxY = y0; }
plotPixelNoMark(s, x0, y0, colorIndex);
if (x0 == x1 && y0 == y1) {
break;
}
@ -446,32 +564,55 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
y0 = (int16_t)(y0 + sy);
}
}
// Clamp the accumulated box to the surface and mark it once. A line
// entirely off-surface still ran the loop (so the box holds its raw
// extents); clamping collapses it to a no-op when nothing landed on
// the surface.
if (minX < 0) { minX = 0; }
if (minY < 0) { minY = 0; }
if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
if (maxY > SURFACE_HEIGHT - 1) { maxY = SURFACE_HEIGHT - 1; }
if (minX <= maxX && minY <= maxY) {
surfaceMarkDirtyRect(s, minX, minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
}
}
void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
uint8_t *byte;
uint8_t nibble;
if (s == NULL) {
return;
}
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
#ifdef JOEYLIB_PLATFORM_IIGS
// Hot per-pixel path: the asm plot (halFastDrawPixel -> iigsDrawPixelInner)
// plus an inline dirty mark, with NO plotPixelNoMark / surfaceMarkDirtyRect
// call layers. NB: an all-C inline RMW here (using the gRowOffsetLut table,
// no multiply) was measured SLOWER on hardware (1875 -> 1672 ops/sec) --
// the far-pointer code for s->pixels[...] costs more than the
// cross-segment JSL into the hand-tuned asm, so the asm plot stays. The
// single-row dirty mark is widened inline, only for the stage.
if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
return;
}
(void)halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
if (s == jlStageGet()) {
uint8_t word = SURFACE_WORD_INDEX(x);
if (!halFastDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex)) {
/* Cast to uint16_t before shift -- already validated x >= 0,
* so unsigned semantics match. Avoids ~SSHIFTRIGHT helper. */
byte = &s->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
nibble = colorIndex & 0x0F;
if (x & 1) {
*byte = (uint8_t)((*byte & 0xF0) | nibble);
} else {
*byte = (uint8_t)((*byte & 0x0F) | (nibble << 4));
if (word < gStageMinWord[y]) {
gStageMinWord[y] = word;
}
if (word > gStageMaxWord[y]) {
gStageMaxWord[y] = word;
}
}
#else
plotPixelNoMark(s, x, y, colorIndex);
// Public single-pixel path keeps the original per-call dirty mark
// with the raw (x, y); only the Bresenham fallbacks batch the mark
// into one accumulated box. plotPixelNoMark already no-op'd the
// pixel itself if it was off-surface.
surfaceMarkDirtyRect(s, x, y, 1, 1);
#endif
}
@ -489,6 +630,26 @@ void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
jlFillRect(s, x, y, w, h, colorIndex);
return;
}
// Fast path: when the OUTER rect is fully on-surface every one of
// the four edge spans is also fully on-surface, so each can skip
// jlFillRect's 32-bit clip and route straight through the shared
// on-surface tail. Test in 32-bit -- w/h are uint16_t, so x + w can
// exceed int16_t range. Output is identical because on an
// on-surface span jlFillRect's clip is a no-op.
if (x >= 0 && y >= 0 &&
(int32_t)x + (int32_t)w <= SURFACE_WIDTH &&
(int32_t)y + (int32_t)h <= SURFACE_HEIGHT) {
// Top edge.
fillRectOnSurface(s, x, y, (int16_t)w, 1, colorIndex);
// Bottom edge.
fillRectOnSurface(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex);
// Left edge (interior only -- top and bottom corners already drawn).
fillRectOnSurface(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
// Right edge (interior only).
fillRectOnSurface(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
return;
}
// General (possibly clipped) case: let jlFillRect clip each edge.
// Top edge.
jlFillRect(s, x, y, w, 1, colorIndex);
// Bottom edge.
@ -504,9 +665,10 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t y;
int16_t x;
int16_t ir;
uint16_t xx;
uint16_t yy;
uint16_t r2;
uint32_t xx;
uint32_t yy;
uint32_t r2;
bool onSurface;
if (s == NULL) {
return;
@ -516,26 +678,39 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
return;
}
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT &&
halFastFillCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(uint16_t)(2 * ir + 1), (uint16_t)(2 * ir + 1));
return;
// Fast path: a circle with r >= SURFACE_HEIGHT can never fit fully
// on-surface, and casting such an r to int16_t can wrap negative
// (r > 32767), spuriously passing the bounds test and feeding the
// asm / dirty-rect garbage. Reject it here before the cast.
onSurface = false;
if (r < SURFACE_HEIGHT) {
ir = (int16_t)r;
if (cx - ir >= 0 && cx + ir < SURFACE_WIDTH &&
cy - ir >= 0 && cy + ir < SURFACE_HEIGHT) {
// The whole bounding circle is on-surface, so every scanline
// span below is too. Remember this so the C fallback can
// route spans through the lighter on-surface fill instead of
// re-clipping each one through jlFillRect.
onSurface = true;
if (halFastFillCircle(s, cx, cy, r, colorIndex)) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir),
(int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
return;
}
}
}
// For each y from 0 to r, find the largest x such that x*x + y*y
// <= r*r and emit a horizontal span. Maintain xx=x*x, yy=y*y
// incrementally so the hot loop never does a 32-bit multiply --
// critical on 65816 / 68000 / 286 where mul is slow or absent.
// (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. r is uint16_t
// so xx, yy, r2 fit in uint16_t for any r where x*x+y*y can equal
// r2 (i.e. r <= 255 -> r2 <= 65025).
/* Same `+ +` pattern as jlDrawCircle so ORCA-C doesn't emit ~SMUL2 /
* ~CUMUL2 helpers for the `2 * ...` constants. spanWidth is hoisted
// (y+1)^2 = y^2 + 2y + 1; (x-1)^2 = x^2 - 2x + 1. xx, yy and r2 are
// uint32_t so r*r does not wrap for r >= 256 (a uint16_t product
// overflows at r == 256 -> r2 == 0).
/* Same `+ +` pattern as jlDrawCircle so the compiler doesn't emit
* multiply helpers for the `2 * ...` constants. spanWidth is hoisted
* because both jlFillRect calls in the body need it. */
xx = (uint16_t)(r * r);
xx = (uint32_t)r * (uint32_t)r;
r2 = xx;
yy = 0;
x = (int16_t)r;
@ -543,15 +718,25 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
uint16_t spanWidth;
while (xx + yy > r2) {
xx = (uint16_t)(xx - (uint16_t)((uint16_t)x + (uint16_t)x - 1u));
xx = xx - (uint32_t)((uint16_t)x + (uint16_t)x - 1u);
x--;
}
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex);
if (y > 0) {
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex);
if (onSurface) {
// 0 <= cy +/- y <= cy + ir < SURFACE_HEIGHT and
// 0 <= cx - x ... cx + x < SURFACE_WIDTH for every span, so
// jlFillRect's clip would be a no-op -- skip it.
fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy + y), (int16_t)spanWidth, 1, colorIndex);
if (y > 0) {
fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy - y), (int16_t)spanWidth, 1, colorIndex);
}
} else {
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy + y), spanWidth, 1, colorIndex);
if (y > 0) {
jlFillRect(s, (int16_t)(cx - x), (int16_t)(cy - y), spanWidth, 1, colorIndex);
}
}
yy = (uint16_t)(yy + (uint16_t)((uint16_t)y + (uint16_t)y + 1u));
yy = yy + (uint32_t)((uint16_t)y + (uint16_t)y + 1u);
}
}
@ -561,25 +746,40 @@ void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
int16_t sy;
int16_t sw;
int16_t sh;
bool visible;
int32_t left;
int32_t top;
int32_t right;
int32_t bottom;
if (s == NULL) {
return;
}
sx = x;
sy = y;
sw = (int16_t)w;
sh = (int16_t)h;
clipRect(&sx, &sy, &sw, &sh, &visible);
if (!visible) {
// Clip in 32-bit so any int16_t x/y combined with any uint16_t w/h
// is handled without overflow (a uint16_t w > 32767 would wrap
// negative if narrowed to int16_t before clipping, and a negative x
// with a large w must still fill out to the right surface edge).
// The surface is only 320x200, so the clipped result always fits
// int16_t.
left = (int32_t)x;
top = (int32_t)y;
right = left + (int32_t)w;
bottom = top + (int32_t)h;
if (left < 0) { left = 0; }
if (top < 0) { top = 0; }
if (right > SURFACE_WIDTH) { right = SURFACE_WIDTH; }
if (bottom > SURFACE_HEIGHT) { bottom = SURFACE_HEIGHT; }
if (right <= left || bottom <= top) {
return;
}
if (!halFastFillRect(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex)) {
fillRectClipped(s, sx, sy, sw, sh, colorIndex);
}
halFillRectPlanes(s, sx, sy, (uint16_t)sw, (uint16_t)sh, colorIndex);
surfaceMarkDirtyRect(s, sx, sy, sw, sh);
sx = (int16_t)left;
sy = (int16_t)top;
sw = (int16_t)(right - left);
sh = (int16_t)(bottom - top);
// The clipped rect is now provably on-surface; share the fill +
// planar dual-write + dirty-mark tail with the other on-surface
// span emitters so there is one source of truth for it.
fillRectOnSurface(s, sx, sy, sw, sh, colorIndex);
}
@ -649,7 +849,7 @@ void jlSurfaceClear(jlSurfaceT *s, uint8_t colorIndex) {
}
nibble = colorIndex & 0x0F;
doubled = (uint8_t)((nibble << 4) | nibble);
if (!halFastSurfaceClear(s, doubled)) {
if (!halFastSurfaceClear(s, doubled) && s->pixels != NULL) {
memset(s->pixels, doubled, SURFACE_PIXELS_SIZE);
}
surfaceMarkDirtyAll(s);

View file

@ -191,8 +191,12 @@ uint16_t halFrameHz(void);
// Wall-clock millisecond counter since halInit. Source of truth
// for time-based scheduling (sound event timing, animation cadence
// on platforms where rendering can outpace VBL). Per-port impl
// uses the cheapest sub-frame timer available on that hardware.
// on platforms where rendering can outpace VBL). Per-port impl uses
// the cheapest sub-frame timer available on that hardware: DOS uses
// uclock() (PIT counter 0, ~1 us resolution); ST reads the 200 Hz
// Timer C counter at _hz_200; Amiga uses ReadEClock (~1.4 us PAL);
// IIgs falls back to frame_count * 1000 / 60 since pulling in time()
// bursts the stdio cluster bank.
uint32_t halMillisElapsed(void);
// Audio: per-port engine setup, module + SFX playback, teardown.
@ -234,7 +238,7 @@ void halAudioFrameTick(void);
// caller should fall back to the platform-agnostic C implementation.
//
// Funneling all asm dispatches through hal.c (one TU per port) avoids
// the cumulative ORCA Linker "Expression too complex" failure that
// the cumulative linker "Expression too complex" failure that
// hits when multiple cross-platform TUs each call into a named load
// segment full of asm primitives. Cross-platform code in src/core/
// only ever calls into HAL, so the link-time expression cost is paid
@ -358,7 +362,7 @@ bool halFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, i
//
// Cross-platform code in src/core/*.c is unchanged -- it still calls
// halFastDrawPixel(...) etc. On IIgs the preprocessor swaps that for
// the macro expansion before ORCA-C compiles the file. The matching
// the macro expansion before the file is compiled. The matching
// halFast* C definitions in src/port/iigs/hal.c are deleted, since
// nothing references them once the macros take effect.
//
@ -371,6 +375,7 @@ extern void iigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uin
extern void iigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
extern void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord);
extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t fillWord);
extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
@ -416,7 +421,7 @@ extern uint16_t gFloodRightX;
#undef halFastSurfaceClear
#define halFastSurfaceClear(_s, _d) \
((_s) == jlStageGet() \
? (iigsSurfaceClearInner((_s)->pixels, \
? (iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))), \
true) \
: false)
@ -438,7 +443,7 @@ extern uint16_t gFloodRightX;
// Tile primitives operate on caller-computed row pointers; just
// forward the args. by/bx are tile coords -> bx*4 + by*8*160 byte
// offset within the surface. Use SURFACE_ROW_OFFSET (LUT lookup) to
// dodge ORCA-C 2.2.1's ~CUMUL2 helper for the *160 multiply.
// dodge a software multiply helper for the *160 multiply.
#undef halFastTileFill
#define halFastTileFill(_s, _bx, _by, _fw) \
(iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
@ -461,8 +466,8 @@ extern uint16_t gFloodRightX;
// Tier 2/3 flood fallbacks always returned false on IIgs (the asm
// impls were deleted as unreachable). Macros to constant false so
// ORCA-C dead-code-eliminates the never-taken fallback branches in
// floodFillInternal.
// the compiler dead-code-eliminates the never-taken fallback branches
// in floodFillInternal.
#undef halFastFloodWalk
#define halFastFloodWalk(_row, _sx, _mc, _nc, _me, _sm, _lx, _rx) (false)
@ -494,6 +499,63 @@ extern uint16_t gFloodRightX;
*(_rxOut) = (int16_t)gFloodRightX, \
true)
// =====================================================================
// IIgs planar dual-write elision.
//
// Every cross-platform primitive calls a halXxxPlanes() hook
// unconditionally so planar ports (Amiga, ST) can mirror each draw
// into their bitplanes. IIgs is chunky-native, so those hooks are
// pure no-op stubs -- but the call site still costs a cross-segment
// JSL per primitive (PHB/PHD setup + RTL) for zero work. These macros
// expand each call to ((void)0) at preprocess time so the call site is
// deleted entirely, exactly as the halFast* macros above eliminate the
// wrapper layer. The matching no-op stub definitions in
// src/port/iigs/hal.c are deleted, since nothing references them once
// the macros take effect.
//
// Gated on JOEYLIB_PLATFORM_IIGS specifically (NOT JOEYLIB_NATIVE_CHUNKY):
// DOS is also chunky-native and keeps its real stub functions, because
// its stubs are cheap near-calls and DOS is not the perf reference.
//
// Arg lists match each hook's arity so the preprocessor parses and
// discards the exact call expression; arguments with side effects are
// not present at any halXxxPlanes call site (all args are plain lvalues
// / field reads), so dropping them changes no behavior.
// =====================================================================
#undef halFillRectPlanes
#define halFillRectPlanes(_s, _x, _y, _w, _h, _c) ((void)0)
#undef halSurfaceCopyPlanes
#define halSurfaceCopyPlanes(_dst, _src) ((void)0)
#undef halTileFillPlanes
#define halTileFillPlanes(_s, _bx, _by, _c) ((void)0)
#undef halTileCopyPlanes
#define halTileCopyPlanes(_dst, _dbx, _dby, _src, _sbx, _sby) ((void)0)
#undef halTileCopyMaskedPlanes
#define halTileCopyMaskedPlanes(_dst, _dbx, _dby, _src, _sbx, _sby, _t) ((void)0)
#undef halTilePastePlanes
#define halTilePastePlanes(_dst, _bx, _by, _ct) ((void)0)
#undef halTilePasteMonoPlanes
#define halTilePasteMonoPlanes(_dst, _bx, _by, _mt, _fg, _bg) ((void)0)
#undef halTileSnapPlanes
#define halTileSnapPlanes(_src, _bx, _by, _out) ((void)0)
#undef halSpriteDrawPlanes
#define halSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
#undef halSpriteSavePlanes
#define halSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#undef halSpriteRestorePlanes
#define halSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
#endif /* JOEYLIB_PLATFORM_IIGS */
#endif

View file

@ -15,13 +15,16 @@
// 8 KB fits the largest typical sprite working set (~3-4 KB per
// 32x32 sprite at all opaque) and keeps malloc requests small enough
// for IIgs ORCA-C's small-memory-model heap to satisfy them.
// for the IIgs heap to satisfy them.
#define DEFAULT_CODEGEN_BYTES (8u * 1024u)
// ----- Prototypes -----
static void clearError(void);
static void setError(const char *message);
// coreSetError is the shared error setter (declared in surfaceInternal.h
// and called by surface.c etc.); its definition lives here next to the
// rest of the error-reporting state.
// ----- Module state -----
@ -36,7 +39,7 @@ static void clearError(void) {
}
static void setError(const char *message) {
void coreSetError(const char *message) {
gLastError = message;
}
@ -47,12 +50,12 @@ bool jlInit(const jlConfigT *config) {
clearError();
if (gInitialized) {
setError("jlInit called while already initialized");
coreSetError("jlInit called while already initialized");
return false;
}
if (config == NULL) {
setError("jlInit called with NULL config");
coreSetError("jlInit called with NULL config");
return false;
}
@ -63,19 +66,19 @@ bool jlInit(const jlConfigT *config) {
// SHR setup that halInit performs.
if (!halInit(&gConfig)) {
const char *halMsg = halLastError();
setError(halMsg != NULL ? halMsg : "halInit failed");
coreSetError(halMsg != NULL ? halMsg : "halInit failed");
return false;
}
if (!stageAlloc()) {
setError("failed to allocate stage surface");
coreSetError("failed to allocate stage surface");
halShutdown();
return false;
}
if (!codegenArenaInit(gConfig.codegenBytes != 0 ? gConfig.codegenBytes
: DEFAULT_CODEGEN_BYTES)) {
setError("failed to allocate codegen arena");
coreSetError("failed to allocate codegen arena");
stageFree();
halShutdown();
return false;

View file

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

View file

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

View file

@ -10,6 +10,14 @@
#include "joey/palette.h"
#include "surfaceInternal.h"
// jlPaletteSet / jlPaletteGet reach palette row N via byte-pointer math
// + a single `<< 5` shift (= 32 bytes per row) to avoid a per-access
// multiply helper. That stride is only correct while a palette row is
// exactly SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t) bytes; pin the
// relationship so a future palette-width change fails to compile here
// rather than silently reading the wrong row.
typedef char paletteRowStrideMatchesShift[((1u << 5) == (SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t))) ? 1 : -1];
// Standard 16-color EGA palette in IIgs $0RGB format. Used as the
// per-surface default at allocation time (paletteInitDefault) so a
@ -45,8 +53,14 @@ void paletteInitDefault(jlSurfaceT *s) {
if (s == NULL) {
return;
}
// kDefaultPaletteEga is already valid $0RGB with color 0 == 0x0000,
// so copy it directly into each palette row instead of routing
// through jlPaletteSet 16 times (which would re-mask every entry
// and re-run the stage-dirty check on a surface that is not yet
// the stage). gStagePaletteDirty already defaults to true so the
// default palette is still uploaded on first present.
for (i = 0; i < SURFACE_PALETTE_COUNT; i++) {
jlPaletteSet(s, i, kDefaultPaletteEga);
memcpy(s->palette[i], kDefaultPaletteEga, sizeof(kDefaultPaletteEga));
}
}
@ -62,7 +76,7 @@ void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16) {
if (paletteIndex >= SURFACE_PALETTE_COUNT) {
return;
}
/* Byte-pointer math + shift to skip the ~MUL4 helper -- see
/* Byte-pointer math + shift to skip the multiply helper -- see
* jlPaletteSet for the reasoning. */
row = (const uint16_t *)((const uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
memcpy(out16, row, SURFACE_COLORS_PER_PALETTE * sizeof(uint16_t));
@ -82,10 +96,10 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16)
}
/* Compute the row pointer via byte-pointer math + a single shift
* (16 entries * 2 bytes = 32 = 1 << 5) so ORCA-C doesn't emit a
* ~MUL4 helper for the 2D-array indexing. Then walk both arrays
* with post-increment pointers so the inner loop avoids ~MUL4
* for every `row[i]` / `colors16[i]` index multiply too. */
* (16 entries * 2 bytes = 32 = 1 << 5) so the 2D-array indexing
* avoids a multiply helper. Then walk both arrays with
* post-increment pointers so the inner loop avoids the index
* multiply for every `row[i]` / `colors16[i]` too. */
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
src = colors16;

View file

@ -1,8 +1,8 @@
// Deterministic, seedable PRNG (xorshift32 / Marsaglia).
//
// stdlib rand() is unsuitable: DJGPP, ORCA-C, libnix, and mintlib all
// ship different LCG constants, and ORCA-C's rand() returns a 16-bit
// value while the others return 31 bits. Any game logic relying on
// stdlib rand() is unsuitable: the per-platform C libraries ship
// different LCG constants and differing result widths (some return a
// 16-bit value, others 31 bits). Any game logic relying on
// reproducible sequences (replay, demo records, level seeds, deck
// shuffles) would diverge silently. xorshift32 has no multiply, so
// the 65816 perf floor isn't penalized by a software mul routine.
@ -32,7 +32,12 @@ uint16_t jlRandomRange(uint16_t bound) {
if (bound == 0u) {
return 0u;
}
return (uint16_t)(jlRandom() % bound);
// Multiply-high map over the top 16 (best-mixed) bits of the
// xorshift state. Divide-free -- the 65816/68000 lack a hardware
// divide, so plain `% bound` would pull in a software divide
// routine the multiply-free PRNG was chosen to avoid. Residual
// bias is the same negligible order as modulo.
return (uint16_t)(((uint32_t)(jlRandom() >> 16) * (uint32_t)bound) >> 16);
}

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

View file

@ -8,16 +8,15 @@
#include <string.h>
#include "joey/sprite.h"
#include "joey/tile.h"
#include "codegenArenaInternal.h"
#include "hal.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"
// 8x8 tiles, 4bpp packed = 4 bytes/row * 8 rows = 32 bytes/tile.
#define TILE_BYTES 32
#define TILE_PIXELS 8
#define TILE_BYTES_PER_ROW 4
// 8x8 tile geometry constants come from joey/tile.h -- one source of
// truth for the 4bpp packed layout (32 bytes/tile, 4 bytes/row, 8 px/side).
// Color 0 is always transparent for sprites (DESIGN.md contract).
#define TRANSPARENT_NIBBLE 0
@ -43,10 +42,10 @@
// ----- Prototypes -----
static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY);
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble);
static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY, int16_t *w, int16_t *h);
static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t heightPx);
static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
@ -85,34 +84,6 @@ static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY,
}
// Sample one source pixel from the sprite at (pxX, pxY) where (0,0)
// is the sprite's top-left tile. The tile data is laid out tile by
// tile in row-major order, each tile internally row-major with 4
// bytes per row. High nibble = left pixel within each byte.
static uint8_t srcNibble(const jlSpriteT *sp, int16_t pxX, int16_t pxY) {
int16_t tileX;
int16_t tileY;
int16_t inTileX;
int16_t inTileY;
uint32_t byteOff;
uint8_t byte;
const uint8_t *tile;
tileX = (int16_t)(pxX / TILE_PIXELS);
tileY = (int16_t)(pxY / TILE_PIXELS);
inTileX = (int16_t)(pxX & (TILE_PIXELS - 1));
inTileY = (int16_t)(pxY & (TILE_PIXELS - 1));
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
byteOff = (uint32_t)(inTileY * TILE_BYTES_PER_ROW + (inTileX >> 1));
byte = tile[byteOff];
if (inTileX & 1) {
return (uint8_t)(byte & 0x0F);
}
return (uint8_t)(byte >> 4);
}
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble) {
uint8_t *byte;
@ -139,9 +110,34 @@ static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t he
}
// Initialize a freshly-allocated jlSpriteT's fields to the
// uncompiled/default state. routineOffsets is filled with 0xFF so
// every (shift, op) entry reads as SPRITE_NOT_COMPILED (0xFFFF); 0
// would be a VALID offset and could dispatch into stale arena bytes.
static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData) {
sp->tileData = tileData;
sp->widthTiles = widthTiles;
sp->heightTiles = heightTiles;
sp->ownsTileData = ownsTileData;
sp->slot = NULL;
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
}
// Pixel-by-pixel draw. Used directly when the sprite has no compiled
// slot, and as the clip-edge fallback when a compiled draw would
// extend off the surface.
//
// The source sprite is tile-major (8x8 tiles, 4 bytes/row, consecutive
// tile columns 32 bytes apart). Rather than recompute tile/in-tile
// coordinates per pixel (the old srcNibble did two divisions + a
// multiply EVERY pixel), this walks one source-tile-row pointer that
// advances by TILE_BYTES each time the column crosses an 8-pixel tile
// boundary, so the inner loop is a byte read + nibble extract with no
// division. ~5x on the interpreted path (the every-edge-clip fallback).
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
int16_t dx;
int16_t dy;
@ -150,17 +146,16 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
int16_t w;
int16_t h;
int16_t row;
int16_t col;
uint8_t nibble;
uint8_t *dstRow;
uint8_t wTiles;
dx = x;
dy = y;
w = (int16_t)(sp->widthTiles * TILE_PIXELS);
h = (int16_t)(sp->heightTiles * TILE_PIXELS);
w = (int16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
h = (int16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) {
return;
}
wTiles = sp->widthTiles;
/* Skip the chunky write loop on planar ports (s->pixels == NULL).
* halSpriteDrawPlanes is called by the jlSpriteDraw caller and does
@ -168,13 +163,40 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
* happen there. Phase 9 dropped the chunky shadow on Amiga. */
if (s->pixels != NULL) {
for (row = 0; row < h; row++) {
dstRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW];
uint8_t *dstRow;
const uint8_t *srcTileRow;
uint16_t srcY;
uint16_t tileRowBase;
uint16_t inTileX;
int16_t col;
dstRow = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)(dy + row))];
srcY = (uint16_t)(sy + row);
/* Byte offset to tile-column 0's data for this pixel row:
* (tileY * wTiles) tiles in, each TILE_BYTES, plus the
* in-tile row. Shifts dodge multiply helpers
* (TILE_BYTES == 1<<5, TILE_BYTES_PER_ROW == 1<<2). */
tileRowBase = (uint16_t)((((uint16_t)(srcY >> 3) * wTiles) << 5)
+ ((srcY & 7u) << 2));
inTileX = (uint16_t)((uint16_t)sx & 7u);
srcTileRow = sp->tileData + tileRowBase
+ (((uint16_t)((uint16_t)sx >> 3)) << 5);
for (col = 0; col < w; col++) {
nibble = srcNibble(sp, (int16_t)(sx + col), (int16_t)(sy + row));
if (nibble == TRANSPARENT_NIBBLE) {
continue;
uint8_t byte;
uint8_t nibble;
byte = srcTileRow[inTileX >> 1];
nibble = (inTileX & 1u) ? (uint8_t)(byte & 0x0Fu)
: (uint8_t)(byte >> 4);
if (nibble != TRANSPARENT_NIBBLE) {
writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
}
inTileX++;
if (inTileX == TILE_PIXELS_PER_SIDE) {
inTileX = 0;
srcTileRow += TILE_BYTES;
}
writeDstNibble(dstRow, (int16_t)(dx + col), nibble);
}
}
}
@ -194,15 +216,7 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
if (sp == NULL) {
return NULL;
}
sp->tileData = tileData;
sp->widthTiles = widthTiles;
sp->heightTiles = heightTiles;
sp->ownsTileData = false;
sp->slot = NULL;
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
spriteInitFields(sp, tileData, widthTiles, heightTiles, false);
return sp;
}
@ -226,11 +240,11 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
// Source x/y must be on a tile boundary so each captured tile lands
// on whole bytes -- mid-byte snapshots would lose half a pixel at
// the left edge.
if ((x & (TILE_PIXELS - 1)) != 0 || (y & (TILE_PIXELS - 1)) != 0) {
if ((x & (TILE_PIXELS_PER_SIDE - 1)) != 0 || (y & (TILE_PIXELS_PER_SIDE - 1)) != 0) {
return NULL;
}
widthPx = (uint16_t)(widthTiles * TILE_PIXELS);
heightPx = (uint16_t)(heightTiles * TILE_PIXELS);
widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
if (x < 0 || y < 0 ||
x + (int16_t)widthPx > SURFACE_WIDTH ||
y + (int16_t)heightPx > SURFACE_HEIGHT) {
@ -251,8 +265,8 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
for (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
for (row = 0; row < TILE_PIXELS; row++) {
srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS + row) * SURFACE_BYTES_PER_ROW];
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
srcRow = &src->pixels[((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row) * SURFACE_BYTES_PER_ROW];
memcpy(&dstTile[row * TILE_BYTES_PER_ROW],
&srcRow[((uint16_t)x >> 1) + tx * TILE_BYTES_PER_ROW],
TILE_BYTES_PER_ROW);
@ -275,10 +289,10 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
for (ty = 0; ty < heightTiles; ty++) {
for (tx = 0; tx < widthTiles; tx++) {
dstTile = &buf[(ty * widthTiles + tx) * TILE_BYTES];
for (row = 0; row < TILE_PIXELS; row++) {
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS + row);
for (col = 0; col < TILE_PIXELS; col += 2) {
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS + col);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
py = (uint16_t)((uint16_t)y + ty * TILE_PIXELS_PER_SIDE + row);
for (col = 0; col < TILE_PIXELS_PER_SIDE; col += 2) {
px = (uint16_t)((uint16_t)x + tx * TILE_PIXELS_PER_SIDE + col);
hiNibble = (uint8_t)(halSamplePixel(src, (int16_t)px, (int16_t)py) & 0x0Fu);
loNibble = (uint8_t)(halSamplePixel(src, (int16_t)(px + 1), (int16_t)py) & 0x0Fu);
dstTile[row * TILE_BYTES_PER_ROW + (col >> 1)] =
@ -294,15 +308,7 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
free(buf);
return NULL;
}
sp->tileData = buf;
sp->widthTiles = widthTiles;
sp->heightTiles = heightTiles;
sp->ownsTileData = true;
sp->slot = NULL;
memset(sp->routineOffsets, 0, sizeof(sp->routineOffsets));
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
return sp;
}
@ -329,8 +335,8 @@ void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
if (s == NULL || sp == NULL) {
return;
}
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes + fully on surface. Off-surface draws
// fall back to the interpreter so the compiled routines never
@ -384,8 +390,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
hTiles = sp->heightTiles;
slot = sp->slot;
widthPx = (uint16_t)(wTiles * TILE_PIXELS);
heightPx = (uint16_t)(hTiles * TILE_PIXELS);
widthPx = (uint16_t)(wTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(hTiles * TILE_PIXELS_PER_SIDE);
// Fast path: compiled bytes available, fully on surface, backup
// buffer supplied. Save fills out backup->{x,y,width,height,
@ -422,18 +428,8 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
}
// .spr file format:
// offset bytes field
// ------ ----- --------------------------------------------
// 0 1 widthTiles
// 1 1 heightTiles
// 2 2 codeSize (LE16)
// 4 2 tileBytes (LE16) = widthTiles*heightTiles*32
// 6 ... offsets table (JOEY_SPRITE_SHIFT_COUNT *
// SPRITE_OP_COUNT * uint16_t)
// ... codeSize compiled code
// ... tileBytes raw tile data (4bpp packed, tile-major)
//
// Defragment the codegen arena (compiled sprite slots). The live .spr
// on-disk format is documented beside the loader in core/assetLoad.c.
void jlSpriteCompact(void) {
codegenArenaCompact();
}
@ -471,7 +467,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
/* Validate. Note: SURFACE_WIDTH - bx and SURFACE_HEIGHT - by stay
* in uint16_t range once bx >= 0 / by >= 0 has been checked, so
* the right-edge / bottom-edge tests don't need 32-bit arithmetic
* (which would invoke ORCA-C's ~GRTL helper, ~50 cyc per call). */
* (which would invoke a costly long-compare helper). */
if (backup->bytes == NULL ||
bw == 0 || bh == 0 ||
bx < 0 || by < 0 ||
@ -483,22 +479,32 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
}
sp = backup->sprite;
if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS) {
if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS_PER_SIDE) {
uint16_t routeIdx;
uint16_t routeOffset;
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (int16_t)(bw >> 1);
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
/* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1));
if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup);
if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
// The compiled restore routine walks a FIXED per-row byte count
// derived purely from the chosen shift (spriteBytesPerRow for
// shift 0, spriteBytesPerRow+1 for shift 1). A horizontally
// clipped save records a narrower backup->width / copyBytes that
// matches neither, so only take the fast path when copyBytes is
// exactly one of those two compiled widths; any other width must
// fall through to the interpreted memcpy that honors copyBytes.
if (copyBytes == (int16_t)spriteBytesPerRow ||
copyBytes == (int16_t)(spriteBytesPerRow + 1)) {
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
/* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1));
if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup);
if (!COMPILED_SPRITE_WRITES_PLANES) {
halSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
}
surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
return;
}
surfaceMarkDirtyRect(s, bx, by, (int16_t)bw, (int16_t)bh);
return;
}
}
@ -526,7 +532,7 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
/* Only fast-path locals here. Slow-path declarations live inside
* the slow-path block below so ORCA-C with -b doesn't reserve
* the slow-path block below so the compiler doesn't reserve
* stack frame for them on every fast-path call. */
uint16_t widthPx;
uint16_t heightPx;
@ -540,8 +546,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
backup->sizeBytes = 0;
slot = sp->slot;
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
widthPx = (uint16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Compiled fast path: fully on surface and the platform emitted
// bytes for SAVE at this shift. The compiled routine assumes a
@ -549,8 +555,8 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// through to the interpreted memcpy loop below.
//
// The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as
// explicit byte-pointer arithmetic to dodge ORCA-C 2.2.1's ~MUL4
// helper that gets emitted for `uint16_t arr[N][M]` indexing.
// explicit byte-pointer arithmetic to dodge the multiply helper
// that gets emitted for `uint16_t arr[N][M]` indexing.
if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
uint16_t routeIdx;
uint16_t routeOffset;

View file

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

View file

@ -30,20 +30,20 @@ uint8_t gStageMinWord[SURFACE_HEIGHT];
uint8_t gStageMaxWord[SURFACE_HEIGHT];
// "Stage SCB / palette has changed since last present-side upload."
// Cheap flag check at present time replaces the 200+512 byte memcmps
// the IIgs port used to run every frame in halPresentRect's
// uploadScbAndPaletteIfNeeded -- ~7 ms / frame saved on demos that
// don't churn palette/SCB (i.e., almost all demos).
// A cheap flag check at present time lets a port skip re-uploading the
// 200-byte SCB and 512-byte palette when neither changed -- ~7 ms /
// frame saved on demos that don't churn palette/SCB (i.e., almost all
// demos).
//
// Initially true so the first present uploads. jlScbSet*/jlPaletteSet
// re-mark dirty when the stage's data changes; per-port present code
// clears the flag after consuming.
// Initially true so the first present uploads. jlScbSet*, jlPaletteSet,
// and the bulk stage-replace paths (jlSurfaceCopy / jlSurfaceLoadFile)
// re-mark dirty when the stage's data changes; the per-port present
// code reads and clears the flags after uploading.
bool gStageScbDirty = true;
bool gStagePaletteDirty = true;
// ----- Internal helpers (alphabetical) -----
#ifndef JOEYLIB_PLATFORM_IIGS
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
if (minWord < gStageMinWord[y]) {
gStageMinWord[y] = minWord;
@ -52,7 +52,6 @@ static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
gStageMaxWord[y] = maxWord;
}
}
#endif
// ----- Public API (alphabetical) -----
@ -70,6 +69,13 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
memcpy(dst->palette, src->palette, sizeof(src->palette));
halSurfaceCopyPlanes(dst, src); /* 4 plane memcpys on planar ports; no-op on chunky */
surfaceMarkDirtyAll(dst);
// Bulk-replacing the stage's SCB/palette must re-mark them dirty so
// the per-port present re-uploads them; surfaceMarkDirtyAll only
// widens the pixel bands. Matches the jlScbSet / jlPaletteSet path.
if (dst == gStage) {
gStageScbDirty = true;
gStagePaletteDirty = true;
}
}
@ -78,12 +84,31 @@ jlSurfaceT *jlSurfaceCreate(void) {
s = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
if (s == NULL) {
coreSetError("out of memory allocating surface");
return NULL;
}
/* halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
* primary storage is the port-allocated planes via portData below. */
// halSurfaceAllocPixels returns NULL on planar ports (Amiga); the
// primary storage is the port-allocated planes via portData below.
// Classify NULL by the build's storage model: chunky ports need
// pixels, planar ports need portData. A NULL in the required slot
// is an out-of-memory failure (header contract: return NULL).
s->pixels = halSurfaceAllocPixels();
s->portData = halSurfaceAllocPortData(s, false);
#ifdef JOEYLIB_NATIVE_CHUNKY
if (s->pixels == NULL) {
halSurfaceFreePortData(s, false, s->portData);
free(s);
coreSetError("out of memory allocating surface pixels");
return NULL;
}
#else
if (s->portData == NULL) {
halSurfaceFreePixels(s->pixels);
free(s);
coreSetError("out of memory allocating surface planes");
return NULL;
}
#endif
paletteInitDefault(s);
return s;
}
@ -102,30 +127,11 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
}
// Cheapest deterministic hash that still detects per-byte changes:
// (hash << 1) ^ byte, a single 16-bit accumulator. ORCA-C / 65816
// compiles to ASL + EOR -- about 35 cyc per byte. A 32-bit multiply
// FNV-style hash takes ~200 cyc per byte via ~UMUL4, which adds
// 80+ seconds to a UBER run on IIgs. Discrimination is weaker than
// FNV but plenty for cross-port validation: we only need "did the
// same logical-pixel sequence produce the same hash?" -- not
// crypto-grade collision resistance over arbitrary inputs.
//
// Walks the chunky pixel buffer byte-by-byte, the same logical-pixel
// ordering on every chunky-format port (IIgs, DOS, Amiga and ST
// while still chunky). When the planar rewrite drops s->pixels on
// Amiga/ST this function will need a HAL hook (halSurfaceHash) to
// read planes natively while producing the same logical hash.
/* Cross-port FNV-style hash of pixels + SCB + palette. The hash logic
* (multiplier streams, byte ordering for palette) is identical across
* ports, but the pixel READS go through the port HAL so chunky ports
* walk s->pixels and planar ports walk plane bits and assemble nibble
* pairs into chunky bytes for the hash. Both produce the same logical-
* pixel hash because they hash the same logical pixel sequence in the
* same chunky byte order. SCB and palette are still hashed inline
* here because they live in the jlSurfaceT struct on every port (no
* port-specific storage) and the byte/value-with-explicit-byte-order
* walks are already endian-independent. */
// Cross-port hash of the surface's logical pixel content (plus SCB and
// palette), delegated to the port HAL so each port reads its native
// pixel storage (chunky on IIgs/DOS, planes on Amiga/ST) while
// producing the same logical-pixel hash. Used by the UBER validation
// harness to pixel-compare ports against an IIgs golden reference.
uint32_t jlSurfaceHash(const jlSurfaceT *s) {
if (s == NULL) {
return 0u;
@ -172,6 +178,12 @@ bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path) {
}
fclose(fp);
surfaceMarkDirtyAll(dst);
/* Loading a file into the stage overwrites its SCB/palette; re-mark
* them dirty so the next present re-uploads them. */
if (dst == gStage) {
gStageScbDirty = true;
gStagePaletteDirty = true;
}
return true;
}
@ -204,15 +216,14 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path) {
void surfaceMarkDirtyAll(const jlSurfaceT *s) {
int16_t row;
if (s != gStage) {
return;
}
for (row = 0; row < SURFACE_HEIGHT; row++) {
gStageMinWord[row] = 0;
gStageMaxWord[row] = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
}
// Whole-stage mark is a constant fill of both band arrays; memset
// lowers to a tight fill (MVN-seeded on IIgs) and drops the 200-iter
// indexed-store loop. One source of truth for "all rows fully dirty".
memset(gStageMinWord, 0, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_FULL_MAX, SURFACE_HEIGHT);
}
@ -234,15 +245,23 @@ void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w,
if (w <= 0 || h <= 0) {
return;
}
/* Clipped x/w are non-negative; cast to uint16_t before `>> 2` so
* ORCA-C lowers to a pair of LSRs instead of the ~SSHIFTRIGHT
* helper signed shifts emit. */
minWord = (uint8_t)((uint16_t)x >> 2);
maxWord = (uint8_t)((uint16_t)(x + w - 1) >> 2);
/* Clipped x/w are non-negative; SURFACE_WORD_INDEX casts to uint16_t
* before `>> 2` so the shift lowers to a pair of LSRs instead of a
* signed-shift helper. */
minWord = SURFACE_WORD_INDEX(x);
maxWord = SURFACE_WORD_INDEX(x + w - 1);
yEnd = y + h;
#ifdef JOEYLIB_PLATFORM_IIGS
iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd,
(uint16_t)minWord, (uint16_t)maxWord);
// Single-row marks (every jlDrawPixel, every horizontal span) widen
// inline in C; this is the common case and avoids the cross-segment
// JSL into iigsMarkDirtyRowsInner. Multi-row marks still use the asm
// marker, where the per-call JSL is amortized across the rows.
if (h == 1) {
widenRow(y, minWord, maxWord);
} else {
iigsMarkDirtyRowsInner((uint16_t)y, (uint16_t)yEnd,
(uint16_t)minWord, (uint16_t)maxWord);
}
#else
for (row = y; row < yEnd; row++) {
widenRow(row, minWord, maxWord);
@ -261,14 +280,31 @@ bool stageAlloc(void) {
if (gStage == NULL) {
return false;
}
/* halStageAllocPixels returns NULL on planar ports (Amiga) where
* the chunky shadow doesn't exist; the planes from portData are
* the source of truth. NULL pixels is no longer a failure. */
// halStageAllocPixels returns NULL on planar ports (Amiga) where
// the chunky shadow doesn't exist; the planes from portData are
// the source of truth. On chunky ports NULL pixels means the
// back-buffer alloc failed (IIgs pins a fixed address and never
// returns NULL, so this is a real OOM only on DOS).
gStage->pixels = halStageAllocPixels();
if (gStage->pixels != NULL) {
memset(gStage->pixels, 0, SURFACE_PIXELS_SIZE);
}
gStage->portData = halSurfaceAllocPortData(gStage, true);
#ifdef JOEYLIB_NATIVE_CHUNKY
if (gStage->pixels == NULL) {
halSurfaceFreePortData(gStage, true, gStage->portData);
free(gStage);
gStage = NULL;
return false;
}
#else
if (gStage->portData == NULL) {
halStageFreePixels(gStage->pixels);
free(gStage);
gStage = NULL;
return false;
}
#endif
stageDirtyClearAll();
paletteInitDefault(gStage);
return true;

View file

@ -33,13 +33,32 @@ struct jlSurfaceT {
// 4 px per 16-bit word. SURFACE_BYTES_PER_ROW (160) / 2 = 80 words.
// Dirty tracking grain is 16-bit words because that matches the IIgs
// PEI / PHA slam unit and the Amiga / ST c2p group is 16 px = 4 words.
#define SURFACE_PIXELS_PER_WORD 4
#define SURFACE_WORDS_PER_ROW (SURFACE_BYTES_PER_ROW / 2)
// x pixel -> dirty word index (4 px per 16-bit word). x is non-negative
// at every call site (clipped to surface bounds), so it is cast to
// uint16_t before the shift to keep the compiler from emitting a signed
// shift helper.
#define SURFACE_WORD_INDEX(_x) ((uint8_t)((uint16_t)(_x) >> 2))
// Index of the last word in a fully-dirty row.
#define STAGE_DIRTY_FULL_MAX ((uint8_t)(SURFACE_WORDS_PER_ROW - 1))
// Sentinels for "row is clean": min > max can never happen for a real
// dirty range, so the present loop tests `min > max` to skip a row.
#define STAGE_DIRTY_CLEAN_MIN 0xFFu
#define STAGE_DIRTY_CLEAN_MAX 0x00u
// True when row `y`'s dirty band is the CLEAN sentinel (min > max).
#define STAGE_DIRTY_ROW_CLEAN(_y) (gStageMinWord[_y] > gStageMaxWord[_y])
// The per-row dirty bands are stored as uint8_t, which is only valid
// because SURFACE_WORDS_PER_ROW - 1 (79) fits in a byte. A future width
// change that pushes the word count past 255 must fail to compile here
// rather than silently truncate the band arrays.
typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 : -1];
// Per-row dirty word bands for the stage. gStageMinWord[y] is the
// leftmost dirty 16-bit column on row y (inclusive); gStageMaxWord[y]
// is the rightmost (inclusive). Both default to the CLEAN sentinels
@ -48,8 +67,9 @@ extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b.
// Strength-reduced to shifts so ORCA-C doesn't emit `~UMUL2` (~150 cyc
// per call); 32 KB hashed twice -> ~5 minutes per UBER run. The
// Strength-reduced to shifts so the compiler doesn't emit a software
// multiply (~150 cyc per call); 32 KB hashed twice -> ~5 minutes per
// UBER run. The
// shift form is 16-bit-equivalent (mod 2^16) so hash values are
// identical to the original `* 31u` / `* 251u` form.
// lo *= 31 == (lo << 5) - lo
@ -94,8 +114,8 @@ void stageDirtyClearAll(void);
// On IIgs this expands to a single indexed long-mode read against
// gRowOffsetLut (built once at halInit).
//
// The explicit (y << 1) byte-pointer arithmetic dodges ORCA-C 2.2.1's
// `~MUL4` helper that gets emitted for `uint16_t arr[N]` indexing
// The explicit (y << 1) byte-pointer arithmetic dodges the multiply
// helper that gets emitted for `uint16_t arr[N]` indexing
// (the implicit *sizeof(uint16_t)). With the byte-cast + shift, the
// compiler emits a single ASL + indexed long-mode read.
//
@ -109,6 +129,11 @@ extern const uint16_t gRowOffsetLut[200];
#define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
#endif
// Record a human-readable error string for jlLastError(). Defined in
// init.c; usable by other core TUs (e.g. surface.c on allocation /
// limit failure). The string must have static lifetime.
void coreSetError(const char *message);
// Allocate and free the library-owned stage (the back-buffer surface
// that jlStagePresent flips to the display). Called from init.c during
// jlInit / jlShutdown. The stage's pixel storage is supplied by

View file

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

View file

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

View file

@ -40,6 +40,7 @@
#include <graphics/view.h>
#include <hardware/custom.h>
#include <hardware/blit.h>
#include <proto/exec.h>
#include <proto/intuition.h>
@ -48,7 +49,6 @@
#include "hal.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"
#include "draw68k_inline.h"
extern struct Custom custom;
@ -86,6 +86,8 @@ typedef struct {
// ----- Prototypes -----
static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES], uint16_t widthWords, uint16_t height);
static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, bool setBits);
static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t color);
static void buildCopperList(const jlSurfaceT *src);
static void installCopperList(void);
@ -124,6 +126,16 @@ static volatile uint16_t *gBplPokes[AMIGA_BITPLANES][2]; // [plane][0=PTH,1=PTL
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// "The previous frame's snapshot recorded at least one dirty row."
// Lets halPresent early-out of the whole union scan + 400-byte
// snapshot copy on consecutive idle frames: if neither the previous
// frame nor the current frame touched any row, union(prev,current) is
// empty AND the snapshot would copy clean->clean (a no-op), so there
// is nothing to present and nothing to record. Seeded true to match
// the "all dirty" bootstrap of gPrevStage* above, so the first two
// presents still run.
static bool gPrevAnyDirty = true;
// Cached SCB + palettes from the last present. halPresent* only needs
// to rebuild/install the copper list when SCB assignments or palette
// RGB values differ from what is already on screen; pure pixel updates
@ -143,6 +155,86 @@ static bool paletteOrScbChanged(const jlSurfaceT *src);
// ----- Internal helpers (alphabetical) -----
// Volatile view of the custom-chip register block. The Custom struct
// fields are plain (non-volatile) UWORD/APTR, so a direct
// `custom.bltcon0 = x` is eligible for -O2 reordering / dead-store
// elimination. Accessing every blitter register through a volatile *
// makes each member write volatile-qualified, which (a) keeps the
// compiler from reordering BLTSIZE (the start trigger) ahead of the
// setup writes and (b) stops it from dropping a "redundant" register
// store. This is the standard idiom for poking the custom chip from C.
static volatile struct Custom *const gCust = &custom;
// Blitter geometry shared by every block op here: each plane row is
// AMIGA_BYTES_PER_ROW (40) bytes = 20 words. BLTSIZE packs the row
// count in the high 10 bits (<< 6) and the word count in the low 6
// bits (a width of 0 would mean 64 words, but our rows are 20 wide).
#define AMIGA_BLT_WORDS_PER_ROW (AMIGA_BYTES_PER_ROW / 2u) // 20
#define AMIGA_BLT_SIZESHIFT 6
#define AMIGA_BLT_MASK_ALL 0xFFFFu
// Blitter A->D straight copy of `height` rows of `widthWords` words
// across all four planes. src[i]/dst[i] are the per-plane start
// addresses (already advanced to the first row/byte to copy by the
// caller). Both must be CHIP RAM and word-aligned; rows are
// contiguous (mod = 0) because every copy here is whole rows of a
// 40-byte-stride plane. minterm $F0 (A_TO_D): D := A, no read of B/C/D.
//
// OwnBlitter once for the whole group; WaitBlit before each register
// setup (the blitter is single-resource, so we must wait for the
// previous plane's blit to drain before reprogramming) and a final
// WaitBlit before DisownBlitter so the planes are fully written when
// the caller (or the present copy) next reads them.
static void amigaBlitterCopyPlanes(uint8_t *dst[AMIGA_BITPLANES], uint8_t *src[AMIGA_BITPLANES],
uint16_t widthWords, uint16_t height) {
uint16_t i;
uint16_t bltsize;
bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
OwnBlitter();
for (i = 0; i < AMIGA_BITPLANES; i++) {
WaitBlit();
gCust->bltcon0 = (uint16_t)(SRCA | DEST | A_TO_D); // $09F0
gCust->bltcon1 = 0u;
gCust->bltafwm = AMIGA_BLT_MASK_ALL;
gCust->bltalwm = AMIGA_BLT_MASK_ALL;
gCust->bltamod = 0u;
gCust->bltdmod = 0u;
gCust->bltapt = (APTR)src[i];
gCust->bltdpt = (APTR)dst[i];
gCust->bltsize = bltsize; // starts the blit
}
WaitBlit();
DisownBlitter();
}
// Blitter DEST-only block fill of one plane: `height` rows of
// `widthWords` words, with `dstModBytes` added to the dest pointer
// after each row (0 for a full-stride fill, (40 - rowBytes) for a
// sub-row rect). setBits selects the constant written: $FFFF (set,
// minterm $FF) or $0000 (clear, minterm $00). USED only -- no source
// channel is read, so the minterm alone determines the output and the
// fill needs no source data in memory. Caller owns the blitter and
// has already WaitBlit'd if a prior blit may be in flight; this
// routine WaitBlit's before its own register setup for safety when
// called back-to-back across planes.
static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
uint16_t dstModBytes, bool setBits) {
uint16_t minterm;
minterm = setBits ? 0x00FFu : 0x0000u;
WaitBlit();
gCust->bltcon0 = (uint16_t)(DEST | minterm);
gCust->bltcon1 = 0u;
gCust->bltafwm = AMIGA_BLT_MASK_ALL;
gCust->bltalwm = AMIGA_BLT_MASK_ALL;
gCust->bltdmod = dstModBytes;
gCust->bltdpt = (APTR)dst;
gCust->bltsize = (uint16_t)((height << AMIGA_BLT_SIZESHIFT) | widthWords);
}
// Build a user copper list for per-scanline palette (SCB emulation).
// One WAIT + 16 MOVEs per displayed scanline + one CEND. The list is
// stored in gNewUCL until installCopperList swaps it onto the screen.
@ -647,6 +739,31 @@ void halPresent(const jlSurfaceT *src) {
return;
}
// Idle-frame early-out. When the previous frame's snapshot recorded
// no dirty rows (gPrevAnyDirty == false) AND the current frame
// touched no row, union(prev,current) is empty: there is nothing to
// copy, no buffer to flip, and the snapshot below would only copy
// clean->clean. Skip the whole 200-row union scan and the 400-byte
// snapshot copy in that case. A row is dirty iff its min word was
// widened below STAGE_DIRTY_CLEAN_MIN, so a single byte test per row
// with early exit on the first dirty row suffices. Copper/palette
// updates already ran above, so display state stays correct. This
// cannot be taken while a stale band from the previous frame is
// still pending (gPrevAnyDirty guards that), preserving the
// 2-buffer staleness convergence.
if (!gPrevAnyDirty) {
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (gStageMinWord[y] != STAGE_DIRTY_CLEAN_MIN) {
anyDirty = true;
break;
}
}
if (!anyDirty) {
return;
}
}
// Phase 10 page-flip:
// * Write to the OFF-screen plane set so the beam never crosses
// in-flight pixels (no tearing regardless of copy speed).
@ -687,6 +804,16 @@ void halPresent(const jlSurfaceT *src) {
}
if (firstRow >= 0) {
// The present copy reads pd->planes (the shadow) with the CPU.
// The bulk plane ops (clear / full-row fill / surface copy) now
// run on the blitter; each of those helpers WaitBlit's before
// returning, so the blitter is normally idle here. Add a
// WaitBlit anyway before the CPU read to guarantee no blit is
// still in flight against the shadow planes -- a CPU read racing
// a blitter write would copy half-written rows to the display.
// WaitBlit is a no-op when the blitter is already idle and does
// not require OwnBlitter.
WaitBlit();
// Each planar byte covers 8 px = 2 chunky words.
byteStart = (uint16_t)(unionMinWord >> 1);
bytesPerRow = (uint16_t)(((unionMaxWord >> 1) + 1u) - byteStart);
@ -710,13 +837,21 @@ void halPresent(const jlSurfaceT *src) {
// Snapshot current dirty bands as "previous" for next present.
// jlStagePresent clears the live bands after halPresent returns,
// so we have to grab them now or they're gone.
// so we have to grab them now or they're gone. Record whether any
// row was dirty so the next present can take the idle-frame early-
// out above. A row is dirty iff its min word stayed below the clean
// sentinel.
{
uint16_t i;
bool anyDirty = false;
for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = gStageMinWord[i];
gPrevStageMaxWord[i] = gStageMaxWord[i];
if (gStageMinWord[i] != STAGE_DIRTY_CLEAN_MIN) {
anyDirty = true;
}
}
gPrevAnyDirty = anyDirty;
}
}
@ -746,9 +881,11 @@ static bool gVblInstalled = false;
// Server protocol: called by the interrupt dispatcher with A1 =
// is_Data, A6 = ExecBase. We compile without fbaserel so the small-
// data ABI is not in use; gFrameCount is a plain absolute global and
// no __saveds is needed to reach it. Return Z=0 (non-zero result) to
// keep the chain going so other VBL servers further down the priority
// list still fire.
// no __saveds is needed to reach it. Return 0 (D0 zero, Z flag set)
// so the interrupt dispatcher treats this server as "not claimed" and
// continues walking the INTB_VERTB chain, letting other VBL servers
// further down the priority list still fire. Returning non-zero would
// claim the interrupt and terminate the chain.
static ULONG vblServer(void) {
gFrameCount = (uint16_t)(gFrameCount + 1u);
return 0;
@ -837,19 +974,19 @@ void halShutdown(void) {
FreeMem(gBitMap, (ULONG)sizeof(struct BitMap));
gBitMap = NULL;
}
if (gNewUCL != NULL) {
FreeMem(gNewUCL, sizeof(struct UCopList));
gNewUCL = NULL;
}
// gNewUCL is never live at shutdown: buildCopperList and
// installCopperList run back-to-back inside updateCopperIfNeeded,
// so the built list is always consumed (gNewUCL == NULL) before we
// get here. A bare FreeMem of the UCopList struct would also leak
// the attached CopList chain that CINIT/CWAIT/CMOVE allocate, so we
// deliberately do NOT attempt to free it here.
}
// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
// surface68k.s). Same primitives used by the Atari ST port -- the
// stage / surfaces are an identical 4bpp packed layout on both.
extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
// Shared 68k fast paths from src/shared68k/surface68k.s. Only the
// planar span fill and the planar circle outline apply on Amiga (pure
// planar after Phase 9); the chunky-layout primitives that file also
// exports are unused here.
extern void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3,
uint16_t numMid, uint8_t leftMask, uint8_t rightMask,
uint8_t fb0, uint8_t fb1, uint8_t fb2, uint8_t fb3);
@ -859,10 +996,14 @@ extern void surface68kAmigaCircleOutline(uint8_t *p0, uint8_t *p1, uint8_t *p2,
// Phase 3 planar dual-write: write the bitplanes alongside the
// Phase 9: Amiga is pure planar. Every halFast* below returns true
// to suppress the cross-platform chunky fallback path -- there is no
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The actual
// planar work happens in dedicated halXxxPlanes hooks called by
// cross-platform code AFTER each halFast*.
// for any surface that has planar storage (s->portData != NULL) to
// suppress the cross-platform chunky fallback path -- there is no
// chunky shadow on Amiga (s->pixels is NULL post-Phase 9). The gate
// is portData presence, NOT stage identity, so non-stage surfaces
// created by jlSurfaceCreate use their OWN planes instead of falling
// into the chunky C fallback (which would deref s->pixels == NULL).
// The actual planar work happens either inline here or in dedicated
// halXxxPlanes hooks called by cross-platform code AFTER each halFast*.
//
// halFastSurfaceClear inlines the planar fill (no separate hook).
// All other halFast* are short-circuit stubs that return true.
@ -870,19 +1011,29 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
AmigaPlanarT *pd;
uint8_t colorIndex;
uint16_t i;
uint8_t planeByte;
bool setBits;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
colorIndex = (uint8_t)(doubled & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
for (i = 0; i < AMIGA_BITPLANES; i++) {
planeByte = ((colorIndex >> i) & 1u) ? 0xFFu : 0x00u;
memset(pd->planes[i], planeByte, AMIGA_PLANE_SIZE);
}
// Blitter DEST-only block fill of every whole plane to $00 (clear)
// or $FF (set). Each plane is 200 rows x 20 words, contiguous, so
// BLTSIZE = (200 << 6) | 20 and BLTDMOD = 0. The planes are now
// CHIP RAM (see halSurfaceAllocPortData) so the blitter can reach
// them. Own the blitter once and fill all four planes; the final
// WaitBlit before DisownBlitter guarantees the planes are fully
// written before any CPU read (a subsequent draw or the present
// copy) touches them.
OwnBlitter();
for (i = 0; i < AMIGA_BITPLANES; i++) {
setBits = ((colorIndex >> i) & 1u) != 0u;
amigaBlitterFillOne(pd->planes[i], AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT, 0u, setBits);
}
WaitBlit();
DisownBlitter();
return true;
}
@ -893,7 +1044,7 @@ bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h
* partial-byte mask handling. We just claim ownership so the C
* chunky fallback never runs. */
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
return true;
@ -962,52 +1113,26 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
yEnd = y + (int16_t)h;
/* Full-row fast path: no partial-byte RMW on either edge, so each
* plane is a pure long-fill of (h * 40) bytes. jlFillRect 320x200
* is the dominant case and lands here; saves 200 rows of leading
* byte RMW + 200 of trailing byte RMW per plane = 1600 chip-bus
* read+write cycles per plane on top of the actual data write. */
* plane row is a whole 40-byte (20-word) store. jlFillRect 320x200
* (clear-to-color, full-screen wipes) is the dominant case and
* lands here. With the shadow planes now in CHIP RAM the blitter
* can do this DEST-only block fill -- (h << 6) | 20 words, BLTDMOD
* = 0 because consecutive full-width rows are contiguous. Set bits
* write $FFFF (minterm $FF), clear bits write $0000 (minterm $00).
* One OwnBlitter for all four planes; WaitBlit before DisownBlitter
* so the planes are complete before the present copy / any later
* CPU draw reads them. */
if (byteFirst == 0u && byteLast == (uint16_t)(AMIGA_BYTES_PER_ROW - 1u)) {
uint16_t totalLongs;
uint16_t groups;
uint16_t tail;
uint32_t fillLong;
uint32_t *p32;
uint8_t *rowBase;
/* AMIGA_BYTES_PER_ROW = 40 = 10 longs / row. 8x-unrolled inner
* loop amortizes the dec+bne to ~2 cyc per store; net ~12 cyc
* per long including chip-bus contention. Tail handles the
* 0..7 longs that don't fit a full group. */
totalLongs = (uint16_t)((uint16_t)h * (AMIGA_BYTES_PER_ROW / 4u));
groups = (uint16_t)(totalLongs >> 3);
tail = (uint16_t)(totalLongs & 7u);
OwnBlitter();
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
fullByte = bitVal ? 0xFFu : 0x00u;
fillLong = (uint32_t)fullByte * 0x01010101UL;
p32 = (uint32_t *)(pd->planes[plane]
+ (uint16_t)y * AMIGA_BYTES_PER_ROW);
if (groups > 0u) {
uint16_t n = groups;
do {
p32[0] = fillLong;
p32[1] = fillLong;
p32[2] = fillLong;
p32[3] = fillLong;
p32[4] = fillLong;
p32[5] = fillLong;
p32[6] = fillLong;
p32[7] = fillLong;
p32 += 8;
} while (--n);
}
{
uint16_t t = tail;
while (t > 0u) {
*p32++ = fillLong;
t--;
}
}
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
rowBase = pd->planes[plane] + (uint16_t)y * AMIGA_BYTES_PER_ROW;
amigaBlitterFillOne(rowBase, AMIGA_BLT_WORDS_PER_ROW, h, 0u, bitVal != 0u);
}
WaitBlit();
DisownBlitter();
return;
}
@ -1015,9 +1140,9 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
* bytes (leftMask == rightMask == 0xFF) every byte in the row is
* a full overwrite -- no RMW needed. UBER jlFillRect 80x80 at x=120
* lands here (byteFirst=15, byteLast=24). Plane bases are
* MEMF_FAST-allocated long-aligned, and y*40 is also a multiple
* of 4, so rowP alignment is determined by byteFirst alone --
* computed once, not per-row. */
* AllocMem(MEMF_CHIP)-returned and thus long-aligned, and y*40 is
* also a multiple of 4, so rowP alignment is determined by
* byteFirst alone -- computed once, not per-row. */
if (leftMask == 0xFFu && rightMask == 0xFFu) {
uint16_t nBytes = (uint16_t)(byteLast - byteFirst + 1u);
uint8_t alignBytes = (uint8_t)((4u - (byteFirst & 3u)) & 3u);
@ -1108,8 +1233,8 @@ void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t
/* Hoist middle-region alignment outside both per-plane and
* per-row loops. midStart = planeBase + y*40 + byteFirst + 1.
* Plane bases are MEMF_FAST long-aligned and y*40 is a
* multiple of 4, so midStart's alignment is determined by
* Plane bases are AllocMem(MEMF_CHIP) long-aligned and y*40 is
* a multiple of 4, so midStart's alignment is determined by
* (byteFirst + 1) & 3 alone -- constant across planes/rows. */
uint8_t midAlignBytes = (uint8_t)((4u - ((byteFirst + 1u) & 3u)) & 3u);
uint16_t midRem;
@ -1316,55 +1441,57 @@ void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, cons
// Phase 9 sprite save/restore plane-backup hooks.
// Sprite save at (x, y, w, h) writes 4 plane stripes into backup
// buffer; restore reads them back. x and w are 2-pixel aligned by
// cross-platform code; we round x DOWN and w UP to 8-pixel boundaries
// here so plane writes are byte-aligned. Backup layout (matches the
// h * w/2 = 4 * h * w/8 sizing the cross-platform code allocates):
// bytes [0 .. h*bpr ): plane 0 rows
// bytes [h*bpr .. 2*h*bpr ): plane 1 rows
// bytes [2*h*bpr .. 3*h*bpr ): plane 2 rows
// bytes [3*h*bpr .. 4*h*bpr ): plane 3 rows
// where bpr = bytesPerPlaneRow = roundedW/8.
// Sprite save at (x, y, w, h) records the destination region into the
// caller's backup buffer; restore writes it back. x and w are 2-pixel
// aligned by cross-platform code.
//
// If the rect's rounded width is wider than the chunky-sized backup
// would hold (h * (w/2) bytes), we silently truncate -- the planar
// stripes for partial-byte-aligned sprites won't fit. This case is
// rare for tile-aligned sprites; document if it bites.
static void amigaSpriteRoundRect(int16_t *xp, uint16_t *wp, uint16_t *bprp) {
int16_t xIn = *xp;
uint16_t wIn = *wp;
int16_t xOut = (int16_t)(xIn & ~7); /* round down to 8-pixel */
uint16_t span = (uint16_t)(((uint16_t)xIn + wIn) - (uint16_t)xOut);
uint16_t wOut = (uint16_t)((span + 7u) & ~7u);
*xp = xOut;
*wp = wOut;
*bprp = (uint16_t)(wOut >> 3);
}
// Backup layout matches the CHUNKY-packed format the cross-platform
// contract sizes for ((widthTiles*4)+1)*(heightTiles*8) bytes (see
// include/joey/sprite.h): one byte per pixel-pair, 2 pixels/byte,
// high nibble = even (left) pixel, low nibble = odd (right) pixel,
// (w/2) bytes per row, (w/2)*h bytes total. This is the same format
// the Atari ST port uses and never overruns the contract buffer --
// unlike a plane-major layout, which would round x down to an 8-pixel
// boundary and width up and then write (4*roundedW/8)*h = up to
// (w/2 + 3)*h bytes for an odd-of-8 x, overrunning the buffer. Each
// pixel's 4bpp color is assembled from / written to the 4 plane bits
// at byte x>>3, bit 0x80 >> (x & 7).
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
const AmigaPlanarT *pd;
uint16_t bpr;
uint16_t planeStripe;
uint16_t i;
uint16_t pairs;
uint16_t row;
uint16_t pair;
uint8_t *dst;
const uint8_t *src;
pd = (const AmigaPlanarT *)s->portData;
if (pd == NULL || dstPlaneBytes == NULL) {
if (pd == NULL || dstPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
amigaSpriteRoundRect(&x, &w, &bpr);
planeStripe = (uint16_t)((uint16_t)h * bpr);
for (i = 0; i < AMIGA_BITPLANES; i++) {
dst = dstPlaneBytes + i * planeStripe;
for (row = 0; row < (uint16_t)h; row++) {
src = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
memcpy(dst, src, bpr);
dst += bpr;
pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) {
const uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
const uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
dst = dstPlaneBytes + (uint16_t)(row * pairs);
for (pair = 0; pair < pairs; pair++) {
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
uint16_t byteOff = (uint16_t)(px >> 3);
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
uint8_t hi = 0u;
uint8_t lo = 0u;
if (p0[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x01u); }
if (p1[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x02u); }
if (p2[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x04u); }
if (p3[byteOff] & maskHi) { hi = (uint8_t)(hi | 0x08u); }
if (p0[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x01u); }
if (p1[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x02u); }
if (p2[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x04u); }
if (p3[byteOffLo] & maskLo) { lo = (uint8_t)(lo | 0x08u); }
dst[pair] = (uint8_t)((hi << 4) | lo);
}
}
}
@ -1372,25 +1499,41 @@ void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
AmigaPlanarT *pd;
uint16_t bpr;
uint16_t planeStripe;
uint16_t i;
uint16_t pairs;
uint16_t row;
uint8_t *dst;
uint16_t pair;
const uint8_t *src;
pd = (AmigaPlanarT *)s->portData;
if (pd == NULL || srcPlaneBytes == NULL) {
if (pd == NULL || srcPlaneBytes == NULL || w == 0u || h == 0u) {
return;
}
amigaSpriteRoundRect(&x, &w, &bpr);
planeStripe = (uint16_t)((uint16_t)h * bpr);
for (i = 0; i < AMIGA_BITPLANES; i++) {
src = srcPlaneBytes + i * planeStripe;
for (row = 0; row < (uint16_t)h; row++) {
dst = pd->planes[i] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3);
memcpy(dst, src, bpr);
src += bpr;
pairs = (uint16_t)(w >> 1);
for (row = 0; row < (uint16_t)h; row++) {
uint8_t *p0 = pd->planes[0] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p1 = pd->planes[1] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p2 = pd->planes[2] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
uint8_t *p3 = pd->planes[3] + ((uint16_t)y + row) * AMIGA_BYTES_PER_ROW;
src = srcPlaneBytes + (uint16_t)(row * pairs);
for (pair = 0; pair < pairs; pair++) {
uint8_t b = src[pair];
uint8_t hi = (uint8_t)(b >> 4);
uint8_t lo = (uint8_t)(b & 0x0Fu);
uint16_t px = (uint16_t)(x + (int16_t)(pair << 1));
uint16_t byteOff = (uint16_t)(px >> 3);
uint8_t maskHi = (uint8_t)(0x80u >> (px & 7u));
uint8_t notHi = (uint8_t)(~maskHi);
uint16_t byteOffLo = (uint16_t)((px + 1u) >> 3);
uint8_t maskLo = (uint8_t)(0x80u >> ((px + 1u) & 7u));
uint8_t notLo = (uint8_t)(~maskLo);
p0[byteOff] = (uint8_t)((p0[byteOff] & notHi) | ((hi & 0x01u) ? maskHi : 0u));
p1[byteOff] = (uint8_t)((p1[byteOff] & notHi) | ((hi & 0x02u) ? maskHi : 0u));
p2[byteOff] = (uint8_t)((p2[byteOff] & notHi) | ((hi & 0x04u) ? maskHi : 0u));
p3[byteOff] = (uint8_t)((p3[byteOff] & notHi) | ((hi & 0x08u) ? maskHi : 0u));
p0[byteOffLo] = (uint8_t)((p0[byteOffLo] & notLo) | ((lo & 0x01u) ? maskLo : 0u));
p1[byteOffLo] = (uint8_t)((p1[byteOffLo] & notLo) | ((lo & 0x02u) ? maskLo : 0u));
p2[byteOffLo] = (uint8_t)((p2[byteOffLo] & notLo) | ((lo & 0x04u) ? maskLo : 0u));
p3[byteOffLo] = (uint8_t)((p3[byteOffLo] & notLo) | ((lo & 0x08u) ? maskLo : 0u));
}
}
}
@ -1422,8 +1565,12 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
* 1. reads 4 chunky bytes (one row of one 8-pixel tile column)
* 2. converts to 4 plane bytes + a shape mask (1 bit per pixel
* where the chunky nibble is non-transparent)
* 3. shifts the per-plane bytes by (dx & 7) so the sprite can land
* at any pixel x, not just 8-px boundaries
* 3. shifts the per-plane bytes by (x & 7) so the sprite can land
* at any pixel x, not just 8-px boundaries; the shift is derived
* from the ORIGINAL signed x so a left-edge clip keeps the true
* source-to-dest sub-byte phase
* 3a. each plane-byte write is guarded by a signed [0,40) dest-byte
* range check, which handles left, right, top and bottom clip
* 4. writes the left byte (and the spilled right byte for shifted
* sprites) via AND-mask + OR to all 4 plane buffers
*
@ -1433,9 +1580,7 @@ void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
* code per cel that the old emitter generated. */
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
AmigaPlanarT *pd;
int16_t dx;
int16_t dy;
int16_t sx;
int16_t sy;
int16_t w;
int16_t h;
@ -1445,14 +1590,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
int16_t tileY;
int16_t inTileY;
int16_t startByteX;
int16_t dstByteIdx;
uint16_t wTiles;
uint16_t rowEnd;
uint8_t shift;
uint8_t bit;
uint8_t p0, p1, p2, p3, mask;
uint8_t leftMask, notMask;
uint8_t rshift, rightMask;
uint16_t dstByteOff;
uint16_t dstRowBase;
const uint8_t *tile;
const uint8_t *chunky;
@ -1465,37 +1609,34 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
wTiles = sp->widthTiles;
w = (int16_t)(wTiles * 8u);
h = (int16_t)(sp->heightTiles * 8u);
dx = x;
dy = y;
sx = 0;
sy = 0;
/* Clip dst rect against surface (sx/sy track offset INTO sprite). */
if (dx < 0) { sx = (int16_t)(-dx); w = (int16_t)(w - sx); dx = 0; }
/* Vertical clip only (sy tracks offset INTO sprite); horizontal
* clip is handled per-byte by the dstByteIdx range guards below so
* the source sub-byte phase is preserved on a left-edge clip. */
if (dy < 0) { sy = (int16_t)(-dy); h = (int16_t)(h - sy); dy = 0; }
if (dx >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || w <= 0 || h <= 0) {
if (x >= SURFACE_WIDTH || dy >= SURFACE_HEIGHT || (int16_t)(x + w) <= 0 || h <= 0) {
return;
}
if (dx + w > SURFACE_WIDTH) { w = (int16_t)(SURFACE_WIDTH - dx); }
if (dy + h > SURFACE_HEIGHT) { h = (int16_t)(SURFACE_HEIGHT - dy); }
shift = (uint8_t)(dx & 7u);
startByteX = (int16_t)(dx >> 3);
/* Derive shift / startByteX from the ORIGINAL signed dest x so a
* partial left clip keeps the true source-to-dest sub-byte phase.
* shift in 0..7 (floored modulo), startByteX = floor(x / 8). For
* x = -3: shift = 5, startByteX = -1 -- source column 0 lands
* shifted by 5 into dest bytes -1 (off-surface, skipped) and 0. */
shift = (uint8_t)(((x % 8) + 8) % 8);
startByteX = (int16_t)((x - (int16_t)shift) / 8);
for (row = 0; row < h; row++) {
spritePy = (int16_t)(sy + row);
tileY = (int16_t)(spritePy >> 3);
inTileY = (int16_t)(spritePy & 7);
dstRowBase = (uint16_t)((uint16_t)(dy + row) * AMIGA_BYTES_PER_ROW);
rowEnd = (uint16_t)(dstRowBase + AMIGA_BYTES_PER_ROW);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
if (spriteBx >= (int16_t)wTiles) {
break;
}
tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
@ -1515,9 +1656,13 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
continue;
}
dstByteOff = (uint16_t)(dstRowBase + (uint16_t)(startByteX + byteCol));
/* Signed dest byte index for this source column. Guard each
* write against [0, AMIGA_BYTES_PER_ROW) in SIGNED math so
* a negative startByteX never wraps to a huge unsigned. */
dstByteIdx = (int16_t)(startByteX + byteCol);
leftMask = (uint8_t)(mask >> shift);
if (leftMask != 0u && dstByteOff < rowEnd) {
if (leftMask != 0u && dstByteIdx >= 0 && dstByteIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
uint16_t dstByteOff = (uint16_t)(dstRowBase + (uint16_t)dstByteIdx);
notMask = (uint8_t)(~leftMask);
pd->planes[0][dstByteOff] = (uint8_t)((pd->planes[0][dstByteOff] & notMask) | (uint8_t)(p0 >> shift));
pd->planes[1][dstByteOff] = (uint8_t)((pd->planes[1][dstByteOff] & notMask) | (uint8_t)(p1 >> shift));
@ -1526,17 +1671,16 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
}
if (shift != 0u) {
int16_t rIdx = (int16_t)(dstByteIdx + 1);
rshift = (uint8_t)(8u - shift);
rightMask = (uint8_t)(mask << rshift);
if (rightMask != 0u) {
uint16_t rOff = (uint16_t)(dstByteOff + 1u);
if (rOff < rowEnd) {
notMask = (uint8_t)(~rightMask);
pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift));
pd->planes[2][rOff] = (uint8_t)((pd->planes[2][rOff] & notMask) | (uint8_t)(p2 << rshift));
pd->planes[3][rOff] = (uint8_t)((pd->planes[3][rOff] & notMask) | (uint8_t)(p3 << rshift));
}
if (rightMask != 0u && rIdx >= 0 && rIdx < (int16_t)AMIGA_BYTES_PER_ROW) {
uint16_t rOff = (uint16_t)(dstRowBase + (uint16_t)rIdx);
notMask = (uint8_t)(~rightMask);
pd->planes[0][rOff] = (uint8_t)((pd->planes[0][rOff] & notMask) | (uint8_t)(p0 << rshift));
pd->planes[1][rOff] = (uint8_t)((pd->planes[1][rOff] & notMask) | (uint8_t)(p1 << rshift));
pd->planes[2][rOff] = (uint8_t)((pd->planes[2][rOff] & notMask) | (uint8_t)(p2 << rshift));
pd->planes[3][rOff] = (uint8_t)((pd->planes[3][rOff] & notMask) | (uint8_t)(p3 << rshift));
}
}
}
@ -1645,16 +1789,20 @@ void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd;
uint16_t i;
dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
return;
}
for (i = 0; i < AMIGA_BITPLANES; i++) {
memcpy(dstPd->planes[i], srcPd->planes[i], AMIGA_PLANE_SIZE);
}
// Straight blitter A->D copy of every whole plane (200 rows x 20
// words, contiguous). Both source and dest planes are CHIP RAM now
// (halSurfaceAllocPortData) so the blitter can reach both. The
// helper owns the blitter, copies all four planes, and WaitBlit's
// before DisownBlitter so the dest planes are complete before any
// CPU read.
amigaBlitterCopyPlanes(dstPd->planes, srcPd->planes,
AMIGA_BLT_WORDS_PER_ROW, SURFACE_HEIGHT);
}
@ -1758,21 +1906,19 @@ static void amigaPlanarSetPixel(AmigaPlanarT *pd, int16_t x, int16_t y, uint8_t
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
AmigaPlanarT *pd;
uint8_t nibLo;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
nibLo = (uint8_t)(colorIndex & 0x0Fu);
pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
}
amigaPlanarSetPixel(pd, (int16_t)x, (int16_t)y, nibLo);
return true;
}
// Bresenham's diagonal line, planar-only walk. Same algorithm as
// cross-platform jlDrawLine's fallback so the pixel set matches the
// chunky walker (draw68kLine) bit-for-bit.
// cross-platform jlDrawLine's fallback so the pixel set matches it
// bit-for-bit.
static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color) {
int16_t dx;
int16_t dy;
@ -1815,29 +1961,25 @@ static void amigaPlanarLine(AmigaPlanarT *pd, int16_t x0, int16_t y0, int16_t x1
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
AmigaPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
amigaPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
return true;
}
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
AmigaPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
pd->planes[2], pd->planes[3],
(uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu));
}
surface68kAmigaCircleOutline(pd->planes[0], pd->planes[1],
pd->planes[2], pd->planes[3],
(uint16_t)cx, (uint16_t)cy, r,
(uint8_t)(colorIndex & 0x0Fu));
return true;
}
@ -1904,32 +2046,30 @@ bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
int16_t spanX;
uint16_t spanW;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (AmigaPlanarT *)s->portData;
if (pd != NULL) {
/* Inline per-span plane fill -- avoids 4 halFillRectPlanes
* dispatches per midpoint iter (~320 dispatches for r=40). */
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
spanX = (int16_t)(cx - x);
spanW = (uint16_t)(2 * x + 1);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex);
spanX = (int16_t)(cx - y);
spanW = (uint16_t)(2 * y + 1);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex);
y++;
if (err <= 0) {
err = (int16_t)(err + y + y + 1);
} else {
x--;
err = (int16_t)(err + y + y - x - x + 1);
}
/* Inline per-span plane fill -- avoids 4 halFillRectPlanes
* dispatches per midpoint iter (~320 dispatches for r=40). */
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
spanX = (int16_t)(cx - x);
spanW = (uint16_t)(2 * x + 1);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + y), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - y), spanW, colorIndex);
spanX = (int16_t)(cx - y);
spanW = (uint16_t)(2 * y + 1);
amigaFillSpanInline(pd, spanX, (int16_t)(cy + x), spanW, colorIndex);
amigaFillSpanInline(pd, spanX, (int16_t)(cy - x), spanW, colorIndex);
y++;
if (err <= 0) {
err = (int16_t)(err + y + y + 1);
} else {
x--;
err = (int16_t)(err + y + y - x - x + 1);
}
}
return true;
@ -2375,26 +2515,20 @@ void *halSurfaceAllocPortData(jlSurfaceT *s, bool isStage) {
* is gPlanes[], CHIP, updated only by halPresent). Non-stage uses
* these as the surface's only planes.
*
* MEMF_FAST (no MEMF_CHIP) explicitly demands fast RAM. The
* shadow planes are CPU-only (no blitter/copper access); freeing
* them from chip RAM cuts halPresent's chip-bus contention in
* half (read FAST + write CHIP instead of read CHIP + write CHIP)
* and gives RMW drawing primitives 2-3x speedup. If fast RAM is
* unavailable (bare A500, no expansion), we fall back to chip
* via the loop below. */
* MEMF_CHIP because the bulk plane ops (clear, full-row fill,
* surface copy) now run on the BLITTER, which can only reach chip
* RAM. This is a deliberate performance tradeoff: chip RMW is
* slower than fast RAM for per-pixel CPU draws (line / circle /
* single pixel), but the blitter wins big on the full-plane
* clear/fill/copy paths and runs in parallel with the CPU. Net is
* workload-dependent; see the blitter helpers above. The planes
* must be MEMF_CHIP unconditionally now -- a fast-RAM fallback
* would put the blitter helpers out of reach and corrupt output. */
for (i = 0; i < AMIGA_BITPLANES; i++) {
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
(ULONG)(MEMF_FAST | MEMF_CLEAR));
if (pd->planes[i] == NULL) {
/* No fast RAM available; fall back to chip. */
pd->planes[i] = (uint8_t *)AllocMem((ULONG)AMIGA_PLANE_SIZE,
(ULONG)(MEMF_CHIP | MEMF_CLEAR));
jlLogF("amiga: shadow plane %u in CHIP (fast unavailable, addr=$%08lX)",
(unsigned)i, (unsigned long)pd->planes[i]);
} else {
jlLogF("amiga: shadow plane %u in FAST (addr=$%08lX)",
(unsigned)i, (unsigned long)pd->planes[i]);
}
(ULONG)(MEMF_CHIP | MEMF_CLEAR));
jlLogF("amiga: shadow plane %u in CHIP (blitter, addr=$%08lX)",
(unsigned)i, (unsigned long)pd->planes[i]);
if (pd->planes[i] == NULL) {
/* Roll back any planes already allocated. */
while (i > 0u) {

View file

@ -46,6 +46,12 @@
#define ST_YM_SELECT ((volatile uint8_t *)0xFFFF8800L)
#define ST_YM_DATA ((volatile uint8_t *)0xFFFF8802L)
// YM mixer (reg 7) high bits 6+7 are the I/O port directions TOS
// leaves configured (port A output, port B = centronics printer
// output). Always preserved when we rewrite the mixer.
#define YM_MIXER_IO_DIR 0xC0u
#define YM_REG_MIXER 7u
#define MFP_TA_BIT 0x20
#define MFP_TACR_STOP 0x00
#define MFP_TACR_DIV200 0x07
@ -81,8 +87,23 @@ static void (*gOldTimerAVec)(void) = NULL;
// Mixed in over libxmp's MOD output during halAudioFrameTick.
static AudioSfxSlotT gSfxSlots[JOEY_AUDIO_SFX_SLOTS];
// Single shadow of YM mixer reg 7's low 6 bits (tone+noise disables).
// Both the PWM (installTimerA) and PSG-tone (applyYmVoice) paths
// drive the mixer through this one shadow so they share one source of
// truth: 0x3F = all tones and noise off (PWM-DAC default). The YM
// can't be read back, so this shadow IS the authoritative state.
static volatile uint8_t gYmToneMask = 0x3Fu;
// ----- Internal helpers -----
// Write YM mixer reg 7 from the gYmToneMask shadow, preserving the
// TOS I/O-port-direction high bits. Supervisor context required.
static void ymWriteMixer(void) {
*ST_YM_SELECT = YM_REG_MIXER;
*ST_YM_DATA = (uint8_t)(YM_MIXER_IO_DIR | gYmToneMask);
}
static long installTimerA(void) {
uint16_t i;
@ -114,8 +135,8 @@ static long installTimerA(void) {
// disable bits over an assumed-safe TOS-default mask. Bit 6 set
// (port A output) matches stock TOS; bit 7 set (port B output)
// matches the centronics-printer direction TOS configures.
*ST_YM_SELECT = 7;
*ST_YM_DATA = 0xFF; // all tones + noises off; I/O ports A+B output (TOS default)
gYmToneMask = 0x3Fu; // all tones + noise disabled (PWM-DAC default)
ymWriteMixer(); // -> 0xFF: tones+noise off, I/O ports A+B output
*ST_YM_SELECT = 8;
*ST_YM_DATA = 0; // channel A volume = 0 to avoid a pop at start
*ST_YM_SELECT = 9;
@ -271,7 +292,7 @@ void halAudioTone(uint16_t freqHz) {
static volatile uint8_t gYmVoice;
static volatile uint16_t gYmFreqHz;
static volatile uint8_t gYmAtten;
static volatile uint8_t gYmToneMask = 0x3Fu; // bits 0-5 all off (no tones, no noise)
// gYmToneMask is the shared YM mixer shadow, defined in module state.
static long applyYmVoice(void) {
@ -287,8 +308,7 @@ static long applyYmVoice(void) {
// Silence: disable tone on this channel (mixer bit 0/1/2 set),
// zero volume. Don't touch other channels' bits.
gYmToneMask = (uint8_t)(gYmToneMask | ((uint8_t)1u << gYmVoice));
*ST_YM_SELECT = 7;
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask); // preserve I/O port-direction high bits
ymWriteMixer();
*ST_YM_SELECT = volReg;
*ST_YM_DATA = 0;
return 0;
@ -313,8 +333,7 @@ static long applyYmVoice(void) {
// Enable tone on this channel (clear its bit in mixer).
gYmToneMask = (uint8_t)(gYmToneMask & ~((uint8_t)1u << gYmVoice));
*ST_YM_SELECT = 7;
*ST_YM_DATA = (uint8_t)(0xC0u | gYmToneMask);
ymWriteMixer();
return 0;
}
@ -331,7 +350,10 @@ bool halAudioTickRegister(jlAudioTickFnT fn, uint16_t hz) {
void halAudioCriticalEnter(void) {
// No audio ISR registered -> no critical section needed.
// No tick-callback ISR is registered (halAudioTickRegister returns
// false). The Timer A MOD ISR synchronizes with the producer via
// the gNeedRefill flags, not this critical section, so masking
// here would only starve that ~12 kHz audio ISR for no benefit.
}
@ -340,7 +362,7 @@ void halAudioCriticalExit(void) {
void halAudioVoice(uint8_t voice, uint16_t freqHz, uint8_t atten) {
if (voice >= 3u) {
if (voice >= JOEY_AUDIO_VOICES) {
return;
}
// The PWM-via-volume engine in halAudioInit hammers YM reg 8
@ -383,10 +405,24 @@ void halAudioStopSfx(uint8_t slot) {
// Drains ISR-raised refill flags by re-running libxmp's mixer into
// the consumed half then overlaying any active SFX. Called from the
// game loop, never from IRQ.
//
// Soft-real-time constraint: the producer (this function) must refill
// a consumed half within MIX_HALF / MIX_RATE = 1024 / 12288 ~= 83 ms
// of the ISR raising its flag, or the ISR drains into the half still
// being rewritten (audible tearing). A full MOD-frame mix plus
// audioSfxOverlayMix can approach that budget under load, so callers
// should run the game loop comfortably faster than ~12 Hz.
void halAudioFrameTick(void) {
if (!gReady) {
return;
}
// Underrun guard: both flags set means the producer fell a full
// buffer behind, so the ISR is now draining a half this tick is
// about to rewrite. Silence first so the in-flight torn read plays
// as silence rather than garbage, then refill both halves below.
if (gNeedRefill[0] && gNeedRefill[1]) {
silenceMixBuffer();
}
if (gNeedRefill[0]) {
if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);

View file

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

View file

@ -51,13 +51,13 @@
lsr.w #4,%d0 | groupLast
sub.w %d1,%d0 | numGroups
move.w %d0,4(%sp)
| leftMask via LUT[bitFirst]; a5 = leftMaskLut base
| leftMask via LUT[bitFirst]; a5 = _gStLeftMaskLut base
move.w 0(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
move.w (%a5,%d0.w),%d1
move.w %d1,0(%sp)
| rightMask via LUT[bitLast]; a6 = rightMaskLut base
| rightMask via LUT[bitLast]; a6 = _gStRightMaskLut base
move.w 2(%sp),%d0
and.w #15,%d0
add.w %d0,%d0
@ -216,8 +216,10 @@ _surface68kStFillCircle:
| LUT bases. a5/a6 = mask LUTs (used by COMPUTE_PAIR_MASKS).
| a2 = shared _gStRowOffsetLut (used by SPAN_BODY for y*160).
lea leftMaskLut(%pc),%a5
lea rightMaskLut(%pc),%a6
| All three LUTs are the shared definitions in lineSpan.s;
| reference them absolute (single source of truth).
lea _gStLeftMaskLut,%a5
lea _gStRightMaskLut,%a6
lea _gStRowOffsetLut,%a2
| loLong = ((c&1)?0xFFFF0000:0) | ((c&2)?0x0000FFFF:0)
@ -299,17 +301,5 @@ _surface68kStFillCircle:
rts
.align 2
| leftMaskLut[i] = (1 << (16 - i)) - 1, indexed by bitFirst (0..15)
leftMaskLut:
.word 0xFFFF, 0x7FFF, 0x3FFF, 0x1FFF
.word 0x0FFF, 0x07FF, 0x03FF, 0x01FF
.word 0x00FF, 0x007F, 0x003F, 0x001F
.word 0x000F, 0x0007, 0x0003, 0x0001
| rightMaskLut[i] = ~((1 << (15 - i)) - 1), indexed by bitLast (0..15)
rightMaskLut:
.word 0x8000, 0xC000, 0xE000, 0xF000
.word 0xF800, 0xFC00, 0xFE00, 0xFF00
.word 0xFF80, 0xFFC0, 0xFFE0, 0xFFF0
.word 0xFFF8, 0xFFFC, 0xFFFE, 0xFFFF
| (leftMaskLut/rightMaskLut now live in lineSpan.s as the shared
| _gStLeftMaskLut / _gStRightMaskLut, referenced absolute above.)

View file

@ -38,7 +38,6 @@
#include "surfaceInternal.h"
#include "spriteInternal.h"
#include "joey/tile.h"
#include "draw68k_inline.h"
// ----- Constants -----
@ -63,10 +62,12 @@
// Amiga's 4 separate plane buffers -- same total bytes, very different
// access pattern.
//
// The stage gets its own SHADOW planar buffer (NOT aliased to
// gScreenBase) so drawing primitives don't appear until jlStagePresent
// memcpy's shadow -> screen. Same rationale as Amiga's per-stage
// shadow planes.
// The stage gets its own SHADOW planar buffer (NOT aliased to a
// displayed page) so drawing primitives don't appear until
// jlStagePresent copies the union(prev,current) dirty rows from the
// shadow into the off-screen page and flips the shifter to it. Same
// rationale as Amiga's per-stage shadow planes feeding its 2-buffer
// page flip.
typedef struct {
uint8_t *base; // 32000-byte interleaved planar buffer
uint8_t *raw; // unaligned malloc result for free()
@ -76,6 +77,17 @@ typedef struct {
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
// Shifter video base address registers. The shifter latches the base
// at the start of each frame's display (during vblank), so a write
// here mid-visible-frame takes effect on the NEXT frame -- a deferred,
// tear-free page flip with no TOS involvement. STF exposes only the
// high and mid bytes; there is no low byte, which is why the ST screen
// must be 256-byte aligned (the low 8 bits of the base are hardwired
// to zero). Both bytes are odd-addressed byte registers; byte access
// to an odd address is legal on 68000 (only word/long faults).
#define ST_VID_BASE_HI ((volatile uint8_t *)0xFFFF8201L) // bits 23..16
#define ST_VID_BASE_MID ((volatile uint8_t *)0xFFFF8203L) // bits 15..8
// MFP hardware addresses.
#define ST_MFP_TBCR ((volatile uint8_t *)0xFFFFFA1BL) // Timer B control
#define ST_MFP_TBDR ((volatile uint8_t *)0xFFFFFA21L) // Timer B data
@ -136,6 +148,7 @@ static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPl
}
static uint16_t quantizeColorToSt(uint16_t orgb);
static void flattenScbPalettes(const jlSurfaceT *src);
static void setShifterVideoBase(const uint8_t *page);
static void writeDiagnostics(void);
static long writePrevPaletteRegs(void);
@ -147,19 +160,51 @@ static void refreshPaletteStateIfNeeded(const
// ----- Module state -----
// Screen buffer: enough for 320x200x4bpp planar plus padding for
// runtime 256-byte alignment. TOS .PRG format only supports 2-byte
// object-file alignment, so we overallocate and align the pointer
// manually in halInit.
static uint8_t gScreenBuffer[SURFACE_PIXELS_SIZE + ST_SCREEN_ALIGN];
// Screen buffer: TWO 320x200x4bpp planar pages for page-flipping, plus
// padding for runtime 256-byte alignment. TOS .PRG format only supports
// 2-byte object-file alignment, so we overallocate and align the first
// page pointer manually in halInit; the second page sits exactly one
// page (SURFACE_PIXELS_SIZE = 32000, itself a multiple of 256) after
// the first, so it stays 256-byte aligned as the shifter requires.
static uint8_t gScreenBuffer[(2 * SURFACE_PIXELS_SIZE) + ST_SCREEN_ALIGN];
// Two physical screen pages. gFrontIdx selects the one the shifter is
// currently displaying; the OFF-screen page (gFrontIdx ^ 1) is the
// flip target written by the next present. Drawing primitives target
// the stage's own shadow planar buffer (StPlanarT.base), NOT a page,
// so the union(prev,cur) dirty copy at present brings the off-screen
// page fully up to date before the flip -- mirrors the Amiga 2-buffer
// gPrevStage* staleness pattern (shadow -> off-screen buffer -> flip).
static uint8_t *gScreenPage[2] = { NULL, NULL };
static uint8_t gFrontIdx = 0;
static uint8_t *gScreenBase = NULL;
static void *gPrevPhysbase = NULL;
static void *gPrevLogbase = NULL;
static int16_t gPrevRez = 0;
static uint16_t gPrevPalette[SURFACE_COLORS_PER_PALETTE];
static bool gModeSet = false;
// Snapshot of the previous frame's dirty bands. With two physical
// pages the off-screen page holds content from TWO frames ago, so each
// present must refresh the union(prev, current) of dirty rows from the
// shadow -- not just this frame's dirty rows -- or stale streaks from
// two frames back survive on the page we are about to display. Seeded
// to "all dirty" so the first two presents do full-screen refreshes
// and converge both pages to the current shadow. After that we pay
// only the small per-row union(prev,current) copy.
static uint8_t gPrevStageMinWord[SURFACE_HEIGHT];
static uint8_t gPrevStageMaxWord[SURFACE_HEIGHT];
// "The previous frame's snapshot recorded at least one dirty row."
// Lets halPresent skip the whole 200-row union scan + 200-row snapshot
// copy on consecutive idle frames: if neither the previous frame nor
// the current frame touched any row, union(prev,current) is empty, the
// off-screen page already matches the displayed page (the settling
// flip happened on the first idle frame), and the snapshot would only
// copy clean->clean. Seeded true to match the "all dirty" bootstrap of
// gPrevStage* above so the first two presents still run.
static bool gPrevAnyDirty = true;
// Per-scanline pre-quantized palette table. Indexed by display line;
// each row is a 16-word palette ready to be copied straight into the
// shifter registers. Written at present() time, read by the Timer B
@ -303,12 +348,18 @@ static uint16_t quantizeColorToSt(uint16_t orgb) {
// auto-reload keep counting for the next fire. We deliberately do
// NOT stop/reload/restart the timer here: that sequence would cost
// 1-2 HBL edges each fire, and those losses compound across 7+
// transitions into a visible "last band short" drift. Updating
// TBDR in place is enough for variable-length bands -- the new
// value takes effect on the fire AFTER next, which is acceptable
// when adjacent bands have similar lengths; uniform bands (like
// pattern.c) don't need TBDR updates at all so stay perfectly
// aligned with no drift.
// transitions into a visible "last band short" drift.
//
// MFP event-count auto-reload latches the CURRENT TBDR at each
// underflow, so a value written here does not take effect on the
// NEXT fire (that interval was already reloaded at the underflow
// that triggered this ISR) but on the fire AFTER next. To time
// variable-height bands correctly we therefore program one fire
// ahead: in the ISR that transitions to `band` we load TBDR with
// the band+1 -> band+2 delta (the interval that the band+1
// underflow will reload and use to time the band+2 transition).
// Uniform bands (like pattern.c) have all deltas equal, so the
// one-ahead value is identical and they stay perfectly aligned.
static void timerBIsr(void) {
uint16_t band;
uint8_t palIdx;
@ -344,14 +395,19 @@ static void timerBIsr(void) {
dst[15] = src[15];
if (band + 1 < gBandCount) {
// Update TBDR for the fire-after-next (auto-reload at
// the NEXT fire still uses the old value). Don't stop
// the timer.
nextDelta = gBandStart[band + 1] - gBandStart[band];
if (nextDelta == 0 || nextDelta > 255) {
nextDelta = 1;
// A transition to band+1 is still pending; keep Timer B
// running. The band+1 underflow has already had its reload
// value latched (set one fire earlier), so program TBDR
// now with the band+1 -> band+2 delta, which the band+1
// underflow will reload and use to time band+2. Don't
// stop the timer.
if (band + 2 < gBandCount) {
nextDelta = gBandStart[band + 2] - gBandStart[band + 1];
if (nextDelta == 0 || nextDelta > 255) {
nextDelta = 1;
}
*ST_MFP_TBDR = (uint8_t)nextDelta;
}
*ST_MFP_TBDR = (uint8_t)nextDelta;
*ST_MFP_ISRA = MFP_TB_CLEAR;
return;
}
@ -410,6 +466,19 @@ static void vblIsr(void) {
*ST_MFP_TBDR = (uint8_t)delta;
*ST_MFP_ISRA = MFP_TB_CLEAR;
*ST_MFP_TBCR = MFP_TBCR_EVENT;
// The restart above loads the counter from TBDR (= the band
// 0 -> 1 delta), so the first fire is correctly timed. Now
// pre-program TBDR with the band 1 -> 2 delta so the band-1
// underflow auto-reloads with the right interval (one fire
// ahead, matching timerBIsr). Writing TBDR while counting
// only changes the next reload, not the in-flight count.
if (gBandCount > 2) {
delta = gBandStart[2] - gBandStart[1];
if (delta == 0 || delta > 255) {
delta = 1;
}
*ST_MFP_TBDR = (uint8_t)delta;
}
} else {
*ST_MFP_TBCR = MFP_TBCR_STOP;
*ST_MFP_ISRA = MFP_TB_CLEAR;
@ -417,6 +486,23 @@ static void vblIsr(void) {
}
// Point the shifter at a screen page by writing the high and mid base
// bytes. The low byte does not exist on STF (base is 256-aligned), so
// two byte writes fully specify the address. The shifter latches the
// base at the next frame start, so calling this mid-visible-frame (as
// halPresent does, after the app's jlWaitVBL) makes the flip take
// effect on the following frame with no tearing. No TOS / Setscreen
// involvement, which matters because our VBL ISR replaced TOS's and
// does not run TOS's Setscreen-latch path.
static void setShifterVideoBase(const uint8_t *page) {
uintptr_t addr;
addr = (uintptr_t)page;
*ST_VID_BASE_HI = (uint8_t)((addr >> 16) & 0xFFu);
*ST_VID_BASE_MID = (uint8_t)((addr >> 8) & 0xFFu);
}
static long writePrevPaletteRegs(void) {
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
@ -448,30 +534,45 @@ static void writeDiagnostics(void) {
bool halInit(const jlConfigT *config) {
uintptr_t addr;
uint16_t i;
(void)config;
// Align screen buffer to 256 bytes inside the static storage.
addr = (uintptr_t)gScreenBuffer;
addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
gScreenBase = (uint8_t *)addr;
// Align page 0 to 256 bytes inside the static storage; page 1
// follows exactly one 32000-byte (256-aligned) page later, so it
// is 256-aligned too. The shifter only displays 256-byte-aligned
// bases, so both pages must satisfy that.
addr = (uintptr_t)gScreenBuffer;
addr = (addr + (ST_SCREEN_ALIGN - 1)) & ~((uintptr_t)ST_SCREEN_ALIGN - 1);
gScreenPage[0] = (uint8_t *)addr;
gScreenPage[1] = gScreenPage[0] + SURFACE_PIXELS_SIZE;
gFrontIdx = 0;
memset(gScreenBase, 0, SURFACE_PIXELS_SIZE);
memset(gScreenPage[0], 0, SURFACE_PIXELS_SIZE);
memset(gScreenPage[1], 0, SURFACE_PIXELS_SIZE);
// Bootstrap: pretend the previous frame dirtied EVERY row so the
// first two presents do full-page refreshes and bring both pages
// into sync with the shadow. After that we converge to the small
// per-row union(prev,current) cost.
for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = 0u;
gPrevStageMaxWord[i] = STAGE_DIRTY_FULL_MAX;
}
gPrevPhysbase = Physbase();
gPrevLogbase = Logbase();
gPrevRez = Getrez();
// Capture current palette so we can restore exactly on shutdown.
{
uint16_t i;
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
}
for (i = 0; i < SURFACE_COLORS_PER_PALETTE; i++) {
gPrevPalette[i] = (uint16_t)Setcolor((int16_t)i, -1);
}
// Switch to ST low-res: 320x200x16, mode 0.
Setscreen((long)gScreenBase, (long)gScreenBase, 0);
// Switch to ST low-res: 320x200x16, mode 0. Display page 0 first;
// halPresent flips to the off-screen page via the shifter base
// registers from here on.
Setscreen((long)gScreenPage[0], (long)gScreenPage[0], 0);
gModeSet = true;
// Force hardware palette entry 0 to black so the overscan border
@ -502,13 +603,23 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) {
StPlanarT *pd;
uint8_t *backPage;
uint8_t backIdx;
int16_t y;
int16_t firstDirty;
int16_t lastDirty;
int16_t runStart;
uint8_t curMin;
uint8_t curMax;
uint8_t prvMin;
uint8_t prvMax;
uint8_t minWord;
uint8_t maxWord;
uint16_t groupStart;
uint16_t groupEnd;
uint16_t byteStart;
uint16_t byteLen;
bool fullWidth;
if (src == NULL || !gModeSet) {
return;
@ -519,23 +630,168 @@ void halPresent(const jlSurfaceT *src) {
}
refreshPaletteStateIfNeeded(src);
// Planar buffer -> screen RAM. Each dirty word covers 4 pixels
// (a quarter of an 8-byte group). Round to whole groups for a
// simple aligned memcpy, since planar groups are the natural
// copy unit.
for (y = 0; y < SURFACE_HEIGHT; y++) {
minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
if (minWord > maxWord) {
continue;
// Idle-frame early-out. When the previous frame's snapshot recorded
// no dirty rows (gPrevAnyDirty == false) AND the current frame
// touched no row, union(prev,current) is empty: both pages already
// match the shadow (the one settling flip after the last activity
// ran on the first idle frame), so there is nothing to copy, no
// page to flip, and the snapshot below would only copy clean->clean.
// Skip the whole 200-row union scan and the 200-row snapshot copy.
// A row is dirty iff its min word stayed at or below its max word;
// a single byte test per row with early exit on the first dirty row
// suffices. Palette/SCB updates already ran above, so display state
// stays correct.
if (!gPrevAnyDirty) {
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (gStageMinWord[y] <= gStageMaxWord[y]) {
anyDirty = true;
break;
}
}
groupStart = (uint16_t)(minWord >> 2);
groupEnd = (uint16_t)((maxWord >> 2) + 1);
byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
memcpy(&gScreenBase[(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
&pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
byteLen);
if (!anyDirty) {
return;
}
}
// PAGE FLIP: draw into the stage's shadow (pd->base), bring the
// OFF-screen page up to date with the union(prev,current) dirty
// rows, then point the shifter at it. The off-screen page holds
// content from TWO frames ago (strict A/B alternation), so any row
// dirtied in either of the last two frames may differ from the
// current shadow -- the union refresh covers exactly those rows.
// The displayed (front) page is never touched, so there is no
// tearing regardless of copy duration.
backIdx = (uint8_t)(gFrontIdx ^ 1u);
backPage = gScreenPage[backIdx];
// WIN 2: bound the present to the union dirty row range. The vast
// majority of game frames touch a small vertical band; finding the
// first/last union-dirty row up front lets the copy loop skip the
// huge clean head/tail of the page, and lets us bail the whole copy
// body when nothing is dirty (steady-state idle frames). The scan
// is 200 cheap byte compares -- far less than 200 memcpy setups.
firstDirty = -1;
lastDirty = -1;
for (y = 0; y < SURFACE_HEIGHT; y++) {
curMin = gStageMinWord[y];
curMax = gStageMaxWord[y];
prvMin = gPrevStageMinWord[y];
prvMax = gPrevStageMaxWord[y];
minWord = (curMin < prvMin) ? curMin : prvMin;
maxWord = (curMax > prvMax) ? curMax : prvMax;
if (minWord <= maxWord) {
if (firstDirty < 0) {
firstDirty = y;
}
lastDirty = y;
}
}
if (firstDirty >= 0) {
// Shadow -> off-screen page. Each dirty word covers 4 pixels
// (a quarter of an 8-byte group). Round to whole groups for a
// simple aligned memcpy, since planar groups are the natural
// copy unit.
//
// WIN 1: backPage and pd->base share the 160-byte row stride and
// are both contiguous, so a run of consecutive FULL-WIDTH dirty
// rows is one contiguous span of (rows * 160) bytes. Coalesce
// such runs into a single memcpy: mintlib memcpy bursts long-
// aligned blocks with movem.l/move.l, so one big copy beats many
// tiny per-row ones (fewer call setups, longer inner burst).
// Partial-width rows keep their own per-row group-aligned memcpy,
// so narrow dirty bands stay correct and cheap. runStart < 0
// means "no full-width run is open". The per-row band is the
// union(prev,current) band so a row stale on the off-screen page
// from two frames ago is fully refreshed, not just this frame's
// sub-span.
runStart = -1;
for (y = firstDirty; y <= lastDirty; y++) {
curMin = gStageMinWord[y];
curMax = gStageMaxWord[y];
prvMin = gPrevStageMinWord[y];
prvMax = gPrevStageMaxWord[y];
minWord = (curMin < prvMin) ? curMin : prvMin;
maxWord = (curMax > prvMax) ? curMax : prvMax;
if (minWord > maxWord) {
// Clean row breaks any open full-width run.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
runStart = -1;
}
continue;
}
// Defense-in-depth: an upstream clipping bug could leave a
// maxWord past the row; clamp so the groupEnd-driven byteLen
// can never push the memcpy into the next scanline.
if (maxWord >= SURFACE_WORDS_PER_ROW) {
maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
}
groupStart = (uint16_t)(minWord >> 2);
groupEnd = (uint16_t)((maxWord >> 2) + 1);
byteStart = (uint16_t)(groupStart * ST_BYTES_PER_GROUP);
byteLen = (uint16_t)((groupEnd - groupStart) * ST_BYTES_PER_GROUP);
fullWidth = (groupStart == 0 && byteLen == (uint16_t)ST_BYTES_PER_ROW);
if (fullWidth) {
// Extend (or open) the contiguous full-width run; defer
// the copy until a non-full-width row or the range end
// flushes it.
if (runStart < 0) {
runStart = y;
}
continue;
}
// Partial-width row: flush any open full-width run first so
// the page stays in row order, then copy just this row's band.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(y - runStart) * (uint16_t)ST_BYTES_PER_ROW);
runStart = -1;
}
memcpy(&backPage [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
&pd->base [(uint16_t)y * ST_BYTES_PER_ROW + byteStart],
byteLen);
}
// Flush a full-width run that reached the end of the dirty range.
if (runStart >= 0) {
memcpy(&backPage [(uint16_t)runStart * ST_BYTES_PER_ROW],
&pd->base [(uint16_t)runStart * ST_BYTES_PER_ROW],
(uint16_t)(lastDirty + 1 - runStart) * (uint16_t)ST_BYTES_PER_ROW);
}
// The off-screen page now holds the complete current image.
// Point the shifter at it; the latch happens at the next frame
// start (we are mid-visible-frame here, after the app's
// jlWaitVBL), so the swap is tear-free. Only flip when there was
// something to refresh -- flipping on a fully idle frame would
// oscillate the display between the two pages with no benefit.
setShifterVideoBase(backPage);
gFrontIdx = backIdx;
}
// Snapshot this frame's dirty bands as "previous" for the next
// present. jlStagePresent clears the live bands right after
// halPresent returns, so we must grab them now. Next present unions
// these with that frame's dirty rows to refresh whatever the other
// page is stale on. Record whether any row was dirty so the next
// present can take the idle-frame early-out above.
{
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
gPrevStageMinWord[y] = gStageMinWord[y];
gPrevStageMaxWord[y] = gStageMaxWord[y];
if (gStageMinWord[y] <= gStageMaxWord[y]) {
anyDirty = true;
}
}
gPrevAnyDirty = anyDirty;
}
}
@ -577,6 +833,7 @@ uint16_t halFrameHz(void) {
// fault on user-mode access. Resolution is 5 ms, plenty for sound
// scheduling at 60 Hz AGI ticks.
static volatile uint32_t gMillisBase200 = 0;
static volatile bool gMillisBaseSet = false;
static long readHz200(void) {
return (long)*((volatile uint32_t *)0x4BAUL);
@ -585,11 +842,15 @@ static long readHz200(void) {
uint32_t halMillisElapsed(void) {
uint32_t ticks;
ticks = (uint32_t)Supexec(readHz200);
if (gMillisBase200 == 0u) {
// 0 is a legal counter value (power-on tick 0 and every ~248-day
// wrap), so use an explicit one-shot flag instead of a value-0
// sentinel to capture the base.
if (!gMillisBaseSet) {
gMillisBase200 = ticks;
gMillisBaseSet = true;
return 0u;
}
// 200 Hz counter -> ms = ticks * 5.
// 200 Hz counter -> ms = ticks * 5. Unsigned subtraction is wrap-safe.
return (uint32_t)((ticks - gMillisBase200) * 5u);
}
@ -626,12 +887,9 @@ void halShutdown(void) {
}
// Shared 68k fast paths for the chunky surface buffer (src/shared68k/
// surface68k.s). Same primitives used by the Amiga port -- the stage /
// surfaces are identical 4bpp packed layout on both 68k targets.
extern void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
extern void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h, uint16_t fillByte);
extern void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes, uint16_t h, uint16_t fillByte);
// ST word-interleaved planar 68k fast paths. The surface68kSt*
// primitives are ST-specific (plane-major group layout); they live in
// src/shared68k/surface68k.s alongside the Amiga-only planar helpers.
extern void surface68kStCircleOutline(uint8_t *base, uint16_t cx, uint16_t cy, uint16_t r, uint8_t color);
extern void surface68kStDrawLine(uint8_t *base, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t color);
extern void surface68kStFillSpan(uint8_t *base, int16_t left, int16_t right, int16_t y, uint8_t color);
@ -654,16 +912,11 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
uint8_t color;
uint32_t loLong;
uint32_t hiLong;
uint32_t *p32;
uint16_t groups;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
color = (uint8_t)(doubled & 0x0Fu);
/* Per-group: [p0_word][p1_word][p2_word][p3_word] = 8 bytes = 2 longs.
* loLong = (p0_word << 16) | p1_word; hiLong = (p2_word << 16) | p3_word. */
@ -671,8 +924,6 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
| ((color & 2u) ? 0x0000FFFFul : 0ul);
hiLong = ((color & 4u) ? 0xFFFF0000ul : 0ul)
| ((color & 8u) ? 0x0000FFFFul : 0ul);
(void)p32;
(void)groups;
surface68kStLongFill(pd->base,
(uint16_t)(ST_PLANAR_SIZE / ST_BYTES_PER_GROUP),
loLong, hiLong);
@ -686,7 +937,7 @@ bool halFastSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// never runs.
bool halFastFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)x; (void)y; (void)w; (void)h; (void)colorIndex;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
return true;
@ -726,13 +977,11 @@ bool halFastTileSnap(uint8_t *dstTilePixels, const uint8_t *srcRow0) {
bool halFastDrawPixel(jlSurfaceT *s, uint16_t x, uint16_t y, uint8_t colorIndex) {
StPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
if (pd != NULL) {
stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
}
stPlanarSetPixel(pd, (int16_t)x, (int16_t)y, (uint8_t)(colorIndex & 0x0Fu));
return true;
}
@ -845,19 +1094,17 @@ static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r
bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, uint8_t colorIndex) {
StPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
if (pd != NULL) {
// Asm walker assumes fully on-surface; partial-clip lines fall
// back to the C walker which clips per-pixel.
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
&& x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
} else {
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
// Asm walker assumes fully on-surface; partial-clip lines fall
// back to the C walker which clips per-pixel.
if (x0 >= 0 && x0 < SURFACE_WIDTH && y0 >= 0 && y0 < SURFACE_HEIGHT
&& x1 >= 0 && x1 < SURFACE_WIDTH && y1 >= 0 && y1 < SURFACE_HEIGHT) {
surface68kStDrawLine(pd->base, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
} else {
stPlanarLine(pd, x0, y0, x1, y1, (uint8_t)(colorIndex & 0x0Fu));
}
return true;
}
@ -865,13 +1112,10 @@ bool halFastDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t
bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
/* Off-surface circles fall back to the per-pixel C walker which
* does the clip per plot; the asm assumes fully-on-surface so it
* can drop the clip check from the inner loop. */
@ -890,13 +1134,10 @@ bool halFastDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_
bool halFastFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
StPlanarT *pd;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return true;
}
// Off-surface bounding box falls back to the C span walker, which
// clips each span; the asm assumes the whole circle is on-surface.
if ((int32_t)cx - (int32_t)r < 0
@ -990,7 +1231,7 @@ bool halFastFloodWalkAndScans(uint8_t *pixels, int16_t x, int16_t y, uint8_t mat
// false would dereference NULL s->pixels.
bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
(void)bx; (void)by; (void)fillWord;
if (s != jlStageGet()) {
if (s->portData == NULL) {
return false;
}
return true;
@ -998,8 +1239,9 @@ bool halFastTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint16_t fillWord) {
// Phase 2: allocate a shadow word-interleaved planar buffer per
// surface. Both stage and non-stage get their own buffer (gScreenBase
// remains the single display target).
// surface. Both stage and non-stage get their own buffer; the two
// displayed pages (gScreenPage[0/1]) are the flip targets, kept
// separate from every surface's shadow.
//
// LONG alignment is required, not just word: the full-row long-fill
// path and circle.s both do `move.l` writes on this buffer, and
@ -1605,10 +1847,20 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
* (where half_byte = dst_byte_col & 1). This replaces the old
* per-pixel walker which was ~30 cycles/px = ~17K cycles/draw and
* murdered the frame rate on shifted sprites (passenger walk,
* flame, anything else not on an 8-px boundary). */
* flame, anything else not on an 8-px boundary).
*
* Phase / placement is driven from the ORIGINAL signed x so a
* sub-byte left clip (x negative and not a multiple of 8) keeps
* its true source-to-destination phase. shift = x & 7 picks the
* low 3 bits of x (correct two's-complement modulo-8 even when x
* is negative); startByteX = x >> 3 (arithmetic) is the signed
* destination byte column for source byte column 0. Each source
* byte column is walked in full and the per-byte guards drop the
* off-surface portions, so the left byte and its right-carry spill
* are clipped independently. */
{
uint8_t shift = (uint8_t)(spritePxStart & 7);
int16_t startByteX = (int16_t)(spritePxStart >> 3);
uint8_t shift = (uint8_t)(x & 7);
int16_t startByteX = (int16_t)(x >> 3);
int16_t byteCol;
rowBase = pd->base + (uint16_t)spritePyStart * ST_BYTES_PER_ROW;
@ -1618,11 +1870,7 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
int16_t inTileY = (int16_t)(spritePy & 7);
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
int16_t spriteBx = (int16_t)((sx >> 3) + byteCol);
if (spriteBx >= (int16_t)wTiles) {
break;
}
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + spriteBx) * 32u);
const uint8_t *tile = sp->tileData + (uint32_t)((tileY * wTiles + byteCol) * 32u);
const uint8_t *chunky = tile + inTileY * 4u;
/* c2p: 8 pixels (4 chunky bytes) -> 4 plane bytes + mask. */
@ -1645,18 +1893,20 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
/* Compute ST address for the dst byte at byte_col_dst.
* Plane bytes are interleaved word-wise inside groups of
* 16 pixels = 8 bytes (4 planes x 2 bytes). */
* 16 pixels = 8 bytes (4 planes x 2 bytes). The left byte
* and the right-carry spill are guarded independently so
* a negative dstByteCol (sub-byte left clip) still lands
* its spill into on-surface byte dstByteCol+1. */
int16_t dstByteCol = (int16_t)(startByteX + byteCol);
if (dstByteCol < 0 || dstByteCol >= (int16_t)(SURFACE_WIDTH / 8)) {
continue;
}
{
uint16_t group = (uint16_t)(dstByteCol >> 1);
uint16_t half = (uint16_t)(dstByteCol & 1);
uint8_t leftMask = (uint8_t)(mask >> shift);
if (leftMask != 0u) {
uint8_t notM = (uint8_t)(~leftMask);
uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
if (leftMask != 0u
&& dstByteCol >= 0
&& dstByteCol < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t group = (uint16_t)(dstByteCol >> 1);
uint16_t half = (uint16_t)(dstByteCol & 1);
uint8_t notM = (uint8_t)(~leftMask);
uint8_t *p = rowBase + group * ST_BYTES_PER_GROUP + half;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 >> shift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 >> shift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 >> shift));
@ -1665,18 +1915,18 @@ void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t
if (shift != 0u) {
uint8_t rshift = (uint8_t)(8u - shift);
uint8_t rightMask = (uint8_t)(mask << rshift);
if (rightMask != 0u) {
int16_t rdc = (int16_t)(dstByteCol + 1);
if (rdc < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t rgroup = (uint16_t)(rdc >> 1);
uint16_t rhalf = (uint16_t)(rdc & 1);
uint8_t notM = (uint8_t)(~rightMask);
uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
}
int16_t rdc = (int16_t)(dstByteCol + 1);
if (rightMask != 0u
&& rdc >= 0
&& rdc < (int16_t)(SURFACE_WIDTH / 8)) {
uint16_t rgroup = (uint16_t)(rdc >> 1);
uint16_t rhalf = (uint16_t)(rdc & 1);
uint8_t notM = (uint8_t)(~rightMask);
uint8_t *p = rowBase + rgroup * ST_BYTES_PER_GROUP + rhalf;
p[0] = (uint8_t)((p[0] & notM) | (uint8_t)(p0 << rshift));
p[2] = (uint8_t)((p[2] & notM) | (uint8_t)(p1 << rshift));
p[4] = (uint8_t)((p[4] & notM) | (uint8_t)(p2 << rshift));
p[6] = (uint8_t)((p[6] & notM) | (uint8_t)(p3 << rshift));
}
}
}

View file

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

View file

@ -94,9 +94,15 @@
// ----- Mix configuration -----
// MIX_RATE is the nominal target; the SB's 8-bit time-constant DAC can
// only clock at 1000000/(256-tc) Hz, which is not exactly 22050. The
// SB hardware rate is the fixed one, so the effective rate derived from
// the same time constant (see gEffRate, set in halAudioInit) is the
// single source of truth handed to libxmp and the SFX resampler.
#define MIX_RATE 22050
#define MIX_DMA_BUFFER 4096 // total; two 2048-byte halves
#define MIX_HALF (MIX_DMA_BUFFER / 2)
#define SB_DSP_CLOCK_HZ 1000000u // 8-bit DSP time-constant base
#define ISR_LOCK_SIZE 4096
@ -109,6 +115,12 @@ static uint8_t gDspMajor = 0;
static uint8_t gDspMinor = 0;
static bool gDspPresent = false;
// Effective SB playback rate derived from the DSP time constant. Set
// once in halAudioInit and used as the single rate source of truth for
// libxmp's mixer and the SFX resampler, so they match what the DAC
// actually clocks (~22222 Hz for a 22050 Hz target) instead of drifting.
static uint16_t gEffRate = MIX_RATE;
// Conventional-memory DMA buffer.
static int gDmaSelector = 0;
static uint32_t gDmaLinear = 0;
@ -153,6 +165,7 @@ static void dspWrite(uint8_t value);
static void freeDmaBuffer(void);
static void haltDspDma(void);
static void parseBlasterEnv(void);
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit);
static void picMaskIrq(uint8_t irq);
static void picUnmaskIrq(uint8_t irq);
static int picVectorForIrq(uint8_t irq);
@ -324,13 +337,13 @@ static void programDmaController(void) {
}
// 8-bit DSP time constant: tc = 256 - 1000000 / rate. SB plays at
// 1000000 / (256 - tc) Hz, so we round to the nearest representable
// rate. For 22050 Hz: tc = 256 - 45 = 211.
// 8-bit DSP time constant: tc = 256 - SB_DSP_CLOCK_HZ / rate. SB plays
// at SB_DSP_CLOCK_HZ / (256 - tc) Hz, so we round to the nearest
// representable rate. For 22050 Hz: tc = 256 - 45 = 211.
static uint8_t dspTimeConst(uint16_t rate) {
uint32_t tc;
tc = 256u - (1000000u / rate);
tc = 256u - (SB_DSP_CLOCK_HZ / rate);
if (tc < 4) {
tc = 4;
}
@ -364,6 +377,19 @@ static void haltDspDma(void) {
}
// Decode an IRQ number to its PIC Interrupt Mask Register port and the
// mask bit within that register. IRQ 0-7 live in PIC1, 8-15 in PIC2.
static void picIrqPortBit(uint8_t irq, uint16_t *port, uint8_t *bit) {
if (irq < 8) {
*port = PIC1_IMR;
*bit = (uint8_t)(1u << irq);
} else {
*port = PIC2_IMR;
*bit = (uint8_t)(1u << (irq - 8));
}
}
// Unmask the IRQ line in the PIC's Interrupt Mask Register so the IRQ
// can reach the CPU. Some environments (DOSBox in particular) leave the
// SB IRQ masked at boot; without this the hooked vector never fires
@ -373,13 +399,7 @@ static void picUnmaskIrq(uint8_t irq) {
uint8_t bit;
uint8_t mask;
if (irq < 8) {
port = PIC1_IMR;
bit = (uint8_t)(1u << irq);
} else {
port = PIC2_IMR;
bit = (uint8_t)(1u << (irq - 8));
}
picIrqPortBit(irq, &port, &bit);
mask = inportb(port);
mask = (uint8_t)(mask & ~bit);
outportb(port, mask);
@ -398,13 +418,7 @@ static void picMaskIrq(uint8_t irq) {
uint8_t bit;
uint8_t mask;
if (irq < 8) {
port = PIC1_IMR;
bit = (uint8_t)(1u << irq);
} else {
port = PIC2_IMR;
bit = (uint8_t)(1u << (irq - 8));
}
picIrqPortBit(irq, &port, &bit);
mask = inportb(port);
mask = (uint8_t)(mask | bit);
outportb(port, mask);
@ -474,6 +488,11 @@ bool halAudioInit(void) {
gDspMajor = (uint8_t)major;
gDspMinor = (uint8_t)minor;
// Lock in the effective playback rate from the same time constant
// the SB DAC will receive, so libxmp and the SFX resampler agree
// with the hardware instead of using the nominal MIX_RATE.
gEffRate = (uint16_t)(SB_DSP_CLOCK_HZ / (256u - dspTimeConst(MIX_RATE)));
if (!allocDmaBuffer()) {
return false;
}
@ -484,6 +503,20 @@ bool halAudioInit(void) {
return false;
}
// Allocate the iret wrapper first so its failure path needs no
// unlock; the page locks below are only required once the vector
// is actually hooked.
vector = picVectorForIrq(gSbIrq);
_go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
gNewIrqVec.pm_offset = (unsigned long)sbIsr;
gNewIrqVec.pm_selector = _go32_my_cs();
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
xmp_free_context(gXmpCtx);
gXmpCtx = NULL;
freeDmaBuffer();
return false;
}
// Lock ISR code + the data sbIsr touches at IRQ time. The mixer
// refill no longer runs in interrupt context, so refillHalf and
// libxmp internals do not need locking.
@ -493,16 +526,6 @@ bool halAudioInit(void) {
_go32_dpmi_lock_data((void *)&gSbBase, sizeof(gSbBase));
_go32_dpmi_lock_data((void *)&gSbIrq, sizeof(gSbIrq));
vector = picVectorForIrq(gSbIrq);
_go32_dpmi_get_protected_mode_interrupt_vector(vector, &gOldIrqVec);
gNewIrqVec.pm_offset = (unsigned long)sbIsr;
gNewIrqVec.pm_selector = _go32_my_cs();
if (_go32_dpmi_allocate_iret_wrapper(&gNewIrqVec) != 0) {
xmp_free_context(gXmpCtx);
gXmpCtx = NULL;
freeDmaBuffer();
return false;
}
_go32_dpmi_set_protected_mode_interrupt_vector(vector, &gNewIrqVec);
gIrqHooked = true;
@ -569,7 +592,7 @@ void halAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
return;
}
gXmpLoaded = true;
if (xmp_start_player(gXmpCtx, MIX_RATE,
if (xmp_start_player(gXmpCtx, gEffRate,
XMP_FORMAT_8BIT | XMP_FORMAT_UNSIGNED | XMP_FORMAT_MONO) != 0) {
xmp_release_module(gXmpCtx);
gXmpLoaded = false;
@ -588,7 +611,7 @@ void halAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, gEffRate);
}
@ -596,7 +619,7 @@ void halAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
if (!gDspPresent || slot >= JOEY_AUDIO_SFX_SLOTS) {
return;
}
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, gEffRate);
}

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
// one of 16 palettes per scanline. Mode 13h is 8bpp; the three pairs of
@ -17,6 +17,7 @@
#include <string.h>
#include <time.h> // uclock / UCLOCKS_PER_SEC
#include <dos.h>
#include <dpmi.h>
#include <go32.h>
#include <pc.h>
@ -40,6 +41,7 @@
static void crashHandler(int sig);
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst);
static void installCrashLog(void);
static void rebuildExpandLut(uint8_t palBase);
static void uploadPalette(const jlSurfaceT *src);
static void uploadPaletteIfNeeded(const jlSurfaceT *src);
@ -49,48 +51,74 @@ static uint8_t *gVgaMem = NULL;
static bool gNearEnabled = false;
static FILE *gCrashLog = NULL;
// Frame counter via $3DA bit 3 polling; rising edge marks the start
// of vertical retrace. Caller polls fast enough that no edge is
// missed (UBER's hot loop is far below 70 Hz period even on a 386).
static uint16_t gFrameCount;
static uint8_t gPrevInVret;
// Cached palette from the last present. VGA DAC programming is ~768
// outportb calls; skip it when the source palette is unchanged.
static uint16_t gCachedPalette[SURFACE_PALETTE_COUNT][SURFACE_COLORS_PER_PALETTE];
static bool gCacheValid = false;
// Scratch row buffer for halPresent. expandAndWriteLine builds the
// 8bpp form of a dirty band here, then a single memcpy lowers to REP
// MOVS to push the whole band into VGA RAM in one bus-batched run.
// The original per-pixel loop wrote one byte at a time, which stalled
// the CPU on every ISA cycle (~125 ns at 8 MHz bus); REP MOVS pipelines
// 16-bit ISA writes back-to-back, much better on slow CPUs.
static uint8_t gExpandRow[SURFACE_WIDTH];
// Expand LUT for halPresent. Maps a 4bpp-packed source byte to the
// 2-byte little-endian mode-13h output pair for the current scanline's
// SCB. The low byte (= dst[x], lower VGA address = LEFT pixel) holds
// the packed byte's HIGH nibble; the high byte (= dst[x+1] = RIGHT
// pixel) holds the LOW nibble. Storing lut[byte] as one 16-bit x86
// write therefore lands the left pixel at the lower address, matching
// the old two-byte sequence exactly. Rebuilt only when the SCB-derived
// palBase changes from the previous row (most stages run long runs of
// equal SCB), so the per-row cost is one compare, not a 256-entry fill.
static uint16_t gExpandLut[256];
static uint8_t gExpandLutBase = 0;
static bool gExpandLutValid = false;
// ----- Internal helpers (alphabetical) -----
static void expandAndWriteLine(const jlSurfaceT *src, int16_t y, int16_t x, uint16_t w, uint8_t *dst) {
uint8_t palBase;
const uint8_t *srcBytes;
uint8_t *out;
uint16_t *out;
uint16_t pairs;
uint16_t i;
uint8_t byte;
palBase = (uint8_t)(src->scb[y] << 4);
palBase = (uint8_t)(src->scb[y] << 4);
if (!gExpandLutValid || palBase != gExpandLutBase) {
rebuildExpandLut(palBase);
}
// Dirty bands are chunky-word aligned (halPresent computes
// pixelX = minWord*4, pixelW = ...*4), so x is even and 1 source
// byte (= 2 nibbles) maps to 2 destination bytes -- no per-pixel
// odd/even shift branch needed.
// byte (= 2 nibbles) maps to 2 destination bytes. The even x keeps
// &dst[x] 16-bit aligned, so each pair is one aligned 16-bit store
// straight into VGA RAM -- no scratch row, no trailing memcpy.
srcBytes = &src->pixels[y * SURFACE_BYTES_PER_ROW + (x >> 1)];
out = gExpandRow;
out = (uint16_t *)&dst[x];
pairs = (uint16_t)(w >> 1);
for (i = 0; i < pairs; i++) {
byte = *srcBytes++;
*out++ = (uint8_t)(palBase | (byte >> 4));
*out++ = (uint8_t)(palBase | (byte & 0x0Fu));
*out++ = gExpandLut[*srcBytes++];
}
}
// Single bus-batched copy to VGA. memcpy in DJGPP libc lowers to
// REP MOVS, which queues 16-bit ISA write cycles back-to-back
// instead of stalling the CPU on every byte store.
memcpy(&dst[x], gExpandRow, w);
static void rebuildExpandLut(uint8_t palBase) {
uint16_t byte;
uint8_t leftPixel;
uint8_t rightPixel;
for (byte = 0; byte < 256u; byte++) {
// Packed high nibble = LEFT pixel (lower x); packed low nibble
// = RIGHT pixel. The little-endian word puts the LEFT pixel in
// the low byte so a 16-bit store lands it at the lower (even)
// VGA address -- byte-for-byte identical to the old pair store.
leftPixel = (uint8_t)(palBase | (byte >> 4));
rightPixel = (uint8_t)(palBase | (byte & 0x0Fu));
gExpandLut[byte] = (uint16_t)((uint16_t)rightPixel << 8 | (uint16_t)leftPixel);
}
gExpandLutBase = palBase;
gExpandLutValid = true;
}
@ -102,6 +130,12 @@ static void uploadPalette(const jlSurfaceT *src) {
uint8_t g;
uint8_t b;
// The whole index+data burst relies on the DAC's auto-increment
// index latch staying in step across the 768 data writes. Mask
// interrupts so no installed ISR can desync the latch by touching
// $3C8/$3C9 mid-burst. Invariant: no installed ISR may write the
// VGA DAC ports while a palette upload is in progress.
disable();
outportb(DAC_INDEX_PORT, 0);
for (p = 0; p < SURFACE_PALETTE_COUNT; p++) {
for (c = 0; c < SURFACE_COLORS_PER_PALETTE; c++) {
@ -109,11 +143,14 @@ static void uploadPalette(const jlSurfaceT *src) {
r = (uint8_t)((rgb >> 8) & 0x0F);
g = (uint8_t)((rgb >> 4) & 0x0F);
b = (uint8_t)(rgb & 0x0F);
// VGA DAC is 6-bit per channel; expand JoeyLib's 4-bit
// channel to 6 bits (0..63).
outportb(DAC_DATA_PORT, (uint8_t)((r << 2) | (r >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((g << 2) | (g >> 2)));
outportb(DAC_DATA_PORT, (uint8_t)((b << 2) | (b >> 2)));
}
}
enable();
}
@ -256,6 +293,13 @@ void halPresent(const jlSurfaceT *src) {
if (minWord > maxWord) {
continue;
}
// Defense-in-depth: an upstream clipping bug that wrote
// maxWord >= SURFACE_WORDS_PER_ROW would push the expand copy
// past the 320-byte VGA row into the next scanline. Clamp to
// the in-contract maximum word index.
if (maxWord >= SURFACE_WORDS_PER_ROW) {
maxWord = (uint8_t)(SURFACE_WORDS_PER_ROW - 1);
}
pixelX = (int16_t)((uint16_t)minWord << 2);
pixelW = (uint16_t)(((uint16_t)maxWord - minWord + 1u) << 2);
expandAndWriteLine(src, y, pixelX, pixelW, &gVgaMem[y * VGA_STRIDE]);
@ -279,12 +323,6 @@ void halWaitVBL(void) {
}
// Frame counter via $3DA bit 3 polling; rising edge marks the start
// of vertical retrace. Caller polls fast enough that no edge is
// missed (UBER's hot loop is far below 70 Hz period even on a 386).
static uint16_t gFrameCount = 0;
static uint8_t gPrevInVret = 0;
uint16_t halFrameCount(void) {
uint8_t now;
@ -322,9 +360,12 @@ extern volatile uint32_t gAudioIsrFires;
extern uint16_t gAudioIsrHz;
extern volatile bool gAudioIsrActive;
static uclock_t gMillisBase = 0;
static uclock_t gMillisBase = 0;
static bool gMillisBaseSet = false;
uint32_t halMillisElapsed(void) {
uclock_t delta;
if (gAudioIsrActive && gAudioIsrHz > 0u) {
// Each ISR fire = 1000/hz ms. Hardware-IRQ-locked, so
// wall-clock-correct and monotonic. uint64 promotion in
@ -333,11 +374,20 @@ uint32_t halMillisElapsed(void) {
return (uint32_t)(((uint64_t)gAudioIsrFires * 1000uL)
/ (uint32_t)gAudioIsrHz);
}
if (gMillisBase == 0) {
gMillisBase = uclock();
// Use an explicit one-shot flag rather than gMillisBase == 0, so a
// legitimate uclock() value of 0 on the first call cannot re-arm
// the base every call.
if (!gMillisBaseSet) {
gMillisBase = uclock();
gMillisBaseSet = true;
return 0u;
}
return (uint32_t)((uclock() - gMillisBase) / (UCLOCKS_PER_SEC / 1000u));
// Pre-scale by 1000 before dividing by UCLOCKS_PER_SEC instead of
// dividing by the truncated UCLOCKS_PER_SEC/1000 (== 1193, not
// 1193.18), which would run the clock ~0.015% fast. The uint64
// intermediate keeps delta*1000 from overflowing.
delta = uclock() - gMillisBase;
return (uint32_t)(((uint64_t)delta * 1000uL) / (uint64_t)UCLOCKS_PER_SEC);
}

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_1] = (btn & BIOS_BTN_B2) == 0;
// Announce presence from the successful switches read that produced
// live button state, independent of the axis decode below -- matches
// the ST/IIgs convention. Doing it here (not after the position read)
// keeps connected status consistent with the button state.
gJoyConnected[JOYSTICK_0] = true;
gJoyConnected[JOYSTICK_1] = true;
memset(&r, 0, sizeof(r));
r.h.ah = BIOS_FN_GAMEPORT;
r.x.dx = BIOS_GAMEPORT_DX_POS;
__dpmi_int(BIOS_INT, &r);
if (r.x.flags & BIOS_FLAGS_CARRY) {
// A failed position read leaves the stick connected (buttons are
// live), but report centered axes rather than stale values.
gJoyAxisX[JOYSTICK_0] = 0;
gJoyAxisY[JOYSTICK_0] = 0;
gJoyAxisX[JOYSTICK_1] = 0;
gJoyAxisY[JOYSTICK_1] = 0;
return;
}
gJoyAxisX[JOYSTICK_0] = scaleAxis(r.x.ax);
gJoyAxisY[JOYSTICK_0] = scaleAxis(r.x.bx);
gJoyAxisX[JOYSTICK_1] = scaleAxis(r.x.cx);
gJoyAxisY[JOYSTICK_1] = scaleAxis(r.x.dx);
gJoyConnected[JOYSTICK_0] = true;
gJoyConnected[JOYSTICK_1] = true;
}
@ -296,6 +306,12 @@ static void mousePoll(void) {
memset(&r, 0, sizeof(r));
r.x.ax = MOUSE_FN_GET_STATE;
__dpmi_int(MOUSE_INT, &r);
// Assumes a mode-13h-aware driver (DOSBox / DOSBox-X) that returns
// native 0..SURFACE_WIDTH-1 X. Some real MS-compatible drivers keep
// an internal 640-wide virtual space in mode 13h where fn 7/8 only
// clamp min/max (not scale), so X comes back doubled and would need
// a >>1; that scale-detection is not done here. Y is unaffected
// (only X is virtualized in 320-wide modes).
gMouseX = (int16_t)r.x.cx;
gMouseY = (int16_t)r.x.dx;
btn = r.x.bx;

View file

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

View file

@ -7,20 +7,19 @@
// $9E00 - $9FFF 16 palettes x 16 colors x 2 bytes, $0RGB
//
// NEWVIDEO register at $00C029 controls SHR enable. Bit 7 turns SHR on.
// ORCA/C must be built with 32-bit pointer mode (-w or equivalent) so
// that the long addresses resolve to bank $E1.
// Long (24-bit) addresses are used so they resolve to bank $E1.
//
// DIRTY-WALK + PEI-SLAM PRESENT
// -----------------------------
// halPresent walks the per-row dirty bands maintained by drawing
// primitives in src/core/*.c. Fully-dirty rows go through the PEI
// slam in src/port/iigs/peislam.asm (~530 cyc/row, ~55% faster than
// memcpy/MVN); partial-dirty rows use memcpy, which ORCA-C lowers
// slam in src/port/iigs/peislam.s (~530 cyc/row, ~55% faster than
// memcpy/MVN); partial-dirty rows use memcpy, which lowers
// to MVN (7 cyc/byte) -- the fastest 65816 way to move bytes into
// bank $E1 when the dirty band is too narrow to amortize the slam's
// per-call AUXWRITE/RAMRD/shadow toggle.
//
// peislam.asm declares its load segment as DRAWPRIMS so the linker
// peislam.s declares its load segment as DRAWPRIMS so the linker
// places it in its own bank, separate from AUDIO's _ROOT (where
// audio_full.c + Memory Manager + stdio + NTPstreamsound already
// crowd up against the 64 KB-per-bank limit).
@ -33,7 +32,7 @@
#include "hal.h"
#include "surfaceInternal.h"
/* GetTick wrapper in peislam.asm: invokes the Misc Toolset GetTick
/* GetTick wrapper in peislam.s: invokes the Misc Toolset GetTick
* ($2503) and returns the low 16 bits of the system's tick counter
* (firmware VBL ISR-driven). Polling $C019 from C user code missed
* transitions for any op over ~1 ms; the system's tick counter is
@ -48,10 +47,9 @@ static uint16_t gFrameHz = 60u;
// hal.c is the single TU that calls into joeyDraw.asm. Cross-
// platform draw.c / tile.c / etc. dispatch through halFast*
// functions defined here; they never reference the asm symbols
// directly. This avoids the cumulative ORCA-Linker-Expression-
// too-complex-in-13/SysLib failure that hit when each cross-
// platform TU brought its own asm extern.
JOEYLIB_SEGMENT("DRAWPRIMS")
// directly. Keeping the asm externs in a single TU avoids the
// link-time fragility seen when each cross-platform TU brought its
// own asm extern.
// 32 KB stack-slam fill via AUXWRITE. ~25 ms full-screen.
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
@ -81,9 +79,10 @@ extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_
extern void iigsDrawCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
// Stage-to-SHR full upload: pixels (MVN $01->$E1), SCB, palette.
// Asm uses post-MVN DBR=$E1 to do sta abs,Y for SCB/palette.
// Replaces ORCA-C's memcpy path which silently fails when called
// from halPresent (DBR-state quirk after prior asm primitives).
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr);
// The asm performs the upload directly rather than a C-side memcpy,
// which is unreliable when called from halPresent (DBR-state quirk
// after prior asm primitives).
extern void iigsBlitStageToShr(uint8_t *scbPtr, uint16_t *palettePtr, uint16_t uploadFlags);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by halFastFloodWalkAndScans.
extern uint16_t gFloodSeedMatch;
@ -102,7 +101,7 @@ extern void iigsInitRowLut(void);
// screen presents (halPresentRect). srcOffset is the byte offset
// within bank $01 of the FIRST byte to copy on the FIRST row;
// subsequent rows are at srcOffset + 160, etc. ~9 cyc/byte vs
// ORCA-C memcpy's ~30 cyc/byte.
// C memcpy's ~30 cyc/byte.
extern void iigsBlitRectStageToShr(uint16_t srcOffset, uint16_t copyBytes, uint16_t rowsLeft);
// PEI-slam variant of the per-row rect blit. ~3 cyc/byte vs MVN's
// ~9 cyc/byte. Constraints: copyBytes must be even and 2..80
@ -165,7 +164,7 @@ static bool gModeSet = false;
// SCB / palette upload skipping is now driven by gStageScbDirty /
// gStagePaletteDirty (core/surface.c). The old per-frame memcmp-
// against-cached-copy approach was costing ~7 ms / frame on ORCA-C.
// against-cached-copy approach was costing ~7 ms / frame.
// PEI slam scratch. File-scope non-static so the asm can `ext` them;
// all accesses inside the slam use long-mode `>` addressing so they
@ -175,8 +174,10 @@ volatile uint8_t gPeiOrigShadow;
volatile uint16_t gPeiTempRowBase;
volatile uint16_t gPeiCurRow; // row counter saved across slam (stack is hijacked)
volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across slam (Y reg storage)
volatile uint16_t gPeiMvnSrc; // narrow-extent present: MVN source offset
volatile uint16_t gPeiMvnCount; // narrow-extent present: MVN byte count
// peislam.asm's per-row peiSlamFullRow helper is no longer wired in;
// peislam.s's per-row peiSlamFullRow helper is no longer wired in;
// the present pipeline now does its own PEI-slam loop inside
// iigsBlitStageToShr above (with dirty-row skip).
@ -184,7 +185,7 @@ volatile uint16_t gPeiChunkRow; // in-chunk row counter saved across sl
// Upload SCB / palette into bank-$E1 SHR memory only when the
// matching dirty flag is set. Replaces a per-frame 712-byte memcmp
// pair (~7 ms / frame on ORCA-C with -b) with a 2-cyc flag check.
// pair (~7 ms / frame) with a 2-cyc flag check.
// gStageScbDirty / gStagePaletteDirty live in core/surface.c; they
// start true (forces the very first present to upload), get set true
// again whenever jlScbSet* / jlPaletteSet mutate the stage's data, and
@ -231,16 +232,22 @@ const char *halLastError(void) {
void halPresent(const jlSurfaceT *src) {
uint16_t uploadFlags;
if (src == NULL) {
return;
}
// iigsBlitStageToShr does pixels (MVN $01->$E1) + SCB + palette
// upload entirely in asm via DBR=$E1 + sta abs,Y indexed stores.
// ORCA-C's C-side memcpy to bank $E1 has been unreliable from
// C-side memcpy to bank $E1 has been unreliable from
// halPresent's calling context, so we route everything through
// the asm path. Future: re-introduce per-row dirty-band logic
// for partial-screen updates (currently we always blit 32K).
iigsBlitStageToShr(src->scb, &src->palette[0][0]);
// the asm path. uploadFlags gates the optional SCB / palette
// uploads on the dirty bits so we don't re-push 712 unchanged
// bytes every frame: bit 0 = SCB, bit 1 = palette (pixels always).
uploadFlags = (uint16_t)gStageScbDirty | ((uint16_t)gStagePaletteDirty << 1);
iigsBlitStageToShr(src->scb, &src->palette[0][0], uploadFlags);
gStagePaletteDirty = false;
gStageScbDirty = false;
}
@ -266,7 +273,7 @@ void halShutdown(void) {
// halFastFillRect: macro-dispatched in core/hal.h, same as the other
// halFast* primitives. The C wrapper that used to live here was kept
// as load-bearing _ROOT mass to defeat ORCA-Linker bank fragility;
// as load-bearing _ROOT mass to defeat linker bank fragility;
// since the CORESYS migration drained _ROOT, the macro form is safe.
@ -298,50 +305,9 @@ void halSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData) {
}
// IIgs SHR is chunky-native; no bitplanes to update.
void halFillRectPlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t colorIndex) {
(void)s;
(void)x;
(void)y;
(void)w;
(void)h;
(void)colorIndex;
}
void halSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
(void)dst;
(void)src;
}
void halTileFillPlanes(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
(void)s; (void)bx; (void)by; (void)colorIndex;
}
void halTileCopyPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy;
}
void halTileCopyMaskedPlanes(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
(void)dst; (void)dstBx; (void)dstBy; (void)src; (void)srcBx; (void)srcBy; (void)transparentIndex;
}
void halTilePastePlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
(void)dst; (void)bx; (void)by; (void)chunkyTile;
}
void halTilePasteMonoPlanes(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
(void)dst; (void)bx; (void)by; (void)monoTile; (void)fgColor; (void)bgColor;
}
void halTileSnapPlanes(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyTileOut) {
(void)src; (void)bx; (void)by; (void)chunkyTileOut;
}
void halSpriteDrawPlanes(jlSurfaceT *s, const jlSpriteT *sp, int16_t x, int16_t y) {
(void)s; (void)sp; (void)x; (void)y;
}
void halSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t *dstPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)dstPlaneBytes;
}
void halSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, const uint8_t *srcPlaneBytes) {
(void)s; (void)x; (void)y; (void)w; (void)h; (void)srcPlaneBytes;
}
// IIgs is chunky-native: the hal*Planes hooks are compiled out by
// no-op macros in core/hal.h (gated on JOEYLIB_PLATFORM_IIGS), so no
// stub functions are defined here.
/* Phase 9 chunky reader hooks: IIgs reads from s->pixels just like
@ -436,7 +402,7 @@ void halWaitVBL(void) {
// context where ToolBox interrupt setup would be intrusive.
//
// gFrameCount uses an explicit lda+adc+sta read-modify-write rather
// than `gFrameCount++` because ORCA-C lowers the post-increment to
// than `gFrameCount++` because the post-increment can lower to
// `inc |gFrameCount` (the only INC abs form on 65816 -- there is no
// INC long-abs). With this file in the DRAWPRIMS load segment but
// halFrameCount called from CORESYS via JSL, DBR isn't pointing at
@ -454,8 +420,7 @@ uint16_t halFrameHz(void) {
}
// IIgs: frame-counter approximation. ORCA-C's time() bursts the
// stdio cluster bank limit (see feedback_orca_c_time_segment), so
// IIgs: frame-counter approximation. time() is avoided here, so
// we approximate wall-clock ms from the 16-bit frame counter at
// gFrameHz (60 Hz). The 16-bit counter wraps every ~1090 sec, so
// callers tracking deltas should use this for short intervals only

View file

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

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
| same 4bpp packed (2 px / byte, 160 bytes / row, 32000 bytes / surface)
| format -- the c2p / blit-to-screen step that converts to native
| pixel layout happens only at present time. So the per-byte buffer
| operations (fill, clear) can be shared across both ports.
| Holds surface68kFillSpan4Planes -- the per-row body of the 4-plane
| planar rect/circle fill. Only the Amiga port calls it (ST is
| word-interleaved and uses its own St* routines); it lives here
| because the file is globbed into both ports' builds. The earlier
| chunky clear/fill primitives that this file once also held were
| removed once both ports went native-planar.
|
| Calling convention: cdecl (gcc-amigaos / gcc-atari-mint).
| d0/d1/a0/a1 are caller-save (scratch).
@ -16,245 +17,6 @@
.text
| ----------------------------------------------------------------
| void surface68kClearLong(uint8_t *pixels, uint16_t fillByte);
|
| Fill 32000 bytes (SURFACE_PIXELS_SIZE) starting at `pixels` with
| `fillByte`. Unrolled move.l with the byte expanded to a 4-byte
| word so we can write 32 bytes per loop body instead of 1.
| ----------------------------------------------------------------
.globl _surface68kClearLong
.equ SURF_CLEAR_BYTES, 32000
.equ SURF_CLEAR_LONGS, (SURF_CLEAR_BYTES / 4)
.equ SURF_CLEAR_PER_ITER, 8
.equ SURF_CLEAR_ITERS, (SURF_CLEAR_LONGS / SURF_CLEAR_PER_ITER)
| Save d2 (used as dbra counter). a0/a1/d0/d1 are
| caller-save so we may clobber them freely. Stack
| offset to args = 4 (saved d2) + 4 (return PC) = 8.
.equ CLR_SAVED, 4
_surface68kClearLong:
move.l %d2,-(%sp)
move.l 4+CLR_SAVED(%sp),%a0 | pixels
| Zero d0 BEFORE the move.b -- m68k move.b only touches
| the low byte of the destination, so d0[31..8] would
| otherwise hold whatever garbage the caller left in d0
| and pollute the OR-replicate chain below.
moveq #0,%d0
move.b 8+CLR_SAVED+3(%sp),%d0 | fillByte (low byte of int)
| Build a long with fillByte replicated four times.
| d0 = $000000FB -> $0000FBFB -> $FBFBFBFB
move.l %d0,%d1
lsl.l #8,%d1
or.l %d1,%d0 | d0 = $0000FBFB
move.l %d0,%d1
swap %d1 | d1 = $FBFB0000
or.l %d1,%d0 | d0 = $FBFBFBFB
move.l %a0,%a1 | dest cursor
move.w #(SURF_CLEAR_ITERS - 1),%d2 | dbra count
.LclearLoop:
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
dbra %d2,.LclearLoop
move.l (%sp)+,%d2
rts
| ----------------------------------------------------------------
| void surface68kFillRectFull(uint8_t *pixels, int16_t y, uint16_t h,
| uint16_t fillByte);
|
| Full-width (320 px = 160 byte) rectangle fill. `pixels` is the
| surface base; the row at `y` is `pixels + y * 160`. Fills `h`
| consecutive rows using move.l writes.
| ----------------------------------------------------------------
.globl _surface68kFillRectFull
.equ SURF_FRF_BYTES_PER_ROW, 160
| Save d2/d3 (used for fillByte replicate scratch and
| row counter). 8 bytes saved + 4 ret PC = 12 to args.
.equ FRF_SAVED, 8
_surface68kFillRectFull:
movem.l %d2-%d3,-(%sp)
move.l 4+FRF_SAVED(%sp),%a0 | pixels base
| Zero d0/d1 BEFORE move.b/move.w -- those only update
| sub-register portions and would otherwise carry the
| caller's garbage upper bits into the replicate OR.
moveq #0,%d0
moveq #0,%d1
moveq #0,%d2
move.w 8+FRF_SAVED+2(%sp),%d1 | y (low word)
move.w 12+FRF_SAVED+2(%sp),%d2 | h (low word)
move.b 16+FRF_SAVED+3(%sp),%d0 | fillByte
| Bail on degenerate height.
tst.w %d2
ble .Lfrf_done
| Build replicated fillByte in d0.
move.l %d0,%d3
lsl.l #8,%d3
or.l %d3,%d0
move.l %d0,%d3
swap %d3
or.l %d3,%d0 | d0 = byte * $01010101
| row pointer = pixels + y * 160 = pixels + y*128 + y*32
ext.l %d1
move.l %d1,%d3
lsl.l #7,%d3 | y * 128
lsl.l #5,%d1 | y * 32
add.l %d3,%d1 | y * 160
add.l %d1,%a0 | a0 = first row to fill
subq.w #1,%d2 | row dbra count
.Lfrf_rowLoop:
move.l %a0,%a1
| 40 longs per row, fully unrolled.
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
move.l %d0,(%a1)+
lea SURF_FRF_BYTES_PER_ROW(%a0),%a0
dbra %d2,.Lfrf_rowLoop
.Lfrf_done:
movem.l (%sp)+,%d2-%d3
rts
| ----------------------------------------------------------------
| void surface68kFillRectByteAligned(uint8_t *rowFirst, uint16_t midBytes,
| uint16_t h, uint16_t fillByte);
|
| Per-row middle-bytes fill for a jlFillRect where the leading and
| trailing nibble fixups have already been done (or none are
| needed because x and w are both even). `rowFirst` points at the
| first FULLY-INSIDE byte of the rect on row 0; subsequent rows
| are at +160. `midBytes` is how many full bytes per row to fill.
| ----------------------------------------------------------------
.globl _surface68kFillRectByteAligned
.equ SURF_FRB_STRIDE, 160
| Save d2-d6 (5 regs = 20 bytes). Args at +20+4 = +24.
.equ FRB_SAVED, 20
_surface68kFillRectByteAligned:
movem.l %d2-%d6,-(%sp)
move.l 4+FRB_SAVED(%sp),%a0 | rowFirst
| Zero scratch regs BEFORE the sub-register loads --
| move.w / move.b only touch low portions, leaving
| caller's garbage in the upper bits which would
| otherwise pollute the replicate-OR chain below.
moveq #0,%d0
moveq #0,%d1
moveq #0,%d2
move.w 8+FRB_SAVED+2(%sp),%d1 | midBytes
move.w 12+FRB_SAVED+2(%sp),%d2 | h
move.b 16+FRB_SAVED+3(%sp),%d0 | fillByte
tst.w %d1
beq .Lfrb_done
tst.w %d2
ble .Lfrb_done
| Build replicated fillByte in d0.
move.l %d0,%d3
lsl.l #8,%d3
or.l %d3,%d0
move.l %d0,%d3
swap %d3
or.l %d3,%d0
| Pre-compute long-count and trailing-byte residual.
move.w %d1,%d4
lsr.w #2,%d4 | longCount
move.w %d1,%d5
and.w #3,%d5 | trailBytes
subq.w #1,%d2 | row dbra count
.Lfrb_rowLoop:
move.l %a0,%a1
move.w %d4,%d6 | longs left
tst.w %d6
beq .Lfrb_tail
subq.w #1,%d6
.Lfrb_longLoop:
move.l %d0,(%a1)+
dbra %d6,.Lfrb_longLoop
.Lfrb_tail:
move.w %d5,%d6 | trailing bytes
tst.w %d6
beq .Lfrb_rowDone
subq.w #1,%d6
.Lfrb_byteLoop:
move.b %d0,(%a1)+
dbra %d6,.Lfrb_byteLoop
.Lfrb_rowDone:
lea SURF_FRB_STRIDE(%a0),%a0
dbra %d2,.Lfrb_rowLoop
.Lfrb_done:
movem.l (%sp)+,%d2-%d6
rts
| ----------------------------------------------------------------
| void surface68kFillSpan4Planes(uint8_t *p0, uint8_t *p1,
| uint8_t *p2, uint8_t *p3,
@ -283,13 +45,15 @@ _surface68kFillRectByteAligned:
| -- branchless: the same arithmetic produces "set" or "clear" based
| on whether fbN is 0xFF or 0x00.
|
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save (movem'd here).
| Stack offset to first arg after MOVEM: 11 regs * 4 = 44 bytes saved
| + 4 ret PC = 48.
| ABI: m68k cdecl. d2-d7/a2-a6 callee-save. Only a2/a3 are actually
| used by the body (planes 2/3); a4-a6 are not touched, so only
| d2-d7/a2-a3 are movem'd here.
| Stack offset to first arg after MOVEM: 8 regs * 4 = 32 bytes saved
| + 4 ret PC = 36.
| ----------------------------------------------------------------
.globl _surface68kFillSpan4Planes
.equ SP_SAVED, 44
.equ SP_SAVED, 32
.equ SP_RPC, 4
.equ SP_OFF, (SP_SAVED + SP_RPC)
@ -305,43 +69,56 @@ _surface68kFillRectByteAligned:
.equ SP_FB2, SP_OFF + 36 + 3
.equ SP_FB3, SP_OFF + 40 + 3
| Macro: per-plane work fully inlined. Args:
| plane_an = the address register holding this plane's pointer.
| fb_off = the stack offset for this plane's fillByte.
| Uses d6/d7 as scratch; d1=leftMask, d2=~leftMask, d3=rightMask,
| d4=~rightMask; d0=numMid-1 (only valid if mid_count > 0). The mid
| loop is skipped via .LfsSkipMid_<n> when numMid was 0 at entry --
| the per-plane caller branches to the right tail label.
| Per-plane work is fully inlined (hand-unrolled 4x rather than
| bsr+rts, to dodge ~12 cyc per return + the per-plane re-test of
| numMid that an earlier build paid). Register convention across all
| four planes:
| d1 = leftMask, d2 = ~leftMask, d3 = rightMask, d4 = ~rightMask
| d5 = this plane's fb byte, d6/d0/d7 = per-plane scratch.
|
| Hand-unrolled 4x rather than using bsr+rts to dodge ~12 cyc per
| return + the per-plane re-test of numMid that the previous build
| paid. The mid-loop label suffix is the plane index so all four
| inline copies can coexist without label collisions.
| The unmasked middle run is filled with long stores rather than
| byte-at-a-time. fb is 0x00 or 0xFF, so the long is built with a
| byte->word->long sign-extend (ext.w / ext.l). The mid pointer is
| advanced to a long boundary by storing 0-3 fb bytes (align run),
| then move.l (an)+ fills (midRem >> 2) longs, then a 0-3 byte tail
| drains the remainder. The masked left/right edge bytes keep their
| per-plane read-modify-write; only the unmasked middle changed.
|
| Plain text version of the per-plane body (translate to asm 4x with
| different a-regs and fb stack offsets):
|
| move.b (an),%d6
| move.b (an),%d6 | --- leading masked edge byte ---
| and.b %d2,%d6
| move.b fb,%d7
| move.b fb,%d5
| move.b %d5,%d7
| and.b %d1,%d7
| or.b %d7,%d6
| move.b %d6,(an)+
| < if has-middle path: >
| move.w %d0,%d7
| .midN:
| move.b fb,(an)+
| dbra %d7,.midN
| < trailing: >
| move.b %d5,%d7 | build replicated fill long in d7
| ext.w %d7
| ext.l %d7
| move.l an,%d6 | align = (-an) & 3, clamped to numMid
| neg.l %d6
| and.w #3,%d6
| move.w numMid,%d0
| < clamp d6 to d0; d0 = midRem = numMid - align >
| < store d6 fb bytes to reach a long boundary >
| < midLongs = midRem >> 2; tail = midRem & 3 >
| .longN:
| move.l %d7,(an)+
| dbra midLongs,.longN
| < store tail fb bytes >
| < trailing masked edge byte: >
| move.b (an),%d6
| and.b %d4,%d6
| move.b fb,%d7
| move.b %d5,%d7
| and.b %d3,%d7
| or.b %d7,%d6
| move.b %d6,(an)
_surface68kFillSpan4Planes:
movem.l %d2-%d7/%a2-%a6,-(%sp)
movem.l %d2-%d7/%a2-%a3,-(%sp)
move.b SP_LMASK(%sp),%d1
move.b %d1,%d2
@ -356,12 +133,27 @@ _surface68kFillSpan4Planes:
move.l SP_P3(%sp),%a3
| One-time numMid test. d0.w = numMid; if 0 jump to
| the no-middle entry, otherwise pre-decrement for dbra
| and fall into the with-middle entry. Both paths
| unroll all 4 planes.
| the no-middle entry. The with-middle path now fills
| the unmasked middle run with long stores (move.l
| (an)+) instead of byte-at-a-time, after aligning the
| mid pointer to a long boundary and draining a 0-3
| byte tail. The masked left/right edge bytes keep their
| per-plane read-modify-write exactly as before.
|
| The mid fill byte fbN is 0x00 or 0xFF, so the long
| replication is a byte->word->long sign-extend (ext.w
| then ext.l): 0xFF -> 0xFFFFFFFF, 0x00 -> 0x00000000.
|
| Per-plane register use during the mid run:
| d5 = fbN byte (held for tail + trailing edge)
| d7 = fbN replicated to a long (move.l fill value)
| d6 = scratch / align count / midLongs count
| d0 = numMid, then midRem, then tail-byte count
| Masks d1-d4 stay live across all four planes. Local
| numeric labels 1f-7f are reused per plane; each
| reference resolves to the nearest copy.
move.w SP_NMID(%sp),%d0
beq .LfsNoMid
subq.w #1,%d0
| ---- WITH-MIDDLE PATH ----
| Plane 0
@ -372,10 +164,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a0)+
move.w %d0,%d7
.LfsMid0: move.b %d5,(%a0)+
dbra %d7,.LfsMid0
move.b (%a0),%d6
move.b %d5,%d7
ext.w %d7
ext.l %d7
move.l %a0,%d6
neg.l %d6
and.w #3,%d6
move.w SP_NMID(%sp),%d0
cmp.w %d0,%d6
bls.s 1f
move.w %d0,%d6
1: sub.w %d6,%d0
tst.w %d6
beq.s 2f
3: move.b %d5,(%a0)+
subq.w #1,%d6
bne.s 3b
2: move.w %d0,%d6
lsr.w #2,%d6
and.w #3,%d0
tst.w %d6
beq.s 5f
subq.w #1,%d6
4: move.l %d7,(%a0)+
dbra %d6,4b
5: tst.w %d0
beq.s 7f
6: move.b %d5,(%a0)+
subq.w #1,%d0
bne.s 6b
7: move.b (%a0),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@ -390,10 +208,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a1)+
move.w %d0,%d7
.LfsMid1: move.b %d5,(%a1)+
dbra %d7,.LfsMid1
move.b (%a1),%d6
move.b %d5,%d7
ext.w %d7
ext.l %d7
move.l %a1,%d6
neg.l %d6
and.w #3,%d6
move.w SP_NMID(%sp),%d0
cmp.w %d0,%d6
bls.s 1f
move.w %d0,%d6
1: sub.w %d6,%d0
tst.w %d6
beq.s 2f
3: move.b %d5,(%a1)+
subq.w #1,%d6
bne.s 3b
2: move.w %d0,%d6
lsr.w #2,%d6
and.w #3,%d0
tst.w %d6
beq.s 5f
subq.w #1,%d6
4: move.l %d7,(%a1)+
dbra %d6,4b
5: tst.w %d0
beq.s 7f
6: move.b %d5,(%a1)+
subq.w #1,%d0
bne.s 6b
7: move.b (%a1),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@ -408,10 +252,36 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a2)+
move.w %d0,%d7
.LfsMid2: move.b %d5,(%a2)+
dbra %d7,.LfsMid2
move.b (%a2),%d6
move.b %d5,%d7
ext.w %d7
ext.l %d7
move.l %a2,%d6
neg.l %d6
and.w #3,%d6
move.w SP_NMID(%sp),%d0
cmp.w %d0,%d6
bls.s 1f
move.w %d0,%d6
1: sub.w %d6,%d0
tst.w %d6
beq.s 2f
3: move.b %d5,(%a2)+
subq.w #1,%d6
bne.s 3b
2: move.w %d0,%d6
lsr.w #2,%d6
and.w #3,%d0
tst.w %d6
beq.s 5f
subq.w #1,%d6
4: move.l %d7,(%a2)+
dbra %d6,4b
5: tst.w %d0
beq.s 7f
6: move.b %d5,(%a2)+
subq.w #1,%d0
bne.s 6b
7: move.b (%a2),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
@ -426,17 +296,43 @@ _surface68kFillSpan4Planes:
and.b %d1,%d7
or.b %d7,%d6
move.b %d6,(%a3)+
move.w %d0,%d7
.LfsMid3: move.b %d5,(%a3)+
dbra %d7,.LfsMid3
move.b (%a3),%d6
move.b %d5,%d7
ext.w %d7
ext.l %d7
move.l %a3,%d6
neg.l %d6
and.w #3,%d6
move.w SP_NMID(%sp),%d0
cmp.w %d0,%d6
bls.s 1f
move.w %d0,%d6
1: sub.w %d6,%d0
tst.w %d6
beq.s 2f
3: move.b %d5,(%a3)+
subq.w #1,%d6
bne.s 3b
2: move.w %d0,%d6
lsr.w #2,%d6
and.w #3,%d0
tst.w %d6
beq.s 5f
subq.w #1,%d6
4: move.l %d7,(%a3)+
dbra %d6,4b
5: tst.w %d0
beq.s 7f
6: move.b %d5,(%a3)+
subq.w #1,%d0
bne.s 6b
7: move.b (%a3),%d6
and.b %d4,%d6
move.b %d5,%d7
and.b %d3,%d7
or.b %d7,%d6
move.b %d6,(%a3)
movem.l (%sp)+,%d2-%d7/%a2-%a6
movem.l (%sp)+,%d2-%d7/%a2-%a3
rts
.LfsNoMid:
@ -501,5 +397,5 @@ _surface68kFillSpan4Planes:
or.b %d7,%d6
move.b %d6,(%a3)
movem.l (%sp)+,%d2-%d7/%a2-%a6
movem.l (%sp)+,%d2-%d7/%a2-%a3
rts

View file

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

View file

@ -5,7 +5,7 @@
# Installs all cross-compilers, assemblers, and libraries needed to build
# JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local
# toolchains/ folder. Nothing is installed system-wide. Free tools are
# fetched and built; non-free tools (ORCA/C, GoldenGate) print clear
# fetched and built; tools that must be placed manually print clear
# placement instructions and the script verifies they were placed
# correctly on a subsequent run.
#
@ -177,7 +177,8 @@ should_install() {
}
# ------------------------------------------------------------------------
# IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual)
# IIgs: Merlin32 (free) + NinjaTrackerPlus audio source. The w65816
# clang/llvm-mos toolchain is referenced via LLVM816_ROOT (see env.sh).
# ------------------------------------------------------------------------
install_audio_libxmp() {
@ -352,188 +353,6 @@ EOF
fi
fi
# GoldenGate Unix-side host tools (iix, dumpobj, profuse, ...).
# These are built from the GoldenGate source repo at stuff/GoldenGate/.
# They must NOT live inside the IIgs filesystem tree that
# $GOLDEN_GATE points at, because that tree represents the emulated
# IIgs volume (case-insensitive, with its own bin/ of IIgs utilities).
# Install layout:
# toolchains/iigs/gg-tools/bin/ Unix binaries on host PATH
# toolchains/iigs/goldengate/ $GOLDEN_GATE root (IIgs filesystem)
local gg_tools_dir="${base}/gg-tools"
local gg_root="${base}/goldengate"
local stuff_dir="${REPO_DIR}/stuff"
# Migrate any Unix tools the user may have placed at goldengate/bin/
# over to gg-tools/bin/ so the goldengate directory can be repopulated
# with the IIgs filesystem tree from gg-linux.tgz. iix's siblings gno
# and orca are typically symlinks to iix -- use -e (not -f) so broken
# symlinks from a prior partial migration also match.
if [ -x "${gg_root}/bin/iix" ] && [ ! -x "${gg_tools_dir}/bin/iix" ]; then
info "Relocating Unix host tools from goldengate/bin to gg-tools/bin"
mkdir -p "${gg_tools_dir}/bin"
# Move iix last so the symlinks that point at it resolve during mv.
for f in dumpobj profuse mkfs-profuse opus-extractor orca gno iix; do
if [ -e "${gg_root}/bin/${f}" ] || [ -L "${gg_root}/bin/${f}" ]; then
mv "${gg_root}/bin/${f}" "${gg_tools_dir}/bin/"
fi
done
rmdir "${gg_root}/bin" 2>/dev/null || true
fi
# Clean up any dangling symlinks that reference a now-moved iix.
if [ -d "${gg_root}/bin" ]; then
find "${gg_root}/bin" -maxdepth 1 -type l -xtype l -delete 2>/dev/null || true
fi
# Recreate gno/orca symlinks next to iix in gg-tools if they're
# missing (iix behaves differently when invoked as 'gno' or 'orca').
if [ -x "${gg_tools_dir}/bin/iix" ]; then
for alias in gno orca; do
if [ ! -e "${gg_tools_dir}/bin/${alias}" ]; then
ln -s iix "${gg_tools_dir}/bin/${alias}"
fi
done
fi
# Verify Unix tools
if [ -x "${gg_tools_dir}/bin/iix" ]; then
ok "GoldenGate host tools present at ${gg_tools_dir}/bin/"
STATUS[iigs_gg_tools]="ok"
else
STATUS[iigs_gg_tools]="missing"
INSTRUCTIONS[iigs_gg_tools]=$(cat <<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
# source (ZIP) plus binary tool disk images on ninjaforce.com.
# 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):
"""Return {op: ops/sec} from a UBER log. Last entry per op wins."""
perf = {}
@ -144,6 +162,8 @@ def main(argv):
ordered = ref_ops + extras
table = render_table(port_data, ordered, args.ref)
if args.ref == "iigs":
table += "\n" + IIGS_CEILING_NOTE
if args.out:
with open(args.out, "w") as f:
f.write(table + "\n")