More performance work

This commit is contained in:
Scott Duensing 2026-07-06 18:00:16 -05:00
parent a0931e8d16
commit 6110f714af
105 changed files with 8397 additions and 2481 deletions

141
LLVM816-ASKS.md Normal file
View file

@ -0,0 +1,141 @@
# Asks for the llvm816 toolchain session
JoeyLib-side findings that need toolchain-side fixes. Context:
PERF-AUDIT.md (findings #79, #77) and PERF-AUDIT-PLAN.md Phase 1.
JoeyLib does not modify toolchains/iigs/llvm-mos per the ownership
split -- these are requests, with evidence.
## 1. MVN-based memset/memcpy in runtime/src/libc.c (PERF-AUDIT #79)
`memset`, `memcpy` (and `memmove`'s copy loops) in
`toolchains/iigs/llvm-mos/runtime/src/libc.c` are one-byte-at-a-time
loops through 24-bit pointers:
void *memset(void *dst, int c, size_t n) {
char *d = (char *)dst;
while (n--) *d++ = (char)c;
return dst;
}
That is ~30 cycles/byte on the 65816. The ORCA/C baseline these
replaced lowers both to MVN block moves (~7 cycles/byte after setup).
Measured impact on the reference platform (UBER, honest post-Phase-0
clock): jlScbSetRange -- a 200-byte memset plus a flag write --
measures 163 ops/sec on the clang build vs 1005 ops/sec on the
ORCA-era capture (~4x regression at the whole-op level). Every
memset/memcpy-based JoeyLib path pays it: surface copies, sprite
save/restore fallbacks, SCB range fills, stage-alloc clears, the log
buffer, and all example/game code.
Ask: MVN-based memset/memcpy in libc.c (or intrinsic lowering in the
backend). Requirements from the JoeyLib side:
- Handle bank-crossing source/destination (MVN wraps within a bank;
the routine must split runs at bank boundaries).
- memset needs a 1-byte seed write + overlapping MVN (classic
self-copy fill), or an unrolled STA loop for short fills.
- Keep the size_t=unsigned long (32-bit) signature -- JoeyLib call
sites pass 32-bit counts today and the current behavior is correct,
just slow.
- A short-count fast path (n < ~16) matters too: the sprite
interpreter's per-row copies are 4-17 bytes.
## 2. FYI: sprite-codegen corruption under investigation (PERF-AUDIT #77)
JoeyLib's IIgs runtime sprite code generation corrupts memory when
enabled (bisected; disabled by default in spriteCompile.c until
root-caused). The current suspect list is JoeyLib-side (emit-time
buffer handling), but if the hunt lands on codegen/ABI behavior of
clang-emitted calls into arena-resident routines, it becomes a
toolchain question. Nothing to do yet -- flagged so the context is not
a surprise if it arrives.
## 3. fflush -> FST commit semantics (PERF-AUDIT #80)
JoeyLib's logger (src/core/debug.c) fflush()es after every line so a
crash/hang leaves the log on disk. On the clang runtime the lines do
not reach the disk image until fclose: a run that executed 33 logged
operations left 3 lines on disk after a forced emulator exit. Question:
does the libc fflush propagate to a GS/OS FST flush for the file, or
only drain the userland buffer? If the latter, an FST-flush call in
fflush (or an exposed fsync-like hook) would restore crash-durable
logging. Not urgent -- JoeyLib works around it with a memory mailbox
under emulation -- but real-hardware crash forensics wants it.
## 4. malloc/calloc returns 1 (not NULL) at heap exhaustion (PERF-AUDIT #81) -- CRITICAL
Empirically confirmed under MAME with in-app probes: with the default
link-layout heap (665 bytes at $00:BC67-$00:BF00 in the JoeyLib UBER
build), `calloc(1, ~720)` returns **1**. The bump-path overflow branch
in libc.c malloc -- whose comment already records a "historical
`p + HDR_SZ + n > heapEnd` over-heap miscompile (only manifested for
oversized n)" and assumes the path is unexercised -- is still
miscompiled in exactly that oversized case. Downstream, JoeyLib ran for
weeks on a phantom struct at (void*)1 whose first field happened to
alias the real framebuffer pointer; see PERF-AUDIT.md #81 for the full
forensics. JoeyLib no longer allocates anything large on the C heap,
but ANY program that exhausts this heap gets silent corruption instead
of NULL. Minimal repro: link any GS/OS app with default heap, call
calloc(1, 720), observe the return value.
## 5. Segment BSS/heap spans are not reserved from the Memory Manager (PERF-AUDIT #81/#77/#85) -- CRITICAL, EMPIRICALLY PROVEN
Three independent confirmations, in escalating severity:
1. The bank-0 C heap (unreserved) -- #81's phantom-stage chain.
2. The pinned SHR framebuffer $01:2000-$9CFF (unreserved) -- an MM
handle landed at $01:8B04 inside the display (#85).
3. THE GENERAL CASE: a 32KB NewHandle was placed at $08:4200 -- inside
the app's own ENTRY-SEGMENT BANK, overlapping seg1's BSS span. The
loader only claims each segment's image bytes; everything above
(BSS, heap) is MM-free. ANY GS/OS allocation -- the app's own or
the system's -- can land on live program data.
JoeyLib has stopgap claims for cases 1-2 (jlpInit reserves the
framebuffer and $00:B900-$BEFF). Case 3 was attempted app-side on
2026-07-05 (spacetaxi acceptance test) and the attempt produced two
findings you should have:
(a) The app CAN learn its BSS spans: the linker-emitted
__bss_seg{0..3}_{lo16,bank,size} symbols (the ones crt0.s zeroes
BSS through) are readable from C via a `.long` DATA32 table in an
asm TU -- direct C IMM16 references fail to link ("cross-segment
reloc ... uses 2-byte form; only IMM24 / DATA32 supports cINTERSEG
patching"). The DATA32 entries resolve as RELOCATED FULL ADDRESSES:
the lo16 entry reads back as the 24-bit runtime span base
($08:4F00 observed for the spacetaxi build), the size entry
carries the length in its low 16 bits, empty segments read as
bank-base-only. Recipe verified working end-to-end under MAME.
(b) BUT actually claiming the span kills the app: NewHandle(
attrAddr|attrFixed|attrLocked, base=$08:4F00, size=$6619) from
jlpInit SUCCEEDS -- and the app then dies before reaching main's
first log line, with the PC profile showing GS/OS desktop idle
(silent return-to-Finder). Reproduced twice; reverting only the
claim restores the previous behavior. Mechanism unknown from the
app side -- presumably the Loader/MM bookkeeping objects to a
user handle overlapping a loaded segment's address range, or the
claim starves something during the remaining init. The reverted
implementation was never committed; the full code (asm table +
claim loop) is reproduced verbatim in the copy of this item handed
to the toolchain session at /home/scott/claude/llvm816/JOEYLIB-ASKS.md.
Ask: crt0/omfEmit/loader cooperation so every segment's FULL
in-memory span (image + BSS + heap) is Memory-Manager-reserved at
load time -- with (b) suggesting the reservation must be made BY the
loader glue (or before/while segments are registered), not by app
code after the fact. This single fix unblocks IIgs sprite codegen
(PERF-AUDIT #77), the Space Taxi IIgs port (a 32KB font-surface
jlpBigAlloc lands on live state layout-dependently -- three distinct
failure modes observed as the binary layout shifted), and removes the
whole corruption class.
link816 defines [__heap_start, __heap_end) as the leftover entry-bank
space above BSS, but crt0Gsos never claims that range from the GS/OS
Memory Manager, so NewHandle can legally place blocks on top of live
malloc data (a second, independent corruption channel -- and the
crt0Gsos comments already record a Phase-1.1 fopen-hang caused by this
class). Ask: reserve the heap range at startup (attrAddr|attrBank|
attrFixed|attrLocked), or reimplement malloc on top of an MM handle.
Related hardening: pass attrNoSpec on NewHandle calls in the runtime,
and consider reserving the pinned SHR stage range $01:2000-$9FFF that
JoeyLib's jlpStageAllocPixels hands out as a bare constant.

750
PERF-AUDIT-PLAN.md Normal file
View file

@ -0,0 +1,750 @@
# PERF-AUDIT Fix Plan
Execution plan for all 71 confirmed findings in PERF-AUDIT.md plus the
benchmark-harness fix from its preamble. Finding numbers below (#N) are
PERF-AUDIT.md numbers. Read each finding's "Verifier correction" before
implementing -- several tighten the fix or the claimed impact, and this
plan encodes those corrections as constraints.
This plan was itself adversarially reviewed (completeness, ordering
conflicts, gate adequacy -- 31 issues found and folded in). The most
important consequence: Phase 0 builds the verification harness that
every later gate depends on, because the existing gates cannot see most
of the changes this plan makes.
Ground rules for every phase:
- Source toolchains/env.sh before any make; a missing $AMIGA_CC etc.
silently produces stale binaries.
- After editing any IIgs .s/.asm file, delete the affected example
binaries before rebuilding (stale-binary trap).
- Hash gates compare against the golden hashes captured at the end of
Phase 0 (the harness additions change the old hashes once, then they
are frozen).
- "iigs-verify" in a gate always names its example and compares
before/after checksums: `make iigs-verify VERIFY_EXAMPLE=<name>`
(default is draw; that only covers draw-path changes). It reads SHR
memory -- there is NO MAME headless screenshot; do not plan for one.
- Scott commits; the plan never includes git operations.
- Style rules apply to all touched code (camelCase, // comments, braces
always, prototypes at top and alphabetized, two blank lines between
functions, ASCII only).
Phase order rationale: fix the clock and build the gates first
(otherwise no perf change can be validated and most bug fixes have no
covering check), then correctness bugs, then shared infrastructure,
then per-subsystem work, and the new platform asm last (highest effort
and risk, benefits from everything before it being measurable).
---
## Phase 0 -- Fix the clock and build the verification harness
The audit's headline: every small-op UBER number carries ~600-800
cycles of per-iteration harness tax on IIgs (GetTick toolbox chain).
And the plan review's headline: most fixes below are invisible to the
current gates (UBER only hashes on-surface compiled-path ops; Amiga/ST
have no screen-content check; several ops are not exercised at all).
Phase 0 fixes both.
1. **uber.c batch polling.** In runForFrames, run the op in batches
between jlFrameCount() polls. Batch adaptively: batch=1 for ops
slower than ~1 frame (present, surfaceClear, fillRect-full would
otherwise overrun the window by up to a full batch), 8-16 for fast
ops (pick from the first iteration's duration). Fix the false
"~10-30 cyc" comment. Ops are fixed-coordinate/idempotent (review
verified), so batching does not change what an iteration means.
2. **#15 + #71** -- collapse the IIgs jlFrameCount forwarding: declare
iigsGetTickWord in the IIgs section of port.h and
`#define jlpFrameCount() iigsGetTickWord()`; delete the hal.c
wrapper AND its stale 15-line $C019/gFrameCount comment (#71 -- the
comment sits on the function being deleted, so it lands here, not in
Phase 8); put a one-line GetTick pointer next to the new macro.
Remove the now-stale JL_HAS_FRAME_COUNT entry at platform.h:108.
CAUTION (verifier): do NOT do the same for jlpFrameHz -- gFrameHz is
hal.c-private state; leave it a function.
3. **uber.c non-timed correctness section.** New hashed/logged checks
that run after the timed ops and before the final hash, added NOW so
golden hashes cover every path later phases touch:
- edge-case draws: off-surface pixel spam (both signs, both axes),
jlDrawRect w=65535 and h=2 cases, jlFillCircle huge-r (r=300 with
clipping, r=40000 full-coverage), jlDrawLine far-off-surface
endpoints;
- clipped-sprite draws: all four edges, a corner, fully off-surface
(exercises the interpreted path on every port);
- jlTilePasteMono and jlFloodFill ops drawn into the stage (neither
is currently in the hashed set -- Phase 7 replaces both paths);
- jlDrawText including a string starting out-of-range (Phase 5
changes its validation);
- jlPaletteSet readback assert via jlPaletteGet: one conforming row,
one deliberately non-conforming row, row[0] forced to 0x0000,
logged PASS/FAIL (palette is not in the surface hash);
- jlRandom golden-sequence check (first N values from a known seed);
- allocator torture: sprite create/compile/destroy churn to fragment
then compact the arena (#5/#34 coverage), a sprite-bank
load/destroy pass (#31 coverage -- allocator mismatch is fatal on
IIgs), and a jlShutdown -> jlInit -> destroy-stale-sprite sequence
(#34) IF the harness structure allows re-init; otherwise this one
becomes a dedicated tiny example.
4. **bench-iigs.sh** -- the IIgs capture path, which does not exist
today (PERF.md's IIgs column was carried over, not re-run): boot
UBER off a writable joey.2mg under headless MAME, cadius-extract
joeylog.txt afterward (or extend the verify-iigs.sh Lua to dump the
log lines from emulated memory). Without this, no "hashes identical
x4" gate in this plan is executable on the reference port.
5. **millis host harness** -- audioSfxMix-style host compile of
genericPort millis logic (check-blank.sh pattern) driving simulated
frame-delta sequences including the 16-bit wrap, for Phase 1 #23.
6. **#25 (first half)** -- run the new harness on all four ports;
record the new honest baseline table in PERF.md marked
post-clock-fix. MANDATORY: tag the SEI-holding rows (present,
surfaceClear, fillRect 320x200) as clock-unreliable in the table --
the SEI tick-loss artifact survives the batching fix.
7. **#73 (landed during Phase 0 out of necessity)** -- DOS jlpFrameCount
was an $3DA edge-poll counter that lost every frame whose ~1ms
retrace pulse fell between calls; the batched timing loop stalled on
it, and it had been inflating the old DOS column 5-40x on slow ops.
Now derived from the PIT/uclock millis source (poll-rate-independent);
jlpWaitVBL still syncs to real retrace.
8. **#76 (landed during Phase 0 out of necessity)** -- IIgs jlpWaitVBL
polled $C019's VBL bit, which never asserts under MAME's apple2gs:
every VBL-paced program hung forever in emulation, and UBER (the
only in-tree caller) could never complete headless -- the real
reason PERF.md's IIgs column was "carried over". Now waits on a
GetTick change (VBL-interrupt-driven, same boundary semantics).
9. **Freeze golden hashes** -- DONE 2026-07-04 for DOS/ST/Amiga in
tests/goldens/uber/ (43/43/42 hash lines; the Amiga log lost its
final check line to the bench-script timeout -- re-capture with a
longer TIMEOUT when convenient). The IIgs golden is PARTIAL (3 op
lines, codegen-off): the clang-era IIgs port cannot complete UBER
yet -- sprite codegen corrupts memory (#77), the codegen-off run
stalls in the jlDrawPixel timing window after 3 ops (uninvestigated
-- first Phase 1 stabilization item), and jlAudioInit fails
headless. PERF.md documents the 3-port baseline and the IIgs
status; the old IIgs column was ORCA-era and is retired.
Gate: 4-port build; all new correctness checks PASS; goldens recorded.
Expect small-op numbers to jump 2-10x against the old table.
Effort: ~half a day to a day (the harness additions are the bulk).
---
## Phase 1 -- Correctness bugs + IIgs stabilization
All behavior fixes land before perf work so later phases optimize
correct code. Every item here has a covering check from Phase 0.
THE IIGS STABILIZATION CLUSTER COMPLETED 2026-07-04: UBER runs
end-to-end (green screen, audio live, all checks PASS, 39/43 hashes
matching DOS, honest floor numbers in PERF.md). Fixed along the way:
#81 (phantom stage / tiny-heap malloc-returns-1), #82 (RAM-ring
logger), #83/#86 (line+fillCircle asm DBR-vs-D scratch stash), #85
(framebuffer + bank-0 MM reservations), audio packaging. #77 is
root-caused to the toolchain (loader does not MM-reserve segment
BSS spans; arena landed at $08:4200 inside the app's own bank) --
codegen stays off until LLVM816-ASKS item 5 lands. New/remaining
items for the rest of Phase 1: #87 (circle-outline pixel divergence,
the last 4 hash mismatches). Original cluster notes follow:
(a) #77 sprite-codegen corruption (MAME write-watchpoint hunt);
(b) the codegen-off UBER "stall" -- RESOLVED 2026-07-04 in three
layers: #80 (log-durability illusion misdirected the hunt), #81
(the master bug: 665-byte unprotected heap + malloc-returns-1 ->
phantom stage struct; fixed by moving all core structs to
jlpBigAlloc), and #82 (per-line floppy log writes stalled the app
in SmartPort firmware; fixed with the RAM-ring logger + live Lua
drain). UBER now completes end-to-end on IIgs (33 ops + 17 checks,
green screen); 20 hashes match DOS. Remaining IIgs defects filed
as #83 (diag-line asm pixel divergence) and #84 (interpreted
sprite ops ~1000x slow + wrong) -- both queued in this cluster;
(c) headless jlAudioInit failure -- FIXED 2026-07-04: the audio code
was wired all along; the merlin32-built "ntpplayer" binary was
simply never packed onto joey.2mg. iigs-disk now builds and adds
it; UBER logs "audioInit OK";
(d) #79 flag to the llvm816 session (libc memset/memcpy byte loops).
Only after (b) is fixed can the IIgs golden hashes be frozen and the
IIgs column return to PERF.md.
1. **#1 (CRITICAL)** draw.c:549 -- non-IIgs jlDrawPixel: bounds-check
x/y (same `(uint16_t)x >= SURFACE_WIDTH` form as the IIgs branch)
after the NULL check and return early; then call jlpDrawPixel
directly and mark. Deletes the redundant plotPixelNoMark layer on
the public path.
2. **#11** draw.c:591 -- jlDrawRect general path: compute bottom/right
edge coordinates in int32_t; skip an edge whose coordinate is
off-surface instead of letting the int16_t cast wrap. ALSO (from the
#65 verifier correction, same function): skip the interior-edge
fills entirely when h == 2, so jlDrawRect stops passing h == 0 rects
into fillRectOnSurface/surfaceMarkDirtyRect -- this restores the
"already clipped, positive dimensions" contract that Phase 2 #65
relies on before it drops the guard.
3. **#72** draw.c:417 -- jlDrawLine H/V fast paths: clip x0 (resp. y0)
into range BEFORE anchoring the clamped span (`if (x0 < 0) { span +=
x0; x0 = 0; }` after the 32-bit span, return if span <= 0, then clamp
to the surface size). Today a line from far off-surface through the
whole visible row draws nothing. Found during Phase 0 harness work;
covered by the "edge-lines" uber check (its hash changes with this
fix).
4. **#12** draw.c:656 -- jlFillCircle/jlDrawCircle huge-r handling:
32-bit full-coverage test (farthest corner dist^2 <= r^2 -> full
surface fill / early out), clamp the span loop to intersecting rows,
uint16_t loop variable. Verifier notes: seed yy = y0*y0 when jumping
into the row range (the incremental chain breaks on a jump); the
initial x walk-down from r stays O(r) once per call -- document it.
5. **#23** genericPort.c:40 -- millisElapsed: accumulate milliseconds
incrementally from uint16_t frame deltas (per verifier: do NOT keep
a total frame count and multiply -- that re-overflows at ~20h; carry
the fractional remainder, e.g. delta*20 at 50Hz, delta*50/3 with
mod-3 carry at 60Hz). Update the statics with explicit
load/add/store-style C (no `++`/`+=` on globals -- ORCA-C inc-abs
DBR trap). Document the poll-at-least-once-per-65535-frames bound
(~18 min @60Hz, ~22 min @50Hz -- NOT hours). Delete the dangling
iigs/hal.c:382-386 comment. Verify against the Phase 0 host harness.
6. **#8** codegenArena.c:236 -- IIgs: clamp totalBytes to 64KB with a
specific coreSetError string on failure; fix adventure.c to set
codegenBytes per-platform (#ifdef: small on chunky ports, 256KB only
where a flat heap exists). Document the cap at jlConfigT.codegenBytes.
7. **#46** sprite.c:346 -- gate the IIgs/DOS compiled-draw fast path on
`routineOffsets[shift][SPRITE_OP_DRAW] != SPRITE_NOT_COMPILED` (read
once via the existing byte-pointer idiom; reuse for the call).
8. **#34** codegenArena.c:294 -- shutdown/re-init dangling slots.
PREFERRED (the audit's own fix): a small sprite registry in sprite.c
-- spriteSystemShutdown, called from jlShutdown BEFORE
codegenArenaShutdown, walks live sprites and NULLs sp->slot. Zero
hot-path cost and it fixes BOTH arms: the destroy-after-reinit heap
corruption AND the draw-after-reinit stale-offset execution (an
epoch checked only at free time would miss the second arm).
Alternatives if the registry is unwanted: arena epoch stamped into
jlSpriteT and checked at every dispatch gate (hot-path cost), or a
documented "jlShutdown invalidates all sprites" contract (doc-only).
9. **#5** spriteCompile.c:202 -- on codegenArenaAlloc failure, run
codegenArenaCompact() and retry once; coreSetError on final failure
so prewarm failures are diagnosable.
10. **#31** assetLoad.c:167 -- move sprite allocations to
jlpBigAlloc/jlpBigFree UNIFORMLY. Alloc sites: cel/tileData buffers
(assetLoad.c:167, sprite.c jlSpriteCreateFromSurface buf) AND the
jlSpriteT header mallocs (sprite.c:215, sprite.c:306,
assetLoad.c:175 -- verifier requires these switch in the same
change). Free sites -- ALL FOUR for tileData plus the headers:
jlSpriteDestroy (sprite.c:325 tileData, sprite.c free(sp)), the
jlSpriteCreateFromSurface error path (sprite.c:308), and both
assetLoad error paths (assetLoad.c:172, assetLoad.c:177). A single
missed free() of a jlpBigAlloc pointer corrupts the IIgs heap
(BIG_ALLOC_HDR offset), and three of these sites are failure-path
only -- exercised by the Phase 0 torture check.
11. **#58** atarist/hal.c:1410 -- DELETE the dead dst/pd NULL checks
(callers validate; the finding's category is
unnecessary-conditional, not misordered-check), and apply the same
cleanup to the identical pattern in jlpTileSnap (deref at 1454,
dead checks at 1458/1462) per the verifier correction.
12. **#74** -- root-cause the DOS jlSpriteCompile failure (pre-existing;
UBER logs it; every DOS sprite bench measured the interpreter).
Instrument the sizing/emit/arena-alloc chain under DOSBox; the x86
emitter itself emits real code for shifts 0-1. Gates Phase 4's
compiled-path verification on DOS.
13. **#75** -- resolve the jlTilePasteMono three-way pixel divergence
(chunky generic vs Amiga vs ST overrides, caught by the Phase 0
tilePasteMono hash). Decide the contract truth (tile.h: non-zero
mono nibble -> fgColor), fix the divergent implementations, and
re-golden that hash line on the fixed ports. Blocks Phase 7 #3.
14. **#77** -- root-cause the IIgs sprite-codegen memory corruption
(MAME write-watchpoint on the stage struct / gStage static during
jlSpriteCompile + first compiled draw). Until fixed, IIgs builds
run JOEY_IIGS_SPRITE_CODEGEN=0 and the IIgs baseline measures the
interpreter. HIGHEST PRIORITY of the phase with #74: both reference
platforms' compiled sprite paths are currently dead.
15. **#78** -- fix ST jlFillCircle and jlTileFill pixel divergence vs
the chunky reference (DOS==Amiga agree); re-golden ST hash lines.
16. **#79** -- FLAG TO SCOTT (toolchain-owned): llvm-mos runtime
memset/memcpy are ~30 cyc/byte byte-loops vs ORCA's ~7 cyc/byte
MVN; reference-platform regression ~4x on every memset/memcpy path.
Library-side mitigations are #9/#19/#21; the libc fix belongs to
the llvm816 session.
Gate: 4-port builds; Phase 0 correctness checks all PASS (they cover
every edge case fixed here); hashes match Phase 0 goldens;
`make iigs-verify VERIFY_EXAMPLE=draw` checksum unchanged.
Effort: ~4-6 hours.
---
## Phase 2 -- Shared hot-path infrastructure
The cross-TU-call cluster. Everything here touches surfaceInternal.h /
port.h / surface.c / draw.c and ripples through every port; do it as
one unit with a full 4-port verify.
1. **#10 + #17** -- make gStage a plain extern (`extern jlSurfaceT
*gStage;` in surfaceInternal.h, drop static in surface.c; keep
jlStageGet() as the public accessor). Replace jlStageGet() with a
direct `== gStage` compare in: port.h IIgs macros (jlpFillRect,
jlpSurfaceClear, jlpFillCircle), draw.c:533 jlDrawPixel, palette.c,
scb.c, surface.c internal uses. Precedent: codegenArenaBase did
exactly this.
2. **#20 + #65** -- surfaceMarkDirtyRect: move the stage test and the
h==1 widen into a macro in surfaceInternal.h (ORCA/C 2.2.1 has no
inline -- macro form is a hard constraint; audit macro argument
usage for multiple-evaluation before converting call sites).
Non-stage surfaces then cost one inline compare; single-row marks
widen with no call; multi-row marks keep one call
(iigsMarkDirtyRowsInner on IIgs, C loop elsewhere). Drop the w/h
guard (#65) ONLY after Phase 1 item 2 restored the contract
(jlDrawRect h==2 was the violator); empty ranges then cannot reach
the macro.
3. **#21** -- stageDirtyClearAll: two memsets (0xFF / 0x00), matching
its sibling surfaceMarkDirtyAll.
4. **#47** -- dirty-row test unification, TWO-macro form per the
verifier correction (NOT a blanket four-site replacement): convert
DOS/ST to STAGE_DIRTY_ROW_CLEAN (min > max); keep Amiga's
`min != 0xFF` as a second documented macro (STAGE_DIRTY_ROW_TOUCHED
-- it is deliberate and cheaper under the clipped-marks invariant);
pin the IIgs present asm's compare with a comment naming the
sentinel values it assumes.
5. **#14 + #24** -- pixel read: expose the chunky nibble read as a
macro in surfaceInternal.h using SURFACE_ROW_OFFSET (verifier:
macro, not static inline, so floodFillInternal's chunky-fallback
reads can use it too). jlSamplePixel uses it inline after its bounds
check on chunky builds; jlpGenericSamplePixel body becomes the same
macro. (Do NOT expect a sprite-capture delta: the sprite.c:296-297
branch only runs on planar ports, which use their own platform
jlpSamplePixel -- review corrected the audit here.)
6. **#13** -- jlFillRect: add the 16-bit on-surface fast path before
the 32-bit clip, in the guarded form (test `w <= SURFACE_WIDTH` and
`h <= SURFACE_HEIGHT` FIRST, then `x >= 0 && y >= 0 &&
x <= (int16_t)(SURFACE_WIDTH - (int16_t)w)` etc. -- guarding before
the subtraction is what makes 16-bit safe against w up to 65535).
Verifier: keep the three span emitters calling fillRectOnSurface
directly; do NOT reroute them.
7. **#16** -- jlPaletteSet: memcpy the row + force row[0] = 0x0000,
dropping the undocumented per-entry $0RGB re-mask (pending the
decision-table sign-off; if the mask must stay, at least widen the
loop counter to uint16_t). Mirrors jlPaletteGet. Covered by the
Phase 0 palette readback assert (the surface hash cannot see
palette changes).
Gate: hashes match goldens x4 (dirty-band changes are hash-blind --
that is what the next line is for); present-output check: captureDos.sh
against a pre-phase golden PNG for DOS, iigs-verify
VERIFY_EXAMPLE=draw checksum compare for IIgs, and a documented
run-amiga.sh / run-atarist.sh visual pass of the draw example after
multiple presents (68k C dirty loops diverge from the IIgs asm; a
dirty-band under-mark shows as stale rows only at present time).
Capture bench delta against the Phase 0 baseline.
Effort: ~4-6 hours (mostly the 4-port present verification).
---
## Phase 3 -- draw.c micro-optimizations
1. **#9 + #37 + #36** -- jlDrawLine/jlDrawCircle fallbacks: compute the
dirty box from the endpoints (line) / center+radius (circle) before
the loop, clamped after, as now; delete the per-pixel and
per-iteration box accumulation (the circle's 8 compares per
iteration go with it). RISK NOTE: a too-small precomputed box
under-marks and is invisible to hashes -- the Phase 2 gate's
present-output checks re-run here.
2. **#38** -- jlFillCircle C span loop: one bounding-box dirty mark
after the loop instead of per-span marks. Over-approximates each
row's band to the circle width; pixel output identical; matches
what the IIgs asm path already does. (Over-marking is the SAFE
direction, unlike item 1.)
3. **#35** -- floodFillInternal C fallback: hoist the matchEqual
predicate out of the walk loops (two loop variants), use the Phase 2
read macro instead of jlpSamplePixel calls where the port is chunky.
4. **#61** -- plotPixelNoMark: drop the NULL re-check (all callers
validate); KEEP the per-pixel bounds check (load-bearing for
clipping).
5. **#62** -- define the shared on-surface rect predicate ONCE, here,
in its final form (review blocker): the #13-style guarded 16-bit
macro -- `w <= SURFACE_WIDTH && h <= SURFACE_HEIGHT` first, then the
subtraction compares. That form is correct for BOTH users: jlDrawRect
(raw uint16_t w/h up to 65535) and sprite.c (widthPx/heightPx up to
2040 = 255 tiles * 8 -- NOT "bounded by 320/200"). jlDrawRect's
fast-path test switches to it now; sprite.c merges in Phase 4 as a
pure call-site change with NO rewrite of the macro.
Gate: hashes match goldens x4 (the Phase 0 edge-case checks cover the
w=65535 / huge-r paths through the new predicate); present-output
checks as in Phase 2; bench diag-line/circle deltas on DOS (the only
port that always runs the fallbacks).
Effort: ~2-3 hours.
---
## Phase 4 -- Sprite pipeline + codegen
Landing order within the phase (review): land items 2-12 first as one
reviewable, hash-gated unit; land item 1 (the risky inner-loop rewrite)
LAST as its own change with its own before/after clipped-sprite hash
comparison, so a failed rewrite reverts alone. Within items 2-12, do
the deletions (#55, #27) BEFORE the shared-derive refactor (#29 + #64)
so the refactor never touches code that is about to be deleted.
1. **#2 + #18 + #45** -- rewrite spriteDrawInterpreted's inner loop
(LAND LAST):
- hoist row-invariant source math out of the column loop (#45),
- carry dst byte pointer + parity incrementally (kill the
writeDstNibble call per pixel, drop its redundant & 0x0F masks),
- aligned-phase byte loop: src byte == 0 skips two pixels,
both-nibbles-opaque stores the whole byte, single-nibble merges
stay RMW; chunk per tile column (4 contiguous bytes, then advance
srcTileRow by TILE_BYTES -- verifier: the naive *src++ sketch
glosses this); keep the per-pixel walker for the misaligned phase.
Rollback: revert just the loop; the writeDstNibble path stays in git
history.
2. **#19** -- jlSpriteSaveUnder/RestoreUnder fallbacks: hoist
SURFACE_ROW_OFFSET-based start pointers, walk with += stride adds;
kill both per-row multiplies.
3. **#44** -- switch isFullyOnSurface's body to the Phase 3 shared
predicate (pure call-site merge; the macro is already 16-bit-safe
for the 2040-px sprite domain -- do not modify it).
4. **#55** -- remove spriteEmit68k.c from the Amiga/ST build lists.
DECISION for Scott: delete the file (git history keeps it) or leave
it unbuilt.
5. **#27** -- Amiga compiled save/restore dispatchers: replace the
bodies with ST-style empty linker stubs -- FULL DELETION BREAKS THE
LINK (sprite.c calls them unconditionally; verifier correction).
Delete the shift-alias loop and the aspirational comments at
spriteCompile.c:172-178/241-245. Note for the future: when real
Amiga save/restore emitters land, the restore gate must accept
planar copyBytes = spriteBytesPerRow+4.
6. **#29 + #64** -- one shared macro/helper derives (shift, routeIdx,
routeOffset, fnAddr) once; the surviving dispatcher gates and
spriteCompiled* callees consume it.
7. **#28** -- spriteCompiledDraw (IIgs): byte-pointer idiom for
routineOffsets like the rest of the file.
8. **#59** -- SPLIT_BACKUP_CACHED: move the split cache per-sprite (the
jlSpriteT already caches banks) or delete it; measure both on IIgs.
9. **#32** -- jlSpriteBankLoad calls spriteInitFields (then overrides
ownsTileData); fix the spriteInternal.h comment.
10. **#39** -- DEFAULT_CODEGEN_BYTES per-platform (#ifdef: keep 8KB
IIgs, size 68k ports from their emitters' measured bytes/sprite;
document at jlConfigT).
11. **#56** -- dedupe emitter helpers (c2pColumn/putWord into a shared
m68k header; shiftedByteAt/spriteSourceByte into a shared codegen
header).
12. **#54 + #63** -- fix the stale spriteEmitIigs.c contract comment
and the sprite.c:343 "IIgs never takes this" comment.
Gate: hashes match goldens x4 -- the Phase 0 clipped-sprite checks are
the load-bearing coverage here (UBER's timed sprite ops are compiled
and on-surface; they cannot see interpreter regressions).
`make iigs-verify VERIFY_EXAMPLE=sprite` checksum before/after;
captureDos.sh sprite example compare on DOS. Bench: clipped-sprite ops
expect 2-3x; compiled ops modest gains from dispatch dedup.
Effort: ~1-2 days.
---
## Phase 5 -- Tile path + generic fallbacks
1. **#22** -- jlDrawText: validate bx/by once at entry (verifier: the
wrap logic does not sanitize the FIRST position -- reject
out-of-range starts), validate srcBx/srcBy from asciiMap per glyph,
call jlpTileCopyMasked directly, accumulate one union dirty rect for
the string. Covered by the Phase 0 drawText hash check (uber.c has
no other drawText coverage).
2. **#48** -- tile.c wrappers: shifts (<< 3) for pixel coords, or fold
coord derivation into the dirty-mark call site.
3. **#51** -- jlpGenericTileCopyMasked: whole-byte store when both
nibbles are opaque; keep the merge for mixed bytes. Bit-identical.
4. **#50 + #70** -- shared 4-byte row-copy macro used by TileCopy,
TilePaste, TileSnap; jlpGenericTileFill uses GENERIC_TILE_ROW0.
5. **#69** -- drop the duplicate fg/bg masks (mask once in the public
wrapper, document the precondition in the generic).
6. **#49** -- jlpGenericFillRect: hoist pxStart/pxEnd/midBytes/edge
nibble work out of the row loop; walk the row pointer with
+= stride.
7. **#30** -- assetLoad constants: use TILE_BYTES from joey/tile.h;
derive ASSET_MAX_TILES_W/H from SURFACE_WIDTH/HEIGHT and
TILE_PIXELS_PER_SIDE.
8. **#66, #67, #68, #57, #60** -- delete SURFACE_PIXELS_PER_WORD;
200 -> SURFACE_HEIGHT in the LUT decl; fix tile.c stale comment; fix
surface.h hash doc; fix or implement the assetLoad segment comment
(DECISION: add the segment directive it promises, or correct the
comment -- check IIgs root-segment pressure first).
Gate: hashes match goldens x4 (#51 especially must be bit-identical;
the drawText check covers #22); DOS bench deltas on tile ops (DOS runs
the generics); hello example on DOS + IIgs for text rendering.
Effort: ~3-4 hours.
---
## Phase 6 -- SFX mixer (DOS/ST builds only)
Verification FIRST (review: no named gate could actually verify this
phase): audioSfxMix.c is portable C -- build a host harness on the
check-blank.sh pattern that compiles the OLD and NEW mixer with host
cc, mixes a fixed seed sample set (fixed-buffer and streaming slots,
rate conversion up and down), and diffs output buffers. This proves #7
and #33 bit-exact and bounds #6 to 1 LSB concretely before any
emulator run.
1. **#6** -- interp(): the verifier-validated int16 form (explicit
int16_t locals so gcc emits one muls.w):
`int16_t d = (int16_t)(s1 - s0);`
`int16_t fH = (int16_t)(uint16_t)(frac >> 1);`
`return (int)s0 + (int)(((int32_t)d * fH) >> 15);`
Output may differ by 1 LSB of 8-bit audio (inaudible) -- accepted
deviation, document it.
2. **#7** -- posFp: uint32_t index + uint16_t frac with carry-compare;
split stepFp once per slot; update audioSfxSlotArm/ArmStream/
streamRefill. Bit-identical.
3. **#33** -- cache slot fields in locals across the inner loop
(volatile dst stores force re-reads today), re-reading only after
streamRefill; AND split the fixed-buffer vs streaming cases into two
inner loops so the loop-invariant isStream branch runs once per slot
(both halves are the audited fix; the plan review caught the split
being dropped).
Gate: host-harness diff (above); DOS + ST builds; audio example smoke
on DOSBox/Hatari; absence of the ST ~83ms refill warning under load as
a secondary signal.
Effort: ~3-4 hours including the harness.
---
## Phase 7 -- Platform override work (biggest wins, biggest effort)
Each item is independently landable. Golden hashes for jlTilePasteMono
and jlFloodFill were captured in Phase 0 against the generic paths --
the new implementations must reproduce them exactly.
1. **#3** -- IIgs jlpTilePasteMono: new iigsTilePasteMonoInner in
joeyDraw.s -- 16-entry nibble-pair LUT expands each mono byte
directly into surface rows, no intermediate jlTileT, no second paste
pass. Add JL_HAS_TILE_PASTE_MONO to platform.h IIgs block + port.h
macro. ORCA-M rules: explicit LONGA/LONGI after every REP/SEP, same
load segment as the C callers (DRAWPRIMS), delete stale example
binaries before rebuild. Gate: uber tilePasteMono hash identical to
the Phase 0 generic-path golden on IIgs; `make iigs-verify
VERIFY_EXAMPLE=spacetaxi` before/after (title screen is the
acceptance test -- logo repaint currently costs ~10 frame budgets
per 4-frame period).
2. **#4** -- ST flood plane hooks: implement jlpFloodWalkPlanes +
jlpFloodScanRowPlanes against the word-interleaved StPlanarT layout,
modeled on the Amiga versions (walk 16 px per 4-word group via
combined match masks). Pure 68000, no blitter. C first; hand-asm
only if the C version misses the target. Update the port.h comment
claiming these hooks are Amiga-only. Gate: uber floodFill hash
identical to golden on ST; Hatari flood timing (expect ~5-15x).
3. **#42** -- DOS: #define the planar no-op hooks to ((void)0) like the
IIgs block.
4. **#41** -- fillWord derivation: pick ONE owner. Either the asm
derives it from the nibble (as the port.h comment claims) or C keeps
deriving via a 16-entry uint16_t LUT instead of the * 0x1111
multiply helper. Update the comment to match reality.
5. **#53** -- iigsDrawPixelInner: remove the dead PHB/PLB framing and
fold the row offset via `adc gRowOffsetLut,x` to eliminate the
dpxlTmp round-trip. Two verifier-identified hazards (the plan review
killed the earlier AUXWRITE caveat -- it does not apply here, and
dpxlTmp is DBR-absolute, not long-absolute):
(a) deleting phb renumbers EVERY stack-relative arg offset
(8/10/12,s -> 7/9/11,s) -- miss one and the routine reads the wrong
arg while still "working"; update the comment block at
joeyDraw.s:1402-1405;
(b) do NOT relocate the scratch D-relative -- D-relative stores land
at the bank-0 alias, not the data-bank .bss.
Gate: iigs-verify VERIFY_EXAMPLE=draw checksum + before/after bench
on the fixed clock (hottest inner op).
6. **#52** -- iigsBlitStageToShr dead upload paths, verifier keep-list
(the review caught two traps here): the entry becomes zero-arg --
only ONE far pointer + flags word is currently pushed, not two;
delete the marshalling at joeyDraw.s:2765-2772 and both flag-gated
MVN blocks (2787-2859); KEEP the caller-DBR stash at 2780-2784 (the
narrow-row MVN path reloads $E1:9DFE after every MVN -- deleting the
stash corrupts presents); delete the orphaned bsFrame BSS
(4417-4420); update hal.c:98/298 and the stale jlpInit comment at
hal.c:270-273. Gate: iigs-verify against an example that produces
PARTIAL-ROW dirty bands (not just the default draw demo), checksum
before/after.
7. **#26** -- platform.h decorative IIgs flags: make the registry
authoritative AND loud (review: plain #ifdef-consume recreates the
silent-fallback rot this finding describes): keep the flags, and
pair the port.h IIgs macro block with
`#if !defined(JL_HAS_<OP>) #error` checks so a missing/removed flag
is a compile failure, not a silent generic fallback.
Gate (phase-wide): full 4-port UBER re-run against goldens;
per-item gates above.
Effort: ~2-4 days (dominated by the two new implementations).
---
## Phase 8 -- API-surface decisions + final re-baseline
1. **#40** -- input predicates: DECISION for Scott. Option A: macro
fast paths in joey/input.h reading the extern state arrays (keeps
functions for ABI; exposes internals in a public header). Option B:
accept the call cost (the honest post-Phase-0 number is far better
than the old 3382 anyway). Recommendation: B unless a real game
shows input in its frame profile.
2. **#43** -- jlRandom 16-bit-half rewrite, IIGS-ONLY behind a platform
#if (verifier: the decomposed form is a wash-to-slight-LOSS on
68000 -- keep the current 32-bit C for the GCC ports). Output is
bit-identical either way; the Phase 0 golden-sequence check covers
it on all ports.
3. **#25 (final)** -- regenerate PERF.md completely: new IIgs floor,
new ratios, document BOTH artifacts (SEI clock corruption + the
now-fixed per-iteration poll tax) and the honest full-screen
ceiling; drop the stale-baseline caveats.
Gate: full 4-port bench + hash run; PERF.md tells the truth.
Effort: ~1-2 hours plus bench runs.
---
## Decisions Scott must make (collected)
| # | Decision | Recommendation |
| --- | --- | --- |
| #16 | Drop the undocumented $0RGB re-mask in jlPaletteSet (memcpy + force color 0)? Slight behavior change for non-conforming input. | Yes -- contract already requires $0RGB |
| #22 | jlDrawText: out-of-range initial bx/by becomes an explicit reject instead of silently absorbed per-glyph. | Reject at entry |
| #8 | Arena >64KB on IIgs: clamp silently or fail with error? | Clamp + coreSetError note |
| #34 | Sprite registry in sprite.c vs arena epoch vs doc-only contract? | Registry (fixes both corruption arms, zero hot-path cost) |
| #55 | spriteEmit68k.c: delete or keep unbuilt? | Delete (git history keeps it) |
| #38 | Accept dirty-band over-approximation for fillCircle fallback spans? | Yes -- matches IIgs asm path |
| #6 | Accept 1-LSB audio deviation for the muls.w interp rewrite? | Yes -- inaudible |
| #40 | Input predicate macros in public header vs keep functions? | Keep functions (B) -- DECIDED 2026-07-05, documented at the predicate block in src/core/input.c |
| #60 | assetLoad IIgs segment: add the promised directive or fix the comment? | Check root-segment pressure, then decide |
## What this plan deliberately does NOT do
- No runtime dispatch, no C++/newer-C features, no ST blitter, no
wholesale planar-architecture changes (per the audit's ground rules
and the standing 68k-asm-rewrite plan -- deep C micro-opts to the
Amiga/ST HALs beyond the listed findings are out of scope because the
hand-asm rewrite will replace them).
- No toolchain changes (llvm-mos/ORCA are owned elsewhere; anything
needing compiler work gets flagged to Scott instead).
- Refuted findings (PERF-AUDIT.md bottom) are not actioned.
## Progress tracking
Mark each finding number done here as it lands:
- Phase 0: [x] uber-batch [x] #15 [x] #71 [x] uber-checks [x] bench-iigs.sh [x] millis-harness [x] #73 [x] #76 [x] #25a (3-port; IIgs blocked on stabilization) [x] goldens (DOS/ST/Amiga; IIgs partial)
- Phase 1 COMPLETE 2026-07-05 (all four ports 43/43 bit-identical hashes): [x] #1 [x] #11 (+h==2) [x] #72 [x] #12 [x] #74 [x] #75 [x] #77 (root-caused; fix=toolchain ask 5; codegen off) [x] #78 [x] #79-flag [x] #81 [x] #82 [x] #83 [x] #85 [x] #86 [x] #87 [x] #23 [x] #8 [x] #46 [x] #34 [x] #5 [x] #58 [x] #31 -- new: #88 filed (jlTileT snap/mono semantics wart, Phase 5) [ ] #23 [ ] #8 [ ] #46 [ ] #34 [ ] #5 [ ] #31 [ ] #58
- Phase 2 COMPLETE 2026-07-05 (bit-neutral, 43/43 held on all four ports; DOS paletteSet +92%, drawPixel +37%, samplePixel +21%; IIgs paletteSet +8%, samplePixel +6%): [x] #10 [x] #17 [x] #20 [x] #65 [x] #21 [x] #47 [x] #14 [x] #24 [x] #13 [x] #16 (final form = unrolled word copy, NOT memcpy -- the clang IIgs libc memcpy byte-loop regressed it; finding #79 caveat)
- Phase 3 COMPLETE 2026-07-05 (bit-neutral, 43/43 x4; IIgs drawCircle r=80 -> 70 ops/s, fillCircle r=40 -> 42): [x] #9 [x] #37 [x] #36 [x] #38 [x] #35 [x] #61 [x] #62 -- new: #89 filed (diag fallback int16 dx/dy latent, opportunistic)
- Phase 4 COMPLETE 2026-07-05 (43/43 x4; IIgs interpreted spriteDraw 18->48 ops/s (2.7x), save +18%, restore +15%; review harness: 726,915 differential cases, 0 mismatches): all items incl. #19 + #29-runtime stragglers; #55 file deleted; #59 cache deleted; sprite.h docs modernized
- Phase 5 COMPLETE 2026-07-05 (bit-neutral 43/43 x4; DOS tileSnap +42%, tileCopyMasked +29%, tilePaste +18%, fillRect +8-9%): all items; #22 landed as entry-sanitation preserving historical wrap semantics (strict reject would have broken the offgrid golden); jlDrawText needed a noinline union helper for the w65816 reg allocator
- Phase 6 COMPLETE 2026-07-05 (host harness 12/12: #7+#33 bit-identical in exact mode pre-#6; #6 maxAbsDelta=1 on the 3 odd-step scenarios, within the 1-LSB bound; baseline recaptured post-#6; DOS+ST builds clean; ST asm shows 1 muls.w per split loop, 0 __mulsi3): [x] #6 [x] #7 [x] #33 -- emulator audio smoke (DOSBox/Hatari) not yet run
- Phase 7 COMPLETE 2026-07-05: [x] #3 [x] #4 [x] #42 [x] #41 [x] #53 [x] #52 [x] #26 -- #3/#4 landed
+ hash-identical x4 (tilePasteMono=B0829554, floodFill=780DC48B). Perf captured
2026-07-05 via isolated UBER timed ops (same-CPU override-vs-generic before/after,
built by toggling the port.h #3 macro / platform.h #4 flags): #3 IIgs tilePasteMono
asm 1083 vs generic 323 ops/sec = 3.35x (timed hash 8A39C22B identical both paths --
asm output bit-exact); #4 ST flood plane-hooks 13.5 vs generic 75 frames/flood = 5.6x
(within the 5-15x target). Measured via the UBER micro-benchmark rather than the
plan's spacetaxi/flood-scenario gates -- more isolated, and correctness already
proven by the x4 golden match.
+ Golden coverage extended 2026-07-05 (re-analysis follow-up): the original
tilePasteMono/floodFill goldens left two paths hash-blind -- the #3 asm combos
idx 0 (bg:bg) and idx 3 (fg:fg) were never selected by the alternating 0x0F/0xF0
mono tile, and only jlFloodFill (matchEqual) exercised the #4 ST plane hooks, never
the bounded stop-mask + group-skip branch. Added checkTileMonoFloodExtra() in
uber.c (runs LAST so no earlier cumulative hash shifts): tilePasteMonoAll pastes a
0x00/0xFF/0x0F/0xF0 tile (all four combos) and floodFillBounded fills an enclosed
color-3 box across four 16px groups. Re-ran all four ports: both new checks
hash-identical x4 (tilePasteMonoAll=30C3A2C9, floodFillBounded=EF6B6F51); goldens
updated to 45/45. The two previously by-inspection-only paths are now empirically
bit-exact vs the DOS generic reference. NOTE: Amiga bench needed TIMEOUT=600 (the
last-running checks overran the default 150s, same truncation as the Phase 0 note).
+ #41 verified COMPLETE 2026-07-05: found already landed (opportunistically,
during the earlier tileFill/fillCircle/fillRect asm work) -- iigsTileFillInner,
iigsFillCircleInner, and iigsFillRectInner all take the raw nibble and derive
the fill word/bytes in asm (joeyDraw.s:420-430 etc.); the port.h macros pass
(c & 0x0F); no C-side * 0x1111 survives anywhere (grep-verified, comments
only); the port.h:100-105/136-138 comments match reality. surfaceClear's
fillWord stays C-derived via (d | d<<8) -- shifts/OR, no multiply helper, so
ownership is consistent per-op and documented at each site. No code change;
checkbox was stale.
+ #52 verified COMPLETE 2026-07-05: also found already landed (the clang-era
joeyDraw.s rewrite folded it in) -- iigsBlitStageToShr is zero-arg (hal.c:106
extern, hal.c:309 call), the scbPtr/palettePtr/uploadFlags marshalling and both
flag-gated MVN blocks are gone, the caller-DBR stash to $E1:9DFE is KEPT per the
verifier correction (joeyDraw.s:3214-3218), bsFrame BSS deleted (grep-verified),
and the jlpInit/jlpPresent comments in hal.c already tell the truth (jlpPresent
even credits #52). Gate evidence: today's IIgs UBER run exercised the narrow-row
MVN path (fillRect-32x32 present = 16-word dirty band < the 32-word threshold,
~300 iterations) and completed to the green done-screen, which requires the DBR
stash restore to work; UBER 45/45 hashes matched.
+ #53 verified COMPLETE 2026-07-05: same story -- iigsDrawPixelInner has no
PHB/PLB (frame = php+phd = +3; args correctly renumbered to x@7,s y@9,s
nib@11,s with the ABI comment updated, joeyDraw.s:1856-1858), the row offset
folds directly via `adc gRowOffsetLut,x` (line 1899), dpxlTmp is deleted
(grep-verified), and scratch was NOT relocated D-relative per the verifier
hazard note. No isolated before/after bench: #53 landed inside the wholesale
clang-era rewrite (no "before" tree), and the verifier-predicted ~1% delta is
below MAME run-to-run noise. Correctness gates: UBER jlDrawPixel + edge-pixels
hashes bit-exact vs DOS x4 (45/45), and `make iigs-verify VERIFY_EXAMPLE=draw`
PASS -- checksum=08483E, distinctNibbles=15 (RECORDED AS THE BASELINE; no prior
checksum was ever written down -- future draw-path gates compare against this).
RESIDUAL flagged, out of #53 scope: iigsDrawLineInner / iigsDrawCircleInner /
the tile inners still carry phb in their prologues -- the audit verifier
explicitly deferred that same-pattern audit to the IIGS_PERF_PLAN Phase 4
line/circle work; their arg-offset comments all assume the +4 frame, so any
future phb deletion must renumber every N,s in each routine.
+ #3 SPACETAXI TITLE-SCREEN ACCEPTANCE TEST attempted 2026-07-05 (the plan's
original gate, deferred from the Phase 7 landing): BLOCKED ON FINDING #77
(toolchain) -- but the attempt built the entire test rig and fixed four real
bugs found along the way. Landed: IIgs asset bake (examples/spacetaxi/
generated/iigs, .tbk target byte 4), make-iigs-disk.sh DATA-tree packing
(CREATEFOLDER + per-file ADDFILE when STAXI is on the disk -- the spacetaxi
PLAN.md Phase 6 packager task), STAXI now BUILDS on IIgs (the iigs.mk:74
"blocked on 2-byte cross-seg reloc" comment is stale -- the toolchain landed
it; STAXI still deliberately omitted from iigs-examples pending the #77 fix),
scripts/bench-staxi-title.sh (per-frame SHR logo-cell cadence sampler; the
intended 4-frame period vs measured), scripts/diag-staxi-screen.sh (SHR/RAM
dump + PC histogram + ring extraction). Bug fixes landed (all 45/45
golden-verified x4): (1) IIgs jlLogReset did a floppy-truncate write --
first-ever floppy WRITE on the headless path, #82 class; now ring-only, with
jlLogFlush switched to "w" (self-truncating full-ring dump, idempotent);
(2) jlTileBankLoad read tile banks 32 bytes/fread (1000 freads per font
bank!) -- now batched 8KB/fread (ASSET_TILE_READ_BATCH), same truncated-file
contract; (3) NEW API jlTileBankLoadToSurface (tile.h) streams a bank
straight onto a surface's block grid with a 256-byte static scratch --
no caller-side 32KB buffer, which no IIgs example can allocate (heap is
665 bytes; a 32KB static blows the bank-0 BSS ceiling); spacetaxi's
loadFontSheet now uses it; (4) jlpGenericSurfaceAllocPixels used calloc --
on IIgs that's the malloc-returns-1 trap (#81) shredding bank 0 through
pointer 1 on the first OFFSCREEN jlSurfaceCreate (stage uses the pinned
path, so UBER never hit it); now jlpBigAlloc + memset with matching
jlpBigFree in jlpGenericSurfaceFreePixels. THE BLOCKER: with all four
fixed, STAXI boots to "title loaded, tilebank=0" but dies layout-dependently
when jlpBigAlloc handles land inside the app's own unreserved BSS spans
(finding #77 exactly; pixels@$09:0BF4 run survived to title-load, other
layouts died in fopen). An app-side BSS-claim mitigation was built and
REVERTED: the linker's __bss_seg symbols ARE readable via a DATA32 .long
table (C IMM16 refs trip the 2-byte cINTERSEG limit), but a SUCCESSFUL
NewHandle claim over the app's own BSS span kills the app instantly
(silent return-to-Finder, reproduced twice) -- the reservation must be made
loader-side. Full recipe + evidence filed in LLVM816-ASKS.md item 5.
Re-run the gate (scripts/bench-staxi-title.sh both port.h-macro arms) once
the toolchain lands item 5.
- Phase 8 COMPLETE 2026-07-05: [x] #40 [x] #43 [x] #25b -- ALL PHASES DONE.
#40 decided per recommendation B (keep functions; documented at the predicate
block in src/core/input.c and in the decisions table above). #43 landed as an
IIgs-only #if in src/core/random.c: state stored as two uint16_t halves, the
13/17/5 xorshift triple decomposed to 16-bit shift identities (hi-before-lo
update order per step); bit-identity host-proven (UBER golden sequence 4/4 +
50M-state sweep vs the 32-bit reference, scratchpad rngHalvesCheck.c) and
emulator-gated (random-golden PASS x4); GCC ports keep the 32-bit form per the
verifier correction. Note: UBER has no timed jlRandom row, so the ~2-3x/call
win is analytic, not benched. #25b: PERF.md fully regenerated from same-day
4-port captures -- fresh table via tools/uber-perf-table, both clock artifacts
documented (poll tax fixed / SEI tick loss inherent with the three IIgs rows
tagged clock-unreliable against the ~29 ops/sec ceiling), 45/45 x4 correctness
statement, per-port sprite-row caveats (DOS compiled, IIgs interpreter pending
#77), below-floor punch list, stale stabilization/history sections dropped.
Phase-wide gate: full 4-port bench + hash run 2026-07-05, 45/45 goldens on all
four ports, random-golden PASS x4.

1425
PERF-AUDIT.md Normal file

File diff suppressed because it is too large Load diff

244
PERF.md
View file

@ -1,70 +1,120 @@
# JoeyLib cross-port performance (UBER benchmark)
Regenerated 2026-06-30. ST/Amiga/DOS columns are fresh measurements from
the headless harness (`scripts/bench-atarist.sh` Hatari, `bench-amiga.sh`
FS-UAE, `bench-dos.sh` DOSBox). The IIgs column is the perf reference: the
generic+override redesign was bit-identical on IIgs (MAME `iigs-verify`
checksum 0817FE), so its codegen — and thus its ops/sec — is unchanged from
the last IIgs capture. Every non-IIgs cell is a ratio (port / IIgs); the
directive is that every target meet or beat the IIgs floor (>= 1.0x).
Regenerated 2026-07-05 (PERF-AUDIT-PLAN.md Phase 8, #25b) from
same-day captures of all four ports. This supersedes the 2026-07-04
Phase 0 baseline; every phase of the audit fix plan (0-8) has landed,
so these are the numbers the library actually ships with.
Sprite ops now meet or beat the IIgs on BOTH 68k targets after this session's
work (compiled DRAW emitters + raw-plane-byte save/restore); they were the
dominant violations (down to 0.13x). The ST column is honest for the first
time — the ST port was crashing on first present (user-mode I/O poke) until
the `Super(0L)` fix in jlpInit.
**2026-07-06 update:** the IIgs sprite column is now COMPILED. Finding
#77 (the loader/segment-BSS + malloc-heap reservation that forced the
codegen arena into the app's own BSS) was fixed toolchain-side and
JOEY_IIGS_SPRITE_CODEGEN was re-enabled; the four IIgs sprite rows were
re-captured (bench-iigs.sh) and jumped 5-25x (SaveUnder 104->543,
Draw 48->907, RestoreUnder 110->568, SaveAndDraw 15->376 ops/sec),
bit-identical to the interpreter goldens (diff-uber-hashes 45/45). The
ST and DOS columns are unchanged from the 2026-07-05 capture.
Genuine remaining sub-1.0x cells and why:
* **ST jlDrawCircle (0.61-0.65x)** — inherent planar-outline cost: a 4-plane
word RMW per sparse outline pixel (8 memory accesses) vs the IIgs single
nibble, amplified by ST video-DMA contention. Cycle-bound; ~0.80x ceiling
even fully optimized. Not winnable in this format.
* **Amiga jlFillCircle (0.82x)** — already asm span-fill (long-store middles);
closing the gap needs a blitter area-fill rewrite (risk: the blitter has
wedged the system before).
* **ST jlSpriteSaveAndDraw (0.97x)** — ST runs save+draw as two dispatched
ops; the IIgs fuses them. Marginal.
* **ST jlTileFill (0.98x), Amiga jlFillRect 80x80 (0.95x)** — within noise.
* **ST jlFillRect 320x200 (0.53x)** — ARTIFACT: the IIgs reference (60) is the
SEI-inflated full-screen reading (see the ceiling note below; honest ~28).
Against the honest reference ST is ~1.1x. Same caveat for surfaceClear /
stagePresent full.
Also 2026-07-06: the Amiga `jlpFillRect` partial-width path was moved
onto the blitter (BLTAFWM/BLTALWM hardware edge masks + masked minterm),
which lifted `jlFillRect 16x16` 567->1153, `80x80` 71->269 (both from
below-floor to well above it), and -- because they build on fillRect --
`jlDrawLine V` 144->269 and `jlDrawRect 100x100` 122->203, all
hash-identical (Amiga 45/45). The one remaining Amiga below-floor cell
is `jlFillCircle r=40` (74%); a whole-circle asm routine was tried and
did NOT help (per-span dispatch is not the cost -- the 8-way-symmetry
overlap fill dominates), so it stays on the C span walker for now.
| Op | IIgs (ops/sec) | Amiga (vs IIGS) | Atari ST (vs IIGS) | DOS (vs IIGS) |
Correctness statement backing this table: all four ports produce
BYTE-IDENTICAL pixels on every measured op and every correctness
check -- 45/45 surface hashes match the frozen goldens in
tests/goldens/uber/ (34 timed ops + 11 hashed checks; the PASS/FAIL
checks -- palette round-trip, PRNG golden sequence, sprite clip
round-trip, arena churn, sprite-from-surface -- pass everywhere).
The Phase 0-era divergences (#75 tilePasteMono three-way, #78 ST
fillCircle/tileFill, #87 IIgs circle outline) are all fixed.
## Measurement honesty (read before trusting any number)
Three clock artifacts have shaped this table's history. Two are fixed,
one is inherent -- know them before comparing rows:
1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench
loop called jlFrameCount() between every op; on the IIgs that was a
GetTick toolbox chain (~600-800 cycles), so every small-op number
carried the harness cost, not the op cost. Ops now run in adaptive
batches per poll. Small-op numbers jumped 2-10x on every port when
this landed; never compare against pre-Phase-0 captures.
2. **SEI tick loss -- INHERENT on IIgs, rows tagged below.** The
benchmark clock is jlFrameCount = the VBL-interrupt-driven GetTick.
An op that holds SEI while it runs suppresses those interrupts, the
clock loses ticks, and the op over-reports. The three SEI-holding
rows -- `jlSurfaceClear`, `jlFillRect 320x200`, and
`jlStagePresent full` -- are CLOCK-UNRELIABLE in the IIgs column.
The physics check: a full-screen op moves 32000 bytes at ~6
cyc/word, so ~29 ops/sec is the hard ceiling at 2.8 MHz. The
present row's 68 and the clear row's 35 sit above it -- inflated;
the honest values are at or below the ceiling.
3. **ST frame-rate miscalibration -- FOUND and FIXED 2026-07-06; the
ST column below is the corrected (millis-clock) capture.** Before the
fix, ops/sec was `iters * jlFrameHz() / actualFrames` and the ST's
`jlpFrameHz()` hardcoded 50 -- but Hatari runs the emulated ST at
~60 Hz VBL, so every ST number was scaled by 50/60 ~= 0.83 (~17%
pessimistic). Caught by cross-checking against UBER's independent
Timer-C 200 Hz millis clock (refresh-independent), which ran ~16-22%
faster; the Amiga's two clocks agreed exactly (FS-UAE = PAL 50 Hz),
confirming it was ST-only. Two fixes landed: (a) UBER now computes
ops/sec from `jlMillisElapsed` (refresh-independent on all four
ports; identical to the old formula on IIgs where millis is
GetTick-derived), and (b) ST `jlpFrameHz()` now reads the shifter
resolution + sync-mode registers to return the true 50/60/71 Hz. The
per-op `UBER-CLK:` log line reports frame-vs-millis as a standing
cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw
503->585, jlFillRect 80x80 97->115); DOS/IIgs/Amiga unchanged.
## Baseline (2026-07-05): absolute ops/sec per port
The IIgs is the reference and the perf floor (every other port must
meet or beat it -- project directive). Non-IIgs cells show absolute
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
as below-floor. `tools/uber-perf-table` emits this table directly;
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
default cuts off the final checks).
| Op | IIgs (ops/sec) | Amiga (ops/sec, % of IIGS) | Atari ST (ops/sec, % of IIGS) | DOS (ops/sec, % of IIGS) |
| --- | --- | --- | --- | --- |
| jlSurfaceClear | 28 | 2.68x | 1.32x | 449.21x |
| jlPaletteSet | 678 | 9.09x | 4.27x | 22.01x |
| jlScbSetRange | 1005 | 3.62x | 1.86x | 14.20x |
| jlDrawPixel | 1755 | 2.04x | 1.18x | 9.40x |
| jlDrawLine H | 682 | 2.26x | 1.61x | 26.93x |
| jlDrawLine V | 90 | 1.70x | 1.14x | 145.83x |
| jlDrawLine diag | 35 | 1.14x | 1.06x | 475.37x |
| jlDrawRect 100x100 | 75 | 1.67x | 1.16x | 206.49x |
| jlDrawCircle r=16 | 232 | 1.20x | **0.65x** | 64.79x |
| jlDrawCircle r=80 | 56 | 1.16x | **0.61x** | 301.25x |
| jlFillRect 16x16 | 450 | 1.24x | 1.11x | 36.04x |
| jlFillRect 80x80 | 75 | **0.95x** | 1.33x | 205.09x |
| jlFillRect 320x200 | 60 | 1.13x | **0.53x** | 188.92x |
| jlFillCircle r=40 | 38 | **0.82x** | 1.39x | 398.24x |
| jlSamplePixel | 1916 | 3.42x | 1.92x | 18.04x |
| jlTileFill | 1252 | 1.74x | **0.98x** | 10.60x |
| jlTileCopy | 997 | 1.80x | 1.04x | 7.88x |
| jlTileCopyMasked | 498 | 1.81x | 1.10x | 32.48x |
| jlTilePaste | 1106 | 1.99x | 1.12x | 14.64x |
| jlTileSnap | 1473 | 2.29x | 1.53x | 9.96x |
| jlSpriteSaveUnder | 528 | 1.05x | 1.29x | 23.69x |
| jlSpriteDraw | 438 | 2.09x | 1.09x | 23.85x |
| jlSpriteRestoreUnder | 487 | 1.08x | 1.11x | 34.83x |
| jlSpriteSaveAndDraw | 277 | 1.23x | **0.97x** | 53.79x |
| jlStagePresent full | 42 | 7.81x | 1.10x | 412.17x |
| jlInputPoll | 273 | 13.94x | 4.19x | 37.87x |
| jlKeyDown | 3382 | 7.75x | 3.76x | 18.94x |
| jlKeyPressed | 3345 | 7.66x | 3.81x | 18.73x |
| jlMouseX | 4170 | 10.40x | 5.22x | 26.30x |
| joeyJoyConnected | 3378 | 7.71x | 3.71x | 18.96x |
| jlAudioFrameTick | 4106 | 8.00x | 2.50x | 12.42x |
| jlAudioIsPlayingMod | 3536 | 9.30x | 4.33x | 26.78x |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 240 | 1.42x | 1.31x | 47.12x |
| jlSurfaceClear | 35 | 75 (214%) | 44 (126%) | 70 (200%) |
| jlPaletteSet | 1623 | 11503 (709%) | 5256 (324%) | 29474 (1816%) |
| jlScbSetRange | 163 | 3953 (2425%) | 2418 (1483%) | 8474 (5199%) |
| jlDrawPixel | 3423 | 5853 (171%) | **3322 (97%)** | 23524 (687%) |
| jlDrawLine H | 1143 | 1703 (149%) | 1341 (117%) | 5674 (496%) |
| jlDrawLine V | 99 | 269 (272%) | 120 (121%) | 494 (499%) |
| jlDrawLine diag | 42 | **40 (95%)** | 42 (100%) | 110 (262%) |
| jlDrawRect 100x100 | 90 | 203 (226%) | 100 (111%) | 399 (443%) |
| jlDrawCircle r=16 | 306 | **285 (93%)** | **178 (58%)** | 424 (139%) |
| jlDrawCircle r=80 | 70 | **63 (90%)** | **40 (57%)** | 96 (137%) |
| jlFillRect 16x16 | 603 | 1153 (191%) | **585 (97%)** | 2874 (477%) |
| jlFillRect 80x80 | 110 | 269 (245%) | 115 (105%) | 380 (345%) |
| jlFillRect 320x200 | 21 | 66 (314%) | 38 (181%) | 56 (267%) |
| jlFillCircle r=40 | 42 | **31 (74%)** | 75 (179%) | 153 (364%) |
| jlSamplePixel | 5223 | 7253 (139%) | **4862 (93%)** | 44244 (847%) |
| jlTileFill | 2043 | 2303 (113%) | **1516 (74%)** | 8894 (435%) |
| jlTileCopy | 1443 | 1853 (128%) | **1307 (91%)** | 8404 (582%) |
| jlTileCopyMasked | 624 | 953 (153%) | 677 (108%) | 3084 (494%) |
| jlTilePaste | 1803 | 2303 (128%) | **1544 (86%)** | 9454 (524%) |
| jlTileSnap | 2643 | 3503 (133%) | 2907 (110%) | 18624 (705%) |
| jlSpriteSaveUnder | 543 | 567 (104%) | 833 (153%) | 8964 (1651%) |
| jlSpriteDraw | 907 | 953 (105%) | **585 (64%)** | 6094 (672%) |
| jlSpriteRestoreUnder | 568 | **553 (97%)** | 655 (115%) | 5744 (1011%) |
| jlSpriteSaveAndDraw | 376 | **332 (88%)** | **318 (85%)** | 4204 (1118%) |
| jlStagePresent full | 68 | 359 (528%) | **56 (82%)** | 354 (521%) |
| jlInputPoll | 243 | 4003 (1647%) | 1425 (586%) | 3084 (1269%) |
| jlKeyDown | 12303 | 39753 (323%) | 22826 (186%) | 76514 (622%) |
| jlKeyPressed | 14043 | 38003 (271%) | 22826 (163%) | 74064 (527%) |
| jlMouseX | 30123 | 96703 (321%) | 59354 (197%) | 153864 (511%) |
| joeyJoyConnected | 12303 | 39303 (319%) | 22283 (181%) | 77144 (627%) |
| jlAudioFrameTick | 50223 | 56503 (113%) | **11724 (23%)** | 58664 (117%) |
| jlAudioIsPlayingMod | 13503 | 56403 (418%) | 29870 (221%) | 127334 (943%) |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 376 (116%) | 1474 (456%) |
> **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
@ -76,3 +126,75 @@ Genuine remaining sub-1.0x cells and why:
> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
> inflated and cross-check against the cycle model.
## Reading notes
* **Sprite rows measure different code per port.** DOS runs COMPILED
sprites (finding #74 fixed 2026-07-05). Amiga/ST run their
interpreted planar paths (compiled save/restore emitters are stubs
by design, Phase 4 #27). The IIgs now runs COMPILED sprites too
(finding #77 fixed 2026-07-06; JOEY_IIGS_SPRITE_CODEGEN back on) --
the IIgs sprite rows jumped 5-25x and the huge non-IIgs ratios on
these rows collapsed accordingly, which re-exposes a real 68k sprite
gap (see below).
* **The below-floor cells are the standing punch list** (ST column
corrected +17% by the 2026-07-06 clock fix, artifact #3 above -- many
ST cells crossed above the floor; what remains is genuinely below).
ST circles (57-58%) are the worst honest graphics gap -- pure-68000
constraint, no blitter allowed (STF baseline). The IIgs circle-outline
case is a known structural loss (3-register 65816 vs 16-register 68000
Bresenham; see feedback memory). With IIgs sprites now compiled, a
68k sprite gap re-opened: ST jlSpriteDraw 64%, ST jlSpriteSaveAndDraw
85%, ST jlTileFill 74%, and Amiga jlSpriteRestoreUnder 97% +
jlSpriteSaveAndDraw 88% remain below the IIgs floor (they were masked
while the IIgs interpreted; ST jlSpriteRestoreUnder is now 115%, above
floor, after the clock fix). The ST jlSpriteDraw case was investigated
2026-07-06 and is ARCHITECTURAL, the same root as the ST circle gap:
DRAW is already compiled (JIT, shifts 0/1), but the word-interleaved
planar layout forces `move.b #imm,(d16,An)` (~18 cyc) per plane byte,
which loses in wall-time to the IIgs chunky `sta` (~5 cyc @ 2.8 MHz) --
ST actually BEATS the IIgs on the block-copy SAVE (833 vs 543) and only
trails on the c2p+mask DRAW. The one real DRAW win (merge the two tile
columns into one `move.w` per plane word) exists only at shift 0; the
UBER benchmark draws at x=40 = shift 1, where the columns fall in
different interleaved groups, so it is byte-bound and the metric cannot
move. jlSpriteSaveAndDraw (85%) is save+draw and is dominated by that
same DRAW (SAVE is already fast). ST jlAudioFrameTick at 23% is the YM mixer
cost, tracked separately. Amiga jlFillCircle r=40 (74%) was probed
2026-07-06 with a full whole-circle asm routine (Bresenham + inline
4-plane long-fill span): it held 45/45 hashes but did NOT move the
number (34 vs 31, within the coarse 11-iter sample), so the per-span
C dispatch is NOT the bottleneck -- the asm was reverted. The real cost
is the midpoint fill's 8-way symmetry filling each row 2-4x (idempotent
overlap): the true lever is a scanline rewrite (one span per row, no
overlap), which would roughly halve the byte writes and stay
hash-identical. NB: this op is timed at batch=1 (~29 ms each) by the
50 Hz frame counter, a coarse measurement -- the same ~4-5x
model-vs-measured gap shows on ST sprite-draw, so part of these two
cells may be measurement, not silicon.
* **jlRandom has no timed row.** Phase 8 (#43) rewrote its xorshift32
update in 16-bit halves on the IIgs only (~2-3x fewer cycles per
call on the 65816; 32-bit shifts there are per-bit helper loops).
Output is bit-identical -- host-proven over the golden sequence and
a 50M-state sweep, and gated by UBER's random-golden check on all
four ports.
* **Input predicates stay functions** (Phase 8 #40 decision): the
post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere
near a real frame budget; macro forms in the public header were
rejected as not worth the API exposure.
* **IIgs libc memset/memcpy remain ~4x slow** (finding #79,
toolchain-owned): llvm-mos runtime byte loops vs ORCA's MVN. The
jlScbSetRange and jlPaletteSet IIgs rows carry that tax; library-side
mitigations landed (#16 unrolled word copy, #21), the libc fix is
flagged to the llvm816 session.
## History
Pre-2026-07-04 tables (ORCA-era IIgs column, artifact-inflated DOS
numbers, un-batched loops) live in git history only -- every
conclusion drawn from them was re-derived from Phase 0 onward. The
Phase 0 baseline (2026-07-04) and per-phase deltas are recorded in
PERF-AUDIT-PLAN.md's progress tracker; headline gains since Phase 0:
IIgs interpreted spriteDraw 18 -> 48 ops/sec (Phase 4), IIgs
tilePasteMono asm 3.35x (Phase 7 #3), ST flood plane hooks 5.6x
(Phase 7 #4), DOS paletteSet +92% / tileSnap +42% (Phases 2/5),
DOS compiled sprites 284 -> ~6100 ops/sec (#74).

View file

@ -2270,7 +2270,14 @@ int main(void) {
* 2 items = 22 sprites. Amiga planar emits 8 shifts/sprite at
* ~1.5 KB each, so we ask for 256 KB to be safe. Chunky ports
* need ~16 KB. */
#ifdef JOEYLIB_PLATFORM_IIGS
// IIgs: the codegen arena is a single attrNoCross Memory Manager
// block and cannot exceed one 64 KB bank; 16 KB covers the chunky
// emitter's needs for this sprite set.
config.codegenBytes = 16UL * 1024;
#else
config.codegenBytes = 256UL * 1024;
#endif
config.audioBytes = 64UL * 1024;
if (!jlInit(&config)) {

View file

@ -149,7 +149,7 @@ static void updateEgoFromInput(void);
static AgiGameT gGame;
static AgiPicT gPic;
static jlSurfaceT *gBackdrop;
static jlSurfaceT *gStage;
static jlSurfaceT *gAgiStage;
static AgiVmT gVm;
static LogicCacheSlotT gLogicCache[AGI_MAX_RESOURCES];
// View cache: 256 slots of ~5 KB each (~1.3 MB). Far too large for the
@ -546,7 +546,7 @@ static void cbOverlayPic(void *ctx, uint8_t picId) {
static void cbShowPic(void *ctx) {
(void)ctx;
jlLogF("cbShowPic v11=%u", (unsigned)gVm.vars[11]);
if (gStage == NULL) {
if (gAgiStage == NULL) {
return;
}
// Clear the WHOLE stage first so the rows above/below the AGI
@ -556,10 +556,10 @@ static void cbShowPic(void *ctx) {
// copy below and gets restored over every subsequent frame --
// so clear.lines never visually erases anything that sat in
// the border bands.
jlSurfaceClear(gStage, COLOR_MISSING);
agiPicBlit(&gPic, gStage, PIC_DEST_Y);
jlSurfaceClear(gAgiStage, COLOR_MISSING);
agiPicBlit(&gPic, gAgiStage, PIC_DEST_Y);
if (gBackdrop != NULL) {
jlSurfaceCopy(gBackdrop, gStage);
jlSurfaceCopy(gBackdrop, gAgiStage);
}
gPicReady = true;
// The stage just got repainted; any prior save-under refers to
@ -1716,8 +1716,8 @@ int main(int argc, char **argv) {
jlLogReset();
jlLogF("AGI TRACE build=%s %s", __DATE__, __TIME__);
jlLogFlush();
gStage = jlStageGet();
jlPaletteSet(gStage, 0u, kAgiPalette);
gAgiStage = jlStageGet();
jlPaletteSet(gAgiStage, 0u, kAgiPalette);
jlRandomSeed(0xA61DA61Du);
// Bring up the SB DMA + mixer. AGI music feeds the SFX stream
@ -1786,7 +1786,7 @@ int main(int argc, char **argv) {
}
}
jlSurfaceClear(gStage, COLOR_MISSING);
jlSurfaceClear(gAgiStage, COLOR_MISSING);
if (gBackdrop != NULL) {
jlSurfaceClear(gBackdrop, COLOR_MISSING);
}
@ -1852,7 +1852,7 @@ int main(int argc, char **argv) {
{
uint32_t nowMs2 = jlMillisElapsed();
if (nowMs2 - lastRenderMs >= AGI_RENDER_PERIOD_MS) {
renderFrame(gStage);
renderFrame(gAgiStage);
lastRenderMs = nowMs2;
} else {
jlStagePresent();

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1139,42 +1139,34 @@ static void destroySprites(void) {
}
// The 1000-glyph font streams straight onto the font surface via
// jlTileBankLoadToSurface -- no 32KB staging buffer. The old
// malloc(32000) was lethal on IIgs (the clang libc heap is ~665
// bytes and malloc returns 1, not NULL, at exhaustion -- LLVM816-ASKS
// item 4 -- so jlTileBankLoad fread the payload through pointer 1 and
// shredded bank 0 before the first present), and a 32KB static blows
// the bank-0 BSS ceiling. The streaming loader exists for exactly
// this case; its block grid matches ST_FONT_COLS row-major layout.
static bool loadFontSheet(jlSurfaceT *stage) {
jlTileT *fontTiles;
uint16_t count;
uint16_t i;
uint16_t palette[16];
uint16_t count;
uint16_t palette[16];
(void)stage;
// Load font.tbk into a temp tile array, then paste each glyph
// into a regular surface that jlDrawText can sample. ~32 KB of
// temp memory for the 1000-tile font; freed before returning.
fontTiles = (jlTileT *)malloc(sizeof(jlTileT) * ST_FONT_TILES_MAX);
if (fontTiles == NULL) {
return false;
}
count = jlTileBankLoad(ST_FONT_PATH, fontTiles, ST_FONT_TILES_MAX,
NULL, palette);
if (count == 0u) {
free(fontTiles);
return false;
}
gRender.fontSurface = jlSurfaceCreate();
if (gRender.fontSurface == NULL) {
free(fontTiles);
return false;
}
count = jlTileBankLoadToSurface(ST_FONT_PATH, gRender.fontSurface, palette);
if (count == 0u) {
jlSurfaceDestroy(gRender.fontSurface);
gRender.fontSurface = NULL;
return false;
}
// Font palette is authoritative for the font surface so jlDrawText
// reads back the authored colors. Stage palette is untouched.
jlPaletteSet(gRender.fontSurface, 0u, palette);
jlScbSetRange(gRender.fontSurface, 0, SURFACE_HEIGHT - 1, 0);
for (i = 0u; i < count; i++) {
uint8_t bx = (uint8_t)(i % ST_FONT_COLS);
uint8_t by = (uint8_t)(i / ST_FONT_COLS);
jlTilePaste(gRender.fontSurface, bx, by, &fontTiles[i]);
}
gRender.fontReady = true;
free(fontTiles);
return true;
}

View file

@ -9,9 +9,14 @@
// UBER_FRAMES so results are directly comparable across ports
// regardless of CPU speed or VBL rate.
//
// jlFrameCount is wall-clock-based per port; the per-iter poll
// adds ~10-30 cyc per op which shows up as noise on the very
// fastest ops but is below ~5% even for ~500 cyc/op work.
// jlFrameCount is wall-clock-based per port, but READING it is not
// free: on the IIgs it is a Misc Toolset GetTick call (several
// hundred cycles per poll -- an earlier comment here claimed
// ~10-30 cyc, which was wrong by more than an order of magnitude
// and inflated every fast op's measured cost by a near-constant
// ~600-800 cycles/iter). runForFrames therefore polls the clock
// once per batch of UBER_BATCH op calls for sub-frame ops
// (calibrated by the first call), amortizing the poll to noise.
//
// One-shot ops (jlSpriteCompile) get one call each, timed by frame
// delta -- coarser but representative.
@ -40,6 +45,68 @@
#define UBER_FRAMES 16u
// Op calls per clock poll for sub-frame ops. The unrolled batch in
// runForFrames must contain exactly this many op() calls. Ops slower
// than one frame poll every call instead, so a batch cannot overrun
// the measurement window by more than one op.
#define UBER_BATCH 16u
// ----- IIgs progress mailbox (headless-MAME diagnosis) -----
//
// The SHR SCB region covers rows 0..199 at $E1:9D00-$E1:9DC7; the bytes
// at $E1:9DC8-$E1:9DFF are unused by hardware and by jlpPresent. UBER
// mirrors its progress there so a MAME Lua probe can see WHERE a
// headless run is (and whether the timing loop and the tick are alive)
// even when the joeylog FST flush never lands on disk. Layout:
// +0 (9DC8) uint8 current timed-op index (from timeOp); during
// phase 5 it is 100 + the correctness-check number
// +1 (9DC9) uint8 phase (UBER_PHASE_*)
// +2 (9DCA) uint16 heartbeat (one increment per timing-loop pass)
// +4 (9DCC) uint16 last jlFrameCount() value the timing loop saw
// No-ops on every other port.
#define UBER_PHASE_SETUP_SPRITE 1u
#define UBER_PHASE_AUDIO_INIT 2u
#define UBER_PHASE_SHOWCASE 3u
#define UBER_PHASE_TIMED_RUN 4u
#define UBER_PHASE_CHECKS 5u
#define UBER_PHASE_DONE 6u
#ifdef JOEYLIB_PLATFORM_IIGS
#define UBER_MB_OP ((volatile uint8_t *)0xE19DC8L)
#define UBER_MB_PHASE ((volatile uint8_t *)0xE19DC9L)
#define UBER_MB_HEARTBEAT ((volatile uint16_t *)0xE19DCAL)
#define UBER_MB_TICK ((volatile uint16_t *)0xE19DCCL)
// One-time layout facts for probe correlation: +8 stage pointer value,
// +12 address of uber.c's gStage static, +16 log-ring base address,
// +20 log-ring head-counter address (see src/core/debug.c's IIgs RAM
// ring -- the Lua probe drains the log live through these).
#define UBER_MB_MAGIC ((volatile uint16_t *)0xE19DCEL)
#define UBER_MB_STAGEPTR ((volatile uint32_t *)0xE19DD0L)
#define UBER_MB_GSTAGEAT ((volatile uint32_t *)0xE19DD4L)
#define UBER_MB_LOGRING ((volatile uint32_t *)0xE19DD8L)
#define UBER_MB_LOGHEAD ((volatile uint32_t *)0xE19DDCL)
// TEMP #84: the gSprite pointer value, poked after setupSprite so the
// Lua probe can watch the struct bytes get overwritten in place.
#define UBER_MB_SPRITEPTR ((volatile uint32_t *)0xE19DEAL)
// "JL" -- the Lua probes refuse to trust the layout words until this
// appears (the SCB region holds 0x80 fill before the app writes it).
#define UBER_MB_MAGIC_VAL 0x4A4Cu
extern uint8_t gJoeyLogRing[];
extern uint16_t gJoeyLogRingHead;
#define uberMbOp(_i) (*UBER_MB_OP = (uint8_t)(_i))
#define uberMbPhase(_p) (*UBER_MB_PHASE = (uint8_t)(_p))
#define uberMbBeat() (*UBER_MB_HEARTBEAT = (uint16_t)(*UBER_MB_HEARTBEAT + 1u))
#define uberMbTick(_t) (*UBER_MB_TICK = (uint16_t)(_t))
#define uberMbLayout() (*UBER_MB_STAGEPTR = (uint32_t)gStage, *UBER_MB_GSTAGEAT = (uint32_t)&gStage, *UBER_MB_LOGRING = (uint32_t)&gJoeyLogRing[0], *UBER_MB_LOGHEAD = (uint32_t)&gJoeyLogRingHead, *UBER_MB_MAGIC = UBER_MB_MAGIC_VAL)
#else
#define uberMbOp(_i) ((void)0)
#define uberMbPhase(_p) ((void)0)
#define uberMbBeat() ((void)0)
#define uberMbTick(_t) ((void)0)
#define uberMbLayout() ((void)0)
#endif
typedef void (*OpFn)(void);
static const char *gCurName = "(none)";
@ -51,21 +118,70 @@ static unsigned char gBackupBytes[256];
static jlTileT gTileScratch;
// Current timed-op number (1-based); mirrored into the mailbox. Lives
// up here because runForFrames re-writes it every loop pass: the SHR
// SCB upload in jlStagePresent can overwrite the adjacent mailbox
// bytes, so a single write at timeOp start can be stomped mid-window.
static uint8_t gOpIndex = 0;
// Run `op` in a tight loop until `targetFrames` jlFrameCount ticks
// have elapsed. Returns iterations completed.
static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *actualFramesOut) {
static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *actualFramesOut, uint32_t *millisDeltaOut) {
unsigned long count;
uint16_t startFrame;
uint16_t endFrame;
uint16_t now;
uint32_t startMillis;
uint32_t endMillis;
bool slowOp;
count = 0UL;
jlWaitVBL();
startFrame = jlFrameCount();
startFrame = jlFrameCount();
startMillis = jlMillisElapsed();
while ((uint16_t)(jlFrameCount() - startFrame) < targetFrames) {
op();
count++;
// Calibrate: jlWaitVBL aligned us to a tick edge, so a sub-frame op
// cannot see the counter advance during a single call. If the first
// call DOES advance it, the op is slower than one frame -- poll the
// clock every call so a batch cannot overrun the window.
op();
count++;
slowOp = ((uint16_t)(jlFrameCount() - startFrame) != 0u);
for (;;) {
now = jlFrameCount();
uberMbTick(now);
uberMbBeat();
uberMbOp(gOpIndex);
if ((uint16_t)(now - startFrame) >= targetFrames) {
break;
}
if (slowOp) {
op();
count++;
} else {
// Exactly UBER_BATCH calls between clock polls (the poll is
// a toolbox call on IIgs; per-op polling dominated fast ops).
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
op();
count += UBER_BATCH;
}
}
/* Capture the actual elapsed frames -- the last iter typically
* overruns the target. Using actual instead of target as the
@ -73,10 +189,15 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
* (where count is forced low while real time stretches well
* past targetFrames). */
endFrame = jlFrameCount();
endMillis = jlMillisElapsed();
*actualFramesOut = (uint16_t)(endFrame - startFrame);
if (*actualFramesOut == 0u) {
*actualFramesOut = 1u; /* defensive: avoid div-by-zero */
}
*millisDeltaOut = endMillis - startMillis;
if (*millisDeltaOut == 0u) {
*millisDeltaOut = 1u; /* defensive: avoid div-by-zero */
}
return count;
}
@ -91,26 +212,37 @@ static unsigned long runForFrames(OpFn op, unsigned int targetFrames, uint16_t *
static void timeOp(const char *name, OpFn op) {
unsigned long iters;
unsigned long opsPerSec;
unsigned long opsPerSecFr;
uint16_t actualFrames;
uint32_t millisDelta;
uint32_t hash;
gCurName = name;
gOpIndex++;
uberMbOp(gOpIndex);
iters = runForFrames(op, UBER_FRAMES, &actualFrames);
iters = runForFrames(op, UBER_FRAMES, &actualFrames, &millisDelta);
if (iters == 0UL) {
jlLogF("UBER: %s: 0 iters (op too slow?)\n", name);
return;
}
/* Divide by ACTUAL elapsed frames, not the target. For sub-frame
* ops actualFrames ~= UBER_FRAMES so the answer is unchanged;
* for ops that overrun (slow jlStagePresent etc.), this stops
* inflating ops/sec. */
opsPerSec = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
hash = jlSurfaceHash(gStage);
/* Report ops/sec from the MILLISECOND clock, not the frame counter.
* jlMillisElapsed is refresh-independent on every port (ST Timer-C
* 200 Hz, Amiga audio-ISR tick, DOS PIT, IIgs GetTick-derived so
* identical to the old formula there), so it does not depend on
* jlFrameHz() matching the emulator's real VBL rate -- the ST used
* to hardcode 50 Hz while Hatari runs ~60, under-reporting ~17%.
* The frame-derived rate is kept as the UBER-CLK cross-check: after
* the jlpFrameHz fix the two agree; a divergence flags a clock bug. */
opsPerSec = (iters * 1000UL) / (unsigned long)millisDelta;
opsPerSecFr = (iters * (unsigned long)jlFrameHz()) / (unsigned long)actualFrames;
hash = jlSurfaceHash(gStage);
jlLogF("UBER: %s: %lu iters / %u frames = %lu ops/sec | hash=%08lX\n",
name, iters, actualFrames, opsPerSec, (unsigned long)hash);
jlLogF("UBER-CLK: %s: frame=%lu millis=%lu ops/sec (%lu ms)\n",
name, opsPerSecFr, opsPerSec, (unsigned long)millisDelta);
}
@ -300,6 +432,7 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
uint8_t byEnd;
uint16_t held;
uberMbPhase(UBER_PHASE_SHOWCASE);
jlPaletteSet(gStage, 0, gShowcasePal);
jlScbSetRange(gStage, 0, 199, 0);
jlSurfaceClear(gStage, SC_BG_COLOR);
@ -416,9 +549,292 @@ static void DRAWSHOWCASE_ATTR drawShowcase(void) {
}
// ----- Non-timed correctness checks (Phase 0 verification harness) -----
//
// Each check draws a deterministic scene and logs the surface hash so
// tools/diff-uber-hashes can compare ports against each other and against
// the frozen golden logs. Several checks intentionally capture the CURRENT
// behavior of known bugs (PERF-AUDIT.md #1, #11, #12, #72): their hash
// lines are EXPECTED to change when the Phase 1 fixes land -- re-golden
// exactly those lines then, nothing else.
//
// PASS/FAIL lines assert invariants that must hold on every port both
// before and after the fixes (round-trips, forced palette color 0, the
// PRNG golden sequence, arena bookkeeping).
static void __attribute__((noinline)) chkHash(const char *name) {
jlLogF("UBER-CHK: %s: hash=%08lX\n", name, (unsigned long)jlSurfaceHash(gStage));
}
static void __attribute__((noinline)) chkPassFail(const char *name, bool pass) {
jlLogF("UBER-CHK: %s: %s\n", name, pass ? "PASS" : "FAIL");
}
// Edge-coordinate draws. The off-surface pixel coords stay modest (within
// ~200 rows of the surface) so the pre-fix dirty-band overwrite (finding #1,
// non-IIgs) lands inside the paired band arrays instead of unrelated
// globals; Phase 1 turns these into true no-ops.
static void __attribute__((noinline)) checkEdgeDraws(void) {
uberMbOp(101);
jlDrawPixel(gStage, -5, 100, 5);
jlDrawPixel(gStage, 330, 100, 5);
jlDrawPixel(gStage, 100, -3, 5);
jlDrawPixel(gStage, 100, 210, 5);
chkHash("edge-pixels");
// w=65535 draws phantom edges today (finding #11); h==2 exercises the
// zero-height interior-edge fills; the third rect clips normally.
jlDrawRect(gStage, 10, 10, 65535u, 100, 5);
jlDrawRect(gStage, 50, 20, 30, 2, 6);
jlDrawRect(gStage, -10, 150, 340, 40, 7);
chkHash("edge-drawRect");
// r=300 clips every span; r=40000 should cover the surface but draws
// nothing today (finding #12).
jlFillCircle(gStage, 160, 100, 300, 4);
jlFillCircle(gStage, 160, 100, 40000u, 9);
jlDrawCircle(gStage, 10, 10, 40000u, 3);
chkHash("edge-circles");
// Far-off-surface endpoints. The H line should fill the whole visible
// row but draws nothing today (finding #72: the H/V fast path clamps
// span to 320 BEFORE clipping, so a huge span anchored off-surface
// clips away entirely). The diagonal clips per-pixel and is correct.
jlDrawLine(gStage, -20000, 50, 20000, 50, 2);
jlDrawLine(gStage, 60, -20000, 60, 20000, 2);
jlDrawLine(gStage, -300, -200, 620, 400, 8);
chkHash("edge-lines");
}
// Clipped sprite draws take the interpreted path on every port (the
// compiled routines require fully-on-surface): the ONLY cross-port
// coverage of that path, which UBER's timed ops (on-surface, compiled)
// never touch. Also asserts the save/draw/restore round-trip restores
// the exact pre-save pixels at a clipped position.
static void __attribute__((noinline)) checkClippedSprites(void) {
uint32_t before;
uberMbOp(102);
jlSpriteDraw(gStage, gSprite, -8, 50);
jlSpriteDraw(gStage, gSprite, 312, 50);
jlSpriteDraw(gStage, gSprite, 150, -8);
jlSpriteDraw(gStage, gSprite, 150, 192);
jlSpriteDraw(gStage, gSprite, -8, -8);
jlSpriteDraw(gStage, gSprite, 400, 100);
chkHash("sprite-clipped");
before = jlSurfaceHash(gStage);
jlSpriteSaveUnder(gStage, gSprite, -8, 100, &gBackup);
jlSpriteDraw(gStage, gSprite, -8, 100);
jlSpriteRestoreUnder(gStage, &gBackup);
chkPassFail("sprite-clip-roundtrip", jlSurfaceHash(gStage) == before);
}
static void __attribute__((noinline)) checkTileMonoFlood(void) {
static jlTileT mono;
uint8_t i;
uberMbOp(103);
for (i = 0; i < TILE_BYTES; i++) {
mono.pixels[i] = (uint8_t)((i & 1) ? 0x0F : 0xF0);
}
jlTilePasteMono(gStage, 10, 10, &mono, 5, 9);
jlTilePasteMono(gStage, 11, 10, &mono, 14, 0);
chkHash("tilePasteMono");
jlDrawRect(gStage, 240, 150, 40, 30, 1);
jlFloodFill(gStage, 250, 160, 12);
chkHash("floodFill");
}
static void __attribute__((noinline)) checkDrawText(void) {
static uint16_t asciiMap[256];
uint16_t i;
uberMbOp(104);
for (i = 0; i < 256u; i++) {
asciiMap[i] = TILE_NO_GLYPH;
}
// 'A' -> tile (5,5), 'B' -> tile (6,6). Seed both glyph tiles here --
// the section-opening surface clear wiped whatever the timed tile ops
// left there.
jlTileFill(gStage, 5, 5, 9);
jlTileFill(gStage, 6, 6, 3);
asciiMap['A'] = (uint16_t)(5u | (5u << 8));
asciiMap['B'] = (uint16_t)(6u | (6u << 8));
jlDrawText(gStage, 2, 20, gStage, asciiMap, "ABBA");
chkHash("drawText");
// Out-of-range start: the entry sanitation loop (Phase 5, #22)
// reproduces the historical absorb-and-wrap semantics bit-for-bit
// (the first glyph is consumed without drawing, the second wraps
// to (0,21) and draws) while guaranteeing jlpTileCopyMasked never
// sees an out-of-range destination. Separate hash line so any
// future semantic change stays isolated.
jlDrawText(gStage, 200, 20, gStage, asciiMap, "AB");
chkHash("drawText-offgrid");
}
static void __attribute__((noinline)) checkPalette(void) {
static const uint16_t conforming[16] = {
0x0ABC, 0x0111, 0x0222, 0x0333, 0x0444, 0x0555, 0x0666, 0x0777,
0x0888, 0x0999, 0x0AAA, 0x0BBB, 0x0CCC, 0x0DDD, 0x0EEE, 0x0123
};
uint16_t readBack[16];
uint16_t i;
bool ok;
// Contract invariants (stable across the Phase 2 #16 change): color 0
// is forced to $000 even when the caller passes nonzero, and
// conforming $0RGB entries 1..15 round-trip exactly.
uberMbOp(105);
jlPaletteSet(gStage, 2, conforming);
jlPaletteGet(gStage, 2, readBack);
ok = (readBack[0] == 0x0000u);
for (i = 1; i < 16u; i++) {
if (readBack[i] != conforming[i]) {
ok = false;
}
}
chkPassFail("palette-roundtrip", ok);
}
static void __attribute__((noinline)) checkRandom(void) {
static const uint32_t expected[4] = {
0x87985AA5UL, 0x155B24A3UL, 0x4820F4C4UL, 0x81B3AC98UL
};
uint16_t i;
bool ok;
// xorshift32 golden sequence from a fixed seed; bit-identical on
// every port and across the Phase 8 (#43) IIgs rewrite.
uberMbOp(106);
jlRandomSeed(0x12345678UL);
ok = true;
for (i = 0; i < 4u; i++) {
if (jlRandom() != expected[i]) {
ok = false;
}
}
if (jlRandomRange(100u) != 43u) {
ok = false;
}
chkPassFail("random-golden", ok);
jlRandomSeed(1u);
}
// Arena churn: create/compile/destroy so a hole opens and is reused, then
// compact and assert the used-byte counter returns to its pre-churn value.
// Covers the codegen allocator paths UBER's single long-lived sprite never
// exercises (findings #5, #34 land here in Phase 1). Also runs the
// owned-tileData path (jlSpriteCreateFromSurface) that finding #31
// re-allocates in Phase 1, and a sprite-bank load if an asset is present.
static void __attribute__((noinline)) checkAllocator(void) {
jlSpriteT *a;
jlSpriteT *b;
jlSpriteT *c;
uint32_t usedBefore;
// Sub-markers 111+ pinpoint the statement that hangs (Phase 1
// stabilization; the coarse marker froze on this check).
uberMbOp(111);
usedBefore = jlSpriteCodegenBytesUsed();
a = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
b = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (a == NULL || b == NULL) {
chkPassFail("arena-churn", false);
return;
}
uberMbOp(112);
(void)jlSpriteCompile(a);
(void)jlSpriteCompile(b);
uberMbOp(113);
jlSpriteDestroy(a);
uberMbOp(114);
c = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (c == NULL) {
jlSpriteDestroy(b);
chkPassFail("arena-churn", false);
return;
}
(void)jlSpriteCompile(c);
uberMbOp(115);
jlSpriteDestroy(b);
jlSpriteDestroy(c);
uberMbOp(116);
jlSpriteCompact();
uberMbOp(117);
chkPassFail("arena-churn", jlSpriteCodegenBytesUsed() == usedBefore);
uberMbOp(118);
a = jlSpriteCreateFromSurface(gStage, 0, 0, 2, 2);
if (a != NULL) {
(void)jlSpriteCompile(a);
uberMbOp(119);
jlSpriteDraw(gStage, a, 280, 20);
jlSpriteDestroy(a);
}
chkPassFail("sprite-from-surface", a != NULL);
uberMbOp(120);
chkHash("sprite-from-surface-draw");
}
// Coverage the primary checkTileMonoFlood golden misses (found in the
// #3/#4 re-analysis): the alternating 0x0F/0xF0 mono pattern only
// selects IIgs #3 combo indices 1 and 2, and jlFloodFill only drives
// the matchEqual branch of the ST #4 plane hooks. Runs LAST so it
// perturbs no earlier cumulative full-stage hash.
static void __attribute__((noinline)) checkTileMonoFloodExtra(void) {
// 0x00 -> combo idx 0 (bg:bg), 0xFF -> idx 3 (fg:fg), 0x0F -> idx 1
// (bg:fg), 0xF0 -> idx 2 (fg:bg): all four opacity pairs in one tile.
static const uint8_t comboBytes[4] = { 0x00u, 0xFFu, 0x0Fu, 0xF0u };
static jlTileT monoAll;
uint8_t i;
uberMbOp(121);
for (i = 0; i < TILE_BYTES; i++) {
monoAll.pixels[i] = comboBytes[i & 3u];
}
jlTilePasteMono(gStage, 10, 12, &monoAll, 5, 9);
jlTilePasteMono(gStage, 11, 12, &monoAll, 14, 0);
chkHash("tilePasteMonoAll");
// Bounded flood: an enclosed color-3 box over a pre-cleared interior
// so the fill stays contained and drives the bounded stop mask
// (pix == boundary || pix == new) plus the planar group-skip across
// several 16px groups. matchColor = boundaryColor = 3, matchEqual = 0.
jlFillRect(gStage, 40, 140, 64, 40, 0);
jlDrawRect(gStage, 44, 144, 52, 32, 3);
jlFloodFillBounded(gStage, 70, 160, 7, 3);
chkHash("floodFillBounded");
}
static void __attribute__((noinline)) runCorrectnessChecks(void) {
uberMbPhase(UBER_PHASE_CHECKS);
jlLogF("UBER-CHK: ----- begin -----\n");
jlSurfaceClear(gStage, 0);
checkEdgeDraws();
checkClippedSprites();
checkTileMonoFlood();
checkDrawText();
checkPalette();
checkRandom();
checkAllocator();
checkTileMonoFloodExtra();
// jlShutdown -> jlInit -> stale-sprite-destroy (#34) needs a teardown
// of the whole library mid-run; it gets a dedicated micro-example in
// Phase 1 rather than risking the benchmark's display state here.
jlLogF("UBER-CHK: ----- end -----\n");
}
// ----- Main -----
static void __attribute__((noinline)) runAllTests(void) {
uberMbPhase(UBER_PHASE_TIMED_RUN);
jlLogF("UBER: ----- begin -----\n");
// Surface / palette / SCB.
@ -525,6 +941,7 @@ static void __attribute__((noinline)) reportElapsed(uint16_t startFrame) {
static bool __attribute__((noinline)) setupSprite(void) {
uint16_t before;
uberMbPhase(UBER_PHASE_SETUP_SPRITE);
buildBallSprite();
gSprite = jlSpriteCreate(gBallTiles, BALL_TILES_X, BALL_TILES_Y);
if (gSprite == NULL) {
@ -578,6 +995,7 @@ int main(void) {
jlShutdown();
return 1;
}
uberMbLayout();
// A simple visible palette so users see SOMETHING during the run.
setupPalette();
@ -591,6 +1009,9 @@ int main(void) {
jlShutdown();
return 1;
}
#ifdef JOEYLIB_PLATFORM_IIGS
*UBER_MB_SPRITEPTR = (uint32_t)gSprite;
#endif
// Audio: only init/shutdown is exercised. Triggering jlAudioPlaySfx
// without first calling jlAudioPlayMod leaves NTP's engine in a
@ -601,6 +1022,7 @@ int main(void) {
// ship a MOD asset, so we skip the SFX exercise. The frame-tick and
// isPlayingMod calls below still get timed (both are no-op fast
// paths on IIgs).
uberMbPhase(UBER_PHASE_AUDIO_INIT);
if (jlAudioInit()) {
jlLogF("UBER: audioInit OK\n");
} else {
@ -615,9 +1037,12 @@ int main(void) {
runAllTests();
runCorrectnessChecks();
reportElapsed(startFrame);
// Done. Green screen + waitForKey.
uberMbPhase(UBER_PHASE_DONE);
jlSurfaceClear(gStage, 2);
jlStagePresent();

View file

@ -7,7 +7,15 @@
#include "types.h"
typedef struct {
uint32_t codegenBytes; // runtime compiled-sprite cache size
// Runtime compiled-sprite cache size. 0 selects a per-platform
// default: 8 KB on IIgs, 32 KB on Amiga/ST/DOS (the 68k/x86
// emitters are ~4-8x less code-dense than the 65816's, so a
// 32x32 sprite can need 8-16 KB there). Size against
// jlSpriteCompiledSize() when tuning. On the IIgs the cache is one
// Memory Manager block allocated with attrNoCross (it may not span
// a 64 KB bank boundary), so values above 65536 are clamped to
// 65536; even a clamped request can fail if no free bank exists.
uint32_t codegenBytes;
uint32_t audioBytes; // reserved; not yet consulted by any audio engine
} jlConfigT;
@ -58,8 +66,11 @@ uint16_t jlFrameCount(void);
// by to convert iters-per-N-frames to ops/sec.
uint16_t jlFrameHz(void);
// Monotonic wall-clock millisecond counter since jlInit. Decoupled
// from frame rate -- two consecutive calls separated by a long
// Monotonic wall-clock millisecond counter since jlInit. On DOS/ST it
// reads a real hardware timer; on IIgs/Amiga the generic implementation
// derives it from the frame counter (frame-granularity steps, and the
// caller must poll at least once per 65535 frames -- ~18 min at 60Hz --
// or wrapped frames are lost). Two consecutive calls separated by a long
// render report the true elapsed time, not the number of VBLs that
// fired. (Per-port millisecond timer sources are documented at
// jlpMillisElapsed in src/core/port.h.)

View file

@ -96,6 +96,7 @@
#define JL_HAS_TILE_COPY // iigsTileCopyInner, macro
#define JL_HAS_TILE_COPY_MASKED // iigsTileCopyMaskedInner, macro
#define JL_HAS_TILE_PASTE // iigsTilePasteInner, macro
#define JL_HAS_TILE_PASTE_MONO // iigsTilePasteMonoInner (stage), macro -> generic else
#define JL_HAS_TILE_SNAP // iigsTileSnapInner, macro
#define JL_HAS_SPRITE_DRAW // chunky -> codegen; planes mirror is noop macro
#define JL_HAS_SPRITE_SAVE // noop macro (codegen path saves)
@ -105,7 +106,8 @@
#define JL_HAS_STAGE_ALLOC_PIXELS // pinned $01:2000 display memory, function
#define JL_HAS_STAGE_FREE_PIXELS // no-op (pinned), function
#define JL_HAS_FRAME_HZ // runtime 50/60 from battery RAM, function
#define JL_HAS_FRAME_COUNT // GetTick word, function
// (frameCount: port.h #defines jlpFrameCount straight to iigsGetTickWord,
// the GetTick asm wrapper -- no function override to register here.)
#define JL_HAS_WAIT_VBL // $C019 VBL poll, function
// audio: every real port implements the full engine
#define JL_HAS_AUDIO_INIT
@ -202,6 +204,8 @@
#define JL_HAS_SPRITE_DRAW // st planar sprite draw, function
#define JL_HAS_SPRITE_SAVE // st planar sprite save, function
#define JL_HAS_SPRITE_RESTORE // st planar sprite restore, function
#define JL_HAS_FLOOD_WALK_PLANES // st planar flood walk-out, function
#define JL_HAS_FLOOD_SCAN_ROW_PLANES // st planar flood scan-row, function
#define JL_HAS_SURFACE_COPY_PLANES // st planar buffer memcpy, function
#define JL_HAS_SAMPLE_PIXEL // st planar nibble decode, function
#define JL_HAS_SURFACE_HASH // st planar hash, function
@ -242,6 +246,8 @@
#elif defined(JOEYLIB_PLATFORM_DOS)
// chunky generics: surface clear, draw pixel/line/circle, fill circle/rect,
// tiles, surface readers, allocation -- DOS overrides only the timing services.
// (The planar dual-write hooks are not linked to generics: every chunky port
// elides them to ((void)0) in port.h's shared chunky block, PERF-AUDIT #42.)
#define JL_HAS_FRAME_HZ // VGA ~70, function
#define JL_HAS_FRAME_COUNT // $3DA retrace edge counter, function
#define JL_HAS_WAIT_VBL // $3DA retrace poll, function

View file

@ -13,12 +13,14 @@
//
// Performance contract: jlSpriteDraw should be at least as fast as the
// IIgs reference (the DESIGN.md spec calls for runtime-compiled draw
// code per CPU). v1 ships with an interpreted fallback that gives
// correct output everywhere; codegen lands per platform after the
// API stabilizes. jlSpritePrewarm is a hint that the application is
// about to draw the sprite repeatedly -- a future codegen-enabled
// build will use it to compile shift variants ahead of the first
// draw. With the interpreter it is a no-op.
// code per CPU). Every port has a runtime emitter (DOS x86, Amiga/ST
// 68k planar, IIgs 65816); jlSpriteCompile compiles the shift/op
// variants into the codegen arena and the draw/save/restore calls
// dispatch to the compiled routines, falling back to the interpreter
// for clipped draws or uncompiled variants. On the IIgs the compiled
// path is temporarily toolchain-gated off (PERF-AUDIT.md #77).
// jlSpritePrewarm compiles ahead of the first draw so the emit cost
// does not land mid-frame.
#ifndef JOEYLIB_SPRITE_H
#define JOEYLIB_SPRITE_H
@ -82,7 +84,7 @@ void jlSpriteDestroy(jlSpriteT *sp);
// Compile the sprite's draw routines into the codegen arena. After
// this returns true, jlSpriteDraw uses the compiled fast path on
// platforms where the emitter is wired (currently x86/DOS). Returns
// platforms where the emitter is wired (all four ports). Returns
// false if the arena is full (caller may run jlSpriteCompact and
// retry), the platform doesn't have a real emitter yet, or the
// sprite has no source tile data.

View file

@ -63,13 +63,22 @@ bool jlSurfaceSaveFile(const jlSurfaceT *src, const char *path);
// identity (no reallocation).
bool jlSurfaceLoadFile(jlSurfaceT *dst, const char *path);
// FNV-1a 32-bit hash of the surface's logical pixel content (color
// indices in row-major order, 0..15 per pixel). Same logical pixels
// produce the same hash on every port regardless of internal storage
// format -- so a hash captured on IIgs (chunky) compares directly
// against the same op's output on Amiga (planar) once the planar
// rewrite is done. Used by the UBER validation harness to
// pixel-compare ports against an IIgs golden reference.
// 32-bit content hash of the surface. NOT FNV-1a: a two-stream 16-bit
// multiplicative hash -- per input byte b, lo = lo * 31 + b and
// hi = hi * 251 + b (mod 2^16), seeded lo = 0xACE1 / hi = 0x1357, and
// combined as (hi << 16) | lo. The hashed domain, in order:
// 1. the packed chunky pixel byte stream (32000 bytes, two 4-bit
// pixels per byte, high nibble = left pixel, row-major),
// 2. the 200 SCB bytes,
// 3. the 256 palette uint16 entries, each folded high byte then
// low byte.
// SURFACE_HASH_MIX_BYTE in src/core/surfaceInternal.h is the single
// source of truth for the per-byte mix; a port override (the planar
// Amiga/ST readers) must reuse it byte-for-byte and feed the identical
// byte sequence, so the same logical pixels produce the same hash on
// every port regardless of internal storage format. Used by the UBER
// validation harness to pixel-compare ports against an IIgs golden
// reference.
uint32_t jlSurfaceHash(const jlSurfaceT *s);
#endif

View file

@ -84,6 +84,18 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette);
// Stream a baked .tbk straight onto a surface's 8x8 block grid: tile i
// lands at block (i % TILE_BLOCKS_PER_ROW, i / TILE_BLOCKS_PER_ROW),
// the same row-major layout the bake tools author against. Use this
// instead of jlTileBankLoad + a jlTileT[] when the bank is too big for
// a caller-side buffer -- a full 1000-tile font sheet needs 32000
// bytes, which no IIgs example can heap-allocate (the clang libc heap
// is a few hundred bytes) or hold in bank-0 BSS. One file pass with a
// small internal scratch; per-target/magic validation and outPalette
// behave exactly like jlTileBankLoad. Blocks beyond the file's tile
// count keep their prior surface content. Returns tiles pasted.
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette);
// Capture the 8x8 block at (bx, by) into the caller's jlTileT. Used
// for save-under style work where allocating a scratch surface would
// be overkill.

View file

@ -42,9 +42,9 @@ SHARED_S := $(wildcard $(SRC_68K)/*.s)
# code out of every Amiga binary.
CORE_C_SRCS_AMIGA := $(filter-out %/audioSfxMix.c, $(CORE_C_SRCS))
# Sprite codegen: the 68k emitters (spriteEmit68k.c shared with ST, plus the
# Amiga-planar spriteEmitPlanar68k.c) live in src/m68k/ and build into obj/68k/;
# only the CPU-independent dispatch stays in src/codegen.
# Sprite codegen: the Amiga-planar emitter (spriteEmitPlanar68k.c) lives in
# src/m68k/ and builds into obj/68k/; only the CPU-independent dispatch stays
# in src/codegen.
CODEGEN_DIR := $(REPO_DIR)/src/codegen
LIB_OBJS := \
@ -54,7 +54,6 @@ LIB_OBJS := \
$(patsubst $(SRC_DIR)/amiga/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/port/ptplayer.o \
$(BUILD)/obj/68k/spriteEmit68k.o \
$(BUILD)/obj/68k/spriteEmitPlanar68k.o \
$(BUILD)/obj/codegen/spriteCompile.o
@ -160,8 +159,8 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -c $< -o $@
# 68k sprite emitters (spriteEmit68k.c, spriteEmitPlanar68k.c) now live in
# src/m68k/; -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
# 68k sprite emitters (spriteEmitPlanar68k.c) live in src/m68k/;
# -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c
@mkdir -p $(dir $@)
$(AMIGA_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@

View file

@ -28,9 +28,9 @@ PORT_C_SRCS := $(wildcard $(SRC_DIR)/atarist/*.c)
PORT_S_SRCS := $(wildcard $(SRC_DIR)/atarist/*.s)
SHARED_S := $(wildcard $(SRC_68K)/*.s)
# Sprite codegen: the 68k emitters (spriteEmit68k.c shared with Amiga, plus the
# ST-interleaved spriteEmitInterleaved68k.c) live in src/m68k/ and build into
# obj/68k/; only the CPU-independent dispatch stays in src/codegen.
# Sprite codegen: the ST-interleaved emitter (spriteEmitInterleaved68k.c)
# lives in src/m68k/ and builds into obj/68k/; only the CPU-independent
# dispatch stays in src/codegen.
CODEGEN_DIR := $(REPO_DIR)/src/codegen
LIB_OBJS := \
@ -39,7 +39,6 @@ LIB_OBJS := \
$(patsubst $(SRC_DIR)/atarist/%.c,$(BUILD)/obj/port/%.o,$(PORT_C_SRCS)) \
$(patsubst $(SRC_DIR)/atarist/%.s,$(BUILD)/obj/port/%.o,$(PORT_S_SRCS)) \
$(patsubst $(SRC_68K)/%.s,$(BUILD)/obj/68k/%.o,$(SHARED_S)) \
$(BUILD)/obj/68k/spriteEmit68k.o \
$(BUILD)/obj/68k/spriteEmitInterleaved68k.o \
$(BUILD)/obj/codegen/spriteCompile.o
@ -127,8 +126,8 @@ $(BUILD)/obj/68k/%.o: $(SRC_68K)/%.s
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -c $< -o $@
# 68k sprite emitters (spriteEmit68k.c, spriteEmitInterleaved68k.c) now live in
# src/m68k/; -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
# 68k sprite emitters (spriteEmitInterleaved68k.c) live in src/m68k/;
# -I$(CODEGEN_DIR) lets them resolve spriteEmitter.h.
$(BUILD)/obj/68k/%.o: $(SRC_68K)/%.c
@mkdir -p $(dir $@)
$(ST_CC) $(CFLAGS) -I$(CODEGEN_DIR) -c $< -o $@

View file

@ -80,8 +80,11 @@ iigs-examples: $(BINDIR)/PATTERN $(BINDIR)/DRAW $(BINDIR)/KEYS $(BINDIR)/JOY \
# Build the JOEYLIB data disk (build/iigs/bin/joey.2mg) that run-iigs-mame.sh /
# run-iigs.sh mount on flop4. Packs the launchable examples as ProDOS S16 apps
# (an 800KB floppy can't hold every example; see make-iigs-disk.sh).
iigs-disk: iigs-examples
BINDIR="$(BINDIR)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
# NTP_BIN rides along so jlpAudioInit's runtime load of "ntpplayer" finds it
# on the volume -- without it every example logs "audioInit failed" (Phase 1
# IIgs stabilization item c: the audio code is wired; the file was missing).
iigs-disk: iigs-examples $(NTP_BIN)
BINDIR="$(BINDIR)" NTP_BIN="$(NTP_BIN)" $(REPO_DIR)/scripts/make-iigs-disk.sh $(BINDIR)/joey.2mg
# Headless visual-verification gate: boot GS/OS under MAME, launch the DRAW
# example off joey.2mg, and assert the SHR framebuffer was actually rendered

252
scripts/bench-iigs.sh Executable file
View file

@ -0,0 +1,252 @@
#!/usr/bin/env bash
# bench-iigs.sh - Headless UBER perf capture for the IIgs (the reference
# port). Until this script existed, PERF.md's IIgs column was carried over
# from old captures because nothing could get joeylog.txt off the emulated
# disk; every "hashes identical x4" gate needs this.
#
# Boots GS/OS under headless MAME with a WORK COPY of joey.2mg, drives the
# Finder to launch UBER (same keystroke timeline as verify-iigs.sh), then
# polls the Super Hi-Res framebuffer for UBER's solid-green "done" screen
# (jlSurfaceClear color 2 -> SHR bytes 0x22). When seen, MAME exits and
# cadius extracts JOEYLOG.TXT from the work image into build/iigs/bin/
# joeylog.txt, where tools/uber-perf-table and diff-uber-hashes expect it.
#
# Usage: scripts/bench-iigs.sh
# MAME_ROMPATH override ROM dir (default ~/.mame/roms)
# BENCH_MAX_FRAMES hard frame cap before giving up (default 36000)
#
# Requires: toolchains/env.sh sourced; build/iigs/bin/joey.2mg with UBER on
# it (run 'make iigs-disk', ensure UBER was not skipped for space).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?bench-iigs.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg
out_log=$repo/build/iigs/bin/joeylog.txt
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
maxFrames="${BENCH_MAX_FRAMES:-36000}"
[ -x "$CADIUS" ] || { echo "bench-iigs: cadius not found at $CADIUS" >&2; exit 2; }
for f in "$sys_disk" "$data_disk"; do
[ -f "$f" ] || { echo "bench-iigs: missing $f (run 'make iigs-disk')" >&2; exit 2; }
done
if ! "$CADIUS" CATALOG "$data_disk" 2>/dev/null | grep -qE '^ UBER +S16'; then
echo "bench-iigs: UBER not on joey.2mg (800KB floppy full? JOEY_DISK_EXAMPLES=\"UBER\" scripts/make-iigs-disk.sh)" >&2
exit 2
fi
work=$(mktemp -d -t joeylib-bench.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
# Finder keystroke timeline is verify-iigs.sh's, launching UBER. From frame
# 4500 on, sample the SHR framebuffer every 60 frames: UBER ends on a solid
# color-2 clear + present, so nearly every byte reads 0x22. The red "running"
# bar and the showcase can never look like that. On green, exit; the log was
# already flushed (uber.c calls jlLogFlush before the key wait).
cat > "$work/bench.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local function greenDone()
-- 64 samples spread across the 32000-byte SHR buffer; solid color-2
-- fill means byte 0x22 everywhere.
local hits = 0
for i = 0, 63 do
if mem:read_u8(0xE12000 + i * 500) == 0x22 then hits = hits + 1 end
end
return hits >= 60
end
-- UBER's progress mailbox in the unused SCB bytes (see uber.c):
-- 9DC8 opIndex, 9DC9 phase, 9DCA-B heartbeat, 9DCC-D last-seen tick,
-- 9DD8-B log-ring base address, 9DDC-F log-ring head-counter address.
local function mailbox()
local op = mem:read_u8(0xE19DC8)
local phase = mem:read_u8(0xE19DC9)
local hb = mem:read_u8(0xE19DCA) | (mem:read_u8(0xE19DCB) << 8)
local tick = mem:read_u8(0xE19DCC) | (mem:read_u8(0xE19DCD) << 8)
return string.format("op=%d phase=%d heartbeat=%d tick=%d", op, phase, hb, tick)
end
local function rd32(addr)
return mem:read_u8(addr) | (mem:read_u8(addr+1) << 8) | (mem:read_u8(addr+2) << 16) | (mem:read_u8(addr+3) << 24)
end
-- Live drain of the IIgs RAM log ring (src/core/debug.c): the app pokes
-- the ring base and head-counter addresses into the mailbox at startup;
-- we read new bytes each frame and emit whole lines as UBERLOG events.
-- The head is a free-running 16-bit total; ring size 8192 must match
-- JOEY_LOG_RING_BYTES.
local RING_SIZE = 8192
local logRingBase = 0
local logHeadAddr = 0
local logRead = 0
local logPartial = ""
local function drainLog()
if logRingBase == 0 then
-- Latch only after the app's magic word appears: the SCB region
-- holds 0x80 fill before uberMbLayout() runs, and a garbage
-- latch reads noise forever.
local magic = mem:read_u8(0xE19DCE) | (mem:read_u8(0xE19DCF) << 8)
if magic ~= 0x4A4C then
return
end
logRingBase = rd32(0xE19DD8) & 0xFFFFFF
logHeadAddr = rd32(0xE19DDC) & 0xFFFFFF
if logRingBase ~= 0 then
io.write(string.format("BENCH-IIGS logring base=%06X head=%06X\n", logRingBase, logHeadAddr))
io.flush()
end
return
end
local head = mem:read_u8(logHeadAddr) | (mem:read_u8(logHeadAddr + 1) << 8)
while logRead ~= head do
local b = mem:read_u8(logRingBase + (logRead % RING_SIZE))
logRead = (logRead + 1) & 0xFFFF
if b == 10 then
io.write("UBERLOG " .. logPartial .. "\n")
logPartial = ""
elseif b >= 32 and b < 127 then
logPartial = logPartial .. string.char(b)
end
end
io.flush()
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("UBER") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
}
local pcs = {}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
if frame >= 4200 and frame % 20 == 0 then
drainLog()
end
if frame >= 4500 and frame % 2000 == 0 then
io.write(string.format("BENCH-IIGS progress frame=%d %s\n", frame, mailbox()))
-- TEMP #84: sprite-geometry diagnostics from sprite.c pokes.
io.write(string.format("BENCH-IIGS spritediag drawW=%d drawH=%d tiles=%04X saveW=%d saveH=%d\n",
mem:read_u8(0xE19DE0) | (mem:read_u8(0xE19DE1) << 8),
mem:read_u8(0xE19DE2) | (mem:read_u8(0xE19DE3) << 8),
mem:read_u8(0xE19DE4) | (mem:read_u8(0xE19DE5) << 8),
mem:read_u8(0xE19DE6) | (mem:read_u8(0xE19DE7) << 8),
mem:read_u8(0xE19DE8) | (mem:read_u8(0xE19DE9) << 8)))
-- TEMP #84: watch the sprite struct bytes in place (16 bytes at
-- the pointer setupSprite poked; printable ASCII shown).
local sptr = rd32(0xE19DEA) & 0xFFFFFF
if sptr ~= 0 then
local bytes = {}
local text = {}
for i = 0, 15 do
local b = mem:read_u8(sptr + i)
bytes[#bytes + 1] = string.format("%02X", b)
if b >= 32 and b < 127 then
text[#text + 1] = string.char(b)
else
text[#text + 1] = "."
end
end
io.write(string.format("BENCH-IIGS spritemem ptr=%06X %s |%s| stagePtr=%08X\n",
sptr, table.concat(bytes, " "), table.concat(text), rd32(0xE19DD0)))
end
io.flush()
end
if frame >= 4500 and frame % 60 == 0 and greenDone() then
io.write(string.format("BENCH-IIGS done frame=%d %s\n", frame, mailbox()))
io.flush()
manager.machine:exit()
end
-- On timeout, sample the PC across 40 frames first so a hang site
-- can be resolved against the link map (ALIGN=0x10000 means the
-- bank-local offset equals the link offset within its segment).
if frame >= $maxFrames and frame < $maxFrames + 40 then
local pc = cpu.state["PC"].value
pcs[string.format("%06X", pc)] = (pcs[string.format("%06X", pc)] or 0) + 1
end
if frame >= $maxFrames + 40 then
io.write(string.format("BENCH-IIGS timeout frame=%d %s\n", frame, mailbox()))
for k, v in pairs(pcs) do
io.write(string.format("BENCH-IIGS pc %s count=%d\n", k, v))
end
io.flush()
manager.machine:exit()
end
end)
LUA
echo "bench-iigs: running UBER headless under MAME (cap $maxFrames frames)..." >&2
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 1200 mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/bench.lua" </dev/null 2>&1) || true
echo "$out" | grep -E '^BENCH-IIGS (progress|pc|spritediag|spritemem|logring)' | sed 's/^/bench-iigs: /' >&2
marker=$(echo "$out" | grep -E '^BENCH-IIGS (done|timeout)' | tail -1)
echo "bench-iigs: $marker" >&2
if echo "$marker" | grep -q timeout; then
echo "bench-iigs: WARNING -- frame cap hit before the green done screen (partial capture likely)" >&2
fi
if [ -z "$marker" ]; then
echo "bench-iigs: FAIL -- MAME produced no marker (boot/launch failed)" >&2
echo "$out" | tail -15 >&2
exit 1
fi
# Primary log source: the RAM-ring drain (UBERLOG lines emitted live by
# the Lua probe -- immune to the FST non-persistence and floppy-write
# stalls of findings #80/#82). Falls back to cadius extraction of the
# on-disk joeylog.txt only if the ring never produced anything.
echo "$out" | grep -E '^UBERLOG ' | sed 's/^UBERLOG //' > "$out_log"
if [ ! -s "$out_log" ]; then
echo "bench-iigs: no ring log captured; falling back to on-disk extraction" >&2
"$CADIUS" EXTRACTFILE "$work/joey.2mg" "/JOEYLIB/JOEYLOG.TXT" "$work/" >/dev/null 2>&1 || true
extracted=$(find "$work" -maxdepth 1 -iname 'joeylog.txt*' | head -1)
if [ -z "$extracted" ]; then
echo "bench-iigs: FAIL -- no ring log AND no JOEYLOG.TXT on the work image" >&2
"$CADIUS" CATALOG "$work/joey.2mg" >&2 || true
exit 1
fi
# cadius may suffix the ProDOS type (#04xxxx); normalize and strip CRs.
tr -d '\r' < "$extracted" > "$out_log"
fi
ops=$(grep -c "ops/sec" "$out_log" || true)
chks=$(grep -c "UBER-CHK" "$out_log" || true)
done_marker=$(grep -c "press any key to exit" "$out_log" || true)
echo "bench-iigs: captured $ops op lines, $chks check lines (complete=$done_marker)" >&2
echo " -> $out_log" >&2
grep "UBER" "$out_log" | sed 's/ | hash=.*//' || true

160
scripts/bench-staxi-title.sh Executable file
View file

@ -0,0 +1,160 @@
#!/usr/bin/env bash
# bench-staxi-title.sh - Space Taxi title-screen logo cadence measurement
# (the PERF-AUDIT #3 acceptance test: the logo repaints via 103
# jlTilePasteMono calls, intended every 4th frame -- stRender.c
# titleAnimateLogo's `& 3u` divider mirroring the C64 original).
#
# Boots GS/OS under headless MAME with a WORK COPY of joey.2mg (must
# contain STAXI + its DATA tree; build with
# JOEY_DISK_EXAMPLES="STAXI" scripts/make-iigs-disk.sh),
# launches STAXI via the standard Finder keystroke timeline, then
# samples 8 known logo tile cells from the SHR framebuffer EVERY frame.
# Each frame the sampled 256 bytes fold into a rolling hash; a hash
# change means the logo visibly changed on screen (the flip-book tile
# differs on every 4-frame step, so a healthy title changes every 4
# frames; a slow repaint stretches the observed period).
#
# Emits "STAXI-CHG frame=N sum=XXXXXX" per change, heartbeats, and a
# final verify-style summary. Post-process the change frames into
# periods with the analysis snippet in the acceptance report.
#
# Usage: scripts/bench-staxi-title.sh [label]
# MAME_ROMPATH override ROM dir (default ~/.mame/roms)
# STAXI_MAX_FRAMES frame cap (default 15000)
# STAXI_MAX_CHANGES stop after this many logo changes (default 80)
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?bench-staxi-title.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
maxFrames="${STAXI_MAX_FRAMES:-15000}"
maxChanges="${STAXI_MAX_CHANGES:-80}"
label="${1:-run}"
for f in "$sys_disk" "$data_disk"; do
[ -f "$f" ] || { echo "bench-staxi-title: missing $f" >&2; exit 2; }
done
if ! "$CADIUS" CATALOG "$data_disk" 2>/dev/null | grep -qE '^ STAXI +S16'; then
echo "bench-staxi-title: STAXI not on joey.2mg (JOEY_DISK_EXAMPLES=\"STAXI\" scripts/make-iigs-disk.sh)" >&2
exit 2
fi
work=$(mktemp -d -t joeylib-staxi.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
cat > "$work/staxi.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
-- 8 logo tile cells (bx,by) chosen from title.dat's 0x84 map: byte
-- offset in SHR = 0xE12000 + (by*8 + r)*160 + bx*4, 4 bytes per row.
-- Every repaint pastes a different flip tile into these cells, so
-- their content changes on every animation step.
local cells = { {6,1},{11,1},{24,1},{29,1},{12,7},{16,8},{12,9},{15,10} }
local function logoSum()
local sum = 0
for _, c in ipairs(cells) do
local base = 0xE12000 + (c[2] * 8) * 160 + c[1] * 4
for r = 0, 7 do
local off = base + r * 160
for b = 0, 3 do
sum = (sum * 33 + mem:read_u8(off + b)) % 16777213
end
end
end
return sum
end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("STAXI") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
}
local prevSum = -1
local nChanges = 0
local function summary(tag)
local distinct = {}
local nonzero = 0
local checksum = 0
for i = 0, 31999 do
local b = mem:read_u8(0xE12000 + i)
if b ~= 0 then nonzero = nonzero + 1 end
distinct[b & 0x0F] = true
distinct[(b >> 4) & 0x0F] = true
checksum = (checksum + b) & 0xFFFFFF
end
local n = 0
for _ in pairs(distinct) do n = n + 1 end
io.write(string.format("STAXI-%s label=$label frame=%d changes=%d distinctNibbles=%d nonZeroBytes=%d checksum=%06X\n",
tag, frame, nChanges, n, nonzero, checksum))
io.flush()
end
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
if frame >= 3800 then
local s = logoSum()
if s ~= prevSum then
io.write(string.format("STAXI-CHG frame=%d sum=%06X\n", frame, s))
io.flush()
prevSum = s
nChanges = nChanges + 1
if nChanges >= $maxChanges then
summary("DONE")
manager.machine:exit()
end
end
end
if frame % 1500 == 0 then
io.write(string.format("STAXI-HEART frame=%d changes=%d\n", frame, nChanges))
io.flush()
end
if frame >= $maxFrames then
summary("TIMEOUT")
manager.machine:exit()
end
end)
LUA
echo "bench-staxi-title[$label]: running STAXI headless under MAME (cap $maxFrames frames, $maxChanges changes)..." >&2
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout "${STAXI_WALL_TIMEOUT:-900}" mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/staxi.lua" </dev/null 2>&1) || true
echo "$out" | grep -E '^STAXI-(CHG|HEART|DONE|TIMEOUT)' > "$repo/build/iigs/bin/staxi-title-$label.log" || true
tail -3 "$repo/build/iigs/bin/staxi-title-$label.log" >&2
lines=$(grep -c '^STAXI-CHG' "$repo/build/iigs/bin/staxi-title-$label.log" || true)
echo "bench-staxi-title[$label]: $lines change events -> build/iigs/bin/staxi-title-$label.log" >&2

26
scripts/check-millis.sh Executable file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env bash
# check-millis.sh - Host-cc harness for the generic millisecond clock
# (jlpGenericMillisElapsed, the IIgs/Amiga path). Compiles genericPort.c
# with mocked jlpFrameCount/jlpFrameHz (tests/host/millisHost.c) and runs
# frame sequences including the 16-bit wrap.
#
# EXPECTED TO FAIL until the Phase 1 #23 fix lands (the clock runs
# backward at the 65536-frame wrap); after that it must PASS and becomes
# the regression gate. See PERF-AUDIT-PLAN.md Phase 0/Phase 1.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-millis.XXXXXX)
trap 'rm -rf "$work"' EXIT
cd "$repo"
# The BLANK platform block already registers JL_HAS_FRAME_COUNT and
# JL_HAS_FRAME_HZ (blank.c normally supplies them); millisHost.c provides
# the mocks here instead, so no extra -D flags are needed.
"$cc" -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Wall \
src/generic/genericPort.c tests/host/millisHost.c \
-o "$work/millisHost"
"$work/millisHost"

128
scripts/check-sfxmix.sh Executable file
View file

@ -0,0 +1,128 @@
#!/usr/bin/env bash
# check-sfxmix.sh - Host-cc differential harness for the shared SFX
# overlay mixer (src/core/audioSfxMix.c), built on the check-blank.sh /
# check-millis.sh pattern. Compiles the CURRENT mixer TU with the host
# cc plus tests/host/sfxMixHost.c and runs a fixed-seed scenario set
# (fixed-buffer + streaming slots, rate up/down, multi-slot, mid-mix
# refill and end-of-stream, amplitude 0/max, >64k read positions).
#
# First run captures tests/goldens/sfxmix-baseline.txt (capture it
# against the CURRENT mixer (the go-forward reference)); later runs diff per scenario:
# default exact mode: FNV-1a hash + raw bytes must match
# the baseline (gate for findings #7 and #33,
# which must be bit-identical).
# JL_SFX_TOLERANCE=1 per-sample mode for finding #6: prints each
# scenario's max-abs-delta and allows a deviation
# of tolerance * slots per output sample (the
# 1-LSB interp deviation stacks across
# simultaneous slots before the clamp).
#
# The harness binary is also run twice and the outputs compared, so a
# nondeterministic harness (or mixer reading uninitialized memory)
# fails loudly instead of producing a flaky baseline.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cc=${CC:-cc}
work=$(mktemp -d -t joeylib-sfxmix.XXXXXX)
trap 'rm -rf "$work"' EXIT
baseline="$repo/tests/goldens/sfxmix-baseline.txt"
cd "$repo"
# BLANK platform: audioSfxMix.c is portable C with no jlp* hooks; the
# stream fill callback is mocked inside sfxMixHost.c.
"$cc" -DJOEYLIB_PLATFORM_BLANK \
-Iinclude -Iinclude/joey -Isrc/core -Wall \
src/core/audioSfxMix.c tests/host/sfxMixHost.c \
-o "$work/sfxMixHost"
"$work/sfxMixHost" > "$work/out1.txt"
"$work/sfxMixHost" > "$work/out2.txt"
if ! cmp -s "$work/out1.txt" "$work/out2.txt"; then
echo "check-sfxmix: FAIL - harness output differs across two runs (nondeterministic)" >&2
exit 1
fi
if [ ! -f "$baseline" ]; then
mkdir -p "$(dirname "$baseline")"
cp "$work/out1.txt" "$baseline"
count=$(grep -c '^name=' "$baseline")
echo "check-sfxmix: baseline captured ($count scenarios) -> tests/goldens/sfxmix-baseline.txt"
exit 0
fi
tolerance="${JL_SFX_TOLERANCE:-}"
if [ -n "$tolerance" ]; then
echo "check-sfxmix: comparing against baseline (per-sample, tolerance ${tolerance} LSB * slots)"
else
echo "check-sfxmix: comparing against baseline (exact hash mode)"
fi
if python3 - "$baseline" "$work/out1.txt" "$tolerance" <<'PY'
import sys
def parse(path):
order = []
scen = {}
name = None
with open(path) as f:
for line in f:
line = line.strip()
if line.startswith("name="):
fields = dict(p.split("=", 1) for p in line.split())
name = fields["name"]
order.append(name)
scen[name] = {"hash": fields["hash"], "slots": int(fields["slots"]), "raw": b""}
elif line.startswith("raw=") and name is not None:
scen[name]["raw"] = bytes.fromhex(line[4:])
return order, scen
baseOrder, base = parse(sys.argv[1])
curOrder, cur = parse(sys.argv[2])
tolerance = int(sys.argv[3]) if sys.argv[3] else None
fail = False
if curOrder != baseOrder:
print("check-sfxmix: scenario list changed vs baseline")
print(" baseline: " + " ".join(baseOrder))
print(" current: " + " ".join(curOrder))
fail = True
for name in baseOrder:
if name not in cur:
continue
b = base[name]
c = cur[name]
label = " %-24s" % (name + ":")
if len(b["raw"]) != len(c["raw"]):
print("%s FAIL (length %d -> %d)" % (label, len(b["raw"]), len(c["raw"])))
fail = True
continue
if tolerance is None:
ok = (b["hash"] == c["hash"]) and (b["raw"] == c["raw"])
if ok:
print("%s PASS (hash %s)" % (label, c["hash"]))
else:
print("%s FAIL (hash %s -> %s)" % (label, b["hash"], c["hash"]))
fail = True
else:
maxDelta = 0
for x, y in zip(b["raw"], c["raw"]):
d = abs(x - y)
if d > maxDelta:
maxDelta = d
allowed = tolerance * b["slots"]
ok = maxDelta <= allowed
print("%s maxAbsDelta=%d (allowed %d) %s" % (label, maxDelta, allowed, "PASS" if ok else "FAIL"))
fail = fail or not ok
sys.exit(1 if fail else 0)
PY
then
echo "check-sfxmix: PASS"
else
echo "check-sfxmix: FAIL (mixer output diverged from tests/goldens/sfxmix-baseline.txt)" >&2
exit 1
fi

143
scripts/diag-staxi-screen.sh Executable file
View file

@ -0,0 +1,143 @@
#!/usr/bin/env bash
# diag-staxi-screen.sh - one-off diagnostic for the STAXI title test:
# boot + launch STAXI, dump the full SHR framebuffer + palettes as hex
# at a target frame, then extract JOEYLOG.TXT from the work disk so the
# game's own init/asset-load log is visible. Outputs land in
# build/iigs/bin/ (staxi-shr.hex, staxi-joeylog.txt).
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
: "${LLVM816_ROOT:?diag-staxi-screen.sh: source toolchains/env.sh first}"
CADIUS="${CADIUS:-$LLVM816_ROOT/tools/cadius/cadius}"
sys_disk=$repo/toolchains/emulators/support/gsos-system.po
data_disk=$repo/build/iigs/bin/joey.2mg
rompath="${MAME_ROMPATH:-$HOME/.mame/roms}"
dumpFrame="${STAXI_DUMP_FRAME:-9000}"
work=$(mktemp -d -t joeylib-staxidiag.XXXXXX)
trap 'rm -rf "$work"' EXIT
cp "$sys_disk" "$work/boot.po"
cp "$data_disk" "$work/joey.2mg"
cat > "$work/diag.lua" <<LUA
local cpu = manager.machine.devices[":maincpu"]
local mem = cpu.spaces["program"]
local nat = manager.machine.natkeyboard
local frame = 0
local idx = 1
local function field(port, name)
local p = manager.machine.ioport.ports[port]
if p == nil then return nil end
return p.fields[name]
end
local key_cmd = field(":macadb:KEY3", "Command / Open Apple")
local function press(f) if f then f:set_value(1) end end
local function release(f) if f then f:set_value(0) end end
local steps = {
{3000, function() nat:post("J") end},
{3120, function() press(key_cmd) end},
{3126, function() nat:post("o") end},
{3180, function() release(key_cmd) end},
{3540, function() nat:post("STAXI") end},
{3660, function() press(key_cmd) end},
{3666, function() nat:post("o") end},
{3720, function() release(key_cmd) end},
}
local pcs = {}
emu.register_frame_done(function()
frame = frame + 1
while idx <= #steps and frame >= steps[idx][1] do
steps[idx][2]()
idx = idx + 1
end
-- Hang localization: sample the PC over 60 frames before the dump,
-- and probe NEWVIDEO (0xE0C029: bit7 = SHR on -> jlpInit ran).
if frame == $dumpFrame - 120 then
io.write(string.format("SHRPROBE newvideo=%02X shadow=%02X\n",
mem:read_u8(0xE0C029), mem:read_u8(0xE0C035)))
io.flush()
end
if frame >= $dumpFrame - 100 and frame < $dumpFrame - 40 then
local pc = cpu.state["PC"].value
local k = string.format("%06X", pc)
pcs[k] = (pcs[k] or 0) + 1
end
if frame == $dumpFrame - 40 then
for k, v in pairs(pcs) do
io.write(string.format("SHRPC %s count=%d\n", k, v))
end
io.flush()
end
if frame == $dumpFrame - 20 then
-- Locate the jlLog RAM ring without UBER's mailbox: bulk-read every
-- RAM bank and emit it as hex; the host greps for the build-stamp
-- text. read_range failures are reported, not swallowed.
local banks = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,0xE0,0xE1}
for _, bank in ipairs(banks) do
local base = bank * 65536
local ok, buf = pcall(function() return mem:read_range(base, base + 65535, 8) end)
if ok and buf ~= nil then
for off = 0, 65535, 256 do
local chunk = string.sub(buf, off + 1, off + 256)
local hex = {}
for i = 1, #chunk do
hex[#hex + 1] = string.format("%02X", string.byte(chunk, i))
end
io.write(string.format("SHRMEM %02X%04X %s\n", bank, off, table.concat(hex)))
end
else
io.write(string.format("SHRMEM-FAIL bank=%02X err=%s\n", bank, tostring(buf)))
end
end
io.flush()
end
if frame == $dumpFrame then
-- SHR pixel dump: 200 rows x 160 bytes as hex lines.
for row = 0, 199 do
local parts = {}
for b = 0, 159 do
parts[#parts + 1] = string.format("%02X", mem:read_u8(0xE12000 + row * 160 + b))
end
io.write("SHRROW " .. table.concat(parts) .. "\n")
end
-- SCBs + palettes.
local scb = {}
for i = 0, 15 do scb[#scb + 1] = string.format("%02X", mem:read_u8(0xE19D00 + i)) end
io.write("SHRSCB " .. table.concat(scb, " ") .. "\n")
local pal = {}
for i = 0, 31 do pal[#pal + 1] = string.format("%02X", mem:read_u8(0xE19E00 + i)) end
io.write("SHRPAL " .. table.concat(pal, " ") .. "\n")
io.flush()
manager.machine:exit()
end
end)
LUA
echo "diag-staxi: dumping SHR at frame $dumpFrame..." >&2
cd "$work"
out=$(QT_QPA_PLATFORM=offscreen SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \
timeout 900 mame apple2gs \
-rompath "$rompath" \
-flop3 "$work/boot.po" -flop4 "$work/joey.2mg" \
-video none -sound none -nothrottle \
-autoboot_script "$work/diag.lua" </dev/null 2>&1) || true
echo "$out" | grep -E '^SHR(PROBE|PC)|^SHRMEM-FAIL' | sed 's/^/diag-staxi: /' >&2
echo "$out" | grep -E '^SHRMEM ' > "$repo/build/iigs/bin/staxi-mem.hex" || true
echo "$out" | grep -E '^SHR(ROW|SCB|PAL)' > "$repo/build/iigs/bin/staxi-shr.hex"
wc -l "$repo/build/iigs/bin/staxi-shr.hex" >&2
# The game flushed its log to the volume root; pull it off the WORK copy.
rm -f "$repo/build/iigs/bin/staxi-joeylog.txt" "$work/JOEYLOG.TXT"* 2>/dev/null || true
"$CADIUS" EXTRACTFILE "$work/joey.2mg" "/JOEYLIB/JOEYLOG.TXT" "$work/" >/dev/null 2>&1 || true
found=$(ls "$work"/JOEYLOG.TXT* 2>/dev/null | head -1)
if [ -n "$found" ]; then
tr -d '\r' < "$found" > "$repo/build/iigs/bin/staxi-joeylog.txt"
echo "diag-staxi: extracted JOEYLOG.TXT:" >&2
else
echo "diag-staxi: no JOEYLOG.TXT on work disk (flush never ran?)" >&2
fi

View file

@ -60,6 +60,56 @@ for name in "${order[@]}"; do
fi
done
# The NTP replayer binary (audio): jlpAudioInit fopen()s "ntpplayer" from
# the volume at runtime (see src/iigs/audio_full.c NTP_PATH). Plain ProDOS
# BIN ($06) file. Skipped with a warning if it doesn't fit or wasn't built.
if [ -n "${NTP_BIN:-}" ] && [ -f "$NTP_BIN" ]; then
ntpSz=$(stat -c%s "$NTP_BIN")
ntpNeed=$(( (ntpSz + 511) / 512 + 2 ))
if [ "$ntpNeed" -le "$(freeBlocks)" ]; then
cp "$NTP_BIN" "$work/NTPPLAYER#060000"
"$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null
rm -f "$work/NTPPLAYER#060000"
added+=(NTPPLAYER)
else
skipped+=(NTPPLAYER)
fi
fi
# Space Taxi runtime data: the game fopen()s a DATA/ tree (levels/*.dat,
# tiles/tbank*.tbk, sprites/sprites.spr, font.tbk -- see stRender.c /
# stLevel.c). Packed only when STAXI made it onto the disk, from the IIgs
# asset bake at examples/spacetaxi/generated/iigs (levels and .spr are
# cross-target; .tbk banks are IIgs-baked -- assetLoad.c validates the
# header target byte). GS/OS ProDOS lookups are case-insensitive, so the
# uppercase ProDOS names match the game's lowercase fopen paths.
staxiData="$repo/examples/spacetaxi/generated/iigs"
if [[ " ${added[*]:-} " == *" STAXI "* ]]; then
if [ -d "$staxiData" ]; then
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA" >/dev/null
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/LEVELS" >/dev/null
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/TILES" >/dev/null
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/DATA/SPRITES" >/dev/null
dataAdded=0
while IFS= read -r f; do
rel=${f#"$staxiData/"}
dir=$(dirname "$rel")
base=$(basename "$rel")
dst="/$VOL/DATA"
if [ "$dir" != "." ]; then
dst="/$VOL/DATA/${dir^^}"
fi
cp "$f" "$work/${base^^}#060000"
"$CADIUS" ADDFILE "$OUT" "$dst" "$work/${base^^}#060000" >/dev/null
rm -f "$work/${base^^}#060000"
dataAdded=$((dataAdded + 1))
done < <(find "$staxiData" -type f | sort)
added+=("DATA[$dataAdded files]")
else
echo " WARNING: STAXI packed without its DATA tree ($staxiData missing)" >&2
fi
fi
echo "iigs-disk: $OUT (volume /$VOL)"
echo " added: ${added[*]:-none}"
if [ ${#skipped[@]} -gt 0 ]; then

View file

@ -24,6 +24,7 @@
#include <string.h>
#include "joey/debug.h"
#include "joey/tile.h"
#include <exec/types.h>
#include <exec/interrupts.h>
@ -88,6 +89,7 @@ typedef struct {
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 amigaBlitterMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height, uint16_t dstModBytes, uint16_t fwm, uint16_t lwm, 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);
@ -173,6 +175,14 @@ static volatile struct Custom *const gCust = &custom;
#define AMIGA_BLT_SIZESHIFT 6
#define AMIGA_BLT_MASK_ALL 0xFFFFu
// Minterms for the masked solid-fill blit (amigaBlitterMaskedFillOne):
// A is the constant $FFFF cut to the rect by the first/last-word masks,
// C and D both address the destination. Set-bits writes D = A OR C
// (force the masked bits on, preserve the rest); clear-bits writes
// D = (NOT A) AND C (force the masked bits off, preserve the rest).
#define AMIGA_BLT_MINTERM_SET 0xFAu // D = A | C
#define AMIGA_BLT_MINTERM_CLEAR 0x0Au // D = ~A & C
// 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
@ -235,6 +245,41 @@ static void amigaBlitterFillOne(uint8_t *dst, uint16_t widthWords, uint16_t heig
}
// Blitter masked solid fill of one plane: `height` rows of `widthWords`
// words starting at `dst`, `dstModBytes` added to the pointer after each
// row ((40 - widthWords*2) for a sub-row rect). Handles partial-pixel
// left/right edges via the hardware first/last-word masks (fwm/lwm), so
// any rect width -- not just whole words -- fills in a single blit per
// plane with no CPU read-modify-write.
//
// A is left as a constant $FFFF in BLTADAT (SRCA off, no A DMA); the
// blitter still runs that constant through the fwm/lwm masking in the A
// data path, which is what cuts the partial edge words. C reads the
// destination so bits outside the mask survive, D writes it back. When
// widthWords == 1 the blitter ANDs fwm and lwm onto the single word, so
// a one-word-wide rect needs no special case. Caller owns the blitter;
// this WaitBlit's before its own register setup so back-to-back
// per-plane calls are safe.
static void amigaBlitterMaskedFillOne(uint8_t *dst, uint16_t widthWords, uint16_t height,
uint16_t dstModBytes, uint16_t fwm, uint16_t lwm,
bool setBits) {
uint16_t minterm;
minterm = setBits ? AMIGA_BLT_MINTERM_SET : AMIGA_BLT_MINTERM_CLEAR;
WaitBlit();
gCust->bltcon0 = (uint16_t)(SRCC | DEST | minterm);
gCust->bltcon1 = 0u;
gCust->bltadat = AMIGA_BLT_MASK_ALL;
gCust->bltafwm = fwm;
gCust->bltalwm = lwm;
gCust->bltcmod = dstModBytes;
gCust->bltdmod = dstModBytes;
gCust->bltcpt = (APTR)dst;
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.
@ -749,7 +794,7 @@ void jlpPresent(const jlSurfaceT *src) {
if (!gPrevAnyDirty) {
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (gStageMinWord[y] != STAGE_DIRTY_CLEAN_MIN) {
if (STAGE_DIRTY_ROW_TOUCHED(y)) {
anyDirty = true;
break;
}
@ -842,7 +887,7 @@ void jlpPresent(const jlSurfaceT *src) {
for (i = 0; i < SURFACE_HEIGHT; i++) {
gPrevStageMinWord[i] = gStageMinWord[i];
gPrevStageMaxWord[i] = gStageMaxWord[i];
if (gStageMinWord[i] != STAGE_DIRTY_CLEAN_MIN) {
if (STAGE_DIRTY_ROW_TOUCHED(i)) {
anyDirty = true;
}
}
@ -1022,39 +1067,30 @@ void jlpSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
}
// Phase 3 planar dual-write for jlFillRect: writes the four off-screen
// shadow plane buffers alongside the chunky shadow. Caller (cross-
// platform jlFillRect) has already done the chunky write via
// the chunky generic. The shadow planes are off-
// screen so this is invisible until jlStagePresent.
// Planar jlFillRect: fills the four off-screen shadow plane buffers via
// the blitter. Caller (cross-platform jlFillRect) has already clipped
// the rect to the surface, so x/y/w/h are on-surface. The shadow planes
// are off-screen, so this is invisible until jlStagePresent.
//
// Layout reminder (see docs/amiga_planar.md): each plane byte covers
// 8 horizontal pixels; bit 7 = leftmost pixel of that byte. So a
// rect clipped to [x, x+w) needs:
// * bytes [x/8 .. (x+w-1)/8] in each plane row
// * leading partial byte if (x % 8) != 0 (only bits [7-x%8 .. 0]
// get touched -- the upper bits stay)
// * trailing partial byte if ((x+w-1) % 8) != 7 (only bits [7 ..
// 7-(x+w-1)%8] get touched)
// * single-byte case (byteFirst == byteLast) collapses to one
// read-modify-write with a combined mask.
// For each plane, the bit value at every pixel in the rect is
// constant: (colorIndex >> plane) & 1. Set bit -> OR with mask;
// clear bit -> AND with ~mask.
// Layout reminder (see docs/amiga_planar.md): each plane word covers 16
// horizontal pixels; bit 15 = leftmost pixel of that word. The blitter
// fills whole words and cuts the partial-pixel left/right edges with its
// hardware first/last-word masks (fwm/lwm), so a single DEST(+C) blit per
// plane handles any width -- no per-pixel-edge CPU read-modify-write.
// For each plane the bit value at every rect pixel is constant:
// (colorIndex >> plane) & 1; set bits OR the mask into the plane, clear
// bits AND its complement (see the two masked-fill minterms).
void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint8_t colorIndex) {
AmigaPlanarT *pd;
uint16_t byteFirst;
uint16_t byteLast;
uint16_t numMid;
uint8_t leftMask;
uint8_t rightMask;
uint16_t wordFirst;
uint16_t wordLast;
uint16_t widthWords;
uint16_t fwm;
uint16_t lwm;
uint16_t dstMod;
uint16_t plane;
int16_t row;
int16_t yEnd;
uint8_t bitVal;
uint8_t fullByte;
uint8_t *p;
uint8_t *planeBase;
uint8_t *dst;
if (s == NULL || w == 0u || h == 0u) {
return;
@ -1065,243 +1101,62 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
return;
}
/* Variable shifts on 68000 cost 8 cyc per bit shifted -- for shifts
* by up to 7 that's ~30 cyc per mask, ~60 cyc both. LUTs compile
* to a single byte load. jlFillCircle r=40 fills 160 spans, so
* this saves ~160*60=9600 cyc per jlFillCircle. */
static const uint8_t kLeftMaskLut[8] = {
0xFFu, 0x7Fu, 0x3Fu, 0x1Fu, 0x0Fu, 0x07u, 0x03u, 0x01u
};
static const uint8_t kRightMaskLut[8] = {
0x80u, 0xC0u, 0xE0u, 0xF0u, 0xF8u, 0xFCu, 0xFEu, 0xFFu
};
// Word-granular rect geometry. wordFirst/wordLast index the 16-pixel
// plane words the rect touches; fwm/lwm are the blitter first/last-
// word masks that cut the partial-pixel edges (pixel 0 is bit 15 of
// its word, so the left mask drops the high bits of the first word
// and the right mask drops the low bits of the last). One blit per
// plane then fills any width, so there is no byte-aligned or
// per-pixel-edge special case to follow.
wordFirst = (uint16_t)((uint16_t)x >> 4);
wordLast = (uint16_t)(((uint16_t)x + w - 1u) >> 4);
widthWords = (uint16_t)(wordLast - wordFirst + 1u);
fwm = (uint16_t)(AMIGA_BLT_MASK_ALL >> ((uint16_t)x & 15u));
lwm = (uint16_t)(AMIGA_BLT_MASK_ALL << (15u - (((uint16_t)x + w - 1u) & 15u)));
dstMod = (uint16_t)(AMIGA_BYTES_PER_ROW - (widthWords << 1));
byteFirst = (uint16_t)((uint16_t)x >> 3);
byteLast = (uint16_t)(((uint16_t)x + w - 1u) >> 3);
leftMask = kLeftMaskLut [(uint16_t)x & 7u];
rightMask = kRightMaskLut[((uint16_t)x + w - 1u) & 7u];
yEnd = y + (int16_t)h;
/* Full-row fast path: no partial-byte RMW on either edge, so each
* 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)) {
/* Full-width fast path: the rect spans every plane word with both
* edge masks all-ones, so each plane row is a whole 20-word store
* with no destination read. jlFillRect 320x200 (full-screen wipes)
* is the dominant case and lands here. 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 $0000 (minterm $00). One OwnBlitter for all four planes;
* WaitBlit before DisownBlitter so the planes are complete before
* the present copy or any later CPU draw reads them. */
if (wordFirst == 0u && wordLast == (uint16_t)(AMIGA_BLT_WORDS_PER_ROW - 1u)
&& fwm == AMIGA_BLT_MASK_ALL && lwm == AMIGA_BLT_MASK_ALL) {
uint8_t *rowBase;
OwnBlitter();
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
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);
amigaBlitterFillOne(rowBase, AMIGA_BLT_WORDS_PER_ROW, (uint16_t)h, 0u, bitVal != 0u);
}
WaitBlit();
DisownBlitter();
return;
}
/* Byte-aligned partial-row fast path: when both edges are full
* 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
* 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);
uint16_t midBytes;
uint16_t midLongs;
uint16_t tailBytes;
if (alignBytes > nBytes) {
alignBytes = (uint8_t)nBytes;
}
midBytes = (uint16_t)(nBytes - alignBytes);
midLongs = (uint16_t)(midBytes >> 2);
tailBytes = (uint16_t)(midBytes & 3u);
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
fullByte = bitVal ? 0xFFu : 0x00u;
uint32_t fillLong = (uint32_t)fullByte * 0x01010101UL;
planeBase = pd->planes[plane];
uint8_t *rowP = planeBase
+ (uint16_t)y * AMIGA_BYTES_PER_ROW
+ byteFirst;
for (row = y; row < yEnd; row++) {
uint8_t *pp = rowP;
uint8_t ab = alignBytes;
uint16_t ml = midLongs;
uint16_t tb = tailBytes;
while (ab > 0u) {
*pp++ = fullByte;
ab--;
}
if (ml > 0u) {
uint32_t *p32 = (uint32_t *)pp;
do {
*p32++ = fillLong;
} while (--ml);
pp = (uint8_t *)p32;
}
while (tb > 0u) {
*pp++ = fullByte;
tb--;
}
rowP += AMIGA_BYTES_PER_ROW;
}
}
return;
}
/* Hoist bitVal-dependent setup outside the row loop. Two
* specialized per-plane paths (OR for set, AND-NOT for clear)
* give gcc -O2 simple branchless inner loops. Row pointer is
* advanced by += AMIGA_BYTES_PER_ROW instead of recomputed per
* row -- saves the per-iter multiply.
*
* Single-byte case (byteFirst == byteLast) uses the combined
* mask; multi-byte case uses leading + middle long-fill +
* trailing. The middle long-fill path is identical to the
* earlier code (align, long stores, drain) but lifted into the
* per-plane scope so the constants are loop-invariant. */
{
uint8_t notLeftMask = (uint8_t)~leftMask;
uint8_t notRightMask = (uint8_t)~rightMask;
if (byteFirst == byteLast) {
uint8_t mask = (uint8_t)(leftMask & rightMask);
uint8_t notMask = (uint8_t)~mask;
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
planeBase = pd->planes[plane];
p = planeBase + (uint16_t)y * AMIGA_BYTES_PER_ROW + byteFirst;
if ((colorIndex >> plane) & 1u) {
/* OR path */
for (row = y; row < yEnd; row++) {
*p = (uint8_t)(*p | mask);
p += AMIGA_BYTES_PER_ROW;
}
} else {
/* AND-NOT path */
for (row = y; row < yEnd; row++) {
*p = (uint8_t)(*p & notMask);
p += AMIGA_BYTES_PER_ROW;
}
}
}
return;
}
numMid = (uint16_t)(byteLast - byteFirst - 1u);
/* Hoist middle-region alignment outside both per-plane and
* per-row loops. midStart = planeBase + y*40 + byteFirst + 1.
* 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;
uint16_t midLongs;
uint16_t midTail;
if (midAlignBytes > numMid) {
midAlignBytes = (uint8_t)numMid;
}
midRem = (uint16_t)(numMid - midAlignBytes);
midLongs = (uint16_t)(midRem >> 2);
midTail = (uint16_t)(midRem & 3u);
/* Small-numMid byte-only path: when there are no full longs to
* fill, the unified long-fill machinery's runtime ml/tb checks
* cost more than they save. UBER jlFillRect 16x16 (numMid=1)
* lands here. */
if (midLongs == 0u) {
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
fullByte = bitVal ? 0xFFu : 0x00u;
uint8_t leadBits = (uint8_t)(bitVal ? leftMask : notLeftMask);
uint8_t trailBits = (uint8_t)(bitVal ? rightMask : notRightMask);
planeBase = pd->planes[plane];
p = planeBase + (uint16_t)y * AMIGA_BYTES_PER_ROW + byteFirst;
if (bitVal) {
for (row = y; row < yEnd; row++) {
uint8_t *pp = p;
uint16_t m = numMid;
*pp = (uint8_t)(*pp | leadBits); pp++;
while (m > 0u) { *pp++ = fullByte; m--; }
*pp = (uint8_t)(*pp | trailBits);
p += AMIGA_BYTES_PER_ROW;
}
} else {
for (row = y; row < yEnd; row++) {
uint8_t *pp = p;
uint16_t m = numMid;
*pp = (uint8_t)(*pp & leadBits); pp++;
while (m > 0u) { *pp++ = fullByte; m--; }
*pp = (uint8_t)(*pp & trailBits);
p += AMIGA_BYTES_PER_ROW;
}
}
}
return;
}
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
fullByte = bitVal ? 0xFFu : 0x00u;
uint32_t fillLong = (uint32_t)fullByte * 0x01010101UL;
uint8_t leadBits = (uint8_t)(bitVal ? leftMask : notLeftMask);
uint8_t trailBits= (uint8_t)(bitVal ? rightMask : notRightMask);
planeBase = pd->planes[plane];
p = planeBase + (uint16_t)y * AMIGA_BYTES_PER_ROW + byteFirst;
if (bitVal) {
for (row = y; row < yEnd; row++) {
uint8_t *pp = p;
uint8_t ab = midAlignBytes;
uint16_t ml = midLongs;
uint16_t tb = midTail;
*pp = (uint8_t)(*pp | leadBits); pp++;
while (ab > 0u) { *pp++ = fullByte; ab--; }
{
uint32_t *p32 = (uint32_t *)pp;
do { *p32++ = fillLong; } while (--ml);
pp = (uint8_t *)p32;
}
while (tb > 0u) { *pp++ = fullByte; tb--; }
*pp = (uint8_t)(*pp | trailBits);
p += AMIGA_BYTES_PER_ROW;
}
} else {
for (row = y; row < yEnd; row++) {
uint8_t *pp = p;
uint8_t ab = midAlignBytes;
uint16_t ml = midLongs;
uint16_t tb = midTail;
*pp = (uint8_t)(*pp & leadBits); pp++;
while (ab > 0u) { *pp++ = fullByte; ab--; }
{
uint32_t *p32 = (uint32_t *)pp;
do { *p32++ = fillLong; } while (--ml);
pp = (uint8_t *)p32;
}
while (tb > 0u) { *pp++ = fullByte; tb--; }
*pp = (uint8_t)(*pp & trailBits);
p += AMIGA_BYTES_PER_ROW;
}
}
}
/* General masked fill: A is the constant $FFFF cut to the rect by
* the fwm/lwm word masks, C reads and D writes the destination so
* bits outside the mask survive. One blit per plane covers any
* width, partial pixel edges included -- the blitter does the edge
* masking in hardware, replacing the old per-pixel-edge CPU walkers.
* UBER jlFillRect 16x16 / 80x80 and every jlFillCircle span land
* here. widthWords == 1 needs no special case: the blitter ANDs both
* edge masks onto the single word. */
OwnBlitter();
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
bitVal = (uint8_t)((colorIndex >> plane) & 1u);
dst = pd->planes[plane]
+ (uint16_t)y * AMIGA_BYTES_PER_ROW
+ (uint16_t)(wordFirst << 1);
amigaBlitterMaskedFillOne(dst, widthWords, (uint16_t)h, dstMod, fwm, lwm, bitVal != 0u);
}
WaitBlit();
DisownBlitter();
}
@ -1805,18 +1660,22 @@ void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunky
}
/* Planar monochrome paste. monoTile is plane-major (matches the
* Amiga jlpTileSnap output): bytes [0..7] = plane 0 = shape
* bits, bytes [8..15] = plane 1 (zero for monochrome tiles), etc.
* Each pixel's output color: fgColor where shape bit is 1, bgColor
* where 0. Output is written directly to the dst planar buffers via
* the per-plane formula
* out_plane_k = (shape & mask_fg_k) | (~shape & mask_bg_k)
* where mask_X_k = $FF if (X & (1<<k)) else $00. */
// Planar monochrome paste. monoTile follows the cross-port mono
// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky
// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two
// pixels per byte. A pixel renders fgColor when its source nibble is
// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each
// row's 4 source bytes fold into an 8-bit shape mask (bit 7 =
// leftmost pixel, matching the planar bit order), then each plane k
// writes
// outPlaneK = (shape & maskFgK) | (~shape & maskBgK)
// where maskXK = $FF if (X & (1 << k)) else $00.
void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
AmigaPlanarT *pd;
uint8_t shape[TILE_PIXELS_PER_SIDE];
uint8_t plane;
uint8_t row;
uint8_t col;
uint16_t rowBase;
pd = (AmigaPlanarT *)dst->portData;
@ -1824,17 +1683,29 @@ void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *mo
jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor);
return;
}
// Fold each chunky nibble-pair row into one 1bpp shape byte.
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
uint8_t bits = 0u;
for (col = 0; col < TILE_BYTES_PER_ROW; col++) {
uint8_t srcByte = *monoTile++;
bits = (uint8_t)(bits << 2);
if (srcByte & 0xF0u) {
bits = (uint8_t)(bits | 0x02u);
}
if (srcByte & 0x0Fu) {
bits = (uint8_t)(bits | 0x01u);
}
}
shape[row] = bits;
}
rowBase = (uint16_t)((uint16_t)by * 8u) * AMIGA_BYTES_PER_ROW + bx;
/* monoTile[0..7] is the shape (plane 0 of the source tile). */
const uint8_t *shape = monoTile;
for (plane = 0; plane < AMIGA_BITPLANES; plane++) {
uint8_t mask_fg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u);
uint8_t mask_bg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u);
uint8_t *p = pd->planes[plane] + rowBase;
uint8_t maskFg = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u);
uint8_t maskBg = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u);
uint8_t *p = pd->planes[plane] + rowBase;
for (row = 0; row < 8u; row++) {
uint8_t s = shape[row];
p[row * AMIGA_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & mask_fg) |
(uint8_t)((uint8_t)(~s) & mask_bg));
p[row * AMIGA_BYTES_PER_ROW] = (uint8_t)((uint8_t)(s & maskFg) | (uint8_t)((uint8_t)(~s) & maskBg));
}
}
}

View file

@ -1,16 +1,17 @@
| Atari ST word-interleaved planar jlFillCircle -- 68000 hand-rolled.
|
| Bresenham midpoint circle, 4 horizontal spans per Bresenham iter,
| paired by shared x-range so leftMask/rightMask are computed once
| per pair:
| Pair A: x in [cx-bx, cx+bx], rows y = cy+by, cy-by
| Pair B: x in [cx-by, cx+by], rows y = cy+bx, cy-bx
| Exact-disk span walk, bit-identical to the chunky reference span
| loop in src/core/draw.c jlFillCircle: for each y offset in [0, r]
| the largest x with x*x + y*y <= r*r is found incrementally (no
| in-loop multiply), then the span (cx-x, cx+x) is drawn at rows
| cy+y and cy-y. leftMask/rightMask are computed once per row pair.
|
| Caller MUST guarantee the bounding box (cx-r, cy-r) (cx+r, cy+r)
| is fully on-surface. Off-surface circles fall back to the C walker.
| is fully on-surface (so r <= 99 and r*r <= 9801 fits a word).
| Off-surface circles fall back to the C walker.
|
| Phase 10 final: 16-way color dispatch at the OUTER loop. Each color
| variant has its own Bresenham body where SPAN_BODY inlines a hard-
| variant has its own span-walk body where SPAN_BODY inlines a hard-
| coded 4-plane mask RMW (no btst, no bsr/rts). Saves ~120 cyc per
| applyMask call (was ~180 via bsr applyMask with runtime btst on d7).
|
@ -30,7 +31,7 @@
.equ SP_FC_CX, SP_FC_OFF + 4 + 2
.equ SP_FC_CY, SP_FC_OFF + 8 + 2
.equ SP_FC_R, SP_FC_OFF + 12 + 2
.equ SP_FC_COLOR, SP_FC_OFF + 20 + 3
.equ SP_FC_COLOR, SP_FC_OFF + 16 + 3
| ---- COMPUTE_PAIR_MASKS macro -----------------------------------
@ -143,63 +144,59 @@
.endm
| ---- CO_BODY macro: per-color full Bresenham loop body ----------
| ---- CO_BODY macro: per-color exact-disk span loop body ---------
|
| Mirrors src/core/draw.c jlFillCircle's on-surface span loop:
| for y = 0 .. r:
| while xx > r2 - yy: xx -= 2x - 1; x--
| span (cx-x, cx+x) at rows cy+y and cy-y
| yy += 2y + 1
| The y == 0 pass writes row cy twice with identical bytes, which is
| cheaper than a per-color skip branch.
|
| Regs: d2 = x, d3 = y, d4 = xx, d7 = yy, a0 = r2 (loop-invariant).
| d0/d1/a4 are macro scratch; d5/d6 hold the full-group longs.
.macro CO_BODY color
.Lfc_loop_\color:
cmp.w %d3,%d2
bcs.w .Lfc_done
| walk x down: while xx > r2 - yy { xx -= 2x - 1; x-- }
move.w %a0,%d0
sub.w %d7,%d0 | d0 = xxLimit = r2 - yy
.Lfc_walk_\color:
cmp.w %d0,%d4
bls.s .Lfc_walkDone_\color | xx <= xxLimit
sub.w %d2,%d4
sub.w %d2,%d4
addq.w #1,%d4 | xx -= 2x - 1
subq.w #1,%d2 | x--
bra.s .Lfc_walk_\color
.Lfc_walkDone_\color:
| --- Pair A: x range = (cx - bx, cx + bx)
| masks for x range (cx - x, cx + x), shared by both rows
move.w SP_FC_CX(%sp),%d0
move.w %d0,%d1
sub.w %d2,%d0
add.w %d2,%d1
COMPUTE_PAIR_MASKS
| Span A1: y = cy + by
| span at row cy + y
move.w SP_FC_CY(%sp),%d0
add.w %d3,%d0
SPAN_BODY \color
| Span A2: y = cy - by
| span at row cy - y
move.w SP_FC_CY(%sp),%d0
sub.w %d3,%d0
SPAN_BODY \color
| --- Pair B: x range = (cx - by, cx + by)
move.w SP_FC_CX(%sp),%d0
move.w %d0,%d1
sub.w %d3,%d0
add.w %d3,%d1
COMPUTE_PAIR_MASKS
| Span B1: y = cy + bx
move.w SP_FC_CY(%sp),%d0
add.w %d2,%d0
SPAN_BODY \color
| Span B2: y = cy - bx
move.w SP_FC_CY(%sp),%d0
sub.w %d2,%d0
SPAN_BODY \color
| --- Bresenham step
| yy += 2y + 1; y++; loop while y <= r
add.w %d3,%d7
add.w %d3,%d7
addq.w #1,%d7
addq.w #1,%d3
tst.w %d4
bgt.s .Lfc_decBx_\color
add.w %d3,%d4
add.w %d3,%d4
addq.w #1,%d4
bra.w .Lfc_loop_\color
.Lfc_decBx_\color:
subq.w #1,%d2
add.w %d3,%d4
add.w %d3,%d4
sub.w %d2,%d4
sub.w %d2,%d4
addq.w #1,%d4
bra.w .Lfc_loop_\color
cmp.w SP_FC_R(%sp),%d3
bls.w .Lfc_loop_\color
bra.w .Lfc_done
.endm
@ -243,11 +240,14 @@ _surface68kStFillCircle:
ori.l #0xFFFF0000,%d6
.Lfc_hi2:
| Bresenham init: bx=r, by=0, err=1-bx
| Exact-disk init: x = r, y = 0, xx = r2, yy = 0.
| r <= 99 (caller's on-surface guarantee), so r2 fits a
| word and mulu runs once, outside the loop.
move.w SP_FC_R(%sp),%d2
moveq #0,%d3
moveq #1,%d4
sub.w %d2,%d4
move.w %d2,%d4
mulu.w %d2,%d4 | d4 = xx = r * r
move.w %d4,%a0 | a0 = r2 (loop-invariant)
| Dispatch on color (low 4 bits) -> 16 specialized loops.
| Use a4 (gets overwritten in SPAN_BODY's first lea) as
@ -256,6 +256,7 @@ _surface68kStFillCircle:
move.w %d7,%d0
add.w %d0,%d0
add.w %d0,%d0 | * 4 for bra.w table
moveq #0,%d7 | d7 = yy (color consumed)
lea .Lfc_table(%pc),%a4
jmp 0(%a4,%d0.w)

View file

@ -77,6 +77,17 @@ typedef struct {
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
// Shifter resolution ($FF8260, low 2 bits: 0=low 1=med 2=high/mono) and
// sync-mode ($FF820A, bit 1: set=50 Hz PAL, clear=60 Hz NTSC) registers,
// read by jlpFrameHz to report the true refresh instead of assuming PAL.
#define ST_SHIFTER_RES ((volatile uint8_t *)0xFFFF8260L)
#define ST_SYNC_MODE ((volatile uint8_t *)0xFFFF820AL)
#define ST_RES_MONO 0x02u // $FF8260 low-2-bits value for high res (SM124)
#define ST_SYNC_50HZ_BIT 0x02u // $FF820A bit 1: set = 50 Hz
#define ST_HZ_MONO 71u // SM124 mono refresh
#define ST_HZ_PAL 50u
#define ST_HZ_NTSC 60u
// 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,
@ -146,6 +157,38 @@ static inline __attribute__((always_inline)) uint8_t stPlanarGetPixel(const StPl
if (pw[3] & bitMask) { c = (uint8_t)(c | 8u); }
return c;
}
// Flood-fill group decoder (PERF-AUDIT #4). For one 16-pixel group (4
// interleaved plane words) build the 16-bit mask whose bit
// (15 - (x & 15)) is set exactly where pixel x equals color: invert
// each plane word whose color bit is 0, then AND the four words. Four
// word reads decode 16 pixels, vs 4 reads PER pixel through
// stPlanarGetPixel.
static inline __attribute__((always_inline)) uint16_t stGroupEqBits(const uint16_t *gp, uint8_t color) {
uint16_t p0 = gp[0];
uint16_t p1 = gp[1];
uint16_t p2 = gp[2];
uint16_t p3 = gp[3];
if ((color & 1u) == 0u) { p0 = (uint16_t)~p0; }
if ((color & 2u) == 0u) { p1 = (uint16_t)~p1; }
if ((color & 4u) == 0u) { p2 = (uint16_t)~p2; }
if ((color & 8u) == 0u) { p3 = (uint16_t)~p3; }
return (uint16_t)(p0 & p1 & p2 & p3);
}
// Flood walk-out stop mask for one group: bit set where the walk must
// STOP (pixel is not fillable). matchEqual (jlFloodFill): stop where
// pix != matchColor. Bounded (jlFloodFillBounded): stop where
// pix == matchColor (boundary) or pix == newColor (already filled).
static inline __attribute__((always_inline)) uint16_t stFloodStopBits(const uint8_t *rowBase, uint16_t group, uint8_t matchColor, uint8_t newColor, bool matchEqual) {
const uint16_t *gp = (const uint16_t *)(rowBase + group * ST_BYTES_PER_GROUP);
if (matchEqual) {
return (uint16_t)~stGroupEqBits(gp, matchColor);
}
return (uint16_t)(stGroupEqBits(gp, matchColor) | stGroupEqBits(gp, newColor));
}
static uint16_t quantizeColorToSt(uint16_t orgb);
static void flattenScbPalettes(const jlSurfaceT *src);
static void setShifterVideoBase(const uint8_t *page);
@ -662,7 +705,7 @@ void jlpPresent(const jlSurfaceT *src) {
if (!gPrevAnyDirty) {
bool anyDirty = false;
for (y = 0; y < SURFACE_HEIGHT; y++) {
if (gStageMinWord[y] <= gStageMaxWord[y]) {
if (!STAGE_DIRTY_ROW_CLEAN(y)) {
anyDirty = true;
break;
}
@ -805,7 +848,7 @@ void jlpPresent(const jlSurfaceT *src) {
for (y = 0; y < SURFACE_HEIGHT; y++) {
gPrevStageMinWord[y] = gStageMinWord[y];
gPrevStageMaxWord[y] = gStageMaxWord[y];
if (gStageMinWord[y] <= gStageMaxWord[y]) {
if (!STAGE_DIRTY_ROW_CLEAN(y)) {
anyDirty = true;
}
}
@ -839,10 +882,20 @@ uint16_t jlpFrameCount(void) {
uint16_t jlpFrameHz(void) {
/* PAL ST is 50 Hz; NTSC ST and SM124 mono are ~60 / ~70. We
* report 50 as the baseline -- close enough for ops/sec scaling,
* and the actual frame rate is still observable via iter counts. */
return 50u;
/* Report the REAL refresh, not a hardcoded 50. The old assumption
* silently under-reported ops/sec ~17% under Hatari (which runs the
* ST at 60 Hz) and would be wrong on any NTSC/mono machine. The
* shifter resolution register selects mono vs color; for color the
* sync-mode register's 50/60 bit is authoritative. The program holds
* supervisor mode for its lifetime (jlpInit Super(0L)), so these
* $FFFF82xx I/O reads are legal. */
if ((*ST_SHIFTER_RES & 0x03u) == ST_RES_MONO) {
return ST_HZ_MONO;
}
if (*ST_SYNC_MODE & ST_SYNC_50HZ_BIT) {
return ST_HZ_PAL;
}
return ST_HZ_NTSC;
}
@ -1055,23 +1108,32 @@ static void stPlanarFillSpan(StPlanarT *pd, int16_t x0, int16_t x1, int16_t y, u
}
// Exact-disk span walk, same row coverage as src/core/draw.c
// jlFillCircle and the asm surface68kStFillCircle: for each y offset
// the largest x with x*x + y*y <= r*r, tracked incrementally. Defensive
// fallback only -- draw.c routes every off-surface circle through its
// own clipped span loop before jlpFillCircle is ever called, so keep r
// modest (< SURFACE_HEIGHT) if a future caller ever lands here.
static void stPlanarCircleFill(StPlanarT *pd, int16_t cx, int16_t cy, uint16_t r, uint8_t color) {
int16_t bx = (int16_t)r;
int16_t by = 0;
int16_t err = (int16_t)(1 - bx);
uint32_t r2 = (uint32_t)r * (uint32_t)r;
uint32_t xx = r2;
uint32_t yy = 0u;
int32_t x = (int32_t)r;
uint16_t y;
while (bx >= by) {
stPlanarFillSpan(pd, (int16_t)(cx - bx), (int16_t)(cx + bx), (int16_t)(cy + by), color);
stPlanarFillSpan(pd, (int16_t)(cx - bx), (int16_t)(cx + bx), (int16_t)(cy - by), color);
stPlanarFillSpan(pd, (int16_t)(cx - by), (int16_t)(cx + by), (int16_t)(cy + bx), color);
stPlanarFillSpan(pd, (int16_t)(cx - by), (int16_t)(cx + by), (int16_t)(cy - bx), color);
by++;
if (err > 0) {
bx--;
err = (int16_t)(err + 2 * (by - bx) + 1);
} else {
err = (int16_t)(err + 2 * by + 1);
for (y = 0u; y <= r; y++) {
uint32_t xxLimit;
xxLimit = r2 - yy;
while (xx > xxLimit) {
xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
x--;
}
stPlanarFillSpan(pd, (int16_t)(cx - (int16_t)x), (int16_t)(cx + (int16_t)x), (int16_t)(cy + (int16_t)y), color);
if (y > 0u) {
stPlanarFillSpan(pd, (int16_t)(cx - (int16_t)x), (int16_t)(cx + (int16_t)x), (int16_t)(cy - (int16_t)y), color);
}
yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
}
}
@ -1380,67 +1442,64 @@ void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunky
}
/* Planar monochrome paste. monoTile follows the ST tile layout from
* jlpTileSnap: row-major-with-planes-interleaved-per-row, i.e.
* row 0 plane 0 at byte 0
* row 0 plane 1 at byte 1
* row 0 plane 2 at byte 2
* row 0 plane 3 at byte 3
* row 1 plane 0 at byte 4 ...
* For monochrome tile-bank tiles only plane 0 of each row carries
* the shape. Output writes the same layout to dst's planar buffer
* using the standard formula
* out_plane_k = (shape & mask_fg_k) | (~shape & mask_bg_k).
* The ST hardware planar destination has plane bytes interleaved at
* word granularity inside each 16-pixel group (4 plane words = 8
* bytes per group), so each plane row byte is written at
* dstAddr + plane*2
* inside the 16-pixel group starting at (bx/2)*8 bytes from the
* row's base. bx odd selects the second byte of the plane word
* (low 8 pixels of the 16-pixel group). */
// Planar monochrome paste. monoTile follows the cross-port mono
// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky
// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two
// pixels per byte. A pixel renders fgColor when its source nibble is
// nonzero, bgColor when zero; the HIGH nibble is the LEFT pixel. Each
// row's 4 source bytes fold into an 8-bit shape mask (bit 7 = leftmost
// pixel, matching the planar bit order), then each plane k writes
// outPlaneK = (shape & maskFgK) | (~shape & maskBgK)
// at dstAddr + k*2 inside the 16-pixel group (4 plane words = 8 bytes
// per group). bx odd selects the second byte of each plane word (the
// low / right 8 pixels of the group).
void jlpTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
StPlanarT *pd;
uint16_t group;
uint8_t *dstAddr;
int16_t row;
uint8_t row;
uint8_t col;
uint8_t plane;
uint8_t masks_fg[4];
uint8_t masks_bg[4];
uint8_t masksFg[4];
uint8_t masksBg[4];
if (dst->portData == NULL) {
jlpGenericTilePasteMono(dst, bx, by, monoTile, fgColor, bgColor);
return;
}
if (dst == NULL || monoTile == NULL) {
return;
}
pd = (StPlanarT *)dst->portData;
if (pd == NULL) {
return;
}
for (plane = 0u; plane < 4u; plane++) {
masks_fg[plane] = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u);
masks_bg[plane] = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u);
masksFg[plane] = (uint8_t)((fgColor & (1u << plane)) ? 0xFFu : 0x00u);
masksBg[plane] = (uint8_t)((bgColor & (1u << plane)) ? 0xFFu : 0x00u);
}
group = (uint16_t)((uint16_t)bx >> 1);
dstAddr = pd->base
+ (uint16_t)by * 8u * ST_BYTES_PER_ROW
+ group * ST_BYTES_PER_GROUP
+ (uint16_t)(bx & 1u);
for (row = 0; row < 8; row++) {
/* monoTile row layout: byte 0 = plane 0 (shape), bytes 1..3 =
* planes 1..3 (zero for monochrome). Apply per-plane mask. */
uint8_t s = monoTile[0];
dstAddr[0] = (uint8_t)((uint8_t)(s & masks_fg[0]) |
(uint8_t)((uint8_t)(~s) & masks_bg[0]));
dstAddr[2] = (uint8_t)((uint8_t)(s & masks_fg[1]) |
(uint8_t)((uint8_t)(~s) & masks_bg[1]));
dstAddr[4] = (uint8_t)((uint8_t)(s & masks_fg[2]) |
(uint8_t)((uint8_t)(~s) & masks_bg[2]));
dstAddr[6] = (uint8_t)((uint8_t)(s & masks_fg[3]) |
(uint8_t)((uint8_t)(~s) & masks_bg[3]));
dstAddr += ST_BYTES_PER_ROW;
monoTile += TILE_BYTES_PER_ROW;
for (row = 0u; row < TILE_PIXELS_PER_SIDE; row++) {
uint8_t shape = 0u;
for (col = 0u; col < TILE_BYTES_PER_ROW; col++) {
uint8_t srcByte = *monoTile++;
shape = (uint8_t)(shape << 2);
if (srcByte & 0xF0u) {
shape = (uint8_t)(shape | 0x02u);
}
if (srcByte & 0x0Fu) {
shape = (uint8_t)(shape | 0x01u);
}
}
dstAddr[0] = (uint8_t)((uint8_t)(shape & masksFg[0]) |
(uint8_t)((uint8_t)(~shape) & masksBg[0]));
dstAddr[2] = (uint8_t)((uint8_t)(shape & masksFg[1]) |
(uint8_t)((uint8_t)(~shape) & masksBg[1]));
dstAddr[4] = (uint8_t)((uint8_t)(shape & masksFg[2]) |
(uint8_t)((uint8_t)(~shape) & masksBg[2]));
dstAddr[6] = (uint8_t)((uint8_t)(shape & masksFg[3]) |
(uint8_t)((uint8_t)(~shape) & masksBg[3]));
dstAddr += ST_BYTES_PER_ROW;
}
}
@ -1455,13 +1514,7 @@ void jlpTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyO
jlpGenericTileSnap(src, bx, by, chunkyOut);
return;
}
if (src == NULL || chunkyOut == NULL) {
return;
}
pd = (const StPlanarT *)src->portData;
if (pd == NULL) {
return;
}
/* Phase 10.5: write plane-major bytes to jlTileT (4 per row * 8 rows). */
group = (uint16_t)((uint16_t)bx >> 1);
srcAddr = pd->base
@ -2006,6 +2059,152 @@ void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
}
// ----- Flood-fill plane hooks (PERF-AUDIT #4) -----
//
// floodFillInternal's planar walk-out and row-scan tiers, modeled on
// the Amiga versions but against the word-interleaved layout. The
// semantics contract with the core C fallback (bit-for-bit):
// * walk: seed test, then extend left/right while pixels are
// fillable. matchEqual: fillable = (pix == matchColor); bounded:
// fillable = (pix != matchColor && pix != newColor).
// * scan: markBuf[i] = 1 where row pixel (leftX + i) is fillable,
// 0 otherwise, for i in [0 .. rightX - leftX].
// Both decode a 16-pixel group's 4 plane words ONCE into a stop/mark
// bit mask, then test single register bits per pixel -- and the walk
// skips a whole all-fillable group remainder in one step.
bool jlpFloodWalkPlanes(const jlSurfaceT *s, int16_t startX, int16_t y, uint8_t matchColor, uint8_t newColor, bool matchEqual, bool *seedMatched, int16_t *leftXOut, int16_t *rightXOut) {
StPlanarT *pd;
const uint8_t *rowBase;
uint16_t seedGroup;
uint16_t seedStopBits;
uint16_t curGroup;
uint16_t stopBits;
uint16_t rem;
uint16_t g;
int16_t leftX;
int16_t rightX;
int16_t prevX;
int16_t nextX;
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return false;
}
matchColor = (uint8_t)(matchColor & 0x0Fu);
newColor = (uint8_t)(newColor & 0x0Fu);
rowBase = pd->base + (uint16_t)y * ST_BYTES_PER_ROW;
seedGroup = (uint16_t)((uint16_t)startX >> 4);
seedStopBits = stFloodStopBits(rowBase, seedGroup, matchColor, newColor, matchEqual);
if (seedStopBits & (uint16_t)(1u << (15u - ((uint16_t)startX & 15u)))) {
*seedMatched = false;
return true;
}
*seedMatched = true;
// Walk left. Pixel x sits at bit (15 - (x & 15)), so moving left
// means moving toward bit 15: shift the stop mask down so bit 0 is
// the candidate pixel, then consume clear bits upward.
leftX = startX;
curGroup = seedGroup;
stopBits = seedStopBits;
while (leftX > 0) {
prevX = (int16_t)(leftX - 1);
g = (uint16_t)((uint16_t)prevX >> 4);
if (g != curGroup) {
curGroup = g;
stopBits = stFloodStopBits(rowBase, g, matchColor, newColor, matchEqual);
}
rem = (uint16_t)(stopBits >> (15u - ((uint16_t)prevX & 15u)));
if (rem == 0u) {
// Everything from prevX to the group's first pixel is
// fillable: take the rest of the group in one step.
leftX = (int16_t)(g << 4);
continue;
}
while ((rem & 1u) == 0u) {
rem = (uint16_t)(rem >> 1);
leftX--;
}
break;
}
// Walk right: mirror image, shifting the stop mask up so bit 15 is
// the candidate pixel.
rightX = startX;
curGroup = seedGroup;
stopBits = seedStopBits;
while (rightX < SURFACE_WIDTH - 1) {
nextX = (int16_t)(rightX + 1);
g = (uint16_t)((uint16_t)nextX >> 4);
if (g != curGroup) {
curGroup = g;
stopBits = stFloodStopBits(rowBase, g, matchColor, newColor, matchEqual);
}
rem = (uint16_t)(stopBits << ((uint16_t)nextX & 15u));
if (rem == 0u) {
// Everything from nextX to the group's last pixel is
// fillable: take the rest of the group in one step.
rightX = (int16_t)((g << 4) + 15);
continue;
}
while ((rem & 0x8000u) == 0u) {
rem = (uint16_t)(rem << 1);
rightX++;
}
break;
}
*leftXOut = leftX;
*rightXOut = rightX;
return true;
}
bool jlpFloodScanRowPlanes(const jlSurfaceT *s, int16_t leftX, int16_t rightX, int16_t scanY, uint8_t matchColor, uint8_t newColor, bool matchEqual, uint8_t *markBuf) {
StPlanarT *pd;
const uint8_t *rowBase;
const uint16_t *gp;
uint8_t *mb;
uint16_t markBits;
uint16_t bit;
int16_t x;
int16_t groupEnd;
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return false;
}
matchColor = (uint8_t)(matchColor & 0x0Fu);
newColor = (uint8_t)(newColor & 0x0Fu);
rowBase = pd->base + (uint16_t)scanY * ST_BYTES_PER_ROW;
mb = markBuf;
x = leftX;
while (x <= rightX) {
gp = (const uint16_t *)(rowBase + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP);
if (matchEqual) {
markBits = stGroupEqBits(gp, matchColor);
} else {
markBits = (uint16_t)~(stGroupEqBits(gp, matchColor) | stGroupEqBits(gp, newColor));
}
groupEnd = (int16_t)((uint16_t)x | 15u);
if (groupEnd > rightX) {
groupEnd = rightX;
}
bit = (uint16_t)(1u << (15u - ((uint16_t)x & 15u)));
while (x <= groupEnd) {
*mb = (uint8_t)((markBits & bit) ? 1u : 0u);
mb++;
bit = (uint16_t)(bit >> 1);
x++;
}
}
return true;
}
// Phase 7: pixel reader. Pre-Phase-9 reads from the chunky shadow
// (s->pixels) since that's the source-of-truth during transition.
// Once Phase 9 sets s->pixels = NULL the planar shadow becomes

View file

@ -4,7 +4,8 @@
// sp->slot + sp->routineOffsets. spriteCompiledDraw casts the slot
// address to a function pointer and calls it through cdecl.
//
// Each per-CPU emitter (src/codegen/spriteEmit{X86,68k,Iigs}.c)
// Each per-CPU emitter (src/dos/spriteEmitX86.c,
// src/iigs/spriteEmitIigs.c, src/m68k/spriteEmit{Planar,Interleaved}68k.c)
// just produces bytes; this file is the only consumer of the
// codegen arena from the sprite side.
@ -44,10 +45,10 @@
#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.
// Compile-time selection of the per-CPU emitter. One per-CPU
// spriteEmit*.c file (src/dos, src/iigs, src/m68k) is built per
// platform, but the dispatch lives in this file so jlSpriteCompile +
// spriteCompiledDraw aren't duplicated per port.
static uint32_t emitDrawForTarget(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
#if defined(JOEYLIB_PLATFORM_DOS)
return spriteEmitDrawX86(out, cap, sp, shift);
@ -141,6 +142,19 @@ static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
* math -- see spriteCompiledDraw + the prologue/epilogue in spriteEmitIigs.c.
* Codegen is ON by default; set JOEY_IIGS_SPRITE_CODEGEN to 0 to force the
* interpreter. */
/* PERF-AUDIT.md #77 -- ROOT-CAUSED 2026-07-04, FIXED toolchain-side
* 2026-07-06 (LLVM816-ASKS.md item 5, JOEYLIB-REPLY.md): the GS/OS
* loader used to Memory-Manager-claim only each segment's IMAGE bytes,
* so the BSS+heap span above the image was MM-free space and the codegen
* arena's NewHandle could land INSIDE the app's own entry-bank BSS
* ($08:4200 empirically), where emitting routines clobbered live app
* state (the emitted code itself was always byte-valid -- the bug was
* arena PLACEMENT). link816/omfEmit now embed each segment's BSS in the
* image so the Loader reserves it in place, and crt0Gsos moves the C
* heap into a locked MM handle -- so NewHandle/malloc no longer collide
* with live state. Codegen re-enabled on IIgs 2026-07-06; the emitted-
* code path needed no JoeyLib-side change (as predicted). Set to 0 to
* fall back to the interpreter. */
#ifndef JOEY_IIGS_SPRITE_CODEGEN
#define JOEY_IIGS_SPRITE_CODEGEN 1
#endif
@ -169,13 +183,10 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* #19: leave sp->slot NULL -> interpreter path. See the note above. */
return false;
#endif
/* Amiga (post-Phase 9) uses spriteEmitPlanar68k.c which writes
* directly to bitplanes. DRAW emits a unique pre-shifted variant
* per shift in 0..7 (smooth horizontal motion at any pixel x);
* SAVE/RESTORE emit only shift 0 and shift 1 since shifted variants
* 1..7 share identical bytes (plain memcpy of widthTiles+1 plane
* bytes per row). The post-emit pass below aliases slots 2..7
* for save/restore to slot 1's bytes. */
/* Amiga (post-Phase 9) uses spriteEmitPlanar68k.c, which writes
* directly to bitplanes; only the shift-0 DRAW is compiled today
* (save/restore and shifted draws run the interpreted planes
* walkers -- see the emitter's header comment). */
scratch = (uint8_t *)jlpBigAlloc(SPRITE_EMIT_SCRATCH_BYTES);
if (scratch == NULL) {
@ -201,6 +212,18 @@ bool jlSpriteCompile(jlSpriteT *sp) {
slot = codegenArenaAlloc(totalSize);
if (slot == NULL) {
// Fragmented arena: total free space may suffice even though no
// single hole does (sprite create/destroy churn across levels).
// Honor the arena's compact-and-retry contract
// (codegenArenaInternal.h) once before giving up -- failure-path
// only, zero hot-path cost (#5).
codegenArenaCompact();
slot = codegenArenaAlloc(totalSize);
}
if (slot == NULL) {
// Specific message so a swallowed jlSpritePrewarm/jlSpriteCompile
// failure is diagnosable via jlLastError.
coreSetError("jlSpriteCompile: codegen arena full even after compaction; raise jlConfigT.codegenBytes or destroy unused sprites");
jlpBigFree(scratch);
return false;
}
@ -209,22 +232,33 @@ 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);
// Emit into the roomy scratch with the SAME cap the sizing
// pass used, then memcpy the routine into the arena. The
// emitters' bound checks are deliberately conservative (they
// reserve a worst case per row/iteration), so handing pass 2
// an exact-fit capLeft made the LAST routine in every slot
// spuriously overflow -- on DOS that demoted every sprite to
// the interpreter (PERF-AUDIT.md #74). Identical arguments
// in both passes make the return values identical by
// construction; one memcpy per routine at one-shot compile
// time is noise.
switch (op) {
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;
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) {
if (written == SPRITE_EMIT_OVERFLOW || (written != 0 && written > (uint32_t)(totalSize - offset))) {
/* The second pass disagreed with the sizing pass (must
* not happen, but never write past the slot). Roll back. */
* not happen with identical args, but never write past
* the slot). Roll back. */
codegenArenaFree(slot);
jlpBigFree(scratch);
return false;
}
if (written != 0) {
memcpy(dst + offset, scratch, (size_t)written);
}
/* 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
@ -238,42 +272,22 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
}
}
#if defined(JOEYLIB_PLATFORM_AMIGA)
/* Save/restore bytes for any non-zero shift are identical (plain
* memcpy of widthTiles+1 plane bytes per row). The emitter emits
* them once at slot 1; alias slots 2..7 here so the dispatcher
* gate (sprite.c) sees them as compiled. */
for (shift = 2; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
sp->routineOffsets[shift][SPRITE_OP_SAVE] = sp->routineOffsets[1][SPRITE_OP_SAVE];
sp->routineOffsets[shift][SPRITE_OP_RESTORE] = sp->routineOffsets[1][SPRITE_OP_RESTORE];
}
#endif
sp->slot = slot;
// Registry (#34): every creation path (including jlSpriteBankLoad,
// #32) already registered sp via spriteInitFields; this idempotent
// re-add is a belt-and-braces guard, since compile success is the
// moment sp first holds arena state that jlShutdown must reset.
spriteRegistryAdd(sp);
jlpBigFree(scratch);
return true;
}
#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
// The restore shift used to be re-derived here from the backup
// (spriteChunkyRestoreShift). The jlSpriteRestoreUnder gate in
// src/core/sprite.c already derives it to pick the routine, so it now
// passes shift (and routeOffset) straight into the callees and the
// re-derivation is gone (#29).
#if defined(JOEYLIB_PLATFORM_IIGS)
@ -291,8 +305,7 @@ static uint8_t spriteChunkyRestoreShift(const jlSpriteT *sp, const jlSpriteBacku
// have only their MVN bank operands patched per call (a plain C pointer write
// into the arena from the dispatcher). No self-modifying call stub, no inline
// asm -- the machinery that hid the old un-instrumentable #19 corruption.
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
uint8_t shift;
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
uint8_t *dstRow0;
uint32_t fnAddr;
void (*drawFn)(uint8_t *);
@ -303,12 +316,12 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
// per-call bank/offset patching (that machinery was the source of the
// old un-instrumentable corruption). dstRow0 (the sprite's top-left
// screen byte) is passed in A:X; the routine's prologue (spriteEmitIigs.c)
// sets up its own DBR/Y and restores DBR on exit.
shift = (uint8_t)(x & 1);
// sets up its own DBR/Y and restores DBR on exit. routeOffset is the
// gate-derived DRAW offset for this shift (#29).
dstRow0 = &dst->pixels[SURFACE_ROW_OFFSET(y) + ((uint16_t)x >> 1)];
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)sp->routineOffsets[shift][SPRITE_OP_DRAW];
+ (uint32_t)routeOffset;
drawFn = (void (*)(uint8_t *))fnAddr;
drawFn(dstRow0);
}
@ -349,32 +362,17 @@ static void patchMvnBanks(uint8_t *routine, uint16_t heightPx, uint8_t dstBank,
*(_outBank) = spB_[2]; \
} while (0)
// Backup-buffer pointer split cache. backup->bytes is a user-supplied
// buffer (e.g. a static array) and effectively never changes after
// the first call -- caching its split saves both Save and Restore the
// macro expansion per frame.
static const void *gLastBackupBytes = (const void *)0;
static uint16_t gLastBackupBytesLo = 0;
static uint8_t gLastBackupBytesBank = 0;
#define SPLIT_BACKUP_CACHED(_bytes, _outLo, _outBank) \
do { \
if ((const void *)(_bytes) == gLastBackupBytes) { \
*(_outLo) = gLastBackupBytesLo; \
*(_outBank) = gLastBackupBytesBank; \
} else { \
SPLIT_POINTER((_bytes), (_outLo), (_outBank)); \
gLastBackupBytes = (const void *)(_bytes); \
gLastBackupBytesLo = *(_outLo); \
gLastBackupBytesBank = *(_outBank); \
} \
} while (0)
// The backup->bytes pointer is split with SPLIT_POINTER directly. A
// single-entry global split cache used to live here (#59); it assumed
// one backup buffer per program, but the normal pattern is one buffer
// per moving sprite, so with 2+ sprites the cached pointer ping-ponged
// and every call paid a 24-bit compare AND the full split. The split
// itself is ~6 simple loads/stores -- cheaper than the compare on a
// hit -- so the cache was deleted rather than moved per-sprite.
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
uint8_t shift;
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
int16_t clippedX;
uint16_t widthPx;
uint16_t heightPx;
uint16_t copyBytes;
uint16_t screenLo;
@ -387,17 +385,17 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
uint16_t cacheIdx; /* shift * SPRITE_OP_COUNT + SPRITE_OP_SAVE, computed once */
uint8_t *cachedDst; /* &sp->cachedDstBank[0][0] + cacheIdx */
uint8_t *cachedSrc; /* &sp->cachedSrcBank[0][0] + cacheIdx */
uint16_t routineOffset; /* sp->routineOffsets[shift][SPRITE_OP_SAVE], computed once */
shift = (uint8_t)(x & 1);
// shift and routeOffset arrive pre-derived from the dispatcher
// gate (#29); widthPx/heightPx are create-time constants cached in
// the sprite struct.
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
heightPx = sp->heightPx;
copyBytes = (uint16_t)((sp->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);
SPLIT_BACKUP_CACHED(backup->bytes, &backupLo, &backupBank);
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);
backup->sprite = sp;
backup->x = clippedX;
@ -416,19 +414,16 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
backup->sizeBytes = *sizeCachePtr;
}
/* Compute the 1D index into the cached* / routineOffsets 2D arrays
* 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;
/* Same byte-pointer trick as SURFACE_ROW_OFFSET to dodge ~MUL4. */
routineOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (cacheIdx << 1));
/* Compute the 1D index into the cached* bank arrays once;
* SPRITE_ROUTINE_INDEX (spriteInternal.h) owns the multiply-free
* derivation. */
cacheIdx = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_SAVE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)routineOffset;
+ (uint32_t)routeOffset;
// Patch the MVN bank operands only if the dst/src bank pair changed since
// last call (the routine's only dynamic bytes). SAVE copies screen ->
@ -450,8 +445,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
}
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
uint8_t shift;
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
uint16_t heightPx;
uint16_t screenLo;
uint16_t backupLo;
@ -464,26 +458,25 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
uint16_t cacheIdx; /* shift * SPRITE_OP_COUNT + SPRITE_OP_RESTORE, computed once */
uint8_t *cachedDst;
uint8_t *cachedSrc;
uint16_t routineOffset;
sp = backup->sprite;
heightPx = backup->height;
shift = spriteChunkyRestoreShift(sp, backup);
// shift and routeOffset arrive pre-derived from the
// jlSpriteRestoreUnder gate (#29) -- no per-call re-inference of
// the shift from the backup here.
sp = backup->sprite;
heightPx = backup->height;
screenPtr = (uint8_t *)&dst->pixels[SURFACE_ROW_OFFSET(backup->y) + ((uint16_t)backup->x >> 1)];
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
SPLIT_BACKUP_CACHED(backup->bytes, &backupLo, &backupBank);
SPLIT_POINTER(screenPtr, &screenLo, &screenBank);
SPLIT_POINTER(backup->bytes, &backupLo, &backupBank);
/* Hoist 2D-array indexing -- see save-side comment. */
cacheIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_RESTORE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
/* Same byte-pointer trick as SURFACE_ROW_OFFSET to dodge ~MUL4. */
routineOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (cacheIdx << 1));
cacheIdx = SPRITE_ROUTINE_INDEX(shift, SPRITE_OP_RESTORE);
cachedDst = (uint8_t *)sp->cachedDstBank + cacheIdx;
cachedSrc = (uint8_t *)sp->cachedSrcBank + cacheIdx;
fnAddr = codegenArenaBaseAddr()
+ sp->slot->offset
+ (uint32_t)routineOffset;
+ (uint32_t)routeOffset;
// Patch the MVN bank operands only if they changed (the routine's only
// dynamic bytes). RESTORE copies backup -> screen: dst = screen bank,
@ -506,27 +499,24 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
#elif defined(JOEYLIB_PLATFORM_AMIGA)
/* Amiga planar dispatchers. spriteEmitPlanar68k.c emits routines with
* cdecl(p0, p1, p2, p3[, backup]) signatures that write directly to
* bitplanes. Compute byteOff = y*40 + x/8 and pass plane[i]+byteOff
* as the 4 plane args. shift = x % 8 selects the variant; today only
* shift 0 emits non-zero bytes, so callers should already have
* gated on routineOffsets[shift][op] != SPRITE_NOT_COMPILED.
*
* For non-zero shifts (x not 8-px-aligned), the dispatcher in
* src/core/sprite.c (jlSpriteDraw / jlSpriteSaveUnder / jlSpriteRestoreUnder)
* sees SPRITE_NOT_COMPILED for the shift and falls back to the
* interpreter, which handles arbitrary x via jlpSpriteDrawPlanes /
* jlpSpriteSavePlanes / jlpSpriteRestorePlanes. */
// Amiga planar dispatchers. spriteEmitPlanar68k.c emits DRAW routines
// with a cdecl(p0, p1, p2, p3) signature that write directly to
// bitplanes. Compute byteOff = y*40 + x/8 and pass plane[i]+byteOff
// as the 4 plane args. shift = x % 8 selects the variant; today only
// shift 0 emits non-zero bytes, and every caller gate in
// src/core/sprite.c checks routineOffsets[shift][op] !=
// SPRITE_NOT_COMPILED before calling here (#29), so non-zero shifts
// (x not 8-px-aligned) never reach this dispatcher -- they fall back
// to the interpreter, which handles arbitrary x via
// jlpSpriteDrawPlanes / jlpSpriteSavePlanes / jlpSpriteRestorePlanes.
// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40.
// Derived from surface geometry so it cannot drift if the fixed
// surface width ever changes.
#define AMIGA_PLANE_BYTES_PER_ROW (SURFACE_WIDTH / 8)
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
uint8_t shift;
uint16_t byteOff;
uint8_t *p0;
uint8_t *p1;
@ -534,109 +524,66 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
uint8_t *p3;
DrawFn fn;
shift = (uint8_t)(x & 7);
/* Only shift 0 (byte-aligned x) is compiled; shifts 1..7 stay
* SPRITE_NOT_COMPILED. jlSpriteDraw gates only on sp->slot != NULL,
* so guard the per-shift offset here and delegate a non-aligned draw
* to the interpreter (COMPILED_SPRITE_WRITES_PLANES==1 means
* jlSpriteDraw suppresses its own planes hook, so we must run it). */
if (sp->routineOffsets[shift][SPRITE_OP_DRAW] == SPRITE_NOT_COMPILED) {
jlpSpriteDrawPlanes(dst, sp, x, y);
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
p0 = jlpSurfacePlanePtr(dst, 0);
if (p0 == NULL) {
return;
}
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3));
p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = jlpSurfacePlanePtr(dst, 1);
p2 = jlpSurfacePlanePtr(dst, 2);
p3 = jlpSurfacePlanePtr(dst, 3);
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_DRAW]);
p1 = jlpSurfacePlanePtr(dst, 1);
p2 = jlpSurfacePlanePtr(dst, 2);
p3 = jlpSurfacePlanePtr(dst, 3);
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff);
}
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
typedef void (*SaveFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *backup);
uint8_t shift;
int16_t clippedX;
uint16_t widthPx;
uint16_t heightPx;
uint16_t byteOff;
uint8_t *p0;
uint8_t *p1;
uint8_t *p2;
uint8_t *p3;
SaveFn fn;
shift = (uint8_t)(x & 7);
clippedX = (int16_t)(x & ~7);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
/* Shifts 1..7 spill into one extra plane byte per row (= +8 px). */
if (shift != 0u) {
widthPx = (uint16_t)(widthPx + 8u);
}
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)clippedX >> 3));
backup->sprite = sp;
backup->x = clippedX;
backup->y = y;
backup->width = widthPx;
backup->height = heightPx;
/* 4 planes * h * (widthPx/8) bytes = h * widthPx/2. */
backup->sizeBytes = (uint16_t)((uint16_t)heightPx * (widthPx >> 1));
p0 = jlpSurfacePlanePtr(src, 0); if (p0 == NULL) return;
p1 = jlpSurfacePlanePtr(src, 1);
p2 = jlpSurfacePlanePtr(src, 2);
p3 = jlpSurfacePlanePtr(src, 3);
fn = (SaveFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_SAVE]);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes);
// Save/restore aren't compiled on the Amiga (spriteEmitPlanar68k.c
// returns 0 for both, so every routineOffsets[*][SPRITE_OP_SAVE /
// _RESTORE] entry is SPRITE_NOT_COMPILED). The sprite.c gates route
// all save/restore work through the interpreted jlpSpriteSavePlanes /
// jlpSpriteRestorePlanes; these stubs exist only to satisfy the linker
// (sprite.c references them unconditionally).
//
// FUTURE (when real Amiga save/restore emitters land): the compiled
// restore gate in jlSpriteRestoreUnder (src/core/sprite.c) only
// accepts chunky copyBytes == spriteBytesPerRow or +1. A planar
// shifted save records backup->width = widthPx + 8, i.e. copyBytes ==
// spriteBytesPerRow + 4 -- the gate must learn that width or 7 of 8
// x-alignments will silently stay on the interpreted path.
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)src;
(void)sp;
(void)x;
(void)y;
(void)backup;
(void)shift;
(void)routeOffset;
}
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
typedef void (*RestoreFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, const uint8_t *backup);
jlSpriteT *sp;
uint8_t shift;
uint16_t byteOff;
uint8_t *p0;
uint8_t *p1;
uint8_t *p2;
uint8_t *p3;
RestoreFn fn;
sp = backup->sprite;
/* backup->x is 8-px aligned (clippedX from save), so x & 7 is
* useless for picking the original shift. Encode it via
* backup->width: == widthTiles*8 means shift 0; > means shifted.
* 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_PLANE_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
p0 = jlpSurfacePlanePtr(dst, 0); if (p0 == NULL) return;
p1 = jlpSurfacePlanePtr(dst, 1);
p2 = jlpSurfacePlanePtr(dst, 2);
p3 = jlpSurfacePlanePtr(dst, 3);
fn = (RestoreFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_RESTORE]);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff, backup->bytes);
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)dst;
(void)backup;
(void)shift;
(void)routeOffset;
}
#elif defined(JOEYLIB_PLATFORM_ATARIST)
/* ST word-interleaved planar runtime dispatch. The JIT routine takes
* one arg: groupBase = pd->base + y*160 + (x>>4)*8 (the address of
* the first 16-pixel group the sprite touches). It walks rows by
* adda.w #160 at the end of each row. Per (row, tile_col, plane) it
* emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at
* d16(a0).
*
* shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX):
* 0 : byte-aligned x with x mod 16 == 0 (first tile col high half)
* 1 : byte-aligned x with x mod 16 == 8 (first tile col low half)
* 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
* per-shift offset is SPRITE_NOT_COMPILED so the dispatcher
* falls back to jlpSpriteDrawPlanes. */
// ST word-interleaved planar runtime dispatch. The JIT routine takes
// one arg: groupBase = pd->base + y*160 + (x>>4)*8 (the address of
// the first 16-pixel group the sprite touches). It walks rows by
// adda.w #160 at the end of each row. Per (row, tile_col, plane) it
// emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at
// d16(a0).
//
// shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX):
// 0 : byte-aligned x with x mod 16 == 0 (first tile col high half)
// 1 : byte-aligned x with x mod 16 == 8 (first tile col low half)
// 2+ : non-byte-aligned x, never compiled (emitter returns 0); the
// per-shift offset is SPRITE_NOT_COMPILED so the caller gates
// in src/core/sprite.c (#29) never dispatch here and fall back
// to jlpSpriteDrawPlanes.
// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
@ -645,27 +592,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup)
#define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8)
#define ST_BYTES_PER_PLANE_GROUP 8u
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
typedef void (*DrawFn)(uint8_t *groupBase);
uint8_t shift;
uint16_t routeOffset;
uint8_t *base;
uint8_t *groupBase;
DrawFn fn;
shift = SPRITE_SHIFT_INDEX(x);
routeOffset = sp->routineOffsets[shift][SPRITE_OP_DRAW];
if (routeOffset == SPRITE_NOT_COMPILED) {
/* Non-byte-aligned x: cross-platform jlSpriteDraw will call
* jlpSpriteDrawPlanes after this returns (since the dispatcher
* already chose the compiled path based on sp->slot != NULL,
* but COMPILED_SPRITE_WRITES_PLANES is 1 on ST so it normally
* suppresses the planes hook). For non-aligned shifts we
* deliberately want the interpreted planes hook to run, so
* delegate via jlpSpriteDrawPlanes here. */
jlpSpriteDrawPlanes(dst, sp, x, y);
return;
}
base = jlpSurfacePlanePtr(dst, 0);
if (base == NULL) {
return;
@ -678,31 +610,39 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
}
/* Save/Restore aren't compiled on ST yet (emitter returns 0). The
* dispatcher's check on sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE]
* == SPRITE_NOT_COMPILED already routes those through the
* interpreted jlpSpriteSavePlanes / jlpSpriteRestorePlanes. These
* stubs exist only to satisfy the linker. */
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
(void)src; (void)sp; (void)x; (void)y; (void)backup;
// Save/Restore aren't compiled on ST yet (emitter returns 0). The
// caller gates in src/core/sprite.c see SPRITE_NOT_COMPILED for
// sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE] and route those
// ops through the interpreted jlpSpriteSavePlanes /
// jlpSpriteRestorePlanes. These stubs exist only to satisfy the
// linker.
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)src;
(void)sp;
(void)x;
(void)y;
(void)backup;
(void)shift;
(void)routeOffset;
}
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
(void)dst; (void)backup;
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)dst;
(void)backup;
(void)shift;
(void)routeOffset;
}
#else
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
typedef void (*DrawFn)(uint8_t *destRow);
uint8_t shift;
uint8_t *destRow;
DrawFn fn;
shift = (uint8_t)(x & 1);
destRow = &dst->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_DRAW]);
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(destRow);
}
@ -710,22 +650,19 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
// x86 / 68k compiled save: bytes are a cdecl
// void copy(const uint8_t *src, uint8_t *dst)
// that walks heightPx rows of copyBytes from screen (stride
// SURFACE_BYTES_PER_ROW) into the contiguous backup buffer.
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
// SURFACE_BYTES_PER_ROW) into the contiguous backup buffer. shift and
// routeOffset arrive pre-derived from the dispatcher gate (#29).
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
uint8_t shift;
int16_t clippedX;
uint16_t widthPx;
uint16_t heightPx;
uint16_t copyBytes;
uint8_t *screenPtr;
CopyFn fn;
shift = (uint8_t)(x & 1);
clippedX = (int16_t)(x & ~1);
widthPx = (uint16_t)(sp->widthTiles * 8);
heightPx = (uint16_t)(sp->heightTiles * 8);
copyBytes = (uint16_t)((widthPx >> 1) + shift); /* shift is x & 1, exactly 0 or 1 */
heightPx = sp->heightPx;
copyBytes = (uint16_t)((sp->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)];
@ -736,7 +673,7 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
backup->height = heightPx;
backup->sizeBytes = (uint16_t)(copyBytes * heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_SAVE]);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(screenPtr, backup->bytes);
}
@ -744,20 +681,19 @@ void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, in
// Mirror of save: caller swaps arg order so the same emitted shape
// drives backup -> screen. The screen-side stride lives inside the
// emitted bytes, so RESTORE has its own routine bytes (stride is
// applied to dst instead of src).
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup) {
// applied to dst instead of src). routeOffset arrives pre-derived
// from the jlSpriteRestoreUnder gate (#29); shift is unused here (no
// bank-patch cache off-IIgs) but kept for the shared signature.
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
jlSpriteT *sp;
uint8_t shift;
uint8_t *screenPtr;
CopyFn fn;
sp = backup->sprite;
shift = spriteChunkyRestoreShift(sp, backup);
jlSpriteT *sp;
uint8_t *screenPtr;
CopyFn fn;
(void)shift;
sp = backup->sprite;
screenPtr = (uint8_t *)&dst->pixels[(uint16_t)backup->y * SURFACE_BYTES_PER_ROW + ((uint16_t)backup->x >> 1)];
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + sp->routineOffsets[shift][SPRITE_OP_RESTORE]);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(backup->bytes, screenPtr);
}

View file

@ -0,0 +1,94 @@
// Shared emit-time source-pixel helpers for the chunky 4bpp sprite
// emitters -- spriteEmitX86.c (DOS) and spriteEmitIigs.c. These two
// functions ARE the transparency/shift contract that the emitted
// routines must share with the interpreted walkers bit-for-bit, so
// they exist exactly once: a fix applied here reaches both CPUs.
//
// Emit-time only (jlSpriteCompile), never on a draw hot path, so plain
// static functions are acceptable here despite the general
// macros-over-inline preference for shared headers. Tile geometry
// comes from joey/tile.h; TRANSPARENT_NIBBLE from spriteInternal.h.
#ifndef JOEYLIB_SPRITE_EMIT_CHUNKY_H
#define JOEYLIB_SPRITE_EMIT_CHUNKY_H
#include "joey/sprite.h"
#include "joey/tile.h"
#include "spriteInternal.h"
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);
// 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;
// 0x00 means both transparent. value's transparent nibbles are 0.
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask) {
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
bool hasLeft;
bool hasRight;
*outValue = 0;
*outOpaqueMask = 0;
if (shift == 0) {
if (col >= spriteBytesPerRow) {
return;
}
srcByte = spriteSourceByte(sp, row, col);
hi = (uint8_t)((srcByte >> 4) & 0x0Fu);
lo = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
return;
}
// shift = 1
hasLeft = (col >= 1) && ((uint16_t)(col - 1) < spriteBytesPerRow);
hasRight = (col < spriteBytesPerRow);
if (hasLeft) {
srcByte = spriteSourceByte(sp, row, (uint16_t)(col - 1));
hi = (uint8_t)(srcByte & 0x0Fu); // sprite byte's LOW nibble
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
}
if (hasRight) {
srcByte = spriteSourceByte(sp, row, col);
lo = (uint8_t)((srcByte >> 4) & 0x0Fu); // sprite byte's HIGH nibble
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
}
}
// Sample a sprite tile-data byte at (row, col) where col is in
// sprite-byte coordinates (0..spriteBytesPerRow-1).
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col) {
uint16_t tileX;
uint16_t tileY;
uint16_t inTileX;
uint16_t inTileY;
const uint8_t *tile;
tileX = (uint16_t)(col / TILE_BYTES_PER_ROW);
tileY = (uint16_t)(row / TILE_PIXELS_PER_SIDE);
inTileX = (uint16_t)(col & (TILE_BYTES_PER_ROW - 1));
inTileY = (uint16_t)(row & (TILE_PIXELS_PER_SIDE - 1));
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
return tile[inTileY * TILE_BYTES_PER_ROW + inTileX];
}
#endif

View file

@ -1,12 +1,10 @@
// Internal interface for per-CPU sprite emitters.
//
// Each src/codegen/spriteEmit<Cpu>.c file implements its own emit
// function. jlSpriteCompile.c picks the right one at compile time
// (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build. The
// planar 68k emitters now return 0 bytes after Phase 11 -- the
// jlpSpriteDrawPlanes walker reads chunky tile data and synthesizes
// plane bytes at draw time -- but the entry points are kept so the
// dispatcher API stays uniform across ports.
// Each per-CPU emitter file (src/dos/spriteEmitX86.c,
// src/iigs/spriteEmitIigs.c, src/m68k/spriteEmitPlanar68k.c,
// src/m68k/spriteEmitInterleaved68k.c) implements its own emit
// functions. spriteCompile.c picks the right one at compile time
// (via #ifdef on JOEYLIB_PLATFORM_*) for the runtime build.
//
// Each emit function takes the sprite + shift variant and a capacity
// `cap` (bytes available at `out`), and writes position-independent
@ -55,13 +53,12 @@
#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.
// The planar (Amiga) and interleaved (ST) emitters share the same
// worst-case mixed-RMW shape (4 instructions * 4 bytes per dest byte).
#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
@ -72,28 +69,27 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
// Per-CPU emitters return 0 to mean "not implemented" -- the runtime
// dispatch falls back to the interpreted path in that case.
//
// IIgs uses a self-modifying MVN-stub on top of these bytes; x86 and
// 68k use a plain cdecl `void copy(const uint8_t *src, uint8_t *dst)`
// x86 emits 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).
// RESTORE (backup->screen). IIgs emits a C-ABI
// `void copy(uint32_t packed)` (packed = srcOff | (dstOff << 16)) of
// unrolled MVN rows whose bank operand bytes the dispatcher patches
// per call -- see spriteEmitIigs.c's header for the full contract.
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
// bitplane buffers, not a single packed-pixel surface. Calling
// convention for the emitted bytes (cdecl):
// void draw (uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
// void save (uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *backup);
// void restore (uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, const uint8_t *backup);
// Planar 68k emitters (Amiga). Destination addressing is across 4
// separate bitplane buffers, not a single packed-pixel surface.
// Calling convention for the emitted DRAW bytes (cdecl):
// void draw(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
// Each pi is plane_base + byteOff (= y*40 + x/8 already added by the
// 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).
// dispatcher). DRAW 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). SAVE/RESTORE always return 0 --
// not compiled on the Amiga yet; the interpreted jlpSpriteSavePlanes /
// jlpSpriteRestorePlanes path handles them.
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);
@ -101,10 +97,11 @@ uint32_t spriteEmitRestorePlanar68k (uint8_t *out, uint32_t cap, const jlSpriteT
// Word-interleaved planar 68k emitter (ST). Calling convention for
// the emitted bytes:
// void draw(uint8_t *groupBase);
// where groupBase = pd->base + y*160 + (x>>4)*8. Shifts 0 and 1 emit
// 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 jlpSpriteDrawPlanes.
// where groupBase = pd->base + y*160 + (x>>4)*8. DRAW shifts 0 and 1
// emit 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 jlpSpriteDrawPlanes. SAVE/RESTORE always return 0 --
// not compiled on the ST; the interpreted planes path handles them.
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);

View file

@ -24,29 +24,52 @@
#include "joey/sprite.h"
#include "joey/tile.h"
#include "port.h"
#include "spriteInternal.h"
// 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).
// IIgs segment placement (PERF-AUDIT #60): this TU carries NO named
// segment directive, deliberately. An earlier comment here claimed the
// file-IO was concentrated in a named segment to keep the stdio
// cluster from overflowing the root bank -- that directive never
// existed, and the failure mode it guarded against ("Code exceeds code
// bank size" / "Address is not in current bank") was a problem of the
// ORCA-Linker era; the clang/OMF pipeline splits oversized segments
// automatically. Do not add a segment directive here without link-map
// evidence -- root-segment packing is pressure-sensitive (see the
// bank-overflow notes in the feedback memory).
// ----- Constants -----
#define ASSET_HEADER_BYTES 44
#define ASSET_PALETTE_OFFSET 12
#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
// Tile-bank payload reads are batched: jlTileT is exactly
// TILE_BYTES with no padding (compile-time-checked below), so up to
// ASSET_TILE_READ_BATCH tiles stream in one fread. The old per-tile
// loop issued 1000 32-byte freads for a full font bank -- on the IIgs
// each fread is a GS/OS round trip through SmartPort firmware, which
// dominated (and on heavily-sparse .tbk files wedged) asset loading.
// 256 tiles = 8KB per call, safely inside 16-bit size_t on every port.
#define ASSET_TILE_READ_BATCH 256u
// Compile-time guard for the batched read: consecutive jlTileT
// entries must be contiguous pixel payloads (no padding), or the
// bulk fread would misalign every tile after the first.
typedef char AssetTileSizeCheckT[(sizeof(jlTileT) == TILE_BYTES) ? 1 : -1];
// Tile geometry comes from joey/tile.h, the single source of truth for
// the 4bpp packed layout (PERF-AUDIT #30): the per-tile payload read
// below is TILE_BYTES -- exactly what jlTileT.pixels holds -- and the
// largest legitimately drawable sprite cel is the full surface grid,
// TILE_BLOCKS_PER_ROW x TILE_BLOCKS_PER_COL (SURFACE_WIDTH/HEIGHT over
// TILE_PIXELS_PER_SIDE, 40x25 on the 320x200 surface). Any cel larger
// than that can never be fully on-screen, so reject crafted/truncated
// headers claiming more, which would otherwise force an unbounded
// per-cel allocation (DoS) and feed the downstream emitters/draw
// walkers out-of-range dimensions.
#define ASSET_MAX_TILES_W TILE_BLOCKS_PER_ROW
#define ASSET_MAX_TILES_H TILE_BLOCKS_PER_COL
// Per-platform expected target byte for .tbk files. .spr files carry
// target=0 (cross-platform chunky) and pass on every build.
@ -156,7 +179,7 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
return 0u;
}
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)ASSET_TILE_DATA_BYTES;
tileBytes = (uint32_t)widthTiles * (uint32_t)heightTiles * (uint32_t)TILE_BYTES;
toLoad = (cellCount < maxCels) ? cellCount : maxCels;
loaded = 0u;
@ -164,30 +187,26 @@ uint16_t jlSpriteBankLoad(const char *path, jlSpriteT **outCels, uint16_t maxCel
uint8_t *buf;
jlSpriteT *sp;
buf = (uint8_t *)malloc((size_t)tileBytes);
// jlpBigAlloc for the same #31/#81 reasons as sprite.c: these
// buffers overflow the tiny IIgs C heap, whose malloc returns
// garbage instead of NULL at exhaustion.
buf = (uint8_t *)jlpBigAlloc(tileBytes);
if (buf == NULL) {
break;
}
if (fread(buf, 1, (size_t)tileBytes, fp) != (size_t)tileBytes) {
free(buf);
jlpBigFree(buf);
break;
}
sp = (jlSpriteT *)malloc(sizeof(jlSpriteT));
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
if (sp == NULL) {
free(buf);
jlpBigFree(buf);
break;
}
sp->tileData = buf;
sp->widthTiles = widthTiles;
sp->heightTiles = heightTiles;
sp->ownsTileData = true;
sp->slot = NULL;
// 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));
// Shared init path (spriteInternal.h, PERF-AUDIT.md #32): sets
// the 0xFF/0 compiled-state sentinels and registers the sprite
// for shutdown. ownsTileData true: the cel owns its buf copy.
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
outCels[i] = sp;
loaded++;
}
@ -209,6 +228,9 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
uint16_t i;
uint16_t loaded;
uint16_t toLoad;
uint16_t batch;
uint16_t gotTiles;
size_t got;
if (path == NULL || outTiles == NULL || maxTiles == 0u) {
return 0u;
@ -233,15 +255,82 @@ uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
toLoad = (tileCount < maxTiles) ? tileCount : maxTiles;
// Batched payload stream (ASSET_TILE_READ_BATCH). jlTileT is
// pure pixels[TILE_BYTES], so consecutive tiles are contiguous
// and a short final fread loads exactly the whole tiles it
// returned -- same truncated-file contract as the old per-tile
// loop.
loaded = 0u;
for (i = 0; i < toLoad; i++) {
if (fread(outTiles[i].pixels, 1, ASSET_TILE_DATA_BYTES, fp) != ASSET_TILE_DATA_BYTES) {
while (loaded < toLoad) {
batch = (uint16_t)(toLoad - loaded);
if (batch > ASSET_TILE_READ_BATCH) {
batch = ASSET_TILE_READ_BATCH;
}
got = fread(outTiles[loaded].pixels, 1, (size_t)batch * TILE_BYTES, fp);
gotTiles = (uint16_t)(got / TILE_BYTES);
if (outValid != NULL) {
for (i = 0; i < gotTiles; i++) {
outValid[loaded + i] = true;
}
}
loaded = (uint16_t)(loaded + gotTiles);
if (gotTiles < batch) {
break;
}
}
fclose(fp);
return loaded;
}
// Streaming variant of jlTileBankLoad: pastes tiles onto dst's block
// grid as they arrive instead of filling a caller buffer, so the
// caller needs NO tile array at all (contract in joey/tile.h). The
// scratch is a small static -- 8 tiles per fread keeps the read count
// low without stressing the 65816 soft stack.
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst, uint16_t *outPalette) {
static jlTileT scratch[8];
FILE *fp;
uint8_t header[ASSET_HEADER_BYTES];
uint16_t tileCount;
uint16_t toLoad;
uint16_t loaded;
uint16_t batch;
uint16_t gotTiles;
uint16_t i;
uint8_t bx;
uint8_t by;
size_t got;
if (path == NULL || dst == NULL) {
return 0u;
}
if (!openReadValidateHeader(path, "JTB1", EXPECTED_TILE_TARGET, header, outPalette, &fp)) {
return 0u;
}
tileCount = (uint16_t)(header[6] | (header[7] << 8));
toLoad = (uint16_t)(TILE_BLOCKS_PER_ROW * TILE_BLOCKS_PER_COL);
if (tileCount < toLoad) {
toLoad = tileCount;
}
loaded = 0u;
while (loaded < toLoad) {
batch = (uint16_t)(toLoad - loaded);
if (batch > (uint16_t)(sizeof(scratch) / sizeof(scratch[0]))) {
batch = (uint16_t)(sizeof(scratch) / sizeof(scratch[0]));
}
got = fread(scratch[0].pixels, 1, (size_t)batch * TILE_BYTES, fp);
gotTiles = (uint16_t)(got / TILE_BYTES);
for (i = 0; i < gotTiles; i++) {
bx = (uint8_t)((loaded + i) % TILE_BLOCKS_PER_ROW);
by = (uint8_t)((loaded + i) / TILE_BLOCKS_PER_ROW);
jlTilePaste(dst, bx, by, &scratch[i]);
}
loaded = (uint16_t)(loaded + gotTiles);
if (gotTiles < batch) {
break;
}
if (outValid != NULL) {
outValid[i] = true;
}
loaded++;
}
fclose(fp);
return loaded;

View file

@ -5,26 +5,190 @@
#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
// interp()/mix arithmetic (delta * fracHalf up to ~8.4M, and the
// >> 15 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
// Prototypes (file-local, alphabetical).
static int interp(int8_t s0, int8_t s1, uint16_t frac);
static bool streamRefill(AudioSfxSlotT *slot);
void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) {
AudioSfxSlotT *slot;
const uint8_t *smp;
const int8_t *streamBuf;
uint32_t idx;
uint32_t stepIdx;
uint32_t length;
uint16_t frac;
uint16_t stepFrac;
uint16_t streamLen;
uint16_t i;
uint8_t s;
int sample;
int mixed;
int8_t s0;
int8_t s1;
for (s = 0; s < numSlots; s++) {
slot = &slots[s];
if (!slot->active) {
continue;
}
// Split the 16.16 step once per slot: the inner loops advance
// a uint32_t sample index + uint16_t fraction with a carry
// compare instead of 64-bit fixed-point math per sample.
stepIdx = slot->stepFp >> 16;
stepFrac = (uint16_t)(slot->stepFp & 0xFFFFu);
idx = slot->posIdx;
frac = slot->posFrac;
// The streaming and fixed-buffer cases are two separate inner
// loops so the loop-invariant isStream test runs once per slot
// and the slot fields stay cached in locals across the
// volatile dst stores (which would otherwise force per-sample
// re-reads); the short mix body is intentionally duplicated
// between them.
if (slot->fill != NULL) {
// Streaming: refill the prefetch buffer when drained.
streamBuf = slot->streamBuf;
streamLen = slot->streamLen;
for (i = 0; i < len; i++) {
if (idx >= streamLen) {
if (!streamRefill(slot)) {
slot->active = false;
break;
}
// streamRefill rewound the slot position; re-read
// the fields it updated.
idx = slot->posIdx;
frac = slot->posFrac;
streamLen = slot->streamLen;
}
s0 = streamBuf[idx];
s1 = (idx + 1u < streamLen) ? streamBuf[idx + 1u] : s0;
sample = interp(s0, s1, frac);
mixed = (int)dst[i] - 128 + sample;
if (mixed > 127) {
mixed = 127;
} else if (mixed < -128) {
mixed = -128;
}
dst[i] = (uint8_t)(mixed + 128);
frac = (uint16_t)(frac + stepFrac);
if (frac < stepFrac) {
idx++;
}
idx += stepIdx;
}
} else {
// Fixed buffer.
smp = slot->sample;
length = slot->length;
for (i = 0; i < len; i++) {
if (idx >= length) {
slot->active = false;
break;
}
s0 = (int8_t)smp[idx];
s1 = (idx + 1u < length) ? (int8_t)smp[idx + 1u] : s0;
sample = interp(s0, s1, frac);
mixed = (int)dst[i] - 128 + sample;
if (mixed > 127) {
mixed = 127;
} else if (mixed < -128) {
mixed = -128;
}
dst[i] = (uint8_t)(mixed + 128);
frac = (uint16_t)(frac + stepFrac);
if (frac < stepFrac) {
idx++;
}
idx += stepIdx;
}
}
slot->posIdx = idx;
slot->posFrac = frac;
}
}
void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) {
if (rateHz == 0 || outputRate == 0) {
return;
}
slot->sample = sample;
slot->length = length;
slot->posIdx = 0u;
slot->posFrac = 0u;
slot->stepFp = ((uint32_t)rateHz << 16) / outputRate;
slot->fill = NULL;
slot->fillCtx = NULL;
slot->streamLen = 0u;
slot->streamEof = false;
slot->active = true;
}
void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate) {
if (rateHz == 0 || outputRate == 0 || fill == NULL) {
return;
}
slot->sample = NULL;
slot->length = 0u;
slot->posIdx = 0u;
slot->posFrac = 0u;
slot->stepFp = ((uint32_t)rateHz << 16) / outputRate;
slot->fill = fill;
slot->fillCtx = ctx;
slot->streamLen = 0u;
slot->streamEof = false;
slot->active = true;
// Prime the first chunk so the mixer doesn't refill mid-mix on
// its very first sample.
(void)streamRefill(slot);
}
// Linear interpolation between two adjacent signed 8-bit samples.
// `frac` is the 16-bit fractional part of the read position; 0 =
// exactly at s0, 0xFFFF = nearly s1. Result is a signed int in
// [-128, +127].
//
// Formulated as a 16x16->32 signed multiply with explicit int16_t
// locals so m68k gcc matches its mulhisi3 pattern and emits a single
// muls.w instead of a __mulsi3 library call (~250-350 cycles per
// sample on the 68000). Dropping frac's low bit means the result can
// differ from the exact delta * frac >> 16 by at most 1 LSB of 8-bit
// audio -- an accepted, inaudible deviation. The >> 15 of a possibly-
// negative int32_t relies on arithmetic shift, which holds on every
// target this file builds for (DOS/ST).
static int interp(int8_t s0, int8_t s1, uint16_t frac) {
int16_t delta;
int16_t fracHalf;
delta = (int16_t)(s1 - s0);
fracHalf = (int16_t)(uint16_t)(frac >> 1);
return (int)s0 + (int)(((int32_t)delta * fracHalf) >> 15);
}
// 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..]. 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).
// existed) the position 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 the position 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).
@ -59,123 +223,13 @@ static bool streamRefill(AudioSfxSlotT *slot) {
// read interpolates streamBuf[0] (old last sample) -> [1]
// (new first sample), replaying one sample with no audible
// discontinuity click.
slot->posFp = 0;
slot->posIdx = 0u;
slot->posFrac = 0u;
} 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;
slot->posIdx = 1u;
slot->posFrac = 0u;
}
return true;
}
// Linear interpolation between two adjacent signed 8-bit samples.
// `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) {
int32_t delta;
// 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);
}
void audioSfxOverlayMix(volatile uint8_t *dst, uint16_t len, AudioSfxSlotT *slots, uint8_t numSlots) {
AudioSfxSlotT *slot;
uint64_t pos;
uint32_t step;
uint32_t idx;
uint32_t frac;
uint16_t i;
uint8_t s;
int sample;
int mixed;
int8_t s0;
int8_t s1;
bool isStream;
for (s = 0; s < numSlots; s++) {
slot = &slots[s];
if (!slot->active) {
continue;
}
pos = slot->posFp;
step = slot->stepFp;
isStream = (slot->fill != NULL);
for (i = 0; i < len; i++) {
idx = (uint32_t)(pos >> 16);
frac = (uint32_t)(pos & 0xFFFFu);
if (isStream) {
// Streaming: refill the prefetch buffer when drained.
if (idx >= slot->streamLen) {
if (!streamRefill(slot)) {
slot->active = false;
break;
}
pos = slot->posFp;
idx = (uint32_t)(pos >> 16);
frac = (uint32_t)(pos & 0xFFFFu);
}
s0 = slot->streamBuf[idx];
s1 = (idx + 1u < slot->streamLen) ? slot->streamBuf[idx + 1u] : s0;
} else {
// Fixed buffer.
if (idx >= slot->length) {
slot->active = false;
break;
}
s0 = (int8_t)slot->sample[idx];
s1 = (idx + 1u < slot->length) ? (int8_t)slot->sample[idx + 1u] : s0;
}
sample = interp(s0, s1, frac);
mixed = (int)dst[i] - 128 + sample;
if (mixed > 127) {
mixed = 127;
} else if (mixed < -128) {
mixed = -128;
}
dst[i] = (uint8_t)(mixed + 128);
pos += (uint64_t)step;
}
slot->posFp = pos;
}
}
void audioSfxSlotArm(AudioSfxSlotT *slot, const uint8_t *sample, uint32_t length, uint16_t rateHz, uint32_t outputRate) {
if (rateHz == 0 || outputRate == 0) {
return;
}
slot->sample = sample;
slot->length = length;
slot->posFp = 0;
slot->stepFp = ((uint32_t)rateHz << 16) / outputRate;
slot->fill = NULL;
slot->fillCtx = NULL;
slot->streamLen = 0u;
slot->streamEof = false;
slot->active = true;
}
void audioSfxSlotArmStream(AudioSfxSlotT *slot, jlAudioStreamFillT fill, void *ctx, uint16_t rateHz, uint32_t outputRate) {
if (rateHz == 0 || outputRate == 0 || fill == NULL) {
return;
}
slot->sample = NULL;
slot->length = 0u;
slot->posFp = 0;
slot->stepFp = ((uint32_t)rateHz << 16) / outputRate;
slot->fill = fill;
slot->fillCtx = ctx;
slot->streamLen = 0u;
slot->streamEof = false;
slot->active = true;
// Prime the first chunk so the mixer doesn't refill mid-mix on
// its very first sample.
(void)streamRefill(slot);
}

View file

@ -32,13 +32,15 @@
typedef struct {
const uint8_t *sample; // fixed buffer: signed 8-bit PCM, caller-owned. NULL for streaming.
uint32_t length; // fixed buffer: sample bytes. Unused for streaming.
// 48.16 fixed-point read position (uint64_t). The wider counter
// lifts the old uint32_t cap (~65 535 input samples = 3 sec at
// 22 kHz / 8 sec at 8 kHz before pos wraps and the slot loops
// forever); AGI v2 sounds run 15-48 sec and need the headroom.
// For streaming slots, pos is RELATIVE to the current chunk
// (resets on each refill).
uint64_t posFp;
// Read position split as a 32-bit sample index plus a 16-bit
// fraction so the per-sample advance is 16/32-bit adds and a
// carry compare -- no 64-bit fixed-point ops on the 68000. The
// uint32_t index covers 4G input samples, far beyond the old
// 48.16 counter's rationale (AGI v2 sounds run 15-48 sec and
// need > 65535 samples). For streaming slots the position is
// RELATIVE to the current chunk (resets on each refill).
uint32_t posIdx;
uint16_t posFrac;
uint32_t stepFp; // (rateHz << 16) / outputRate
bool active;

View file

@ -17,6 +17,7 @@
#include <iigs/toolbox.h>
#define attrPage 0x0004
#define attrNoCross 0x0010
#define attrNoSpec 0x0020
#define attrFixed 0x4000
#define attrLocked 0x8000
#endif
@ -226,6 +227,15 @@ bool codegenArenaInit(uint32_t totalBytes) {
return false;
}
#if defined(JOEYLIB_PLATFORM_IIGS)
// attrNoCross forbids the block from crossing a 64KB bank boundary,
// so any request larger than one bank (65536 bytes) is unsatisfiable
// and NewHandle would always return NULL. Clamp to a single bank.
// The clamped request can still fail below -- a 64KB attrNoCross
// block needs a completely free bank -- and the caller's error
// message covers that.
if (totalBytes > 65536UL) {
totalBytes = 65536UL;
}
// 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.

View file

@ -1,35 +1,151 @@
// Cross-platform "where did it hang?" logger. Holds joeylog.txt open
// 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.
// Cross-platform "where did it hang?" logger.
//
// 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.
// On DOS/Amiga/ST: joeylog.txt is held open across calls and FLUSHED
// 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.
//
// (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.)
// On the IIgs the per-line-flush design is a triple failure
// (PERF-AUDIT.md #80/#82): the libc fflush does not commit through the
// GS/OS FST (nothing persists until fclose), each real FST write costs
// seconds of emulated floppy time, and a write can wedge indefinitely
// inside SmartPort firmware retries -- one such wedge stalled the UBER
// benchmark for 18+ emulated minutes. So on IIgs the lines land in a
// RAM ring instead: gJoeyLogRing/gJoeyLogRingHead are non-static so
// headless-MAME Lua probes can drain the log LIVE from emulated memory
// (scripts/bench-iigs.sh), and jlLogFlush() writes the accumulated ring
// to joeylog.txt in one open/write/close pass (fclose is the only
// operation that actually commits) for on-hardware persistence.
#include <stdio.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include "joey/platform.h"
#include "joey/debug.h"
static const char *kLogPath = "joeylog.txt";
static FILE *gLogFp = NULL;
#ifdef JOEYLIB_PLATFORM_IIGS
// Ring capacity. Head is a free-running total-bytes-written counter
// (uint16_t, wraps at 65536); the ring index is head % size, so the
// size must divide 65536 for the wrap to stay aligned. A Lua reader
// tracks its own last-read count and detects overruns by delta > size.
#define JOEY_LOG_RING_BYTES 8192u
uint8_t gJoeyLogRing[JOEY_LOG_RING_BYTES];
uint16_t gJoeyLogRingHead = 0;
// ----- Prototypes -----
static void ringAppend(const char *text);
static void ringAppend(const char *text) {
uint16_t head;
uint16_t idx;
// Explicit read-modify-write of the global (never `++`/`+=` on a
// global -- the 65816 inc-abs/DBR trap).
head = gJoeyLogRingHead;
while (*text != '\0') {
idx = (uint16_t)(head & (JOEY_LOG_RING_BYTES - 1u));
gJoeyLogRing[idx] = (uint8_t)*text;
text++;
head = (uint16_t)(head + 1u);
}
gJoeyLogRingHead = head;
}
void jlLog(const char *msg) {
if (msg == NULL) {
return;
}
ringAppend(msg);
ringAppend("\n");
}
void jlLogF(const char *fmt, ...) {
char line[200];
va_list args;
if (fmt == NULL) {
return;
}
va_start(args, fmt);
(void)vsnprintf(line, sizeof(line), fmt, args);
va_end(args);
ringAppend(line);
// Callers' formats mostly end in \n already; jlLog adds one for
// plain strings, so nothing is appended here.
}
// One-shot dump of everything the ring has seen to joeylog.txt.
// open/write/CLOSE per call: fclose is the only point the FST commits
// (#80), and batching all lines into one pass keeps the floppy cost to
// a single visit instead of one per line. Mode "w": the dump always
// contains the ENTIRE surviving ring, so truncate-on-flush both makes
// repeated flushes idempotent and lets jlLogReset stay disk-free (see
// below).
void jlLogFlush(void) {
FILE *fp;
uint16_t head;
uint16_t start;
uint16_t count;
uint16_t i;
uint16_t idx;
head = gJoeyLogRingHead;
if (head == 0u) {
return;
}
fp = fopen(kLogPath, "w");
if (fp == NULL) {
return;
}
// If the ring wrapped, the oldest surviving byte is head - size.
count = head;
start = 0;
if (count > JOEY_LOG_RING_BYTES) {
start = (uint16_t)(head - JOEY_LOG_RING_BYTES);
count = JOEY_LOG_RING_BYTES;
}
for (i = 0; i < count; i++) {
idx = (uint16_t)((uint16_t)(start + i) & (JOEY_LOG_RING_BYTES - 1u));
fputc((int)gJoeyLogRing[idx], fp);
}
fclose(fp);
}
// Ring-only: NO disk I/O here. The old truncate write (fopen "w" +
// fclose) was the first floppy WRITE any app performed and wedged
// forever in SmartPort firmware retries under headless MAME -- the
// same #82 failure class the RAM ring exists to avoid. UBER never
// calls jlLogReset, so the write path lay unexercised until the
// spacetaxi acceptance test hit it (hang before the first log line,
// PC spinning in the ROM WAIT loop). jlLogFlush opens with "w" now,
// so the on-disk file still ends up truncated-to-current on the next
// flush.
void jlLogReset(void) {
gJoeyLogRingHead = 0;
}
#else /* not JOEYLIB_PLATFORM_IIGS */
static FILE *gLogFp = NULL;
/* 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
* coalesce into one host 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. */
* long line across multiple writes. */
#define JOEY_LOG_BUF_BYTES 16384
static char gLogBuf[JOEY_LOG_BUF_BYTES];
static char gLogBuf[JOEY_LOG_BUF_BYTES];
/* Lazy-open. Returns NULL if the open failed (silently disable). */
@ -98,3 +214,5 @@ void jlLogReset(void) {
}
}
}
#endif /* JOEYLIB_PLATFORM_IIGS */

View file

@ -21,12 +21,18 @@
// 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)
// Flood-fill fallback pixel read. On chunky ports (no
// JL_HAS_SAMPLE_PIXEL override) the nibble read inlines via
// SURFACE_READ_NIBBLE instead of paying the jlpGenericSamplePixel
// function hop per walked pixel (PERF-AUDIT #14/#24); planar ports keep
// their platform jlpSamplePixel (plane decode). Every call site passes
// side-effect-free arguments -- SURFACE_READ_NIBBLE evaluates its x
// argument twice.
#if !defined(JL_HAS_SAMPLE_PIXEL)
#define floodReadPixel(_s, _x, _y) SURFACE_READ_NIBBLE((_s), (_x), (_y))
#else
#define floodReadPixel(_s, _x, _y) jlpSamplePixel((_s), (_x), (_y))
#endif
// ----- Prototypes -----
@ -88,7 +94,6 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
int16_t leftX;
int16_t rightX;
uint8_t pix;
bool pixMatch;
uint8_t newNibble;
newNibble = (uint8_t)(newColor & 0x0F);
@ -136,7 +141,8 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
// Walk-out: find the matching run containing the seed. Planar ports
// (Amiga) override with a plane-aware walk; everyone else uses the
// portable jlpSamplePixel walk, which works on chunky and planar.
// portable floodReadPixel walk (inline nibble read on chunky,
// platform jlpSamplePixel on planar).
{
#if defined(JL_HAS_FLOOD_WALK_PLANES)
bool seedMatched;
@ -148,38 +154,64 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
} else
#endif
{
pix = jlpSamplePixel(s, x, y);
pixMatch = (pix == matchColor);
// matchEqual is loop-invariant across the whole fill, so
// fork the walk ONCE per seed pop instead of re-branching
// on it per walked pixel (PERF-AUDIT #35). The two
// variants differ only in the stop test -- (pix !=
// matchColor) vs (pix == matchColor || pix == newNibble)
// -- and the span fill / scan / push code below stays
// shared, so the duplication is confined to these small
// walk loops.
if (matchEqual) {
if (!pixMatch) {
pix = floodReadPixel(s, x, y);
if (pix != matchColor) {
continue;
}
// Walk left to find the start of the matching run.
leftX = x;
while (leftX > 0) {
pix = floodReadPixel(s, (int16_t)(leftX - 1), y);
if (pix != matchColor) {
break;
}
leftX--;
}
// Walk right to find the end.
rightX = x;
while (rightX < SURFACE_WIDTH - 1) {
pix = floodReadPixel(s, (int16_t)(rightX + 1), y);
if (pix != matchColor) {
break;
}
rightX++;
}
} else {
if (pixMatch || pix == newNibble) {
pix = floodReadPixel(s, x, y);
if (pix == matchColor || pix == newNibble) {
continue;
}
}
// Walk left to find the start of the matching run.
leftX = x;
while (leftX > 0) {
pix = jlpSamplePixel(s, (int16_t)(leftX - 1), y);
pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break;
// Walk left to find the start of the fillable run.
leftX = x;
while (leftX > 0) {
pix = floodReadPixel(s, (int16_t)(leftX - 1), y);
if (pix == matchColor || pix == newNibble) {
break;
}
leftX--;
}
leftX--;
}
// Walk right to find the end.
rightX = x;
while (rightX < SURFACE_WIDTH - 1) {
pix = jlpSamplePixel(s, (int16_t)(rightX + 1), y);
pixMatch = (pix == matchColor);
if (matchEqual ? !pixMatch : (pixMatch || pix == newNibble)) {
break;
// Walk right to find the end.
rightX = x;
while (rightX < SURFACE_WIDTH - 1) {
pix = floodReadPixel(s, (int16_t)(rightX + 1), y);
if (pix == matchColor || pix == newNibble) {
break;
}
rightX++;
}
rightX++;
}
}
}
@ -201,7 +233,7 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
// 1/0 per pixel, then walk it for run-edge transitions and push a
// seed at each run's right edge. Planar ports (Amiga) override the
// markBuf fill with a plane-aware scan; everyone else fills it via
// the portable jlpSamplePixel walk.
// the portable floodReadPixel walk.
{
int16_t i;
int16_t spanLen;
@ -232,13 +264,20 @@ static void floodFillInternal(jlSurfaceT *s, int16_t startX, int16_t startY, uin
true
#endif
) {
// C fallback: fill markBuf the slow way.
for (i = 0; i < spanLen; i++) {
pix = jlpSamplePixel(s, (int16_t)(leftX + i), scanY);
pixMatch = (pix == matchColor);
floodMarkBuf[i] = (uint8_t)(matchEqual
? (pixMatch ? 1 : 0)
: ((!pixMatch && pix != newNibble) ? 1 : 0));
// C fallback: fill markBuf the slow way. Same
// loop-invariant matchEqual fork as the walk-out
// above (PERF-AUDIT #35) -- one branch per row scan
// instead of one per scanned pixel.
if (matchEqual) {
for (i = 0; i < spanLen; i++) {
pix = floodReadPixel(s, (int16_t)(leftX + i), scanY);
floodMarkBuf[i] = (uint8_t)((pix == matchColor) ? 1 : 0);
}
} else {
for (i = 0; i < spanLen; i++) {
pix = floodReadPixel(s, (int16_t)(leftX + i), scanY);
floodMarkBuf[i] = (uint8_t)((pix != matchColor && pix != newNibble) ? 1 : 0);
}
}
}
// Walk markBuf for run-edge transitions.
@ -267,15 +306,17 @@ 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, jlpDrawPixel dispatch, and chunky nibble RMW fallback.
// Plot a single clipped pixel WITHOUT marking the dirty rect. Used by
// the Bresenham line/circle fallbacks, which mark one precomputed
// bounding box after their loop (the public jlDrawPixel clips inline
// and calls jlpDrawPixel directly on every platform).
//
// CONTRACT (PERF-AUDIT #61): s is non-NULL -- both callers
// (jlDrawCircle, jlDrawLine) validate it once at API entry, so there
// is no per-pixel NULL re-check here. The per-pixel BOUNDS check is
// load-bearing and stays: it IS the off-surface clipping for these
// fallbacks, matching jlDrawPixel's clip.
static void plotPixelNoMark(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
if (s == NULL) {
return;
}
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return;
}
@ -291,10 +332,10 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
int16_t x;
int16_t y;
int16_t err;
int16_t minX;
int16_t minY;
int16_t maxX;
int16_t maxY;
int32_t minX;
int32_t minY;
int32_t maxX;
int32_t maxY;
if (s == NULL) {
return;
@ -325,31 +366,49 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
}
#endif
// Known limitation (PERF-AUDIT #12): for r >= 32768 the (int16_t)r
// cast below would wrap negative, `while (x >= y)` would be false
// immediately, and the outline draws NOTHING. That is silent but
// harmless: no pixel is plotted, so nothing must be marked either
// -- return explicitly (the old accumulated box stayed empty and
// collapsed to the same no-op). A huge-r outline CAN legitimately
// cross the surface (e.g. cx=-32768, r=33000 passes through
// x ~= 232), but drawing it correctly needs 32-bit Bresenham state
// throughout; that fix is deliberately applied only to
// jlFillCircle, where coverage (not the perimeter) is what
// matters. Accepted as a documented no-op here.
if (r > INT16_MAX) {
return;
}
// Bresenham midpoint: maintain (x, y) on the perimeter, eight-
// 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;
// cross-segment dirty mark per pixel; one bounding box is marked
// after the loop instead.
//
// The dirty box is statically known BEFORE the loop (PERF-AUDIT
// #36/#37): the first iteration runs with x == r, y == 0 and its
// eight reflection folds set exactly [cx - r, cx + r] x
// [cy - r, cy + r]; no later iteration can widen it (both offsets
// never exceed r), and the old accumulation folded every
// reflection whether or not its pixel landed on-surface. So the
// precomputed box, clamped after the loop exactly as before, marks
// the identical region the deleted per-iteration folds produced --
// including the collapse to a no-op mark for a fully off-surface
// circle. Computed in int32: cx +/- r overflows int16 for centers
// near the int16 limits, and an int16 wrap here could under-mark
// (stale rows on present), which clamping int32 values can never
// do.
minX = (int32_t)cx - (int32_t)r;
minY = (int32_t)cy - (int32_t)r;
maxX = (int32_t)cx + (int32_t)r;
maxY = (int32_t)cy + (int32_t)r;
x = (int16_t)r;
y = 0;
err = (int16_t)(1 - x);
while (x >= y) {
// 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);
@ -369,15 +428,17 @@ void jlDrawCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
}
}
// 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.
// Clamp the precomputed box to the surface and mark it once. For a
// circle entirely off one side of the surface the clamp leaves
// min > max on that axis and nothing is marked (same collapse the
// old accumulated box produced). The clamped values fit int16_t,
// so the narrowing casts into the mark are safe.
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));
surfaceMarkDirtyRect(s, (int16_t)minX, (int16_t)minY, (int16_t)(maxX - minX + 1), (int16_t)(maxY - minY + 1));
}
}
@ -411,10 +472,20 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
}
// 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.
// subtraction would otherwise promote to on a 16-bit-int target.
// Clip the START into range BEFORE clamping the length: clamping
// first anchored a surface-width span at a far-off-surface x0,
// so a line crossing the whole visible row clipped to nothing
// (PERF-AUDIT #72). The final clamp keeps the downstream
// uint16_t narrowing safe.
span = (int32_t)x1 - (int32_t)x0 + 1;
if (x0 < 0) {
span = span + x0;
x0 = 0;
}
if (span <= 0) {
return;
}
if (span > SURFACE_WIDTH) {
span = SURFACE_WIDTH;
}
@ -436,7 +507,16 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
y0 = y1;
y1 = tmp;
}
// Same start-clip-then-clamp ordering as the horizontal path
// (PERF-AUDIT #72), mirrored for y0/SURFACE_HEIGHT.
span = (int32_t)y1 - (int32_t)y0 + 1;
if (y0 < 0) {
span = span + y0;
y0 = 0;
}
if (span <= 0) {
return;
}
if (span > SURFACE_HEIGHT) {
span = SURFACE_HEIGHT;
}
@ -469,22 +549,31 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
// 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;
// per-pixel cross-segment dirty mark; one bounding box is marked once
// after the loop.
//
// The dirty box is statically known BEFORE the loop (PERF-AUDIT
// #9/#37): Bresenham walks the monotone path from (x0, y0) to
// (x1, y1) and terminates exactly at (x1, y1), so the visited-point
// bounding box is the endpoint bounding box -- the same four
// ternaries the JL_HAS_DRAW_LINE fast path computes. The old
// per-pixel accumulation folded every VISITED point (clipped or
// not), so after the post-loop clamp this precomputed box marks the
// identical region it did. For endpoints running off-surface the
// clamped endpoint box is a SUPERSET of the box of the actually
// PLOTTED (clipped) pixels, which is safe: over-marking only makes
// present copy more; under-marking would leave stale rows. Computed
// from the ORIGINAL endpoints here, before the loop mutates x0/y0.
minX = (x0 < x1) ? x0 : x1;
minY = (y0 < y1) ? y0 : y1;
maxX = (x0 > x1) ? x0 : x1;
maxY = (y0 > y1) ? y0 : y1;
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) {
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;
@ -500,10 +589,10 @@ void jlDrawLine(jlSurfaceT *s, int16_t x0, int16_t y0, int16_t x1, int16_t y1, u
}
}
// 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.
// Clamp the endpoint box to the surface and mark it once. For a line
// entirely off one side of the surface the clamp leaves min > max on
// that axis and nothing is marked -- exactly how the old accumulated
// box (which held the same raw endpoint extents) collapsed.
if (minX < 0) { minX = 0; }
if (minY < 0) { minY = 0; }
if (maxX > SURFACE_WIDTH - 1) { maxX = SURFACE_WIDTH - 1; }
@ -518,36 +607,30 @@ void jlDrawPixel(jlSurfaceT *s, int16_t x, int16_t y, uint8_t colorIndex) {
if (s == NULL) {
return;
}
#ifdef JOEYLIB_PLATFORM_IIGS
// Hot per-pixel path: the asm plot (jlpDrawPixel -> 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.
// Clip BEFORE plotting and marking (the unsigned casts fold both
// bounds tests per axis). Marking with the raw coords after
// plotPixelNoMark silently clipped violated surfaceMarkDirtyRect's
// already-clipped contract and scribbled outside the stage
// dirty-band arrays for off-surface draws (PERF-AUDIT #1). Calling
// jlpDrawPixel directly also drops plotPixelNoMark's redundant
// bounds re-check on this path; the helper itself stays for the
// Bresenham fallbacks.
//
// This is ONE body for every port: jlpDrawPixel is the compile-time
// selected plot (IIgs asm / planar / generic chunky RMW), and the
// surfaceMarkDirtyRect macro runs the stage compare (direct gStage
// load, no jlStageGet() hop -- PERF-AUDIT #10/#17) and the
// single-row widen inline with no call layers (PERF-AUDIT #20),
// which is exactly the code the old hand-written IIgs branch spelled
// out. NB (IIgs): an all-C inline RMW plot here (gRowOffsetLut, 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.
if ((uint16_t)x >= SURFACE_WIDTH || (uint16_t)y >= SURFACE_HEIGHT) {
return;
}
jlpDrawPixel(s, (uint16_t)x, (uint16_t)y, colorIndex);
if (s == jlStageGet()) {
uint8_t word = SURFACE_WORD_INDEX(x);
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
}
@ -568,38 +651,78 @@ void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
// 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) {
// on-surface tail. The shared 16-bit predicate (PERF-AUDIT #62)
// is semantically identical to the 32-bit x + w <= SURFACE_WIDTH
// test it replaces for every rect that reaches it: w/h >= 2 here
// (w == 0 / h == 0 returned above, w == 1 / h == 1 routed to
// jlFillRect, satisfying the macro's w/h >= 1 contract); any
// w > SURFACE_WIDTH -- w == 65535 included -- fails the macro's
// guarded first compare exactly as x + w <= SURFACE_WIDTH would
// for x >= 0; and for w <= SURFACE_WIDTH the guarded subtraction
// cannot wrap, so x <= SURFACE_WIDTH - w IS x + w <= SURFACE_WIDTH
// (boundary proof at the macro definition: w == 320 passes only
// x == 0; x == 319 / w == 1 passes; x == -1 fails).
if (SURFACE_RECT_ON_SURFACE(x, y, w, h)) {
// 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);
// Interior vertical edges only exist when h > 2 (an h == 2 rect
// is just the two horizontal edges). Skipping them keeps h - 2
// == 0 rects out of fillRectOnSurface, whose contract requires
// positive dimensions (PERF-AUDIT #65 verifier correction).
if (h > 2) {
// 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.
jlFillRect(s, x, (int16_t)(y + (int16_t)h - 1), w, 1, colorIndex);
// Left edge (interior only -- top and bottom corners already drawn).
jlFillRect(s, x, (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex);
// Right edge (interior only).
jlFillRect(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (uint16_t)(h - 2), colorIndex);
// Edge coordinates are computed in 32-bit: w and h are uint16_t, so
// y + h - 1 (or x + w - 1) can reach 65733, and narrowing that
// through an int16_t cast wrapped it back on-surface, drawing
// phantom edges (PERF-AUDIT #11). An edge whose true coordinate is
// off-surface is skipped instead -- exactly what jlFillRect's clip
// would do if it received the unwrapped value.
{
int32_t bottom;
int32_t right;
int32_t innerY;
bottom = (int32_t)y + (int32_t)h - 1;
right = (int32_t)x + (int32_t)w - 1;
innerY = (int32_t)y + 1;
// Top edge.
jlFillRect(s, x, y, w, 1, colorIndex);
// Bottom edge.
if (bottom >= 0 && bottom < SURFACE_HEIGHT) {
jlFillRect(s, x, (int16_t)bottom, w, 1, colorIndex);
}
// Interior vertical edges: skip when h == 2 (same contract note
// as the fast path above) and when their top row is already
// below the surface (innerY can be 32768 for y == INT16_MAX --
// the int16_t cast would wrap it on-surface).
if (h > 2 && innerY < SURFACE_HEIGHT) {
// Left edge (interior only -- top and bottom corners already drawn).
jlFillRect(s, x, (int16_t)innerY, 1, (uint16_t)(h - 2), colorIndex);
// Right edge (interior only).
if (right >= 0 && right < SURFACE_WIDTH) {
jlFillRect(s, (int16_t)right, (int16_t)innerY, 1, (uint16_t)(h - 2), colorIndex);
}
}
}
}
void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t colorIndex) {
int16_t y;
int16_t x;
int16_t ir;
int32_t x;
int32_t yLo;
int32_t yHi;
int32_t lim;
uint16_t y;
uint32_t xx;
uint32_t yy;
uint32_t r2;
@ -616,16 +739,20 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
// 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.
// asm / dirty-rect garbage. Reject it here before the cast. ir's
// init is only for the compiler's flow analysis -- it is read only
// under onSurface, which guarantees the real assignment below ran.
onSurface = false;
ir = 0;
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.
// route spans through the raw jlpFillRect (one batched dirty
// mark after the loop) instead of re-clipping and re-marking
// each one through jlFillRect.
onSurface = true;
#if defined(JL_HAS_FILL_CIRCLE)
// Stage/portData-gated fast path (returns bool). Compiled out where
@ -639,43 +766,180 @@ void jlFillCircle(jlSurfaceT *s, int16_t cx, int16_t cy, uint16_t r, uint8_t col
}
}
// 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.
r2 = (uint32_t)r * (uint32_t)r;
// Full-coverage test (PERF-AUDIT #12): if the farthest surface
// corner from (cx, cy) lies inside the disk, the circle covers the
// whole surface -- fill it in one call. This is also the only path
// that can produce the correct full fill for r >= 32768, which the
// old (int16_t)r cast wrapped negative (drawing nothing). Skipped
// when onSurface: a fully on-surface circle can never reach a
// corner, so the test could not pass. All math is 32-bit: the
// farthest corner delta is at most 33087 per axis, so the squared
// sum (~2.2e9) and r*r (~4.3e9) both fit uint32_t.
if (!onSurface) {
int32_t farDx;
int32_t farDy;
farDx = (int32_t)cx;
if (farDx < 0) {
farDx = -farDx;
}
lim = (int32_t)(SURFACE_WIDTH - 1) - (int32_t)cx;
if (lim < 0) {
lim = -lim;
}
if (lim > farDx) {
farDx = lim;
}
farDy = (int32_t)cy;
if (farDy < 0) {
farDy = -farDy;
}
lim = (int32_t)(SURFACE_HEIGHT - 1) - (int32_t)cy;
if (lim < 0) {
lim = -lim;
}
if (lim > farDy) {
farDy = lim;
}
if ((uint32_t)farDx * (uint32_t)farDx + (uint32_t)farDy * (uint32_t)farDy <= r2) {
jlFillRect(s, 0, 0, SURFACE_WIDTH, SURFACE_HEIGHT, colorIndex);
return;
}
}
// Clamp the span loop to the y offsets whose rows can intersect the
// surface (PERF-AUDIT #12): row cy + y is on-surface for y in
// [-cy, SURFACE_HEIGHT-1 - cy] and row cy - y for y in
// [cy - (SURFACE_HEIGHT-1), cy], so the union hull is
// yLo = max(0, -cy, cy - (SURFACE_HEIGHT-1)) and
// yHi = min(r, max(cy, SURFACE_HEIGHT-1 - cy)). Computed once in
// int32; for a fully on-surface circle this yields exactly [0, r].
// A huge clipped r now costs at most SURFACE_HEIGHT iterations
// instead of r+1.
yLo = -(int32_t)cy;
lim = (int32_t)cy - (int32_t)(SURFACE_HEIGHT - 1);
if (lim > yLo) {
yLo = lim;
}
if (yLo < 0) {
yLo = 0;
}
yHi = (int32_t)cy;
lim = (int32_t)(SURFACE_HEIGHT - 1) - (int32_t)cy;
if (lim > yHi) {
yHi = lim;
}
if (yHi > (int32_t)r) {
yHi = (int32_t)r;
}
if (yLo > yHi) {
return;
}
// For each y offset from yLo to yHi, 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. 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).
// overflows at r == 256 -> r2 == 0). yy is seeded with yLo*yLo (the
// incremental chain breaks on a jump), and the first iteration's
// while walks x down from r to the yLo row's span in one go -- O(r)
// once per call, accepted for this cold clipped path. The walk
// condition is xx > r2 - yy rather than xx + yy > r2 because xx +
// yy can exceed uint32_t range for r >= 32768 (yy <= r2 always, so
// the subtraction never underflows). The loop variable is uint16_t:
// yHi <= max(cy, SURFACE_HEIGHT-1 - cy) <= 32967, so y never wraps.
/* 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 = (uint32_t)r * (uint32_t)r;
r2 = xx;
yy = 0;
x = (int16_t)r;
for (y = 0; y <= (int16_t)r; y++) {
uint16_t spanWidth;
* because both span fills in each branch need it. */
xx = r2;
yy = (uint32_t)yLo * (uint32_t)yLo;
x = (int32_t)r;
for (y = (uint16_t)yLo; y <= (uint16_t)yHi; y++) {
uint32_t xxLimit;
while (xx + yy > r2) {
xx = xx - (uint32_t)((uint16_t)x + (uint16_t)x - 1u);
xxLimit = r2 - yy;
while (xx > xxLimit) {
xx = xx - ((uint32_t)x + (uint32_t)x - 1u);
x--;
}
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
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);
// jlFillRect's clip would be a no-op -- skip it. x <= r <
// SURFACE_HEIGHT here, so the int16_t narrowing is safe.
//
// Spans go through the RAW fill op, not fillRectOnSurface
// (which is jlpFillRect + surfaceMarkDirtyRect), so the
// per-span dirty mark is batched into ONE bounding-square
// mark after the loop (PERF-AUDIT #38). jlpFillRect is the
// planar-aware op on planar ports (the JL_HAS_FILL_RECT
// function override writes the bitplanes), so calling it
// directly preserves the planar write -- pixel output is
// bit-identical to the fillRectOnSurface routing.
uint16_t spanWidth;
spanWidth = (uint16_t)((uint16_t)x + (uint16_t)x + 1u);
jlpFillRect(s, (int16_t)(cx - (int16_t)x), (int16_t)(cy + (int16_t)y), (int16_t)spanWidth, 1, colorIndex);
if (y > 0) {
fillRectOnSurface(s, (int16_t)(cx - x), (int16_t)(cy - y), (int16_t)spanWidth, 1, colorIndex);
jlpFillRect(s, (int16_t)(cx - (int16_t)x), (int16_t)(cy - (int16_t)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);
// Clipped case: clip the span in 32-bit HERE instead of
// narrowing cx - x / 2x + 1 into jlFillRect's int16/uint16
// parameters -- for huge r those narrowings wrap (e.g.
// cx=-32000, r=32767 puts cx - x at -64767, which wrapped
// back on-surface) and either drop visible spans or draw
// phantom ones. The row test replaces jlFillRect's y clip
// the same way. For spans that never wrapped this is
// bit-identical to the old jlFillRect routing (same clip,
// same fillRectOnSurface tail).
int32_t spanLeft;
int32_t spanRight;
int32_t row;
spanLeft = (int32_t)cx - x;
spanRight = (int32_t)cx + x;
if (spanLeft < 0) {
spanLeft = 0;
}
if (spanRight > (int32_t)(SURFACE_WIDTH - 1)) {
spanRight = (int32_t)(SURFACE_WIDTH - 1);
}
if (spanLeft <= spanRight) {
int16_t spanWidth;
spanWidth = (int16_t)(spanRight - spanLeft + 1);
row = (int32_t)cy + (int32_t)y;
if (row >= 0 && row < SURFACE_HEIGHT) {
fillRectOnSurface(s, (int16_t)spanLeft, (int16_t)row, spanWidth, 1, colorIndex);
}
if (y > 0) {
row = (int32_t)cy - (int32_t)y;
if (row >= 0 && row < SURFACE_HEIGHT) {
fillRectOnSurface(s, (int16_t)spanLeft, (int16_t)row, spanWidth, 1, colorIndex);
}
}
}
}
yy = yy + (uint32_t)((uint16_t)y + (uint16_t)y + 1u);
yy = yy + ((uint32_t)y + (uint32_t)y + 1u);
}
// One bounding-square dirty mark for the whole on-surface disk
// (PERF-AUDIT #38) instead of 2r+1 per-span marks. onSurface
// guarantees the loop above ran the full y = 0..r range (yLo/yHi
// resolve to exactly [0, r] for a fully on-surface circle), so the
// square covers every filled span. Rows away from the center are
// over-marked out to the full circle width -- exactly the mark the
// JL_HAS_FILL_CIRCLE fast path issues -- and over-marking is safe
// (present copies more; under-marking would leave stale rows). The
// clipped (!onSurface) spans above keep their per-span marks via
// fillRectOnSurface.
if (onSurface) {
surfaceMarkDirtyRect(s, (int16_t)(cx - ir), (int16_t)(cy - ir), (int16_t)(2 * ir + 1), (int16_t)(2 * ir + 1));
}
}
@ -694,12 +958,31 @@ void jlFillRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
return;
}
// 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.
// 16-bit on-surface fast path (PERF-AUDIT #13): the overwhelmingly
// common case (HUD fills, tile-sized fills) is fully on-surface and
// needs no 32-bit math -- every int32 op below is a multi-
// instruction carry-chained sequence on the 16-bit CPUs. The shared
// predicate (PERF-AUDIT #62) expands to exactly the guarded compare
// chain this test spelled out inline -- w/h upper bounds first so
// the SURFACE_WIDTH - w subtraction cannot wrap for w up to 65535,
// then the x/y compares (boundary proof at the macro definition:
// w == 65535 fails the guard; w == 320 passes only x == 0;
// x == 319 / w == 1 passes; x == -1 fails). w > 0 && h > 0 keeps
// empty rects out of fillRectOnSurface, whose contract requires
// positive dimensions (and satisfies the macro's w/h >= 1
// contract); they fall through to the slow path's empty-clip
// return instead.
if (w > 0 && h > 0 && SURFACE_RECT_ON_SURFACE(x, y, w, h)) {
fillRectOnSurface(s, x, y, (int16_t)w, (int16_t)h, colorIndex);
return;
}
// Slow path: 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;
@ -770,10 +1053,19 @@ uint8_t jlSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
if (x < 0 || x >= SURFACE_WIDTH || y < 0 || y >= SURFACE_HEIGHT) {
return 0;
}
/* jlpSamplePixel reads from whichever storage the port uses --
* chunky ports return a nibble extracted from s->pixels; planar
* ports read 4 plane bits and assemble the nibble. */
#if !defined(JL_HAS_SAMPLE_PIXEL)
// Chunky ports (no JL_HAS_SAMPLE_PIXEL override): inline the nibble
// read. The out-of-line jlpGenericSamplePixel hop re-marshalled
// s/x/y just to run this same macro, and its body paid the y * 160
// software multiply on IIgs (PERF-AUDIT #14/#24). x and y are plain
// locals here, so the macro's double x evaluation is side-effect
// free.
return SURFACE_READ_NIBBLE(s, x, y);
#else
// Planar ports: the platform jlpSamplePixel decodes the portData
// planes (4 plane bits assembled into the nibble).
return jlpSamplePixel(s, x, y);
#endif
}

View file

@ -11,13 +11,30 @@
#include "joey/core.h"
#include "codegenArenaInternal.h"
#include "port.h"
#include "spriteInternal.h"
#include "surfaceInternal.h"
// 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 the IIgs heap to satisfy them.
#define DEFAULT_CODEGEN_BYTES (8u * 1024u)
// Default codegen arena size when jlConfigT.codegenBytes == 0.
// Per-platform because emitter code density differs ~4-8x across CPUs
// (PERF-AUDIT.md #39):
//
// IIgs (65816): ~3-4 KB per all-opaque 32x32 sprite, so 8 KB fits
// the typical working set and keeps the attrNoCross Memory Manager
// request small. Requests above one 64 KB bank are clamped in
// codegenArenaInit (#8).
//
// Amiga/ST/DOS: the 68k/x86 emitters are far less dense -- an
// all-opaque 32x32 costs ~6.4 KB on ST (both shifts) and a
// substantially-mixed one ~8-16 KB by itself, so an 8 KB default
// silently demoted sprites to the interpreter. 32 KB matches what
// the sprite-heavy examples (uber, sprite, spacetaxi) already pass
// explicitly, and these ports have real heaps.
#if defined(JOEYLIB_PLATFORM_IIGS)
#define DEFAULT_CODEGEN_BYTES (8UL * 1024UL)
#else
#define DEFAULT_CODEGEN_BYTES (32UL * 1024UL)
#endif
// ----- Prototypes -----
@ -126,6 +143,11 @@ void jlShutdown(void) {
return;
}
jlpInputShutdown();
// Reset compiled-sprite state (slot pointers, routine offsets, patch
// caches) in every registered sprite BEFORE the arena teardown frees
// the slot structs -- see spriteSystemShutdown in spriteInternal.h
// (PERF-AUDIT.md #34).
spriteSystemShutdown();
codegenArenaShutdown();
stageFree();
jlpShutdown();

View file

@ -88,7 +88,14 @@ 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. */
* always 0 -- the predicate result is unchanged.
*
* DECIDED (PERF-AUDIT #40, Phase 8): these stay out-of-line functions.
* Macro/inline forms in joey/input.h would expose the state arrays in
* a public header and multi-evaluate args, to shave call overhead off
* predicates already measured at 70K+ ops/sec post-clock-fix -- input
* is nowhere near any real game's frame profile. Revisit only if a
* profiled game shows the predicates in its frame budget. */
bool jlKeyDown(jlKeyE key) {
if ((uint16_t)key >= (uint16_t)KEY_COUNT) {
return false;

View file

@ -84,9 +84,7 @@ void jlPaletteGet(const jlSurfaceT *s, uint8_t paletteIndex, uint16_t *out16) {
void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16) {
uint8_t i;
uint16_t *row;
const uint16_t *src;
uint16_t *row;
if (s == NULL || colors16 == NULL) {
return;
@ -95,20 +93,34 @@ void jlPaletteSet(jlSurfaceT *s, uint8_t paletteIndex, const uint16_t *colors16)
return;
}
/* Compute the row pointer via byte-pointer math + a single shift
* (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;
*row++ = 0x0000;
src++;
for (i = 1; i < SURFACE_COLORS_PER_PALETTE; i++) {
*row++ = (uint16_t)(*src++ & 0x0FFF);
}
if (s == jlStageGet()) {
// Compute the row pointer via byte-pointer math + a single shift
// (16 entries * 2 bytes = 32 = 1 << 5) so the 2D-array indexing
// avoids a multiply helper. The caller contract (joey/palette.h)
// guarantees $0RGB input, so copy the row wholesale and force
// color 0 to black -- the only documented silent rewrite. The copy
// is an unrolled sequence of word assignments, NOT memcpy: the
// clang IIgs libc memcpy is a ~30 cyc/byte byte loop (finding #79)
// and measurably REGRESSED this op when tried (1443 -> 603
// ops/sec); 15 direct word stores beat both that and the old
// masked loop on every port.
row = (uint16_t *)((uint8_t *)s->palette + ((uint16_t)paletteIndex << 5));
row[0] = 0x0000;
row[1] = colors16[1];
row[2] = colors16[2];
row[3] = colors16[3];
row[4] = colors16[4];
row[5] = colors16[5];
row[6] = colors16[6];
row[7] = colors16[7];
row[8] = colors16[8];
row[9] = colors16[9];
row[10] = colors16[10];
row[11] = colors16[11];
row[12] = colors16[12];
row[13] = colors16[13];
row[14] = colors16[14];
row[15] = colors16[15];
if (s == gStage) {
gStagePaletteDirty = true;
}
}

View file

@ -56,13 +56,14 @@
extern void iigsDrawPixelInner (uint8_t *pixels, uint16_t x, uint16_t y, uint16_t nibble);
extern void iigsDrawLineInner (uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t nibble);
extern void iigsDrawCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
extern void iigsFillCircleInner (uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
extern void iigsSurfaceClearFastInner(uint8_t *pixels, uint16_t fillWord);
extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t fillWord);
extern void iigsTileFillInner (uint8_t *dstRow0, uint16_t nibble);
extern void iigsTileCopyInner (uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
extern void iigsTilePasteInner (uint8_t *dstRow0, const uint8_t *srcTilePixels);
extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor);
extern void iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void iigsFillRectInner (uint8_t *pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t matchColor, uint16_t newColor, uint16_t matchEqual, int16_t *stackX, int16_t *stackY, uint16_t *spInOut, uint16_t maxSp);
@ -70,6 +71,15 @@ extern uint16_t gFloodSeedMatch;
extern uint16_t gFloodLeftX;
extern uint16_t gFloodRightX;
// Stage identity for the stage-gated macros below: a direct extern
// read of surface.c's gStage (also declared in surfaceInternal.h --
// duplicated here because port.h is included by TUs that never pull in
// surfaceInternal.h) instead of the jlStageGet() accessor, which costs
// a cross-segment JSL per primitive/span (PERF-AUDIT #10/#17). Same
// direct-extern precedent as codegenArenaInternal.h's arena globals.
// Read-only here.
extern jlSurfaceT *gStage;
// drawPixel: migrated to jlpDrawPixel. The asm plots into s->pixels for any
// IIgs (chunky) surface; no generic defer needed.
#define jlpDrawPixel(_s, _x, _y, _c) \
@ -87,23 +97,23 @@ extern uint16_t gFloodRightX;
iigsDrawCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \
(_r), (uint16_t)((_c) & 0x0F))
// fillWord = doubled byte * $0101 = (nib*$11) * $101 = nib * $1111.
// Compile-time arithmetic when caller passes a constant; at most a
// single multiply when the nibble is variable (still cheaper than
// the wrapper's three sequential ORs / shifts).
// fillCircle migrated to jlpFillCircle: stage-only asm, returns bool so the core
// falls to the C span fill for non-stage / off-surface. JL_HAS_FILL_CIRCLE-gated.
// The asm derives its fill bytes from the raw nibble (PERF-AUDIT #41): the
// macro passes (c & 0x0F), never the old * 0x1111 fill word, which lowered
// to a software-multiply helper call for a runtime color (the only call
// site, draw.c, always passes a variable). The jlp layer owns the & 0x0F.
#define jlpFillCircle(_s, _cx, _cy, _r, _c) \
((_s) == jlStageGet() \
((_s) == gStage \
? (iigsFillCircleInner((_s)->pixels, (uint16_t)(_cx), (uint16_t)(_cy), \
(_r), (uint16_t)(((_c) & 0x0F) * 0x1111)), \
(_r), (uint16_t)((_c) & 0x0F)), \
true) \
: false)
// surfaceClear: migrated to the jlp<Op> override model. The asm clears the
// stage surface; non-stage surfaces defer to the portable-C generic default.
#define jlpSurfaceClear(_s, _d) \
((_s) == jlStageGet() \
((_s) == gStage \
? iigsSurfaceClearFastInner((_s)->pixels, \
(uint16_t)((uint16_t)(_d) | ((uint16_t)(_d) << 8))) \
: jlpGenericSurfaceClear((_s), (_d)))
@ -113,7 +123,7 @@ extern uint16_t gFloodRightX;
// CORESYS load-segment migration drained _ROOT; earlier the macro form retripped
// the bank-packing fragility.) Saves ~80 cyc/call.
#define jlpFillRect(_s, _x, _y, _w, _h, _c) \
((_s) == jlStageGet() \
((_s) == gStage \
? iigsFillRectInner((_s)->pixels, (uint16_t)(_x), (uint16_t)(_y), \
(uint16_t)(_w), (uint16_t)(_h), \
(uint16_t)((_c) & 0x0F)) \
@ -123,11 +133,13 @@ extern uint16_t gFloodRightX;
// 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 a software multiply helper for the *160 multiply.
// tileFill: takes colorIndex; the asm derives the fill word ((c&0x0F) * 0x1111).
// tileFill: the macro passes the raw nibble (the jlp layer owns the & 0x0F);
// the asm derives the $NNNN fill word (PERF-AUDIT #41 -- the old C-side
// * 0x1111 lowered to a software-multiply helper for a runtime color).
#define jlpTileFill(_s, _bx, _by, _c) \
iigsTileFillInner(&(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) \
+ ((uint16_t)(_bx) << 2)], \
(uint16_t)(((_c) & 0x0F) * 0x1111))
(uint16_t)((_c) & 0x0F))
// tileCopy / tileCopyMasked: take surface+tile-coords; the asm macro derives
// row-0 pointers (row = by*8 -> SURFACE_ROW_OFFSET(by<<3); col byte = bx*4 -> bx<<2).
@ -148,10 +160,23 @@ extern uint16_t gFloodRightX;
#define jlpTileSnap(_s, _bx, _by, _out) \
iigsTileSnapInner((_out), &(_s)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)])
// tilePasteMono: fused mono colorize + paste in one asm pass (PERF-AUDIT #3)
// -- no intermediate jlTileT and no second paste call. Stage-only like the
// other stage-gated macros; non-stage surfaces defer to the portable-C
// generic (colorize + jlpTilePaste). fg/bg arrive PRE-MASKED to 4 bits (the
// public jlTilePasteMono wrapper owns the & 0x0F, PERF-AUDIT #69), so they
// are passed through un-re-masked per the dispatch-stanza contract below.
#define jlpTilePasteMono(_d, _bx, _by, _mt, _fg, _bg) \
((_d) == gStage \
? iigsTilePasteMonoInner(&(_d)->pixels[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)], \
(_mt), (uint16_t)(_fg), (uint16_t)(_bg)) \
: jlpGenericTilePasteMono((_d), (_bx), (_by), (_mt), (_fg), (_bg)))
// Flood: IIgs overrides only the all-in-one tier (JL_HAS_FLOOD_WALK_AND_SCANS).
// The dispatch tail leaves jlpFloodWalkAndScans defined here; the planar flood
// hooks (jlpFloodWalkPlanes / jlpFloodScanRowPlanes) are Amiga-only and IIgs
// never declares them (their call sites are #if'd out for non-Amiga).
// hooks (jlpFloodWalkPlanes / jlpFloodScanRowPlanes) belong to the planar 68k
// ports (Amiga + Atari ST) and IIgs never declares them (their call sites are
// #if'd out on ports without the JL_HAS_FLOOD_*_PLANES flags).
//
// Tier-1 flood: multi-output. Asm sets gFloodSeedMatch / gFloodLeftX /
// gFloodRightX; macro reads those into the caller's out-ptrs.
@ -176,10 +201,11 @@ extern uint16_t gFloodRightX;
// it #define-s them to ((void)0): the call site is deleted at preprocess time
// rather than paying a cross-segment JSL/RTL for a no-op. Arg lists match each
// op's arity so the preprocessor parses and discards the exact call; all args
// are plain lvalues / field reads, so dropping them changes no behavior. (DOS
// is also chunky but links the real no-op generic -- it is not the perf
// reference. The tile/fillRect planar work is folded into each jlp<Op> op, so
// there are no separate planar hooks left to elide for those.)
// are plain lvalues / field reads, so dropping them changes no behavior. (The
// other chunky ports get the same elision from the shared chunky block after
// the IIgs section -- PERF-AUDIT #42. The tile/fillRect planar work is folded
// into each jlp<Op> op, so there are no separate planar hooks left to elide
// for those.)
// =====================================================================
#define jlpSurfaceCopyPlanes(_dst, _src) ((void)0)
@ -189,8 +215,109 @@ extern uint16_t gFloodRightX;
#define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
// frameCount: straight to the GetTick asm wrapper (peislam.s). The hal.c
// C wrapper this replaces was one of two pure forwarding layers between
// jlFrameCount and the Misc Toolset GetTick call -- ~60-80 cycles each on
// a clock that game loops (and the UBER bench loop) poll constantly.
extern uint16_t iigsGetTickWord(void);
#define jlpFrameCount() iigsGetTickWord()
// =====================================================================
// JL_HAS registry consistency checks (PERF-AUDIT #26).
//
// The dispatch stanzas below short-circuit on #if !defined(jlp<Op>)
// before the JL_HAS_<OP> flag is ever tested, and the core fast-path
// gates (JL_HAS_DRAW_LINE / DRAW_CIRCLE / FILL_CIRCLE /
// FLOOD_WALK_AND_SCANS in core/draw.c) consult only the flag. So a
// macro override above whose flag is missing from the
// include/joey/platform.h registry either turns that registry into
// false documentation or silently drops a core fast path back to
// generic C on the perf-reference machine. These checks make a
// missing/removed flag a compile failure instead: one #error per
// macro override defined above. (The reverse drift -- flag present,
// macro removed -- already fails loudly: the dispatch stanza then
// declares a function override that nothing defines.) Extend this
// list whenever a new IIgs macro override is added.
//
// Deliberately unchecked: jlpFrameCount (registry documents it as
// macro-only, no JL_HAS_FRAME_COUNT on IIgs) and jlpSurfaceCopyPlanes
// (JL_HAS_SURFACE_COPY_PLANES means a real planar function override;
// the IIgs elision above pairs with the JL_HAS_SPRITE_* trio instead).
// =====================================================================
#if !defined(JL_HAS_DRAW_PIXEL)
#error "IIgs jlpDrawPixel macro without JL_HAS_DRAW_PIXEL in platform.h"
#endif
#if !defined(JL_HAS_DRAW_LINE)
#error "IIgs jlpDrawLine macro without JL_HAS_DRAW_LINE in platform.h"
#endif
#if !defined(JL_HAS_DRAW_CIRCLE)
#error "IIgs jlpDrawCircle macro without JL_HAS_DRAW_CIRCLE in platform.h"
#endif
#if !defined(JL_HAS_FILL_CIRCLE)
#error "IIgs jlpFillCircle macro without JL_HAS_FILL_CIRCLE in platform.h"
#endif
#if !defined(JL_HAS_SURFACE_CLEAR)
#error "IIgs jlpSurfaceClear macro without JL_HAS_SURFACE_CLEAR in platform.h"
#endif
#if !defined(JL_HAS_FILL_RECT)
#error "IIgs jlpFillRect macro without JL_HAS_FILL_RECT in platform.h"
#endif
#if !defined(JL_HAS_TILE_FILL)
#error "IIgs jlpTileFill macro without JL_HAS_TILE_FILL in platform.h"
#endif
#if !defined(JL_HAS_TILE_COPY)
#error "IIgs jlpTileCopy macro without JL_HAS_TILE_COPY in platform.h"
#endif
#if !defined(JL_HAS_TILE_COPY_MASKED)
#error "IIgs jlpTileCopyMasked macro without JL_HAS_TILE_COPY_MASKED in platform.h"
#endif
#if !defined(JL_HAS_TILE_PASTE)
#error "IIgs jlpTilePaste macro without JL_HAS_TILE_PASTE in platform.h"
#endif
#if !defined(JL_HAS_TILE_PASTE_MONO)
#error "IIgs jlpTilePasteMono macro without JL_HAS_TILE_PASTE_MONO in platform.h"
#endif
#if !defined(JL_HAS_TILE_SNAP)
#error "IIgs jlpTileSnap macro without JL_HAS_TILE_SNAP in platform.h"
#endif
#if !defined(JL_HAS_SPRITE_DRAW)
#error "IIgs jlpSpriteDrawPlanes elision without JL_HAS_SPRITE_DRAW in platform.h"
#endif
#if !defined(JL_HAS_SPRITE_SAVE)
#error "IIgs jlpSpriteSavePlanes elision without JL_HAS_SPRITE_SAVE in platform.h"
#endif
#if !defined(JL_HAS_SPRITE_RESTORE)
#error "IIgs jlpSpriteRestorePlanes elision without JL_HAS_SPRITE_RESTORE in platform.h"
#endif
#if !defined(JL_HAS_FLOOD_WALK_AND_SCANS)
#error "IIgs jlpFloodWalkAndScans macro without JL_HAS_FLOOD_WALK_AND_SCANS in platform.h"
#endif
#endif /* JOEYLIB_PLATFORM_IIGS */
// =====================================================================
// Chunky planar-hook elision for the other chunky ports (PERF-AUDIT #42).
//
// DOS and the blank template are chunky-native like the IIgs: the planar
// dual-write hooks have no bitplanes to mirror into. Elide them to
// ((void)0) exactly like the IIgs block above instead of paying a real
// call into the empty generic bodies (genericSprite.c and
// jlpGenericSurfaceCopyPlanes) on every sprite draw/save/restore and
// surface copy. Those generic no-op bodies stay in the build only as the
// link-time safety net for a new planar port that has not implemented
// its hooks yet.
// =====================================================================
#if defined(JOEYLIB_NATIVE_CHUNKY) && !defined(JOEYLIB_PLATFORM_IIGS)
#define jlpSurfaceCopyPlanes(_dst, _src) ((void)0)
#define jlpSpriteDrawPlanes(_s, _sp, _x, _y) ((void)0)
#define jlpSpriteSavePlanes(_s, _x, _y, _w, _h, _dst) ((void)0)
#define jlpSpriteRestorePlanes(_s, _x, _y, _w, _h, _src) ((void)0)
#endif /* JOEYLIB_NATIVE_CHUNKY && !JOEYLIB_PLATFORM_IIGS */
// =====================================================================
// Migrated-op dispatch (generic default unless a machine overrides).
//
@ -289,6 +416,11 @@ void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *
#endif
#endif
// tilePasteMono color-masking contract (PERF-AUDIT #69): fgColor/bgColor
// arrive already masked to 4 bits -- the public jlTilePasteMono wrapper owns
// the & 0x0F. Neither the generic nor a machine override may re-mask.
// (tileFill is the opposite: its wrapper does NOT mask, so the jlp layer owns
// the & 0x0F there -- see the IIgs macro above and the planar overrides.)
void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor);
#if !defined(jlpTilePasteMono)
#if defined(JL_HAS_TILE_PASTE_MONO)

View file

@ -17,7 +17,9 @@
void jlStagePresent(void) {
jlSurfaceT *stage;
stage = jlStageGet();
// Direct extern read (finding #10/#17 convention): once per present,
// so this is consistency rather than a hot-path win.
stage = gStage;
if (stage == NULL) {
return;
}

View file

@ -13,11 +13,43 @@
// Initialized to 1 so jlRandom() works without an explicit seed call
// and never starts from xorshift's degenerate all-zero state.
//
// IIgs stores the state as two uint16_t halves (PERF-AUDIT #43): the
// 65816 has no 32-bit shifts, so the 13/17/5 triple lowered to per-bit
// shift helper loops (~400-700 cyc/call). The identical update on
// 16-bit halves is ~2-3x cheaper and BIT-IDENTICAL (host-proven across
// the UBER golden sequence + a 50M-state sweep), preserving the
// cross-port determinism contract in core.h. GCC ports keep the 32-bit
// form: 68000 lsl.l/lsr.l are native, so the halves form is a
// wash-to-slight-loss there (audit verifier correction).
#if defined(JOEYLIB_PLATFORM_IIGS)
static uint16_t gRngLo = 1u;
static uint16_t gRngHi = 0u;
#else
static uint32_t gRngState = 1u;
#endif
// ----- Public API (alphabetical) -----
uint32_t jlRandom(void) {
#if defined(JOEYLIB_PLATFORM_IIGS)
uint16_t lo = gRngLo;
uint16_t hi = gRngHi;
// x ^= x << 13: (x << 13) halves are { hi<<13 | lo>>3, lo<<13 }.
// Each step updates hi BEFORE lo -- the hi identity reads the
// step's incoming lo.
hi = (uint16_t)(hi ^ (uint16_t)((uint16_t)(hi << 13) | (uint16_t)(lo >> 3)));
lo = (uint16_t)(lo ^ (uint16_t)(lo << 13));
// x ^= x >> 17: (x >> 17) halves are { 0, hi>>1 } -- low half only.
lo = (uint16_t)(lo ^ (uint16_t)(hi >> 1));
// x ^= x << 5: (x << 5) halves are { hi<<5 | lo>>11, lo<<5 }.
hi = (uint16_t)(hi ^ (uint16_t)((uint16_t)(hi << 5) | (uint16_t)(lo >> 11)));
lo = (uint16_t)(lo ^ (uint16_t)(lo << 5));
gRngLo = lo;
gRngHi = hi;
return ((uint32_t)hi << 16) | lo;
#else
uint32_t x = gRngState;
x ^= x << 13;
@ -25,6 +57,7 @@ uint32_t jlRandom(void) {
x ^= x << 5;
gRngState = x;
return x;
#endif
}
@ -42,5 +75,13 @@ uint16_t jlRandomRange(uint16_t bound) {
void jlRandomSeed(uint32_t seed) {
gRngState = (seed == 0u) ? 1u : seed;
if (seed == 0u) {
seed = 1u;
}
#if defined(JOEYLIB_PLATFORM_IIGS)
gRngLo = (uint16_t)seed;
gRngHi = (uint16_t)(seed >> 16);
#else
gRngState = seed;
#endif
}

View file

@ -28,7 +28,7 @@ void jlScbSet(jlSurfaceT *s, uint16_t line, uint8_t paletteIndex) {
return;
}
s->scb[line] = paletteIndex;
if (s == jlStageGet()) {
if (s == gStage) {
gStageScbDirty = true;
}
}
@ -61,7 +61,7 @@ void jlScbSetRange(jlSurfaceT *s, uint16_t firstLine, uint16_t lastLine, uint8_t
// 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()) {
if (s == gStage) {
gStageScbDirty = true;
}
}

View file

@ -15,11 +15,13 @@
#include "surfaceInternal.h"
// 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
// Color 0 is always transparent for sprites (DESIGN.md contract);
// TRANSPARENT_NIBBLE comes from spriteInternal.h, shared with every
// per-CPU emitter.
// On Amiga (post-Phase 9 / Phase 6 redux) the compiled sprite emitter
// writes directly to the bitplanes, so the jlpSprite*Planes hooks are
@ -40,13 +42,33 @@
#endif
// ----- Sprite registry (PERF-AUDIT.md #34) -----
//
// jlShutdown frees the codegen arena (codegenArenaShutdown) while
// user-owned sprites may still hold ArenaSlotT pointers. If such a
// sprite survives a shutdown -> re-init cycle, jlSpriteDestroy would
// free a stale slot (heap corruption) and jlSpriteDraw would execute
// stale offsets inside the NEW arena. The registry tracks live
// sprites so spriteSystemShutdown (called from jlShutdown BEFORE
// codegenArenaShutdown) can reset each one's compiled-path state; the
// arena bytes themselves are released by codegenArenaShutdown.
//
// Fixed cap; registration is best-effort. Sprites beyond the cap are
// simply not registered and MUST be destroyed before jlShutdown --
// which matches the implicit contract every sprite had before the
// registry existed.
#define SPRITE_REGISTRY_CAP 64
static jlSpriteT *gSpriteRegistry[SPRITE_REGISTRY_CAP];
// ----- Prototypes -----
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);
static void spriteRegistryRemove(jlSpriteT *sp);
static void spriteResetCompiledState(jlSpriteT *sp);
// ----- Internal helpers (alphabetical) -----
@ -84,46 +106,15 @@ static bool clipRect(int16_t *dstX, int16_t *dstY, int16_t *srcX, int16_t *srcY,
}
static void writeDstNibble(uint8_t *row, int16_t x, uint8_t nibble) {
uint8_t *byte;
byte = &row[x >> 1];
if (x & 1) {
*byte = (uint8_t)((*byte & 0xF0) | (nibble & 0x0F));
} else {
*byte = (uint8_t)((*byte & 0x0F) | ((nibble & 0x0F) << 4));
}
}
// Delegates to the shared 16-bit on-surface predicate (PERF-AUDIT
// #44/#62) instead of the old 32-bit add+compare pair. Safe within the
// macro's contract: every sprite creation path rejects 0 tiles, so
// widthPx/heightPx are at least 8 (w >= 1 holds), and they are bounded
// by 2040 (255 tiles * 8), so the macro's guarded w <= SURFACE_WIDTH /
// h <= SURFACE_HEIGHT form rejects oversized sprites before the
// 16-bit subtractions could wrap.
static bool isFullyOnSurface(int16_t x, int16_t y, uint16_t widthPx, uint16_t heightPx) {
if (x < 0 || y < 0) {
return false;
}
if ((int32_t)x + widthPx > SURFACE_WIDTH) {
return false;
}
if ((int32_t)y + heightPx > SURFACE_HEIGHT) {
return false;
}
return true;
}
// 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));
return SURFACE_RECT_ON_SURFACE(x, y, widthPx, heightPx);
}
@ -132,71 +123,195 @@ static void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t wid
// 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).
// tile columns 32 bytes apart). All row-invariant source math is
// hoisted above the row loop (#45): the tile-band byte offset is
// seeded with the single (sy >> 3) * bandStride multiply, then
// maintained incrementally as inTileY wraps, so no multiply helper
// runs per row. The destination byte pointer and nibble parity are
// carried incrementally per pixel (#2) instead of being rederived
// through a per-pixel function call.
//
// When source and destination share nibble parity (the aligned phase,
// #18) the inner loop runs byte-at-a-time, chunked per tile column
// (up to TILE_BYTES_PER_ROW contiguous source bytes, then
// srcTileRow += TILE_BYTES): a 0x00 source byte skips both pixels, a
// both-nibbles-opaque byte is one whole-byte store, and a single
// opaque nibble merges read-modify-write. The odd leading/trailing
// pixels are handled outside the byte loop. The misaligned phase
// keeps the per-pixel walker.
#if TRANSPARENT_NIBBLE != 0
#error "spriteDrawInterpreted's aligned byte path assumes TRANSPARENT_NIBBLE == 0"
#endif
static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y) {
int16_t dx;
int16_t dy;
int16_t sx;
int16_t sy;
int16_t w;
int16_t h;
int16_t row;
uint8_t wTiles;
int16_t dx;
int16_t dy;
int16_t sx;
int16_t sy;
int16_t w;
int16_t h;
dx = x;
dy = y;
w = (int16_t)(sp->widthTiles * TILE_PIXELS_PER_SIDE);
h = (int16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
w = (int16_t)sp->widthPx;
h = (int16_t)sp->heightPx;
if (!clipRect(&dx, &dy, &sx, &sy, &w, &h)) {
return;
}
wTiles = sp->widthTiles;
/* Skip the chunky write loop on planar ports (s->pixels == NULL).
* jlpSpriteDrawPlanes is called by the jlSpriteDraw caller and does
* its own clip + plane write, so the dirty mark + planar update
* happen there. Phase 9 dropped the chunky shadow on Amiga. */
// Skip the chunky write loop on planar ports (s->pixels == NULL).
// jlpSpriteDrawPlanes is called by the jlSpriteDraw caller and does
// its own clip + plane write, so the dirty mark + planar update
// happen there. Phase 9 dropped the chunky shadow on Amiga.
if (s->pixels != NULL) {
uint16_t bandStride;
uint16_t bandBase;
uint16_t inTileY;
uint16_t inTileXStart;
uint16_t srcColBase;
uint16_t dstByteStart;
uint16_t leadOdd;
uint16_t trailing;
int16_t pairCount;
int16_t row;
bool aligned;
// Row-invariant hoists (#45). Shifts dodge multiply helpers
// (TILE_BYTES == 1<<5, TILE_BYTES_PER_ROW == 1<<2); only the
// bandBase seed pays a multiply, once per draw.
bandStride = (uint16_t)((uint16_t)sp->widthTiles << 5);
bandBase = (uint16_t)((uint16_t)((uint16_t)sy >> 3) * bandStride);
inTileY = (uint16_t)((uint16_t)sy & 7u);
inTileXStart = (uint16_t)((uint16_t)sx & 7u);
srcColBase = (uint16_t)(((uint16_t)((uint16_t)sx >> 3)) << 5);
dstByteStart = (uint16_t)((uint16_t)dx >> 1);
// Aligned phase (#18): src and dst nibble parity match for the
// whole draw, so source pixel pairs map to whole dst bytes.
// leadOdd/pairCount/trailing split each row into an odd
// leading pixel, whole bytes, and an odd trailing pixel.
aligned = (((dx ^ sx) & 1) == 0);
leadOdd = (uint16_t)((uint16_t)dx & 1u);
pairCount = (int16_t)((w - (int16_t)leadOdd) >> 1);
trailing = (uint16_t)((uint16_t)(w - (int16_t)leadOdd) & 1u);
for (row = 0; row < h; row++) {
uint8_t *dstRow;
const uint8_t *srcTileRow;
uint16_t srcY;
uint16_t tileRowBase;
uint16_t inTileX;
int16_t col;
uint8_t *dst;
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);
srcTileRow = sp->tileData + bandBase + (inTileY << 2) + srcColBase;
dst = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)(dy + row)) + dstByteStart];
for (col = 0; col < w; col++) {
uint8_t byte;
uint8_t nibble;
if (aligned) {
uint16_t inTileX;
int16_t pairsLeft;
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 = inTileXStart;
// Odd leading pixel: low nibble of both src and dst.
if (leadOdd != 0u) {
uint8_t nibble;
nibble = (uint8_t)(srcTileRow[inTileX >> 1] & 0x0Fu);
if (nibble != TRANSPARENT_NIBBLE) {
*dst = (uint8_t)((*dst & 0xF0u) | nibble);
}
dst++;
inTileX++;
if (inTileX == TILE_PIXELS_PER_SIDE) {
inTileX = 0;
srcTileRow += TILE_BYTES;
}
}
inTileX++;
if (inTileX == TILE_PIXELS_PER_SIDE) {
inTileX = 0;
srcTileRow += TILE_BYTES;
// Byte loop, chunked per tile column: up to 4
// contiguous source bytes, then hop to the next tile.
pairsLeft = pairCount;
while (pairsLeft > 0) {
const uint8_t *src;
int16_t run;
int16_t i;
run = (int16_t)((TILE_PIXELS_PER_SIDE - inTileX) >> 1);
if (run > pairsLeft) {
run = pairsLeft;
}
src = srcTileRow + (inTileX >> 1);
for (i = 0; i < run; i++) {
uint8_t b;
b = *src++;
if (b == 0x00u) {
// Two TRANSPARENT_NIBBLEs: skip both.
dst++;
} else if ((b & 0xF0u) != 0u && (b & 0x0Fu) != 0u) {
// Both nibbles opaque: whole-byte store.
*dst = b;
dst++;
} else if ((b & 0x0Fu) != 0u) {
// Low nibble opaque (high is 0, so | b works).
*dst = (uint8_t)((*dst & 0xF0u) | b);
dst++;
} else {
// High nibble opaque (low is 0).
*dst = (uint8_t)((*dst & 0x0Fu) | b);
dst++;
}
}
pairsLeft = (int16_t)(pairsLeft - run);
inTileX = (uint16_t)(inTileX + ((uint16_t)run << 1));
if (inTileX == TILE_PIXELS_PER_SIDE) {
inTileX = 0;
srcTileRow += TILE_BYTES;
}
}
// Even trailing pixel: high nibble of both src and dst.
if (trailing != 0u) {
uint8_t b;
b = (uint8_t)(srcTileRow[inTileX >> 1] & 0xF0u);
if (b != 0u) {
*dst = (uint8_t)((*dst & 0x0Fu) | b);
}
}
} else {
// Misaligned phase: per-pixel walker with the dst byte
// pointer and parity carried incrementally (#2).
uint16_t inTileX;
uint16_t dstOdd;
int16_t col;
inTileX = inTileXStart;
dstOdd = leadOdd;
for (col = 0; col < w; col++) {
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) {
if (dstOdd != 0u) {
*dst = (uint8_t)((*dst & 0xF0u) | nibble);
} else {
*dst = (uint8_t)((*dst & 0x0Fu) | (uint8_t)(nibble << 4));
}
}
if (dstOdd != 0u) {
dst++;
dstOdd = 0;
} else {
dstOdd = 1;
}
inTileX++;
if (inTileX == TILE_PIXELS_PER_SIDE) {
inTileX = 0;
srcTileRow += TILE_BYTES;
}
}
}
// Advance the tile band incrementally (#45): the bandBase
// multiply above never reruns.
inTileY++;
if (inTileY == TILE_PIXELS_PER_SIDE) {
inTileY = 0;
bandBase = (uint16_t)(bandBase + bandStride);
}
}
}
@ -204,6 +319,36 @@ static void spriteDrawInterpreted(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16
}
// Drop a sprite from the registry. No-op if it was never registered
// (registry full at creation time).
static void spriteRegistryRemove(jlSpriteT *sp) {
uint8_t i;
for (i = 0; i < SPRITE_REGISTRY_CAP; i++) {
if (gSpriteRegistry[i] == sp) {
gSpriteRegistry[i] = NULL;
return;
}
}
}
// Reset a sprite's compiled-path state to "never compiled".
// 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. The IIgs MVN bank caches return to
// 0xFF ("never patched") and cachedSizeBytes to 0 ("uncached") so a
// recompile after arena teardown cannot skip a patch based on stale
// cache state.
static void spriteResetCompiledState(jlSpriteT *sp) {
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));
}
// ----- Public API (alphabetical) -----
jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles) {
@ -212,7 +357,12 @@ jlSpriteT *jlSpriteCreate(const uint8_t *tileData, uint8_t widthTiles, uint8_t h
if (tileData == NULL || widthTiles == 0 || heightTiles == 0) {
return NULL;
}
sp = (jlSpriteT *)malloc(sizeof(jlSpriteT));
/* jlpBigAlloc, NOT the C heap: the IIgs C heap is a few hundred
* bytes of unprotected bank-0 leftover; at exhaustion its malloc
* returns garbage instead of NULL (PERF-AUDIT.md #81). Other ports
* map jlpBigAlloc to malloc. Applies to every sprite-struct and
* owned-tileData allocation in this file (finding #31). */
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
if (sp == NULL) {
return NULL;
}
@ -252,7 +402,7 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
}
tileBytes = (uint32_t)widthTiles * heightTiles * TILE_BYTES;
buf = (uint8_t *)malloc(tileBytes);
buf = (uint8_t *)jlpBigAlloc(tileBytes);
if (buf == NULL) {
return NULL;
}
@ -303,9 +453,9 @@ jlSpriteT *jlSpriteCreateFromSurface(const jlSurfaceT *src, int16_t x, int16_t y
}
}
sp = (jlSpriteT *)malloc(sizeof(jlSpriteT));
sp = (jlSpriteT *)jlpBigAlloc((uint32_t)sizeof(jlSpriteT));
if (sp == NULL) {
free(buf);
jlpBigFree(buf);
return NULL;
}
spriteInitFields(sp, buf, widthTiles, heightTiles, true);
@ -317,14 +467,15 @@ void jlSpriteDestroy(jlSpriteT *sp) {
if (sp == NULL) {
return;
}
spriteRegistryRemove(sp);
if (sp->slot != NULL) {
codegenArenaFree(sp->slot);
sp->slot = NULL;
}
if (sp->ownsTileData) {
free((void *)sp->tileData);
jlpBigFree((void *)sp->tileData);
}
free(sp);
jlpBigFree(sp);
}
@ -335,21 +486,37 @@ 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_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
widthPx = sp->widthPx;
heightPx = sp->heightPx;
// Fast path: compiled bytes + fully on surface. Off-surface draws
// fall back to the interpreter so the compiled routines never
// need clip math (they walk fixed offsets). On IIgs sp->slot is left
// NULL by jlSpriteCompile (see #19 there), so this is never taken and
// every op runs the interpreter.
// need clip math (they walk fixed offsets). The DRAW offset for
// this shift is read once and gated against SPRITE_NOT_COMPILED
// (#46): a slot whose DRAW emitter returned 0 bytes for this shift
// must never be jumped into (spriteCompiledDraw adds the offset to
// the slot address unchecked on IIgs/DOS). Mirrors the SaveUnder /
// RestoreUnder gates. This compiled branch is the production path
// on every port with a wired emitter; the interpreter is only the
// uncompiled/clip-edge fallback (#63). Exception, temporary: while
// JOEY_IIGS_SPRITE_CODEGEN is 0 (toolchain-owned arena-placement
// bug, see #19 / #77 in spriteCompile.c) jlSpriteCompile leaves
// sp->slot NULL on IIgs, so that port interprets until the fix
// lands -- do NOT treat this branch as dead code there.
if (sp->slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
spriteCompiledDraw(s, sp, x, y);
if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteDrawPlanes(s, sp, x, y);
uint8_t shift;
uint16_t routeOffset;
shift = SPRITE_SHIFT_INDEX(x);
routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW);
if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledDraw(s, sp, x, y, routeOffset);
if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteDrawPlanes(s, sp, x, y);
}
surfaceMarkDirtyRect(s, x, y, (int16_t)widthPx, (int16_t)heightPx);
return;
}
surfaceMarkDirtyRect(s, x, y, (int16_t)widthPx, (int16_t)heightPx);
return;
}
spriteDrawInterpreted(s, sp, x, y);
jlpSpriteDrawPlanes(s, sp, x, y);
@ -377,10 +544,10 @@ uint32_t jlSpriteCompiledSize(const jlSpriteT *sp) {
void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup) {
uint16_t widthPx;
uint16_t heightPx;
uint8_t wTiles;
uint8_t hTiles;
ArenaSlotT *slot;
uint8_t shift;
uint16_t saveOffset;
uint16_t drawOffset;
if (s == NULL || sp == NULL || backup == NULL) {
return;
@ -388,31 +555,23 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
backup->sprite = sp;
backup->sizeBytes = 0;
wTiles = sp->widthTiles;
hTiles = sp->heightTiles;
slot = sp->slot;
widthPx = (uint16_t)(wTiles * TILE_PIXELS_PER_SIDE);
heightPx = (uint16_t)(hTiles * TILE_PIXELS_PER_SIDE);
slot = sp->slot;
widthPx = sp->widthPx;
heightPx = sp->heightPx;
// Fast path: compiled bytes available, fully on surface, backup
// buffer supplied. Save fills out backup->{x,y,width,height,
// sizeBytes}; draw reuses (x,y,widthPx,heightPx) for the dirty
// mark. One mark instead of two (save doesn't dirty -- it's a
// read; only draw dirties).
// read; only draw dirties). shift and both route offsets are
// derived once here and handed to the callees (#29).
if (slot != NULL && backup->bytes != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) {
/* Byte-pointer arithmetic dodges ~MUL4 for 2D-array indexing. */
uint16_t saveIdx;
uint16_t drawIdx;
uint8_t *offsetsBase;
shift = SPRITE_SHIFT_INDEX(x);
saveIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
drawIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_DRAW);
offsetsBase = (uint8_t *)sp->routineOffsets;
if (*(uint16_t *)(offsetsBase + (saveIdx << 1)) != SPRITE_NOT_COMPILED &&
*(uint16_t *)(offsetsBase + (drawIdx << 1)) != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup);
spriteCompiledDraw (s, sp, x, y);
shift = SPRITE_SHIFT_INDEX(x);
saveOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE);
drawOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW);
if (saveOffset != SPRITE_NOT_COMPILED && drawOffset != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup, shift, saveOffset);
spriteCompiledDraw (s, sp, x, y, drawOffset);
if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
jlpSpriteDrawPlanes(s, sp, x, y);
@ -481,10 +640,9 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
}
sp = backup->sprite;
if (sp != NULL && sp->slot != NULL && bh == sp->heightTiles * TILE_PIXELS_PER_SIDE) {
uint16_t routeIdx;
if (sp != NULL && sp->slot != NULL && bh == sp->heightPx) {
uint16_t routeOffset;
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
spriteBytesPerRow = (uint16_t)(sp->widthPx >> 1);
copyBytes = (int16_t)(bw >> 1);
// The compiled restore routine walks a FIXED per-row byte count
// derived purely from the chosen shift (spriteBytesPerRow for
@ -495,12 +653,10 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
// 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));
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1;
routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_RESTORE);
if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup);
spriteCompiledRestoreUnder(s, backup, shift, routeOffset);
if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
}
@ -514,17 +670,21 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
* the port has no chunky shadow (Phase 9 Amiga: s->pixels NULL);
* jlpSpriteRestorePlanes below does the planar restore. */
if (s->pixels != NULL) {
int16_t row;
int16_t byteStart;
uint8_t *dstRow;
int16_t row;
uint8_t *dstRow;
const uint8_t *srcRow;
byteStart = (int16_t)(bx >> 1);
// #19: seed both row cursors once -- SURFACE_ROW_OFFSET is the
// blessed row-address mechanism (LUT read on IIgs, shift-add
// elsewhere) -- then walk them with constant-stride adds so no
// multiply helper runs per row.
copyBytes = (int16_t)(bw >> 1);
dstRow = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)by) + (uint16_t)(bx >> 1)];
srcRow = backup->bytes;
for (row = 0; row < (int16_t)bh; row++) {
dstRow = &s->pixels[(by + row) * SURFACE_BYTES_PER_ROW];
memcpy(&dstRow[byteStart],
&backup->bytes[(uint16_t)row * (uint16_t)copyBytes],
(size_t)copyBytes);
memcpy(dstRow, srcRow, (size_t)copyBytes);
dstRow += SURFACE_BYTES_PER_ROW;
srcRow += copyBytes;
}
}
jlpSpriteRestorePlanes(s, bx, by, bw, bh, backup->bytes);
@ -548,25 +708,19 @@ 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_PER_SIDE);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
widthPx = sp->widthPx;
heightPx = sp->heightPx;
// Compiled fast path: fully on surface and the platform emitted
// bytes for SAVE at this shift. The compiled routine assumes a
// full-size, unclipped rectangle, so anything off-edge falls
// through to the interpreted memcpy loop below.
//
// The routineOffsets[shift][SPRITE_OP_SAVE] access is rewritten as
// 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;
shift = SPRITE_SHIFT_INDEX(x);
routeIdx = (uint16_t)(((uint16_t)shift << 1) + (uint16_t)shift + SPRITE_OP_SAVE);
routeOffset = *(uint16_t *)((uint8_t *)sp->routineOffsets + (routeIdx << 1));
routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE);
if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup);
spriteCompiledSaveUnder(s, sp, x, y, backup, shift, routeOffset);
if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
}
@ -628,13 +782,87 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
/* Chunky save path: skip on planar ports (s->pixels NULL).
* jlpSpriteSavePlanes below covers the planar case. */
if (s->pixels != NULL) {
uint8_t *dstBytes;
// #19: seed both row cursors once (SURFACE_ROW_OFFSET is the
// blessed row-address mechanism) and walk them with
// constant-stride adds; no per-row multiply helpers.
srcRow = &s->pixels[SURFACE_ROW_OFFSET((uint16_t)dy) + (uint16_t)byteStart];
dstBytes = backup->bytes;
for (row = 0; row < h; row++) {
srcRow = &s->pixels[(dy + row) * SURFACE_BYTES_PER_ROW];
memcpy(&backup->bytes[(uint16_t)row * (uint16_t)copyBytes],
&srcRow[byteStart],
(size_t)copyBytes);
memcpy(dstBytes, srcRow, (size_t)copyBytes);
srcRow += SURFACE_BYTES_PER_ROW;
dstBytes += copyBytes;
}
}
jlpSpriteSavePlanes(s, clippedX, dy, (uint16_t)clippedW, (uint16_t)h, backup->bytes);
} /* end slow path */
}
// ----- Internal cross-TU entry points (spriteInternal.h) -----
// Initialize a freshly-allocated jlSpriteT's fields to the
// uncompiled/default state and register it for spriteSystemShutdown
// (see the registry comment near the top of this file). The single
// init path for every sprite: jlSpriteCreate, jlSpriteCreateFromSurface,
// and jlSpriteBankLoad (assetLoad.c) all route through here so the
// 0xFF/0 sentinel rules in spriteResetCompiledState have one source
// of truth (PERF-AUDIT.md #32).
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->widthPx = (uint16_t)(widthTiles * TILE_PIXELS_PER_SIDE);
sp->heightPx = (uint16_t)(heightTiles * TILE_PIXELS_PER_SIDE);
sp->ownsTileData = ownsTileData;
spriteResetCompiledState(sp);
spriteRegistryAdd(sp);
}
// Register a sprite for spriteSystemShutdown. Idempotent (re-adding a
// registered sprite is a no-op); every creation path registers via
// spriteInitFields, and jlSpriteCompile re-registers on success as a
// belt-and-braces guard since compile is the moment the sprite first
// holds arena state that jlShutdown must reset. If the registry is
// full the sprite is silently not registered -- see the cap comment
// above.
void spriteRegistryAdd(jlSpriteT *sp) {
uint8_t i;
int16_t freeIdx;
freeIdx = -1;
for (i = 0; i < SPRITE_REGISTRY_CAP; i++) {
if (gSpriteRegistry[i] == sp) {
return;
}
if (freeIdx < 0 && gSpriteRegistry[i] == NULL) {
freeIdx = (int16_t)i;
}
}
if (freeIdx >= 0) {
gSpriteRegistry[freeIdx] = sp;
}
}
// Called from jlShutdown BEFORE codegenArenaShutdown (#34). Resets
// every registered sprite's compiled-path state so a sprite surviving
// a shutdown -> re-init cycle can neither free a stale ArenaSlotT
// (heap corruption) nor execute stale offsets inside the new arena.
// Does NOT free anything -- codegenArenaShutdown releases the slot
// structs and arena bytes right after. Sprites stay registered: they
// remain live user objects and may be recompiled after re-init.
void spriteSystemShutdown(void) {
uint8_t i;
for (i = 0; i < SPRITE_REGISTRY_CAP; i++) {
jlSpriteT *sp;
sp = gSpriteRegistry[i];
if (sp != NULL) {
spriteResetCompiledState(sp);
}
}
}

View file

@ -35,12 +35,49 @@
// is valid for the first emitted op (typically DRAW shift 0).
#define SPRITE_NOT_COMPILED 0xFFFFu
// The 4bpp nibble value treated as transparent by the interpreters
// (sprite.c) and every per-CPU emitter. Single definition so the
// transparency contract cannot drift between the compiled and
// interpreted paths.
#define TRANSPARENT_NIBBLE 0
// 1D index into the [JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT] tables
// (routineOffsets, cachedDstBank, cachedSrcBank). Spelled as
// (shift << 1) + shift instead of shift * SPRITE_OP_COUNT because the
// multiply lowers to a helper call (~MUL4) on the 65816; the shift/add
// form is two ASLs and an ADC. Must stay in lockstep with
// SPRITE_OP_COUNT == 3.
#define SPRITE_ROUTINE_INDEX(_shift, _op) ((uint16_t)((((uint16_t)(_shift) << 1) + (uint16_t)(_shift)) + (_op)))
// routineOffsets read at a precomputed SPRITE_ROUTINE_INDEX. The
// byte-pointer arithmetic dodges the multiply helper that plain
// `uint16_t arr[N][M]` indexing emits on the 65816. Yields the stored
// uint16_t offset (SPRITE_NOT_COMPILED when the op is absent for that
// shift).
#define SPRITE_ROUTE_OFFSET_AT(_sp, _idx) (*(uint16_t *)((uint8_t *)(_sp)->routineOffsets + ((uint16_t)((_idx) << 1))))
// routineOffsets[shift][op] in one step -- the form every dispatcher
// gate and spriteCompiled* callee uses. Use SPRITE_ROUTINE_INDEX +
// SPRITE_ROUTE_OFFSET_AT instead when the index is also needed for the
// bank-patch cache pointers (IIgs save/restore).
#define SPRITE_ROUTE_OFFSET(_sp, _shift, _op) SPRITE_ROUTE_OFFSET_AT((_sp), SPRITE_ROUTINE_INDEX((_shift), (_op)))
struct jlSpriteT {
const uint8_t *tileData; // wTiles * hTiles * 32 bytes; NULL for loaded sprites
// wTiles * hTiles * 32 bytes; never NULL for a live sprite.
// Caller-owned for jlSpriteCreate; a sprite-owned copy
// (ownsTileData true) for jlSpriteCreateFromSurface and
// jlSpriteBankLoad, freed by jlSpriteDestroy.
const uint8_t *tileData;
uint8_t widthTiles;
uint8_t heightTiles;
bool ownsTileData; // true if jlSpriteDestroy must free tileData
// Create-time pixel dimensions (widthTiles * 8 / heightTiles * 8),
// cached by spriteInitFields so the per-call dispatchers and
// spriteCompiled* callees never re-derive them (#29).
uint16_t widthPx;
uint16_t heightPx;
// Compiled-path state. slot==NULL means not yet compiled (or
// compile failed); jlSpriteDraw falls back to the interpreter.
// The fn-call address for (shift, op) is computed at draw time:
@ -68,14 +105,48 @@ struct jlSpriteT {
};
// Compiled entry points. Implemented alongside jlSpriteCompile in
// src/codegen/jlSpriteCompile.c. Each handles the per-platform calling
// convention the emitted bytes use (cdecl on x86, stack args on 68k,
// inline asm + self-modifying stub on IIgs). The dispatchers in
// src/core/sprite.c call these when sp->slot is non-NULL, the
// matching routineOffsets entry is not SPRITE_NOT_COMPILED, and the
// draw/save/restore is fully on-surface.
void spriteCompiledDraw (jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y);
void spriteCompiledSaveUnder (const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
void spriteCompiledRestoreUnder (jlSurfaceT *dst, const jlSpriteBackupT *backup);
// src/codegen/spriteCompile.c. Each handles the per-platform calling
// convention the emitted bytes use (cdecl stack args on x86 and 68k;
// C-ABI function pointers with per-call MVN bank patching on IIgs).
// The dispatchers in src/core/sprite.c call these when sp->slot is
// non-NULL, the matching routineOffsets entry is not
// SPRITE_NOT_COMPILED, and the draw/save/restore is fully on-surface.
// The gate already derived shift (SPRITE_SHIFT_INDEX) and routeOffset
// (SPRITE_ROUTE_OFFSET) to make that decision, so it passes them in
// and the callees never re-derive them (#29). routeOffset is the
// gate-checked entry for the callee's own op; shift feeds the IIgs
// bank-patch cache index and the chunky save copyBytes (draw needs
// neither, so it only takes routeOffset).
void spriteCompiledDraw (jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset);
void spriteCompiledSaveUnder (const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset);
void spriteCompiledRestoreUnder (jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset);
// Initialize a freshly-allocated jlSpriteT to the uncompiled/default
// state (tile-data fields, 0xFF routine-offset fill, bank/size cache
// sentinels) and register it for spriteSystemShutdown. The single
// init path for every sprite -- jlSpriteCreate and
// jlSpriteCreateFromSurface in sprite.c plus jlSpriteBankLoad in
// assetLoad.c -- so the sentinel rules live in one place
// (PERF-AUDIT.md #32). Defined in sprite.c.
void spriteInitFields(jlSpriteT *sp, const uint8_t *tileData, uint8_t widthTiles, uint8_t heightTiles, bool ownsTileData);
// Sprite registry (sprite.c, PERF-AUDIT.md #34). spriteInitFields
// registers every sprite it initializes; jlSpriteCompile re-registers
// on success (idempotent) as a belt-and-braces guard, since compile
// is the moment the sprite first holds an arena slot.
// jlSpriteDestroy unregisters. The registry holds at most
// SPRITE_REGISTRY_CAP sprites (see sprite.c); beyond that,
// registration is silently skipped and the sprite MUST be destroyed
// before jlShutdown -- the same implicit contract that applied to
// every sprite before the registry existed.
void spriteRegistryAdd(jlSpriteT *sp);
// Called from jlShutdown BEFORE codegenArenaShutdown: resets every
// registered sprite's compiled-path state (slot pointer, routine
// offsets, patch caches) so a sprite surviving a shutdown -> re-init
// cycle can neither free a stale ArenaSlotT (heap corruption) nor
// execute stale offsets inside the new arena. Frees nothing --
// codegenArenaShutdown releases the slots right after.
void spriteSystemShutdown(void);
#endif

View file

@ -10,10 +10,6 @@
#include "port.h"
#include "surfaceInternal.h"
#ifdef JOEYLIB_PLATFORM_IIGS
extern void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
#endif
#define SURFACE_PALETTE_BYTES (SURFACE_PALETTE_ENTRIES * (uint32_t)sizeof(uint16_t))
#define SURFACE_FILE_BYTES (SURFACE_PIXELS_SIZE + SURFACE_HEIGHT + SURFACE_PALETTE_BYTES)
@ -24,7 +20,10 @@ extern void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minW
// ----- Module state -----
static jlSurfaceT *gStage = NULL;
// Non-static: hot paths read it through surfaceInternal.h's extern to
// skip the jlStageGet() cross-TU call (PERF-AUDIT #10/#17). Written
// only by stageAlloc/stageFree below.
jlSurfaceT *gStage = NULL;
uint8_t gStageMinWord[SURFACE_HEIGHT];
uint8_t gStageMaxWord[SURFACE_HEIGHT];
@ -44,6 +43,10 @@ bool gStagePaletteDirty = true;
// ----- Internal helpers (alphabetical) -----
// widenRow serves only the non-IIgs surfaceMarkDirtyRows loop; the
// IIgs multi-row path is the asm marker and the single-row path is
// inlined by the surfaceMarkDirtyRect macro (surfaceInternal.h).
#ifndef JOEYLIB_PLATFORM_IIGS
static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
if (minWord < gStageMinWord[y]) {
gStageMinWord[y] = minWord;
@ -52,6 +55,7 @@ static void widenRow(int16_t y, uint8_t minWord, uint8_t maxWord) {
gStageMaxWord[y] = maxWord;
}
}
#endif
// ----- Public API (alphabetical) -----
@ -82,11 +86,17 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
jlSurfaceT *jlSurfaceCreate(void) {
jlSurfaceT *s;
s = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
// jlpBigAlloc, NOT the C heap: on IIgs the C heap is a few hundred
// bytes of bank-0 leftover that is NEITHER big enough for this
// struct NOR reserved from the Memory Manager (Phase 1 IIgs
// stabilization; PERF-AUDIT.md #77/#81). Other ports map
// jlpBigAlloc to malloc, so behavior is unchanged there.
s = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
if (s == NULL) {
coreSetError("out of memory allocating surface");
return NULL;
}
memset(s, 0, sizeof(jlSurfaceT));
// jlpSurfaceAllocPixels 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
@ -97,14 +107,14 @@ jlSurfaceT *jlSurfaceCreate(void) {
#ifdef JOEYLIB_NATIVE_CHUNKY
if (s->pixels == NULL) {
jlpSurfaceFreePortData(s, false, s->portData);
free(s);
jlpBigFree(s);
coreSetError("out of memory allocating surface pixels");
return NULL;
}
#else
if (s->portData == NULL) {
jlpSurfaceFreePixels(s->pixels);
free(s);
jlpBigFree(s);
coreSetError("out of memory allocating surface planes");
return NULL;
}
@ -123,7 +133,7 @@ void jlSurfaceDestroy(jlSurfaceT *s) {
}
jlpSurfaceFreePortData(s, false, s->portData);
jlpSurfaceFreePixels(s->pixels);
free(s);
jlpBigFree(s);
}
@ -227,47 +237,19 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) {
}
// Drawing primitives pass the rect they actually wrote (already
// clipped to surface bounds, w and h positive). For non-stage surfaces
// the call is a no-op so primitives can call unconditionally without
// branching themselves.
void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h) {
int16_t yEnd;
uint8_t minWord;
uint8_t maxWord;
// Multi-row arm of the surfaceMarkDirtyRect macro (surfaceInternal.h):
// widen rows y..yEnd-1 to cover [minWord, maxWord]. IIgs builds never
// compile this -- the macro routes them straight to the asm marker
// iigsMarkDirtyRowsInner instead.
#ifndef JOEYLIB_PLATFORM_IIGS
int16_t row;
#endif
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
uint16_t row;
if (s != gStage) {
return;
}
if (w <= 0 || h <= 0) {
return;
}
/* 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
// 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);
widenRow((int16_t)row, minWord, maxWord);
}
#endif
}
#endif
// ----- Internal (alphabetical) -----
@ -276,10 +258,13 @@ bool stageAlloc(void) {
if (gStage != NULL) {
return true;
}
gStage = (jlSurfaceT *)calloc(1, sizeof(jlSurfaceT));
// jlpBigAlloc for the same reason as jlSurfaceCreate: the IIgs C
// heap can neither hold nor protect this struct.
gStage = (jlSurfaceT *)jlpBigAlloc((uint32_t)sizeof(jlSurfaceT));
if (gStage == NULL) {
return false;
}
memset(gStage, 0, sizeof(jlSurfaceT));
// jlpStageAllocPixels 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
@ -293,14 +278,14 @@ bool stageAlloc(void) {
#ifdef JOEYLIB_NATIVE_CHUNKY
if (gStage->pixels == NULL) {
jlpSurfaceFreePortData(gStage, true, gStage->portData);
free(gStage);
jlpBigFree(gStage);
gStage = NULL;
return false;
}
#else
if (gStage->portData == NULL) {
jlpStageFreePixels(gStage->pixels);
free(gStage);
jlpBigFree(gStage);
gStage = NULL;
return false;
}
@ -312,12 +297,12 @@ bool stageAlloc(void) {
void stageDirtyClearAll(void) {
int16_t row;
for (row = 0; row < SURFACE_HEIGHT; row++) {
gStageMinWord[row] = STAGE_DIRTY_CLEAN_MIN;
gStageMaxWord[row] = STAGE_DIRTY_CLEAN_MAX;
}
// Constant fill of both band arrays, same memset form as its
// sibling surfaceMarkDirtyAll: lowers to a tight fill (MVN-seeded
// on IIgs) instead of a 200-iteration indexed-store loop, and this
// runs after every jlStagePresent.
memset(gStageMinWord, STAGE_DIRTY_CLEAN_MIN, SURFACE_HEIGHT);
memset(gStageMaxWord, STAGE_DIRTY_CLEAN_MAX, SURFACE_HEIGHT);
}
@ -327,6 +312,6 @@ void stageFree(void) {
}
jlpSurfaceFreePortData(gStage, true, gStage->portData);
jlpStageFreePixels(gStage->pixels);
free(gStage);
jlpBigFree(gStage);
gStage = NULL;
}

View file

@ -33,7 +33,8 @@ 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
// (The 4-px-per-word fact is baked into SURFACE_WORD_INDEX's >> 2 below;
// PERF-AUDIT #66 deleted the unused SURFACE_PIXELS_PER_WORD spelling.)
#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
@ -51,8 +52,19 @@ struct jlSurfaceT {
#define STAGE_DIRTY_CLEAN_MAX 0x00u
// True when row `y`'s dirty band is the CLEAN sentinel (min > max).
// This is the canonical per-row copy test (the IIgs present asm
// implements the same compare against these sentinels).
#define STAGE_DIRTY_ROW_CLEAN(_y) (gStageMinWord[_y] > gStageMaxWord[_y])
// True when row `y` has been marked since the last stageDirtyClearAll.
// Single-load form (used by the Amiga present's idle-frame early-out
// and any-dirty scan). Valid ONLY under the clipped-marks invariant:
// every real mark widens minWord to <= STAGE_DIRTY_FULL_MAX (79), so
// min != 0xFF exactly when the row was touched. Use
// STAGE_DIRTY_ROW_CLEAN for the per-row copy decision; use this
// cheaper test where one load per row matters and the invariant holds.
#define STAGE_DIRTY_ROW_TOUCHED(_y) (gStageMinWord[_y] != STAGE_DIRTY_CLEAN_MIN)
// 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
@ -66,6 +78,15 @@ typedef char surfaceDirtyBandFitsByte[(SURFACE_WORDS_PER_ROW - 1) <= 0xFF ? 1 :
extern uint8_t gStageMinWord[SURFACE_HEIGHT];
extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
// The library-owned stage surface, defined in surface.c (written only
// by stageAlloc/stageFree). Direct extern -- matching the band arrays
// above and codegenArenaInternal.h's direct-extern globals -- so hot
// paths (the port.h IIgs macros, the dirty-mark macro below, the
// palette/scb stage checks) compare surface identity with a plain load
// instead of a cross-TU jlStageGet() call. jlStageGet() stays as the
// public accessor. Treat as read-only outside surface.c.
extern jlSurfaceT *gStage;
// Per-byte mixer for jlSurfaceHash. Two-stream: lo *= 31 + b, hi *= 251 + b.
// Strength-reduced to shifts so the compiler doesn't emit a software
// multiply (~150 cyc per call); 32 KB hashed twice -> ~5 minutes per
@ -86,12 +107,35 @@ extern uint8_t gStageMaxWord[SURFACE_HEIGHT];
extern bool gStageScbDirty;
extern bool gStagePaletteDirty;
// Multi-row arm of surfaceMarkDirtyRect below: widen rows y..yEnd-1
// (yEnd exclusive) to cover [minWord, maxWord]. On IIgs this routes
// straight to the asm marker so a multi-row mark stays a single JSL;
// elsewhere it is the C loop in surface.c.
#ifdef JOEYLIB_PLATFORM_IIGS
void iigsMarkDirtyRowsInner(uint16_t yStart, uint16_t yEnd, uint16_t minWord, uint16_t maxWord);
#define surfaceMarkDirtyRows(_y, _yEnd, _min, _max) iigsMarkDirtyRowsInner((_y), (_yEnd), (uint16_t)(_min), (uint16_t)(_max))
#else
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord);
#endif
// Drawing primitives call this with their already-clipped destination
// rect. If `s` is the stage, the affected rows' [minWord, maxWord]
// bands are widened to cover the rect. If `s` is any other surface,
// the call is a no-op -- non-stage surfaces never get presented, so
// they don't carry dirty state.
//
// CONTRACT: the rect is already clipped to surface bounds and w and h
// are POSITIVE. There is deliberately no w/h <= 0 guard here -- every
// caller proves positive dimensions before marking (jlDrawRect's
// h == 2 interior edges were the last violator, fixed per PERF-AUDIT
// #65's verifier correction), so empty ranges cannot reach this macro.
//
// Macro, not a function (ORCA/C-era rule: no `inline` in headers the
// IIgs build shares): the stage test and the h == 1 single-row widen
// run inline at the call site; only multi-row marks pay a call, into
// surfaceMarkDirtyRows above. Every argument is evaluated exactly
// once, but keep call-site arguments side-effect free anyway.
//
// Planar ports rely on the chunky shadow + c2p path through Phase 8.
// Planar-native primitives (Phases 3+) dual-write: they update both
// the chunky pixels and the bitplanes in the same call, so c2p at
@ -100,7 +144,25 @@ extern bool gStagePaletteDirty;
// per-row planar-vs-chunky tracking even be a possible question, and
// the plan is to avoid it entirely there too (planes become the only
// source of truth).
void surfaceMarkDirtyRect(const jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h);
#define surfaceMarkDirtyRect(_s, _x, _y, _w, _h) do { \
if ((_s) == gStage) { \
int16_t markX_ = (_x); \
int16_t markY_ = (_y); \
int16_t markH_ = (_h); \
uint8_t markMin_ = SURFACE_WORD_INDEX(markX_); \
uint8_t markMax_ = SURFACE_WORD_INDEX(markX_ + (_w) - 1); \
if (markH_ == 1) { \
if (markMin_ < gStageMinWord[markY_]) { \
gStageMinWord[markY_] = markMin_; \
} \
if (markMax_ > gStageMaxWord[markY_]) { \
gStageMaxWord[markY_] = markMax_; \
} \
} else { \
surfaceMarkDirtyRows((uint16_t)markY_, (uint16_t)(markY_ + markH_), markMin_, markMax_); \
} \
} \
} while (0)
// Shorthand for "every row, full width" -- used by jlSurfaceClear and
// the bulk-replace paths (jlSurfaceCopy, jlSurfaceLoadFile). No-op if `s`
@ -122,13 +184,55 @@ void stageDirtyClearAll(void);
// Other ports get the straight multiply -- gcc / OpenWatcom optimize
// the constant 160 to a shift+add chain.
#ifdef JOEYLIB_PLATFORM_IIGS
extern const uint16_t gRowOffsetLut[200];
extern const uint16_t gRowOffsetLut[SURFACE_HEIGHT];
#define SURFACE_ROW_OFFSET(_y) \
(*((const uint16_t *)((const uint8_t *)gRowOffsetLut + ((uint16_t)(_y) << 1))))
#else
#define SURFACE_ROW_OFFSET(_y) ((uint16_t)((uint16_t)(_y) * SURFACE_BYTES_PER_ROW))
#endif
// Chunky 4bpp nibble read: the color index of pixel (x, y) in
// s->pixels. Callers must have already bounds-checked x/y
// (non-negative, on-surface). Macro (not a function -- same ORCA/C-era
// rule as above) so jlSamplePixel, jlpGenericSamplePixel, and the
// flood-fill C fallback walks all inline the read; SURFACE_ROW_OFFSET
// dodges the y * 160 software-multiply helper on IIgs. Only the taken
// parity arm's byte load executes, so each call reads one byte. `_x`
// is evaluated twice -- pass side-effect-free arguments.
#define SURFACE_READ_NIBBLE(_s, _x, _y) \
(((_x) & 1) \
? (uint8_t)((_s)->pixels[SURFACE_ROW_OFFSET(_y) + ((uint16_t)(_x) >> 1)] & 0x0Fu) \
: (uint8_t)((_s)->pixels[SURFACE_ROW_OFFSET(_y) + ((uint16_t)(_x) >> 1)] >> 4))
// True when the rect (x, y) .. (x + w - 1, y + h - 1) lies fully
// on-surface: the shared fast-path gate for jlFillRect, jlDrawRect,
// and sprite.c's isFullyOnSurface (#44) -- one source of
// truth for the on-surface clip invariant (PERF-AUDIT #62). All 16-bit
// on purpose: the int32 form of this test is a multi-instruction
// carry-chained sequence per compare on the 65816/8086. Guard order is
// load-bearing: w <= SURFACE_WIDTH and h <= SURFACE_HEIGHT are tested
// BEFORE the SURFACE_WIDTH - w / SURFACE_HEIGHT - h subtractions so
// those stay in int16_t range (an unguarded 320 - 65535 would wrap
// positive and falsely pass).
//
// Intended argument types: int16_t x/y, uint16_t w/h. CONTRACT: w >= 1
// and h >= 1 -- for w == 0 the macro would accept x == SURFACE_WIDTH,
// so callers exclude empty rects first (jlFillRect tests w > 0 &&
// h > 0; jlDrawRect returns early on w == 0 || h == 0). Boundary
// proof: w == 65535 -> the first compare fails and && short-circuits
// before the subtraction could wrap; w == 320 -> passes only for
// x == 0 (x <= 320 - 320); x == 319 with w == 1 -> passes (319 <= 319,
// the rightmost column); x == -1 -> x >= 0 fails. For w in
// [1, SURFACE_WIDTH] and x >= 0 the subtraction cannot wrap, so
// x <= SURFACE_WIDTH - w is exactly x + w <= SURFACE_WIDTH -- the
// 32-bit form this replaces. Arguments are evaluated up to twice --
// pass side-effect-free expressions.
#define SURFACE_RECT_ON_SURFACE(_x, _y, _w, _h) \
((_w) <= SURFACE_WIDTH && (_h) <= SURFACE_HEIGHT && \
(_x) >= 0 && (_y) >= 0 && \
(_x) <= (int16_t)(SURFACE_WIDTH - (int16_t)(_w)) && \
(_y) <= (int16_t)(SURFACE_HEIGHT - (int16_t)(_h)))
// 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.

View file

@ -3,10 +3,10 @@
// stack jlTileT buffer); no separate tileset container, no allocator.
//
// Block coords (bx, by) map to pixel (bx*8, by*8). At 4bpp packed
// each tile row is 4 bytes wide, so byte-aligned memcpy is the inner
// loop for everything except the masked / transparent variant, which
// has to read-modify-write each byte to preserve destination pixels
// under transparent (color-0 by convention) source nibbles.
// each tile row is 4 bytes wide, so byte-aligned row copies are the
// inner loop for everything except the masked / transparent variant,
// which has to read-modify-write each byte to preserve destination
// pixels under transparent (color-0 by convention) source nibbles.
#include <stddef.h>
@ -14,20 +14,48 @@
#include "port.h"
#include "surfaceInternal.h"
// (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.)
// The chunky tile inner loops live in src/generic/genericTile.c (the
// jlpGenericTile* ops); this TU is validation, dispatch, and dirty
// marking only -- it never touches pixel bytes.
// Block -> pixel coordinate for the dirty-rect bookkeeping below.
// Spelled as a shift because ORCA-C does not strength-reduce the *8
// into one -- it emits the ~150-cycle software multiply helper, the
// same trap SURFACE_ROW_OFFSET in surfaceInternal.h documents
// (PERF-AUDIT #48). The shift count 3 assumes TILE_PIXELS_PER_SIDE is
// 8; the guard keeps the two in sync.
#if TILE_PIXELS_PER_SIDE != 8
#error "TILE_BLOCK_TO_PIXEL assumes TILE_PIXELS_PER_SIDE == 8"
#endif
#define TILE_BLOCK_TO_PIXEL(_b) ((uint16_t)((uint16_t)(_b) << 3))
// Prototype (helper below is the only static in this TU).
static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds);
// ----- Prototypes -----
// (copyTileOpaque / copyTileMasked moved to src/generic/genericTile.c as
// jlpGenericTileCopy / jlpGenericTileCopyMasked.)
// ----- Internal helpers (alphabetical) -----
// (the chunky tile copy/masked-copy helpers now live in
// src/generic/genericTile.c as jlpGenericTileCopy / jlpGenericTileCopyMasked.)
// jlDrawText's per-glyph dirty-union tracking, hoisted out of the
// glyph loop into a noinline helper: keeping five live union locals
// inside the loop overflows the llvm-mos w65816 register allocator
// ("ran out of registers" hard error). bounds[] = {minCx, maxCx,
// minCy, maxCy, drewAny}. noinline is portable gcc/clang syntax.
static void __attribute__((noinline)) drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds) {
if (bounds[4] == 0u) {
bounds[4] = 1u;
bounds[0] = cx;
bounds[1] = cx;
bounds[2] = cy;
} else {
if (cx < bounds[0]) {
bounds[0] = cx;
}
if (cx > bounds[1]) {
bounds[1] = cx;
}
}
// cy never decreases, so the last drawn glyph's row is always the
// union's bottom edge.
bounds[3] = cy;
}
// ----- Public API (alphabetical) -----
@ -39,20 +67,28 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS
uint8_t ch;
uint8_t srcBx;
uint8_t srcBy;
uint8_t bounds[5];
if (dst == NULL || fontSurface == NULL || asciiMap == NULL || str == NULL) {
return;
}
cx = bx;
cy = by;
while (*str != '\0') {
ch = (uint8_t)*str++;
entry = asciiMap[ch];
if (entry != TILE_NO_GLYPH) {
srcBx = (uint8_t)(entry & 0x00FFu);
srcBy = (uint8_t)((entry >> 8) & 0x00FFu);
jlTileCopyMasked(dst, cx, cy, fontSurface, srcBx, srcBy, 0u);
// Entry sanitation (PERF-AUDIT #22 verifier correction): the main
// loop below hands cx/cy straight to jlpTileCopyMasked, which does
// no bounds checking, so an out-of-range start must be disposed of
// here -- the wrap logic only guarantees in-range cx/cy after the
// first wrap. The historical contract is preserved bit-for-bit
// (the chk:drawText-offgrid golden covers it): characters landing
// on an out-of-range position are consumed without drawing while
// the cursor advances and wraps exactly as for drawn characters.
// For the documented bx in [0,39] / by in [0,24] inputs this whole
// loop costs two compares.
while (cx >= TILE_BLOCKS_PER_ROW || cy >= TILE_BLOCKS_PER_COL) {
if (*str == '\0') {
return;
}
str++;
cx++;
if (cx >= TILE_BLOCKS_PER_ROW) {
cx = 0;
@ -62,6 +98,40 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS
}
}
}
bounds[4] = 0u;
while (*str != '\0') {
ch = (uint8_t)*str++;
entry = asciiMap[ch];
if (entry != TILE_NO_GLYPH) {
srcBx = (uint8_t)(entry & 0x00FFu);
srcBy = (uint8_t)((entry >> 8) & 0x00FFu);
// Only the asciiMap-supplied source block still needs
// validation per glyph: cx/cy are in range by the entry
// sanitation + wrap invariant and the NULL checks ran once
// above, so the public jlTileCopyMasked wrapper (which
// would re-check all of it and issue a dirty mark per
// glyph) is skipped (PERF-AUDIT #22).
if (srcBx < TILE_BLOCKS_PER_ROW && srcBy < TILE_BLOCKS_PER_COL) {
jlpTileCopyMasked(dst, cx, cy, fontSurface, srcBx, srcBy, 0u);
drawTextUnion(cx, cy, bounds);
}
}
cx++;
if (cx >= TILE_BLOCKS_PER_ROW) {
cx = 0;
cy++;
if (cy >= TILE_BLOCKS_PER_COL) {
break;
}
}
}
// One union dirty mark for the whole string instead of an 8-row
// mark per glyph (PERF-AUDIT #22). Over-approximates when the
// string wraps rows, which is safe: dirty state only widens what
// present uploads.
if (bounds[4] != 0u) {
surfaceMarkDirtyRect(dst, (int16_t)TILE_BLOCK_TO_PIXEL(bounds[0]), (int16_t)TILE_BLOCK_TO_PIXEL(bounds[2]), TILE_BLOCK_TO_PIXEL(bounds[1] - bounds[0] + 1u), TILE_BLOCK_TO_PIXEL(bounds[3] - bounds[2] + 1u));
}
}
@ -76,8 +146,8 @@ void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
// Single op: machine asm/planar override or the generic chunky default.
jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
@ -96,8 +166,8 @@ void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSur
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return;
}
dstPixelX = (uint16_t)((uint16_t)dstBx * TILE_PIXELS_PER_SIDE);
dstPixelY = (uint16_t)((uint16_t)dstBy * TILE_PIXELS_PER_SIDE);
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
@ -114,8 +184,8 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single compile-time-selected op: machine asm/planar override or the
// generic chunky default.
jlpTileFill(s, bx, by, colorIndex);
@ -124,26 +194,6 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
}
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
// Single compile-time op: planar ports colorize in their own tile format
// (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
// Replaces the old runtime pixels==NULL planar/chunky branch.
jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
surfaceMarkDirtyRect(dst, (int16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE),
(int16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE),
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
uint16_t pixelX;
uint16_t pixelY;
@ -154,8 +204,8 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single op: machine asm/planar override or the generic chunky default.
jlpTilePaste(dst, bx, by, &in->pixels[0]);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
@ -163,6 +213,29 @@ void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
}
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) {
uint16_t pixelX;
uint16_t pixelY;
if (dst == NULL || in == NULL) {
return;
}
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return;
}
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single compile-time op: planar ports colorize in their own tile format
// (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
// Replaces the old runtime pixels==NULL planar/chunky branch.
jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY,
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE);
}
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out) {
if (src == NULL || out == NULL) {
return;

View file

@ -51,11 +51,8 @@ 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;
// (The old $3DA edge-poll frame-counter state lived here; removed with
// the poll-rate-dependent counter itself -- see jlpFrameCount.)
// Cached palette from the last present. VGA DAC programming is ~768
// outportb calls; skip it when the source palette is unchanged.
@ -283,11 +280,13 @@ void jlpPresent(const jlSurfaceT *src) {
// bytes, so pixelX = minWord*4 and pixelW = (maxWord-minWord+1)*4
// gives the byte range expandAndWriteLine needs.
for (y = 0; y < SURFACE_HEIGHT; y++) {
minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
if (minWord > maxWord) {
// Canonical clean-row test (finding #47): min > max is the CLEAN
// sentinel state; see STAGE_DIRTY_ROW_CLEAN in surfaceInternal.h.
if (STAGE_DIRTY_ROW_CLEAN(y)) {
continue;
}
minWord = gStageMinWord[y];
maxWord = gStageMaxWord[y];
// 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
@ -318,15 +317,16 @@ void jlpWaitVBL(void) {
}
// Frame count derived from the millisecond clock (PIT ISR fires or
// uclock -- see jlpMillisElapsed below): frames = ms * 70 / 1000. The
// old implementation edge-polled $3DA bit 3 and only advanced when a
// call happened to land DURING the ~1ms retrace pulse, so any caller
// doing more than ~1ms of work between polls silently lost frames
// (PERF-AUDIT.md finding #73 -- UBER's batched timing loop stalled on
// it outright). This form is poll-rate-independent and monotonic;
// jlpWaitVBL above still synchronizes to the real retrace.
uint16_t jlpFrameCount(void) {
uint8_t now;
now = (uint8_t)(inportb(VGA_INPUT_STAT_1) & VGA_VRETRACE_BIT);
if (now && !gPrevInVret) {
gFrameCount++;
}
gPrevInVret = now;
return gFrameCount;
return (uint16_t)(((uint64_t)jlpMillisElapsed() * 70u) / 1000u);
}
@ -411,8 +411,9 @@ void jlpShutdown(void) {
// nearest-neighbor copy to VGA RAM.
// surfaceCopyPlanes migrated: DOS defines no JL_HAS_SURFACE_COPY_PLANES, so it
// links the no-op generic jlpGenericSurfaceCopyPlanes.
// surfaceCopyPlanes (and the sprite *Planes hooks): elided to ((void)0) by
// port.h's shared chunky block (PERF-AUDIT #42) -- DOS has no bitplanes to
// mirror into, so the former real calls into the no-op generics are gone.
/* Phase 9 reader hooks: chunky ports use the original s->pixels-based
* paths. */

View file

@ -27,16 +27,16 @@
#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitChunky.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_PIXELS 8
#define TILE_BYTES 32
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
// Tile geometry comes from joey/tile.h; TRANSPARENT_NIBBLE from
// spriteInternal.h.
// Stack-arg displacements for the emitted cdecl routines, after the
// prologue saves esi (+esi/edi for save/restore):
@ -64,11 +64,11 @@
// ----- Prototypes -----
// shiftedByteAt / spriteSourceByte -- the transparency contract shared
// with the IIgs emitter -- come from spriteEmitChunky.h.
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);
// ----- Emit helpers (alphabetical) -----
@ -161,77 +161,6 @@ static uint32_t esiOperandLen(uint16_t col) {
}
// 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;
// 0x00 means both transparent. value's transparent nibbles are 0.
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask) {
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
bool hasLeft;
bool hasRight;
*outValue = 0;
*outOpaqueMask = 0;
if (shift == 0) {
if (col >= spriteBytesPerRow) {
return;
}
srcByte = spriteSourceByte(sp, row, col);
hi = (uint8_t)((srcByte >> 4) & 0x0Fu);
lo = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
return;
}
// shift = 1
hasLeft = (col >= 1) && ((uint16_t)(col - 1) < spriteBytesPerRow);
hasRight = (col < spriteBytesPerRow);
if (hasLeft) {
srcByte = spriteSourceByte(sp, row, (uint16_t)(col - 1));
hi = (uint8_t)(srcByte & 0x0Fu); // sprite byte's LOW nibble
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
}
if (hasRight) {
srcByte = spriteSourceByte(sp, row, col);
lo = (uint8_t)((srcByte >> 4) & 0x0Fu); // sprite byte's HIGH nibble
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
}
}
// Sample a sprite tile-data byte at (row, col) where col is in
// sprite-byte coordinates (0..spriteBytesPerRow-1).
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col) {
uint16_t tileX;
uint16_t tileY;
uint16_t inTileX;
uint16_t inTileY;
const uint8_t *tile;
tileX = (uint16_t)(col / TILE_BYTES_PER_ROW);
tileY = (uint16_t)(row / TILE_PIXELS);
inTileX = (uint16_t)(col & (TILE_BYTES_PER_ROW - 1));
inTileY = (uint16_t)(row & (TILE_PIXELS - 1));
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
return tile[inTileY * TILE_BYTES_PER_ROW + inTileX];
}
// 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).
@ -256,7 +185,7 @@ uint32_t spriteEmitDrawX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
@ -396,7 +325,7 @@ uint32_t spriteEmitRestoreX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, u
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
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]
@ -435,7 +364,7 @@ uint32_t spriteEmitSaveX86(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
copyBytes = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW + shift); /* shift is 0 or 1 */
if (cursor + 10u > cap) {

View file

@ -24,35 +24,50 @@ void jlpGenericSurfaceClear(jlSurfaceT *s, uint8_t doubled) {
// Chunky 4bpp on-surface rect fill. Caller has proven the rect is fully on
// surface (x,y >= 0; x+w <= WIDTH; y+h <= HEIGHT; w,h > 0). Planar ports have
// NULL s->pixels and override jlpFillRect with their plane fill, so this is the
// chunky default (IIgs non-stage / DOS).
// chunky default (IIgs non-stage / DOS). The span geometry (edge nibbles +
// whole-byte middle run) depends only on x/w, so it is derived once up front
// and the row loop just advances one pointer by the row stride (PERF-AUDIT #49).
void jlpGenericFillRect(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);
int16_t row;
uint16_t pxStart;
uint16_t pxEnd;
uint8_t nibble = colorIndex & 0x0F;
uint8_t doubled = (uint8_t)((nibble << 4) | nibble);
uint16_t pxStart = (uint16_t)x;
uint16_t pxEnd = (uint16_t)(x + w);
bool hasLeft = false;
bool hasRight = false;
uint16_t leftByte;
uint16_t midByte;
uint16_t midBytes;
uint16_t rightByte;
uint8_t *line;
int16_t row;
if (s->pixels == NULL) {
return;
}
leftByte = (uint16_t)(pxStart >> 1);
if (pxStart & 1u) {
hasLeft = true;
pxStart++;
}
midByte = (uint16_t)(pxStart >> 1);
midBytes = (uint16_t)((pxEnd - pxStart) >> 1);
pxStart = (uint16_t)(pxStart + (midBytes << 1));
if (pxStart < pxEnd) {
hasRight = true;
}
rightByte = (uint16_t)(pxStart >> 1);
line = &s->pixels[SURFACE_ROW_OFFSET(y)];
for (row = 0; row < h; row++) {
line = &s->pixels[SURFACE_ROW_OFFSET(y + row)];
pxStart = (uint16_t)x;
pxEnd = (uint16_t)(x + w);
if (pxStart & 1u) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0xF0) | nibble);
pxStart++;
if (hasLeft) {
line[leftByte] = (uint8_t)((line[leftByte] & 0xF0) | nibble);
}
midBytes = (uint16_t)((pxEnd - pxStart) >> 1);
if (midBytes > 0u) {
memset(&line[pxStart >> 1], doubled, (size_t)midBytes);
pxStart = (uint16_t)(pxStart + (midBytes << 1));
memset(&line[midByte], doubled, (size_t)midBytes);
}
if (pxStart < pxEnd) {
line[pxStart >> 1] = (uint8_t)((line[pxStart >> 1] & 0x0F) | (nibble << 4));
if (hasRight) {
line[rightByte] = (uint8_t)((line[rightByte] & 0x0F) | (nibble << 4));
}
line += SURFACE_BYTES_PER_ROW;
}
}

View file

@ -36,8 +36,65 @@ void jlpGenericWaitVBL(void) {
// Approximate elapsed milliseconds from the frame counter and refresh rate.
// This is the IIgs/Amiga path; ports with a real hardware timer (DOS uclock,
// ST 200 Hz tick) override via JL_HAS_MILLIS_ELAPSED.
//
// Milliseconds accumulate incrementally from uint16_t frame DELTAS, so the
// wrapping 16-bit hardware counter never runs the clock backward: the clock
// is monotonic out to the natural uint32_t limit (~49.7 days) PROVIDED this
// is called at least once every 65535 frames (~18 min at 60Hz, ~22 min at
// 50Hz) -- any per-frame caller satisfies that trivially. The fractional-
// frame remainder is carried across calls, so there is no long-term drift.
// The 50/60Hz fast paths avoid the generic 32-bit divide (hundreds of
// cycles on the 65816). A change in jlpFrameHz() re-bases the clock to 0
// (the ms-per-frame basis and remainder units change; no real port switches
// refresh rate at runtime). Statics are updated via explicit local
// read-modify-write (ORCA-C lowers ++/+= on file-scope data to inc abs, a
// DBR trap on the 65816).
uint32_t jlpGenericMillisElapsed(void) {
return (uint32_t)((uint32_t)jlpFrameCount() * 1000u / (uint32_t)jlpFrameHz());
static uint16_t gLastFrame = 0u;
static uint16_t gLastHz = 0u;
static uint32_t gMillis = 0u;
static uint16_t gRemainder = 0u;
uint16_t now;
uint16_t hz;
uint16_t delta;
uint16_t rem;
uint32_t ms;
uint32_t scaled;
now = jlpFrameCount();
hz = jlpFrameHz();
if (hz != gLastHz) {
gLastHz = hz;
gLastFrame = now;
gMillis = 0u;
gRemainder = 0u;
return 0u;
}
delta = (uint16_t)(now - gLastFrame);
if (delta == 0u) {
return gMillis;
}
ms = gMillis;
rem = gRemainder;
if (hz == 50u) {
// Exact: 20 ms per frame, no fractional remainder.
ms = ms + (uint32_t)delta * 20u;
} else if (hz == 60u) {
// Exact: 50/3 ms per frame, mod-3 remainder carried across calls.
scaled = (uint32_t)delta * 50u + (uint32_t)rem;
ms = ms + scaled / 3u;
rem = (uint16_t)(scaled % 3u);
} else {
// General fallback: 1000/hz ms per frame, mod-hz remainder carried.
scaled = (uint32_t)delta * 1000u + (uint32_t)rem;
ms = ms + scaled / (uint32_t)hz;
rem = (uint16_t)(scaled % (uint32_t)hz);
}
gLastFrame = now;
gMillis = ms;
gRemainder = rem;
return ms;
}

View file

@ -2,7 +2,9 @@
// core/sprite.c (spriteDrawInterpreted) and the per-platform sprite codegen does
// the fast path; jlpSprite*Planes is the PLANAR mirror/interpreter, which has no
// work to do on a chunky target -- so the generic default is a no-op. Planar
// machines (Amiga/ST) override it; IIgs overrides it with a no-op macro (perf).
// machines (Amiga/ST) override it; every chunky port (IIgs/DOS/blank) elides
// the hooks to ((void)0) macros in port.h (PERF-AUDIT #42), so these bodies
// link only for a new planar port that has not implemented its hooks yet.
#include "port.h"

View file

@ -23,17 +23,23 @@ void jlpGenericSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
}
// Release a per-surface chunky pixel buffer. free(NULL) is a no-op, so this is
// safe on planar ports whose surfaces carry no chunky shadow.
// Release a per-surface chunky pixel buffer. Must mirror
// jlpGenericSurfaceAllocPixels' allocator exactly: handing a
// jlpBigAlloc pointer to free() corrupts the IIgs heap
// (BIG_ALLOC_HDR offset, finding #31). NULL is a no-op, so this
// stays safe on planar ports whose surfaces carry no chunky shadow.
void jlpGenericSurfaceFreePixels(uint8_t *pixels) {
free(pixels);
if (pixels != NULL) {
jlpBigFree(pixels);
}
}
// jlSurfaceCopy's planar dual-write hook. No-op default: chunky ports carry no
// bitplanes (the chunky payload is jlpSurfaceCopyChunky's job), and a blank/
// template port links this until it provides its own. Planar ports (Amiga, ST)
// override via JL_HAS_SURFACE_COPY_PLANES.
// bitplanes (the chunky payload is jlpSurfaceCopyChunky's job) and elide the
// call outright via port.h's chunky ((void)0) macros (PERF-AUDIT #42), so this
// body links only for a new planar port that has not provided its own yet.
// Planar ports (Amiga, ST) override via JL_HAS_SURFACE_COPY_PLANES.
void jlpGenericSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
(void)dst;
(void)src;
@ -52,9 +58,11 @@ const char *jlpGenericLastError(void) {
uint8_t jlpGenericSamplePixel(const jlSurfaceT *s, int16_t x, int16_t y) {
uint8_t byte = s->pixels[(uint16_t)y * SURFACE_BYTES_PER_ROW + ((uint16_t)x >> 1)];
if (x & 1) return (uint8_t)(byte & 0x0Fu);
return (uint8_t)((byte & 0xF0u) >> 4);
// SURFACE_READ_NIBBLE routes the row offset through
// SURFACE_ROW_OFFSET, dropping the y * 160 software-multiply helper
// this body used to pay on IIgs (PERF-AUDIT #14/#24). Identical
// pixel result on every port.
return SURFACE_READ_NIBBLE(s, x, y);
}
@ -115,10 +123,25 @@ void jlpGenericStageFreePixels(uint8_t *pixels) {
}
// Per-surface chunky pixels: chunky ports calloc (zero-filled); planar ports
// override to return NULL (storage is the portData planes).
// Per-surface chunky pixels, zero-filled; planar ports override to
// return NULL (storage is the portData planes). jlpBigAlloc, NOT
// calloc: the IIgs C heap is a few hundred bytes of bank-0 leftover
// and its calloc RETURNS 1 (not NULL) at exhaustion (LLVM816-ASKS
// item 4), so the old calloc here zero-filled 32000 bytes through
// pointer 1 and shredded bank 0 on the first OFFSCREEN
// jlSurfaceCreate -- the stage never hit it (pinned $01:2000 path),
// which is why UBER could pass while spacetaxi died in
// loadFontSheet. Other ports map jlpBigAlloc to malloc, so behavior
// is unchanged there (finding #31 pattern).
uint8_t *jlpGenericSurfaceAllocPixels(void) {
return (uint8_t *)calloc(1, SURFACE_PIXELS_SIZE);
uint8_t *pixels;
pixels = (uint8_t *)jlpBigAlloc((uint32_t)SURFACE_PIXELS_SIZE);
if (pixels == NULL) {
return NULL;
}
memset(pixels, 0, SURFACE_PIXELS_SIZE);
return pixels;
}

View file

@ -3,6 +3,8 @@
// JL_HAS_<OP> for the tile op; machine overrides (IIgs asm macros, Amiga/ST
// planar functions) live in the machine folders.
#include <string.h>
#include "joey/tile.h"
#include "surfaceInternal.h"
#include "port.h"
@ -12,22 +14,34 @@
#define GENERIC_TILE_ROW0(_pix, _bx, _by) \
(&(_pix)[SURFACE_ROW_OFFSET((uint16_t)(_by) << 3) + ((uint16_t)(_bx) << 2)])
// Copy TILE_PIXELS_PER_SIDE rows of TILE_BYTES_PER_ROW bytes from _s to _d,
// advancing each pointer by its own stride -- the one row-copy inner loop
// shared by TileCopy / TilePaste / TileSnap (PERF-AUDIT #50). memcpy on
// purpose: the previous byte-at-a-time form let gcc assume d/s alias, which
// blocked store merging; memcpy asserts non-overlap and compiles to one
// 32-bit move per row on DOS (DJGPP). Only DOS/BLANK run these generics --
// IIgs uses asm macros and Amiga/ST override every tile op -- so the IIgs
// byte-loop-libc memcpy caveat (PERF-AUDIT #79) does not apply here.
#define GENERIC_TILE_COPY_ROWS(_d, _dStride, _s, _sStride) do { \
uint8_t copyRow_; \
for (copyRow_ = 0; copyRow_ < TILE_PIXELS_PER_SIDE; copyRow_++) { \
memcpy((_d), (_s), TILE_BYTES_PER_ROW); \
(_d) += (_dStride); \
(_s) += (_sStride); \
} \
} while (0)
void jlpGenericTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint8_t *d;
const uint8_t *s;
uint8_t row;
if (dst->pixels == NULL || src->pixels == NULL) {
return;
}
d = GENERIC_TILE_ROW0(dst->pixels, dstBx, dstBy);
s = GENERIC_TILE_ROW0(src->pixels, srcBx, srcBy);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
d += SURFACE_BYTES_PER_ROW;
s += SURFACE_BYTES_PER_ROW;
}
GENERIC_TILE_COPY_ROWS(d, SURFACE_BYTES_PER_ROW, s, SURFACE_BYTES_PER_ROW);
}
@ -37,7 +51,6 @@ void jlpGenericTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, con
uint8_t row;
uint8_t col;
uint8_t srcByte;
uint8_t dstByte;
uint8_t transHi = (uint8_t)((transparent & 0x0F) << 4);
uint8_t transLo = (uint8_t)(transparent & 0x0F);
uint8_t srcHi;
@ -53,17 +66,18 @@ void jlpGenericTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, con
srcByte = s[col];
srcHi = (uint8_t)(srcByte & 0xF0);
srcLo = (uint8_t)(srcByte & 0x0F);
if (srcHi == transHi && srcLo == transLo) {
continue;
}
dstByte = d[col];
if (srcHi != transHi) {
dstByte = (uint8_t)((dstByte & 0x0F) | srcHi);
if (srcLo != transLo) {
// Both nibbles opaque (the common case in solid glyph
// strokes / tileset art): the merge result is exactly
// srcByte, so skip the destination read (PERF-AUDIT #51).
d[col] = srcByte;
} else {
d[col] = (uint8_t)((d[col] & 0x0F) | srcHi);
}
} else if (srcLo != transLo) {
d[col] = (uint8_t)((d[col] & 0xF0) | srcLo);
}
if (srcLo != transLo) {
dstByte = (uint8_t)((dstByte & 0xF0) | srcLo);
}
d[col] = dstByte;
}
d += SURFACE_BYTES_PER_ROW;
s += SURFACE_BYTES_PER_ROW;
@ -74,6 +88,9 @@ void jlpGenericTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, con
// Colorize a 1bpp-ish mono shape (non-zero nibble -> fg, zero -> bg) into a
// chunky tile, then paste via the jlpTilePaste op (IIgs asm / DOS generic). The
// caller (jlTilePasteMono) marks the rect dirty, so this does not.
// PRECONDITION: fgColor/bgColor arrive already masked to 4 bits -- the public
// jlTilePasteMono wrapper owns the & 0x0F (PERF-AUDIT #69; contract documented
// at the jlpTilePasteMono dispatch stanza in port.h). No re-mask here.
void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *monoTile, uint8_t fgColor, uint8_t bgColor) {
jlTileT colored;
uint8_t i;
@ -81,8 +98,6 @@ void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint
uint8_t hi;
uint8_t lo;
fgColor &= 0x0Fu;
bgColor &= 0x0Fu;
for (i = 0u; i < TILE_BYTES; i++) {
srcByte = monoTile[i];
hi = (uint8_t)((srcByte >> 4) ? fgColor : bgColor);
@ -97,17 +112,12 @@ void jlpGenericTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint
void jlpGenericTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunkyTile) {
uint8_t *d;
const uint8_t *s = chunkyTile;
uint8_t row;
if (dst->pixels == NULL) {
return;
}
d = GENERIC_TILE_ROW0(dst->pixels, bx, by);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
d += SURFACE_BYTES_PER_ROW;
s += TILE_BYTES_PER_ROW;
}
GENERIC_TILE_COPY_ROWS(d, SURFACE_BYTES_PER_ROW, s, TILE_BYTES_PER_ROW);
}
@ -115,31 +125,24 @@ void jlpGenericTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t
void jlpGenericTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, uint8_t *chunkyOut) {
const uint8_t *s;
uint8_t *d = chunkyOut;
uint8_t row;
if (src->pixels == NULL) {
return;
}
s = GENERIC_TILE_ROW0(src->pixels, bx, by);
for (row = 0; row < TILE_PIXELS_PER_SIDE; row++) {
d[0] = s[0]; d[1] = s[1]; d[2] = s[2]; d[3] = s[3];
s += SURFACE_BYTES_PER_ROW;
d += TILE_BYTES_PER_ROW;
}
GENERIC_TILE_COPY_ROWS(d, TILE_BYTES_PER_ROW, s, SURFACE_BYTES_PER_ROW);
}
void jlpGenericTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint8_t doubled = (uint8_t)(((colorIndex & 0x0F) << 4) | (colorIndex & 0x0F));
uint16_t pixelX = (uint16_t)((uint16_t)bx * TILE_PIXELS_PER_SIDE);
uint16_t pixelY = (uint16_t)((uint16_t)by * TILE_PIXELS_PER_SIDE);
uint8_t *row;
uint8_t i;
if (s->pixels == NULL) {
return;
}
row = &s->pixels[SURFACE_ROW_OFFSET(pixelY) + (pixelX >> 1)];
row = GENERIC_TILE_ROW0(s->pixels, bx, by);
for (i = 0; i < TILE_PIXELS_PER_SIDE; i++) {
row[0] = doubled;
row[1] = doubled;

View file

@ -15,11 +15,12 @@ bool jlSpriteCompile(jlSpriteT *sp) {
}
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y) {
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
(void)dst;
(void)sp;
(void)x;
(void)y;
(void)routeOffset;
// unreachable: jlSpriteDraw guards on sp->slot != NULL, which the
// stub never sets. Body is here only to satisfy the linker.
}

View file

@ -25,6 +25,7 @@ typedef void *Pointer;
#define _toolErr 0
#define attrPage 0x0004
#define attrNoCross 0x0010
#define attrNoSpec 0x0020
#define attrFixed 0x4000
#define attrLocked 0x8000

View file

@ -39,6 +39,8 @@
#include <iigs/toolbox.h>
typedef void *Handle;
#define _ownerid (MMStartUp())
#define attrAddr 0x0002
#define attrNoSpec 0x0020
#define attrFixed 0x4000
#define attrLocked 0x8000
// Leading bookkeeping slot: jlAlloc/jlFree traffic in plain pointers but
@ -69,18 +71,23 @@ extern void iigsSurfaceClearInner(uint8_t *pixels, uint16_t fillWord);
// Full-fill asm helper (partial leading byte + middle MVN + partial
// trailing byte). Called by the jlpFillRect macro (core/port.h).
extern void iigsFillRectInner(uint8_t *pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
// 16 STA abs,X stores at fixed offsets along a 160-byte stride.
// ~120 cyc per call.
extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t fillWord);
// 16 fixed-offset word stores along a 160-byte stride. Takes the raw
// fill nibble; the asm derives the $NNNN fill word so C never pays the
// * 0x1111 software-multiply helper (PERF-AUDIT #41). ~120 cyc per call.
extern void iigsTileFillInner(uint8_t *dstRow0, uint16_t nibble);
// Tile copy / paste / snap inner loops. All take 4-byte large-
// model pointers; bank may differ between dst and src (heap
// surface vs stage). Stride contracts:
// tileCopyInner / tileCopyMaskedInner: dst 160, src 160
// tilePasteInner: dst 160, src 4
// tilePasteInner / tilePasteMonoInner: dst 160, src 4
// tileSnapInner: dst 4, src 160
// tilePasteMonoInner is the fused mono colorize + paste (PERF-AUDIT #3):
// per-byte 2-bit opacity index (static table) into four per-call fg/bg
// combo bytes; fg/bg arrive pre-masked to 4 bits (PERF-AUDIT #69).
extern void iigsTileCopyInner(uint8_t *dstRow0, const uint8_t *srcRow0);
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
extern void iigsTilePasteInner(uint8_t *dstRow0, const uint8_t *srcTilePixels);
extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor);
extern void iigsTileSnapInner(uint8_t *dstTilePixels, const uint8_t *srcRow0);
// Single-pixel and Bresenham line plot. jlDrawLine inner takes
// pre-clipped endpoints (caller validates against surface bounds);
@ -90,12 +97,13 @@ extern void iigsDrawLineInner(uint8_t *pixels, uint16_t x0, uint16_t y0, uint16_
// Bresenham midpoint circle outline. Caller has verified the entire
// bbox is on-surface so no per-pixel clip.
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.
// The asm performs the upload directly rather than a C-side memcpy,
// which is unreliable when called from jlpPresent (DBR-state quirk
// after prior asm primitives).
extern void iigsBlitStageToShr(const uint8_t *scbPtr, const uint16_t *palettePtr, uint16_t uploadFlags);
// Stage-to-SHR pixel upload: per-row dirty-skipped PEI slam (wide
// rows) / exact-extent MVN (narrow rows), $01 -> $E1. SCB/palette
// upload is NOT here: uploadScbAndPaletteIfNeeded (below) owns it via
// dirty-flag-gated C memcpy, and the asm's old scbPtr/palettePtr/
// uploadFlags args plus their dead MVN upload paths were removed
// (PERF-AUDIT #52).
extern void iigsBlitStageToShr(void);
// jlFloodFill walk results: written by iigsFloodWalkAndScansInner,
// read back by the jlpFloodWalkAndScans macro (core/port.h).
extern uint16_t gFloodSeedMatch;
@ -108,7 +116,9 @@ extern void iigsFloodWalkAndScansInner(uint8_t *pixels, uint16_t x, uint16_t y,
// One-shot init for the y*160 lookup table (gRowOffsetLut, 400 bytes
// in DRAWPRIMS data). Called once from jlpInit. After this returns,
// every asm primitive that needs row offset can do `lda >lut,x` instead
// of the 7-instruction shift-add.
// of the 7-instruction shift-add. Also builds tpmNibPairIdx (256
// bytes), iigsTilePasteMonoInner's mono-byte -> opacity-pair index
// table (PERF-AUDIT #3).
extern void iigsInitRowLut(void);
// Per-row MVN blit from $01:srcOffset to $E1:srcOffset for partial-
// screen presents (the partial-rect present). srcOffset is the byte offset
@ -121,9 +131,9 @@ extern void iigsBlitRectStageToShr(uint16_t srcOffset, uint16_t copyBytes, uint1
// (caller / dispatcher checks). For sprite-rect presents (typical
// 8 bytes wide x 16 rows) saves ~600 cyc/frame vs the MVN form.
extern void iigsBlitRectStageToShrPEI(uint16_t srcOffset, uint16_t copyBytes, uint16_t rowsLeft);
// Filled circle, scanline-style. fillWord low byte is the doubled
// nibble (e.g., 0x33 for nibble 3).
extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t fillWord);
// Filled circle, scanline-style. Takes the raw fill nibble; the asm
// derives the doubled fill byte and RMW nibble bytes (PERF-AUDIT #41).
extern void iigsFillCircleInner(uint8_t *pixels, uint16_t cx, uint16_t cy, uint16_t r, uint16_t nibble);
// ----- Hardware addresses (24-bit / long pointers) -----
@ -256,8 +266,11 @@ void jlpBigFree(void *p) {
}
static void claimStagePixels(void);
bool jlpInit(const jlConfigT *config) {
(void)config;
claimStagePixels();
gPreviousNewVideo = *IIGS_NEWVIDEO_REG;
gPreviousBorder = *IIGS_BORDER_REG;
gPreviousShadow = *IIGS_SHADOW_REG;
@ -267,10 +280,11 @@ bool jlpInit(const jlConfigT *config) {
// write to $01/2000-9FFF mirrors to $E1 and the off-screen-buffer
// illusion breaks (the user would see drawing in progress).
*IIGS_SHADOW_REG = (uint8_t)(gPreviousShadow | SHADOW_INHIBIT_SHR_MASK);
// SCB and palette are uploaded by jlpPresent's iigsBlitStageToShr
// (asm path, MVN to bank $E1). C-side memset/memcpy to bank $E1
// is unreliable from jlpInit's calling context, so we don't try
// it here -- the first present will set up SCB to 320 mode.
// SCB and palette are uploaded by jlpPresent's C-side
// uploadScbAndPaletteIfNeeded (the dirty flags start true, so the
// first present sets up SCB to 320 mode). C-side memset/memcpy to
// bank $E1 is unreliable from jlpInit's calling context, so we
// don't try it here.
iigsInitRowLut();
gFrameHz = (iigsReadHzParam() == 1u) ? 50u : 60u;
gModeSet = true;
@ -283,19 +297,16 @@ void jlpPresent(const jlSurfaceT *src) {
return;
}
// SCB + palette upload via the C-side memcpy to bank $E1, then pixels.
// The asm palette MVN in iigsBlitStageToShr never wrote $E1:9E00 -- the
// SCB MVN landed but colors showed the GS/OS default (pixels/shapes were
// correct, every color wrong). The C memcpy is reliable now that the
// far-const-pointer bank-drop miscompile is fixed (it formerly hit bank
// $00). uploadScbAndPaletteIfNeeded clears the dirty bits, so the asm
// blit (uploadFlags == 0) does pixels only. The asm's now-unreached
// SCB/palette MVN code is dead -- a removal candidate.
// (The asm's old SCB/palette MVN paths never wrote $E1:9E00 reliably and
// were dead once this C path landed; PERF-AUDIT #52 removed them along
// with the per-present scbPtr/palettePtr/uploadFlags marshalling, so
// iigsBlitStageToShr is pixels-only and takes no args.)
// KNOWN RESIDUAL: on a DRAW the PEI-slam corrupts $E1:9E20+ (palettes
// 1-15; palette 0 and pixels stay correct) -- a peislam.s shadow / soft-
// switch spill, independent of upload order. Single-palette apps are
// unaffected; multi-palette colors above index 0 need the slam fix.
uploadScbAndPaletteIfNeeded(src);
iigsBlitStageToShr(src->scb, &src->palette[0][0], 0);
iigsBlitStageToShr();
}
@ -316,6 +327,56 @@ void jlpShutdown(void) {
// iigs*Inner asm entry points above, so no C wrapper functions live here.
// Memory Manager reservation for the pinned framebuffer. Without it the
// MM considers $01:2000-$9CFF free and can place ANY handle inside the
// display -- jlpBigAlloc'd sprite/surface structs landed there and were
// painted over by surface clears (PERF-AUDIT.md #85, the root of the
// #84 sprite pathology). attrAddr claims the exact address; the range
// is in special memory (bank $01) by definition. The claim MUST happen
// in jlpInit, BEFORE the first jlpBigAlloc: stageAlloc allocates the
// stage struct before it calls jlpStageAllocPixels, so claiming here
// would already be too late to keep that struct out of the display.
static void *gStagePixelsHandle = NULL;
// Bank-0 region the linker uses without telling the Memory Manager:
// the C heap ($BC67-$BF00 in the current layout) AND the bank-0
// ALIASES of joeyDraw.s's D-relative pointer-scratch symbols
// (dpxlScratch/dlnScratch/dcPixPtr/fcPixPtr, ~$BBB8-$BC66) -- the
// [pix],y pattern reads/writes bank 0 at the symbol's OFFSET. If an MM
// handle (codegen arena, jlpBigAlloc scratch) lands there, every draw
// primitive plots through stomped pointers (PERF-AUDIT.md #88, the
// codegen-on showcase hang). Claim generously: $00:B900-$BEFF.
#define IIGS_BANK0_CLAIM_BASE ((void *)0x00B900L)
#define IIGS_BANK0_CLAIM_BYTES 0x0600ul
static void *gBank0ScratchHandle = NULL;
static void claimStagePixels(void) {
if (gStagePixelsHandle == NULL) {
gStagePixelsHandle = NewHandle((unsigned long)SURFACE_PIXELS_SIZE,
_ownerid,
attrAddr | attrFixed | attrLocked,
(void *)IIGS_STAGE_PIXELS);
if (gStagePixelsHandle == NULL) {
// The range is already owned (another app / a prior claim):
// proceed rather than fail init, but leave a breadcrumb for
// jlLastError. (No _toolErr shim in this toolchain yet; a
// NULL handle is the only failure signal.)
coreSetError("stage framebuffer MM reservation failed");
}
}
if (gBank0ScratchHandle == NULL) {
gBank0ScratchHandle = NewHandle(IIGS_BANK0_CLAIM_BYTES,
_ownerid,
attrAddr | attrFixed | attrLocked,
IIGS_BANK0_CLAIM_BASE);
if (gBank0ScratchHandle == NULL) {
coreSetError("bank-0 heap/scratch MM reservation failed");
}
}
}
uint8_t *jlpStageAllocPixels(void) {
return IIGS_STAGE_PIXELS;
}
@ -323,7 +384,15 @@ uint8_t *jlpStageAllocPixels(void) {
void jlpStageFreePixels(uint8_t *pixels) {
(void)pixels;
// Backing memory is hardware-pinned; nothing to free.
// Backing memory is hardware-pinned; release only the MM claims.
if (gStagePixelsHandle != NULL) {
DisposeHandle(gStagePixelsHandle);
gStagePixelsHandle = NULL;
}
if (gBank0ScratchHandle != NULL) {
DisposeHandle(gBank0ScratchHandle);
gBank0ScratchHandle = NULL;
}
}
@ -341,46 +410,29 @@ void jlpStageFreePixels(uint8_t *pixels) {
* the legacy paths did. Same logic as the DOS port. */
// $C019 RDVBLBAR: bit 7 = 0 during vertical blank, 1 during active
// scan. To produce a rising-edge wait (one VBL per call), first spin
// while VBL is currently active (bit 7 = 0), then spin until VBL
// fires again (bit 7 returns to 0). The IIgs SHR refresh is 60 Hz.
// Block until the next VBL boundary by waiting for the firmware tick
// (GetTick, incremented by the VBL interrupt handler) to advance. The
// old form polled $C019's VBL status bit, which never reads high under
// MAME's apple2gs (PERF-AUDIT.md finding #76): the wait-for-scan loop
// spun forever, hanging every program that paces with jlWaitVBL. UBER
// is the only example that calls it, so no verification gate ever
// caught this. Tick granularity IS the VBL boundary, so semantics are
// unchanged on real hardware; the caller must not hold SEI (a masked
// VBL interrupt freezes the tick, exactly as it froze the old bit).
void jlpWaitVBL(void) {
while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) == 0) {
/* already in VBL: wait for active scan */;
}
while ((*IIGS_VBL_STATUS & VBL_BAR_BIT) != 0) {
/* scanning: wait for next VBL */;
uint16_t tick;
tick = iigsGetTickWord();
while (iigsGetTickWord() == tick) {
/* wait for the VBL interrupt to advance the tick */;
}
}
// Frame counter via $C019 polling. Edge-detected on each call: the
// caller (UBER, animation loops) polls fast enough that we never
// miss a VBL transition. No IRQ involvement; safe in the S16 takeover
// context where ToolBox interrupt setup would be intrusive.
//
// gFrameCount uses an explicit lda+adc+sta read-modify-write rather
// 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
// jlpFrameCount called from CORESYS via JSL, DBR isn't pointing at
// DRAWPRIMS's data bank, so the abs INC silently mutates the wrong
// byte and the counter never advances. The explicit lda > / sta >
// pattern uses long-mode addressing throughout, which is
// DBR-independent.
uint16_t jlpFrameCount(void) {
return iigsGetTickWord();
}
// (jlpFrameCount is a port.h macro straight to iigsGetTickWord -- the
// GetTick asm wrapper in peislam.s -- so no C wrapper lives here.)
uint16_t jlpFrameHz(void) {
return gFrameHz;
}
// 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
// (sound event scheduling is fine).

File diff suppressed because it is too large Load diff

View file

@ -1,117 +1,65 @@
// IIgs (65816) sprite codegen. Emits PIC draw routines that write
// 4bpp packed surface bytes via abs,Y indexed addressing.
// IIgs (65816) sprite codegen. Emits position-independent, C-ABI
// routines that jlSpriteCompile places in the codegen arena and the
// IIgs dispatchers in spriteCompile.c invoke through ordinary C
// function pointers -- the compiler emits the cross-bank JSL itself.
// There is NO self-modifying call stub and NO inline asm (that
// machinery was deleted in the #19 rewrite; see the note in
// spriteCompile.c).
//
// 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
// loads Y with destRow then JSLs the routine. The routine assumes:
// - M = 8-bit (set by stub before JSL)
// - X = 16-bit (set by stub)
// - Y = destRow (loaded by stub from immediate)
// - DBR = program bank
// No stack arg, no prologue. Body executes directly.
// CALLING CONVENTION (llvm-mos w65816 cdecl; M=16, X=16 at entry and
// exit):
//
// Routine shape (per-byte emit, no PEA optimization yet):
// ... per byte:
// lda #imm ; A = pixel-pair byte (opaque)
// sta abs,Y ; write to dst[abs]
// ... mixed:
// lda abs,Y; and #~mask; ora #val; sta abs,Y
// rtl
// DRAW: void draw(uint8_t *dstRow0);
// dstRow0 (the sprite's top-left screen byte) is a 24-bit pointer
// arriving as A = low 16 bits, X = bank. The emitted prologue
// saves the caller's DBR (PHB), moves the offset into Y, drops to
// M=8, and loads DBR with the destination bank so the body's
// `sta abs,Y` stores land in the screen bank. The epilogue
// restores M=16, pulls the caller's DBR back (PLB), and RTLs.
//
// Position-independent: only abs constants are dest-byte offsets
// (small, baked at emit time); Y holds the runtime dst pointer.
// SAVE/RESTORE: void copy(uint32_t packed);
// packed = srcOff | (dstOff << 16), arriving as A = srcOff,
// X = dstOff. The routine is an unrolled chain of MVN row copies;
// the only runtime-dynamic bytes are the MVN bank operands, which
// the dispatcher patches per call with a plain C pointer write
// into the arena (patchMvnBanks in spriteCompile.c). The caller's
// DBR is preserved via PHB/PLB (MVN leaves DBR = dst bank);
// returns via RTL with M=16.
//
// DRAW body shape (see spriteEmitDrawIigs for the M=8/M=16 dual-path
// details):
// opaque byte: lda #imm; sta abs,Y
// mixed byte: lda abs,Y; and #~mask; ora #val; sta abs,Y
// opaque run: rep #$20; lda #imm16; sta abs,Y; ...; sep #$20
//
// Position-independent: the only abs operands are dest-byte offsets
// baked at emit time; Y carries the runtime dst offset.
//
// All multi-byte operands are little-endian, written low byte first
// into the output stream.
#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitChunky.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_PIXELS 8
#define TILE_BYTES 32
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
// Tile geometry comes from joey/tile.h; TRANSPARENT_NIBBLE from
// spriteInternal.h.
// ----- Prototypes -----
// shiftedByteAt / spriteSourceByte -- the transparency contract shared
// with the x86 emitter -- come from spriteEmitChunky.h.
static uint32_t emitMvnCabiRoutine(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);
// ----- Emit helpers (alphabetical) -----
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask) {
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
bool hasLeft;
bool hasRight;
*outValue = 0;
*outOpaqueMask = 0;
if (shift == 0) {
if (col >= spriteBytesPerRow) {
return;
}
srcByte = spriteSourceByte(sp, row, col);
hi = (uint8_t)((srcByte >> 4) & 0x0Fu);
lo = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
return;
}
hasLeft = (col >= 1) && ((uint16_t)(col - 1) < spriteBytesPerRow);
hasRight = (col < spriteBytesPerRow);
if (hasLeft) {
srcByte = spriteSourceByte(sp, row, (uint16_t)(col - 1));
hi = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
}
if (hasRight) {
srcByte = spriteSourceByte(sp, row, col);
lo = (uint8_t)((srcByte >> 4) & 0x0Fu);
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
}
}
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col) {
uint16_t tileX;
uint16_t tileY;
uint16_t inTileX;
uint16_t inTileY;
const uint8_t *tile;
tileX = (uint16_t)(col / TILE_BYTES_PER_ROW);
tileY = (uint16_t)(row / TILE_PIXELS);
inTileX = (uint16_t)(col & (TILE_BYTES_PER_ROW - 1));
inTileY = (uint16_t)(row & (TILE_PIXELS - 1));
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
return tile[inTileY * TILE_BYTES_PER_ROW + inTileX];
}
// ----- Emit helpers -----
// writeLE16 was inlined at every call site. Inlining cuts a JSL/RTL
// per emitted 16-bit immediate (4 instructions per byte * 12 sites).
@ -135,20 +83,13 @@ static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col)
// end:
// RTL
//
// The bank operand bytes (offset 4 and 5 within each MVN row) are
// patched per call by spriteCompiledSaveUnder / spriteCompiledRestoreUnder
// so the routine works regardless of where the surface and backup
// happen to live in memory.
//
// Layout:
// row 0 (6 bytes): A9 lo hi 54 db sb
// row R (12 bytes, R>=1): 8A/98 18 69 lo hi AA/A8 A9 lo hi 54 db sb
// end (1 byte): 6B
//
// The MVN at row R has its dstbk at routine offset (12*R + 4) and
// srcbk at (12*R + 5).
// The bank operand bytes are patched per call by
// spriteCompiledSaveUnder / spriteCompiledRestoreUnder so the routine
// works regardless of where the surface and backup happen to live in
// memory; the authoritative byte layout (including the patch offsets)
// is in the C-ABI comment below.
// Worst-case bytes emitted per MVN copy row: row 0 = 6 bytes, any
// later row = 12. Plus the trailing RTL.
// later row = 12. Plus the prologue and the trailing PLB/RTL.
#define IIGS_MVN_MAX_BYTES_PER_ROW 12u
// C-ABI MVN copy routine. Called via a normal C function pointer:
@ -221,11 +162,11 @@ static uint32_t emitMvnCabiRoutine(uint8_t *out, uint32_t cap, uint16_t heightPx
}
// SAVE (screen -> backup). Stub passes X = screen low offset (the
// source), Y = backup low offset (the destination); MVN advances both
// 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.
// SAVE (screen -> backup). The prologue leaves X = screen low offset
// (the source), Y = backup low offset (the destination); MVN advances
// both 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.
uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
@ -235,7 +176,7 @@ uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
return 0u;
}
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
@ -245,9 +186,10 @@ uint32_t spriteEmitSaveIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
}
// 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.
// RESTORE (backup -> screen). The prologue leaves 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.
uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint16_t heightPx;
uint16_t spriteBytesPerRow;
@ -257,7 +199,7 @@ uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp,
return 0u;
}
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
copyBytes = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
@ -272,7 +214,7 @@ uint32_t spriteEmitRestoreIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp,
//
// Two emission paths share the body:
//
// * M=8 byte path (default; matches the stub-set entry mode):
// * M=8 byte path (default; the emitted prologue enters M=8):
// opaque: A9 vv LDA #vv ; 2c
// 99 lo hi STA abs,Y ; 5c
// ; 7c / 5 bytes per byte
@ -318,7 +260,7 @@ uint32_t spriteEmitDrawIigs(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uin
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
destBytesPerRow = (uint16_t)(spriteBytesPerRow + shift); /* shift is 0 or 1 */
wide = false;

View file

@ -1,331 +0,0 @@
// 68k sprite codegen (Amiga + Atari ST). Emits SysV-ish cdecl-
// callable PIC draw / save / restore routines that read or write
// 4bpp packed surface bytes via d16(a0) chains. Same shape as the
// x86 emitter; only the instruction encoding differs.
//
// Calling convention (m68k gcc / mintlib):
// void draw(uint8_t *dst); -- arg in 4(sp)
// void save/restore(const uint8_t *src, uint8_t *dst); -- args in 4(sp)/8(sp)
// a0/a1/d0/d1 are caller-saved.
//
// Per-byte emit (no run coalescing yet):
// - all-transparent: skip
// - all-opaque: move.b #imm, d16(a0) (6 bytes encoded)
// - mixed: move.b d16(a0),d0; andi.b #~mask,d0;
// ori.b #val,d0; move.b d0,d16(a0) (4*4 = 16 bytes)
// Per row (after first): adda.w #SURFACE_BYTES_PER_ROW, a0
// (4 bytes encoded)
// Prologue: movea.l 4(sp), a0 (4 bytes)
// Epilogue: rts (2 bytes)
//
// All multi-byte instruction fields are big-endian; the emit writes
// high-byte-first into the output stream so the target reads them
// in native order.
#include "joey/sprite.h"
#include "joey/surface.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_PIXELS 8
#define TILE_BYTES 32
#define TILE_BYTES_PER_ROW 4
#define TRANSPARENT_NIBBLE 0
// 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 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);
// ----- Emit helpers (alphabetical) -----
// Shared body for save/restore. Walks heightPx rows of copyBytes
// using `move.b (a0)+, (a1)+` byte-wise (safe regardless of pointer
// alignment, since the screen-side x can land on an odd byte). After
// each row except the last, advances either a0 (SAVE: src=screen) or
// a1 (RESTORE: dst=screen) by (SURFACE_BYTES_PER_ROW - copyBytes) so
// the strided side lines up with the next scanline; the contiguous
// side advances naturally via the post-increment.
//
// strideOnSrc=true -> source has the screen stride (SAVE)
// strideOnSrc=false -> destination has the screen stride (RESTORE)
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);
}
if (row + 1u < heightPx) {
// adda.w #advance, a0 (0xD0FC) for SAVE
// adda.w #advance, a1 (0xD2FC) for RESTORE
cursor += writeBE16(out + cursor, strideOnSrc ? 0xD0FCu : 0xD2FCu);
cursor += writeBE16(out + cursor, advance);
}
}
return cursor;
}
// Same logic as the x86 shiftedByteAt -- per-byte transparency
// decomposition for shift in {0,1}. opaqueMask high nibble 0xF0 if
// dest high nibble is opaque, 0x0F if low is opaque.
static void shiftedByteAt(const jlSpriteT *sp, uint16_t row, uint16_t col, uint8_t shift, uint16_t spriteBytesPerRow, uint8_t *outValue, uint8_t *outOpaqueMask) {
uint8_t srcByte;
uint8_t hi;
uint8_t lo;
bool hasLeft;
bool hasRight;
*outValue = 0;
*outOpaqueMask = 0;
if (shift == 0) {
if (col >= spriteBytesPerRow) {
return;
}
srcByte = spriteSourceByte(sp, row, col);
hi = (uint8_t)((srcByte >> 4) & 0x0Fu);
lo = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
return;
}
hasLeft = (col >= 1) && ((uint16_t)(col - 1) < spriteBytesPerRow);
hasRight = (col < spriteBytesPerRow);
if (hasLeft) {
srcByte = spriteSourceByte(sp, row, (uint16_t)(col - 1));
hi = (uint8_t)(srcByte & 0x0Fu);
if (hi != TRANSPARENT_NIBBLE) {
*outValue |= (uint8_t)(hi << 4);
*outOpaqueMask |= 0xF0u;
}
}
if (hasRight) {
srcByte = spriteSourceByte(sp, row, col);
lo = (uint8_t)((srcByte >> 4) & 0x0Fu);
if (lo != TRANSPARENT_NIBBLE) {
*outValue |= lo;
*outOpaqueMask |= 0x0Fu;
}
}
}
static uint8_t spriteSourceByte(const jlSpriteT *sp, uint16_t row, uint16_t col) {
uint16_t tileX;
uint16_t tileY;
uint16_t inTileX;
uint16_t inTileY;
const uint8_t *tile;
tileX = (uint16_t)(col / TILE_BYTES_PER_ROW);
tileY = (uint16_t)(row / TILE_PIXELS);
inTileX = (uint16_t)(col & (TILE_BYTES_PER_ROW - 1));
inTileY = (uint16_t)(row & (TILE_PIXELS - 1));
tile = sp->tileData + ((uint32_t)(tileY * sp->widthTiles + tileX)) * TILE_BYTES;
return tile[inTileY * TILE_BYTES_PER_ROW + inTileX];
}
// Emit a 16-bit big-endian value into the output stream. Returns 2.
static uint16_t writeBE16(uint8_t *out, uint16_t value) {
out[0] = (uint8_t)((value >> 8) & 0xFFu);
out[1] = (uint8_t)(value & 0xFFu);
return 2;
}
// 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;
uint16_t spriteBytesPerRow;
uint16_t destBytesPerRow;
uint8_t value;
uint8_t opaqueMask;
// Chunky 4bpp has only two nibble-alignment positions; the
// dispatcher uses x & 1 so shifts 2..7 are unreachable. Bail
// early so the arena slot stays SPRITE_NOT_COMPILED.
if (shift > 1u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
spriteBytesPerRow = (uint16_t)(sp->widthTiles * TILE_BYTES_PER_ROW);
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, 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);
}
for (col = 0; col < destBytesPerRow; col++) {
shiftedByteAt(sp, row, col, shift, spriteBytesPerRow, &value, &opaqueMask);
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 =
// dst mode (101 = an+d16), bits 5-3 = src mode (111),
// bits 2-0 = src reg (100 = immediate). Source
// extension (imm word, byte in low half) comes BEFORE
// dest extension (d16) in the instruction stream.
cursor += writeBE16(out + cursor, 0x117Cu);
cursor += writeBE16(out + cursor, (uint16_t)value);
cursor += writeBE16(out + cursor, col);
} else {
// move.b d16(a0), d0
cursor += writeBE16(out + cursor, 0x1028u);
cursor += writeBE16(out + cursor, col);
// andi.b #~opaqueMask, d0
cursor += writeBE16(out + cursor, 0x0200u);
cursor += writeBE16(out + cursor, (uint16_t)(~opaqueMask & 0xFFu));
// ori.b #value, d0
cursor += writeBE16(out + cursor, 0x0000u);
cursor += writeBE16(out + cursor, (uint16_t)value);
// move.b d0, d16(a0)
cursor += writeBE16(out + cursor, 0x1140u);
cursor += writeBE16(out + cursor, col);
}
}
}
// 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.
uint32_t spriteEmitRestore68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
if (shift > 1u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
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, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
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.
uint32_t spriteEmitSave68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint16_t heightPx;
uint16_t copyBytes;
if (shift > 1u) {
return 0u;
}
cursor = 0;
heightPx = (uint16_t)(sp->heightTiles * TILE_PIXELS);
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, STACK_ARG0_DISP);
cursor += writeBE16(out + cursor, 0x226Fu);
cursor += writeBE16(out + cursor, STACK_ARG1_DISP);
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;
}

63
src/m68k/spriteEmitC2p.h Normal file
View file

@ -0,0 +1,63 @@
// Shared 68k emit-time helpers: chunky->planar column conversion and
// big-endian opcode-word emission. Included (as static functions) by
// the two 68k emitters -- spriteEmitPlanar68k.c (Amiga) and
// spriteEmitInterleaved68k.c (ST) -- so the c2p pixel walk that must
// match the interpreted planes walkers bit-for-bit (jlpSpriteDrawPlanes
// on Amiga, stSpriteDrawByteAligned on ST) exists exactly once.
//
// Emit-time only (jlSpriteCompile), never on a draw hot path, so plain
// static functions are acceptable here despite the general
// macros-over-inline preference for shared headers.
#ifndef JOEYLIB_SPRITE_EMIT_C2P_H
#define JOEYLIB_SPRITE_EMIT_C2P_H
#include <stdint.h>
#include "spriteInternal.h"
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut);
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w);
// Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4
// plane bytes plus an opaque mask, bit 7 = leftmost pixel. Pixel order
// is hi(b0),lo(b0),hi(b1),lo(b1),hi(b2),lo(b2),hi(b3),lo(b3) at bit
// positions 7..0.
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) {
uint8_t opaque;
uint8_t i;
plane[0] = 0u;
plane[1] = 0u;
plane[2] = 0u;
plane[3] = 0u;
opaque = 0u;
for (i = 0u; i < 8u; i++) {
uint8_t nibble;
uint8_t bit;
nibble = (i & 1u) ? (uint8_t)(trp[i >> 1] & 0x0Fu)
: (uint8_t)(trp[i >> 1] >> 4);
if (nibble != TRANSPARENT_NIBBLE) {
bit = (uint8_t)(0x80u >> i);
opaque = (uint8_t)(opaque | bit);
if (nibble & 1u) { plane[0] = (uint8_t)(plane[0] | bit); }
if (nibble & 2u) { plane[1] = (uint8_t)(plane[1] | bit); }
if (nibble & 4u) { plane[2] = (uint8_t)(plane[2] | bit); }
if (nibble & 8u) { plane[3] = (uint8_t)(plane[3] | bit); }
}
}
*opaqueOut = opaque;
}
// Append a big-endian 16-bit word at cursor; returns the new cursor.
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w) {
out[cursor] = (uint8_t)(w >> 8);
out[cursor + 1u] = (uint8_t)(w & 0xFFu);
return cursor + 2u;
}
#endif

View file

@ -34,15 +34,16 @@
#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitC2p.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_BYTES 32u // bytes per 8x8 4bpp tile
#define TILE_BYTES_PER_ROW 4u // 8 pixels * 4 bits / 8
#define TILE_PIXELS 8u
// Tile geometry (TILE_BYTES / TILE_BYTES_PER_ROW / TILE_PIXELS_PER_SIDE)
// comes from joey/tile.h.
#define ST_PLANAR_STRIDE 160u // 320 px * 4 planes / 8 bits
#define ST_GROUP_BYTES 8u // 4 plane words per 16-px group
@ -68,52 +69,11 @@
#define EPILOGUE_BYTES 2u
// ----- Prototypes -----
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut);
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w);
// ----- Emit helpers -----
// Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4
// plane bytes plus an opaque mask, bit 7 = leftmost pixel. Identical
// pixel walk to stSpriteDrawByteAligned: hi(b0),lo(b0),hi(b1),lo(b1),
// hi(b2),lo(b2),hi(b3),lo(b3) at bit positions 7..0.
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) {
uint8_t opaque;
uint8_t i;
plane[0] = 0u;
plane[1] = 0u;
plane[2] = 0u;
plane[3] = 0u;
opaque = 0u;
for (i = 0u; i < 8u; i++) {
uint8_t nibble;
uint8_t bit;
nibble = (i & 1u) ? (uint8_t)(trp[i >> 1] & 0x0Fu)
: (uint8_t)(trp[i >> 1] >> 4);
if (nibble != 0u) {
bit = (uint8_t)(0x80u >> i);
opaque = (uint8_t)(opaque | bit);
if (nibble & 1u) { plane[0] = (uint8_t)(plane[0] | bit); }
if (nibble & 2u) { plane[1] = (uint8_t)(plane[1] | bit); }
if (nibble & 4u) { plane[2] = (uint8_t)(plane[2] | bit); }
if (nibble & 8u) { plane[3] = (uint8_t)(plane[3] | bit); }
}
}
*opaqueOut = opaque;
}
// Append a big-endian 16-bit word at cursor; returns the new cursor.
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w) {
out[cursor] = (uint8_t)(w >> 8);
out[cursor + 1u] = (uint8_t)(w & 0xFFu);
return cursor + 2u;
}
// c2pColumn (identical pixel walk to stSpriteDrawByteAligned:
// hi(b0),lo(b0),...,hi(b3),lo(b3) at bit positions 7..0) and putWord
// are shared with the Amiga emitter via spriteEmitC2p.h.
// ----- Emit API -----
@ -130,7 +90,7 @@ uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSprite
return 0u;
}
wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS);
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
xMod16 = (uint16_t)(shift * 8u); // shift 0 -> 0, shift 1 -> 8
cursor = 0u;

View file

@ -31,15 +31,16 @@
#include "joey/sprite.h"
#include "joey/surface.h"
#include "joey/tile.h"
#include "spriteEmitC2p.h"
#include "spriteEmitter.h"
#include "spriteInternal.h"
// ----- Constants -----
#define TILE_BYTES 32u // bytes per 8x8 4bpp tile
#define TILE_BYTES_PER_ROW 4u // 8 pixels * 4 bits / 8
#define TILE_PIXELS 8u
// Tile geometry (TILE_BYTES / TILE_BYTES_PER_ROW / TILE_PIXELS_PER_SIDE)
// comes from joey/tile.h.
#define AMIGA_PLANE_STRIDE (SURFACE_WIDTH / 8) // 40 bytes/plane-row
#define AMIGA_PLANES 4u
@ -75,51 +76,10 @@ static const uint16_t OP_MOVE_B_IMM_AN[AMIGA_PLANES] = { 0x117Cu, 0x137Cu, 0x157
#define EPILOGUE_BYTES 6u // movem(4) + rts(2)
// ----- Prototypes -----
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut);
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w);
// ----- Emit helpers -----
// Convert one 8-pixel sprite tile column (4 chunky 4bpp bytes) into 4
// plane bytes plus an opaque mask, bit 7 = leftmost pixel. Identical
// pixel walk to jlpSpriteDrawPlanes' c2p loop.
static void c2pColumn(const uint8_t *trp, uint8_t plane[4], uint8_t *opaqueOut) {
uint8_t opaque;
uint8_t i;
plane[0] = 0u;
plane[1] = 0u;
plane[2] = 0u;
plane[3] = 0u;
opaque = 0u;
for (i = 0u; i < 8u; i++) {
uint8_t nibble;
uint8_t bit;
nibble = (i & 1u) ? (uint8_t)(trp[i >> 1] & 0x0Fu)
: (uint8_t)(trp[i >> 1] >> 4);
if (nibble != 0u) {
bit = (uint8_t)(0x80u >> i);
opaque = (uint8_t)(opaque | bit);
if (nibble & 1u) { plane[0] = (uint8_t)(plane[0] | bit); }
if (nibble & 2u) { plane[1] = (uint8_t)(plane[1] | bit); }
if (nibble & 4u) { plane[2] = (uint8_t)(plane[2] | bit); }
if (nibble & 8u) { plane[3] = (uint8_t)(plane[3] | bit); }
}
}
*opaqueOut = opaque;
}
// Append a big-endian 16-bit word at cursor; returns the new cursor.
static uint32_t putWord(uint8_t *out, uint32_t cursor, uint16_t w) {
out[cursor] = (uint8_t)(w >> 8);
out[cursor + 1u] = (uint8_t)(w & 0xFFu);
return cursor + 2u;
}
// c2pColumn (identical pixel walk to jlpSpriteDrawPlanes' c2p loop)
// and putWord are shared with the ST emitter via spriteEmitC2p.h.
// ----- Emit API -----
@ -136,7 +96,7 @@ uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp
return 0u;
}
wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS);
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
cursor = 0u;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,120 @@
amiga: shadow plane 0 in CHIP (blitter, addr=$00015280)
amiga: shadow plane 1 in CHIP (blitter, addr=$000171C0)
amiga: shadow plane 2 in CHIP (blitter, addr=$00019100)
amiga: shadow plane 3 in CHIP (blitter, addr=$0001B040)
UBER: jlSpriteCompile: 1 call in <= 1 frame
UBER: audioInit OK
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
UBER: ----- begin -----
UBER: jlSurfaceClear: 33 iters / 22 frames = 75 ops/sec | hash=428CEE42
UBER: jlPaletteSet: 2145 iters / 16 frames = 6703 ops/sec | hash=5267FFB1
UBER: jlScbSetRange: 1233 iters / 16 frames = 3853 ops/sec | hash=5267FFB1
UBER: jlDrawPixel: 1361 iters / 16 frames = 4253 ops/sec | hash=75D75161
UBER: jlDrawLine H: 513 iters / 16 frames = 1603 ops/sec | hash=1E97E361
UBER: jlDrawLine V: 49 iters / 16 frames = 153 ops/sec | hash=A0474D91
UBER: jlDrawLine diag: 13 iters / 16 frames = 40 ops/sec | hash=69471D91
UBER: jlDrawRect 100x100: 49 iters / 19 frames = 128 ops/sec | hash=5CAFF471
UBER: jlDrawCircle r=16: 97 iters / 17 frames = 285 ops/sec | hash=7F1F5531
UBER: jlDrawCircle r=80: 33 iters / 26 frames = 63 ops/sec | hash=B249C9AB
UBER: jlFillRect 16x16: 193 iters / 17 frames = 567 ops/sec | hash=F4BD4B0B
UBER: jlFillRect 80x80: 33 iters / 23 frames = 71 ops/sec | hash=7C952E5F
UBER: jlFillRect 320x200: 33 iters / 24 frames = 68 ops/sec | hash=6467AFB1
UBER: jlFillCircle r=40: 10 iters / 16 frames = 31 ops/sec | hash=6467AFB1
UBER: jlSamplePixel: 2321 iters / 16 frames = 7253 ops/sec | hash=6467AFB1
UBER: jlTileFill: 721 iters / 16 frames = 2253 ops/sec | hash=6467AFB1
UBER: jlTileCopy: 593 iters / 16 frames = 1853 ops/sec | hash=6467AFB1
UBER: jlTileCopyMasked: 305 iters / 16 frames = 953 ops/sec | hash=6467AFB1
UBER: jlTilePaste: 737 iters / 16 frames = 2303 ops/sec | hash=6467AFB1
UBER: jlTileSnap: 1137 iters / 16 frames = 3553 ops/sec | hash=6467AFB1
UBER: jlSpriteSaveUnder: 177 iters / 16 frames = 553 ops/sec | hash=CA673FB1
UBER: jlSpriteDraw: 305 iters / 16 frames = 953 ops/sec | hash=82B9646B
UBER: jlSpriteRestoreUnder: 177 iters / 16 frames = 553 ops/sec | hash=CA673FB1
UBER: jlSpriteSaveAndDraw: 113 iters / 16 frames = 353 ops/sec | hash=82B9646B
UBER: jlStagePresent full: 105 iters / 16 frames = 328 ops/sec | hash=82B9646B
UBER: jlInputPoll: 1281 iters / 16 frames = 4003 ops/sec | hash=82B9646B
UBER: jlKeyDown: 12737 iters / 16 frames = 39803 ops/sec | hash=82B9646B
UBER: jlKeyPressed: 12161 iters / 16 frames = 38003 ops/sec | hash=82B9646B
UBER: jlMouseX: 31265 iters / 16 frames = 97703 ops/sec | hash=82B9646B
UBER: joeyJoyConnected: 12465 iters / 16 frames = 38953 ops/sec | hash=82B9646B
UBER: jlAudioFrameTick: 18241 iters / 16 frames = 57003 ops/sec | hash=82B9646B
UBER: jlAudioIsPlayingMod: 17857 iters / 16 frames = 55803 ops/sec | hash=82B9646B
UBER: surfaceMarkDirtyRect (via jlFillRect 32x32): 113 iters / 16 frames = 353 ops/sec | hash=D2B9E46B
UBER: ----- end -----
UBER-CHK: ----- begin -----
UBER-CHK: edge-pixels: hash=5267FFB1
UBER-CHK: edge-drawRect: hash=BC75D707
UBER-CHK: edge-circles: hash=20674FB1
UBER-CHK: edge-lines: hash=D4F5BCA7
UBER-CHK: sprite-clipped: hash=D922E4B4
UBER-CHK: sprite-clip-roundtrip: PASS
UBER-CHK: tilePasteMono: hash=B0829554
UBER-CHK: floodFill: hash=780DC48B
UBER-CHK: drawText: hash=80B5264B
UBER-CHK: drawText-offgrid: hash=9435DA4B
UBER-CHK: palette-roundtrip: PASS
UBER-CHK: random-golden: PASS
UBER-CHK: arena-churn: PASS
UBER-CHK: sprite-from-surface: PASS
UBER-CHK: sprite-from-surface-draw: hash=5F4394C9
UBER-CHK: tilePasteMonoAll: hash=30C3A2C9
UBER-CHK: floodFillBounded: hash=EF6B6F51
UBER-CHK: ----- end -----
UBER: total wall time: 145820 ms (7291 frames @ 50 Hz)

View file

@ -0,0 +1,118 @@
UBER: jlSpriteCompile: 1 call in <= 1 frame
UBER: audioInit OK
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
UBER: ----- begin -----
UBER: jlSurfaceClear: 12 iters / 16 frames = 37 ops/sec | hash=428CEE42
UBER: jlPaletteSet: 1009 iters / 16 frames = 3153 ops/sec | hash=5267FFB1
UBER: jlScbSetRange: 641 iters / 16 frames = 2003 ops/sec | hash=5267FFB1
UBER: jlDrawPixel: 721 iters / 16 frames = 2253 ops/sec | hash=75D75161
UBER: jlDrawLine H: 369 iters / 16 frames = 1153 ops/sec | hash=1E97E361
UBER: jlDrawLine V: 33 iters / 16 frames = 103 ops/sec | hash=A0474D91
UBER: jlDrawLine diag: 12 iters / 16 frames = 37 ops/sec | hash=69471D91
UBER: jlDrawRect 100x100: 33 iters / 19 frames = 86 ops/sec | hash=5CAFF471
UBER: jlDrawCircle r=16: 49 iters / 16 frames = 153 ops/sec | hash=7F1F5531
UBER: jlDrawCircle r=80: 11 iters / 16 frames = 34 ops/sec | hash=B249C9AB
UBER: jlFillRect 16x16: 177 iters / 17 frames = 520 ops/sec | hash=F4BD4B0B
UBER: jlFillRect 80x80: 33 iters / 16 frames = 103 ops/sec | hash=7C952E5F
UBER: jlFillRect 320x200: 11 iters / 17 frames = 32 ops/sec | hash=6467AFB1
UBER: jlFillCircle r=40: 33 iters / 26 frames = 63 ops/sec | hash=6467AFB1
UBER: jlSamplePixel: 1313 iters / 16 frames = 4103 ops/sec | hash=6467AFB1
UBER: jlTileFill: 417 iters / 16 frames = 1303 ops/sec | hash=6467AFB1
UBER: jlTileCopy: 353 iters / 16 frames = 1103 ops/sec | hash=6467AFB1
UBER: jlTileCopyMasked: 193 iters / 17 frames = 567 ops/sec | hash=6467AFB1
UBER: jlTilePaste: 417 iters / 16 frames = 1303 ops/sec | hash=6467AFB1
UBER: jlTileSnap: 785 iters / 16 frames = 2453 ops/sec | hash=6467AFB1
UBER: jlSpriteSaveUnder: 225 iters / 16 frames = 703 ops/sec | hash=CA673FB1
UBER: jlSpriteDraw: 161 iters / 16 frames = 503 ops/sec | hash=82B9646B
UBER: jlSpriteRestoreUnder: 177 iters / 16 frames = 553 ops/sec | hash=CA673FB1
UBER: jlSpriteSaveAndDraw: 97 iters / 18 frames = 269 ops/sec | hash=82B9646B
UBER: jlStagePresent full: 15 iters / 16 frames = 46 ops/sec | hash=82B9646B
UBER: jlInputPoll: 385 iters / 16 frames = 1203 ops/sec | hash=82B9646B
UBER: jlKeyDown: 6049 iters / 16 frames = 18903 ops/sec | hash=82B9646B
UBER: jlKeyPressed: 6049 iters / 16 frames = 18903 ops/sec | hash=82B9646B
UBER: jlMouseX: 15745 iters / 16 frames = 49203 ops/sec | hash=82B9646B
UBER: joeyJoyConnected: 5921 iters / 16 frames = 18503 ops/sec | hash=82B9646B
UBER: jlAudioFrameTick: 3152 iters / 16 frames = 9850 ops/sec | hash=82B9646B
UBER: jlAudioIsPlayingMod: 8065 iters / 16 frames = 25203 ops/sec | hash=82B9646B
UBER: surfaceMarkDirtyRect (via jlFillRect 32x32): 113 iters / 17 frames = 332 ops/sec | hash=D2B9E46B
UBER: ----- end -----
UBER-CHK: ----- begin -----
UBER-CHK: edge-pixels: hash=5267FFB1
UBER-CHK: edge-drawRect: hash=BC75D707
UBER-CHK: edge-circles: hash=20674FB1
UBER-CHK: edge-lines: hash=D4F5BCA7
UBER-CHK: sprite-clipped: hash=D922E4B4
UBER-CHK: sprite-clip-roundtrip: PASS
UBER-CHK: tilePasteMono: hash=B0829554
UBER-CHK: floodFill: hash=780DC48B
UBER-CHK: drawText: hash=80B5264B
UBER-CHK: drawText-offgrid: hash=9435DA4B
UBER-CHK: palette-roundtrip: PASS
UBER-CHK: random-golden: PASS
UBER-CHK: arena-churn: PASS
UBER-CHK: sprite-from-surface: PASS
UBER-CHK: sprite-from-surface-draw: hash=5F4394C9
UBER-CHK: tilePasteMonoAll: hash=30C3A2C9
UBER-CHK: floodFillBounded: hash=EF6B6F51
UBER-CHK: ----- end -----
UBER: total wall time: 302060 ms (15103 frames @ 50 Hz)
UBER: press any key to exit

118
tests/goldens/uber/dos.txt Normal file
View file

@ -0,0 +1,118 @@
UBER: jlSpriteCompile: 1 call in <= 1 frame
UBER: audioInit OK
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
UBER: ----- begin -----
UBER: jlSurfaceClear: 16 iters / 17 frames = 65 ops/sec | hash=428CEE42
UBER: jlPaletteSet: 3505 iters / 16 frames = 15334 ops/sec | hash=5267FFB1
UBER: jlScbSetRange: 1889 iters / 16 frames = 8264 ops/sec | hash=5267FFB1
UBER: jlDrawPixel: 3985 iters / 16 frames = 17434 ops/sec | hash=75D75161
UBER: jlDrawLine H: 1329 iters / 16 frames = 5814 ops/sec | hash=1E97E361
UBER: jlDrawLine V: 81 iters / 16 frames = 354 ops/sec | hash=A0474D91
UBER: jlDrawLine diag: 33 iters / 24 frames = 96 ops/sec | hash=69471D91
UBER: jlDrawRect 100x100: 81 iters / 16 frames = 354 ops/sec | hash=5CAFF471
UBER: jlDrawCircle r=16: 85 iters / 16 frames = 371 ops/sec | hash=7F1F5531
UBER: jlDrawCircle r=80: 19 iters / 16 frames = 83 ops/sec | hash=B249C9AB
UBER: jlFillRect 16x16: 609 iters / 16 frames = 2664 ops/sec | hash=F4BD4B0B
UBER: jlFillRect 80x80: 81 iters / 16 frames = 354 ops/sec | hash=7C952E5F
UBER: jlFillRect 320x200: 13 iters / 16 frames = 56 ops/sec | hash=6467AFB1
UBER: jlFillCircle r=40: 29 iters / 16 frames = 126 ops/sec | hash=6467AFB1
UBER: jlSamplePixel: 8321 iters / 16 frames = 36404 ops/sec | hash=6467AFB1
UBER: jlTileFill: 2081 iters / 16 frames = 9104 ops/sec | hash=6467AFB1
UBER: jlTileCopy: 1569 iters / 16 frames = 6864 ops/sec | hash=6467AFB1
UBER: jlTileCopyMasked: 545 iters / 16 frames = 2384 ops/sec | hash=6467AFB1
UBER: jlTilePaste: 1825 iters / 16 frames = 7984 ops/sec | hash=6467AFB1
UBER: jlTileSnap: 2993 iters / 16 frames = 13094 ops/sec | hash=6467AFB1
UBER: jlSpriteSaveUnder: 1937 iters / 16 frames = 8474 ops/sec | hash=CA673FB1
UBER: jlSpriteDraw: 1393 iters / 16 frames = 6094 ops/sec | hash=82B9646B
UBER: jlSpriteRestoreUnder: 1297 iters / 16 frames = 5674 ops/sec | hash=CA673FB1
UBER: jlSpriteSaveAndDraw: 961 iters / 16 frames = 4204 ops/sec | hash=82B9646B
UBER: jlStagePresent full: 81 iters / 16 frames = 354 ops/sec | hash=82B9646B
UBER: jlInputPoll: 705 iters / 16 frames = 3084 ops/sec | hash=82B9646B
UBER: jlKeyDown: 17441 iters / 16 frames = 76304 ops/sec | hash=82B9646B
UBER: jlKeyPressed: 16897 iters / 16 frames = 73924 ops/sec | hash=82B9646B
UBER: jlMouseX: 35105 iters / 16 frames = 153584 ops/sec | hash=82B9646B
UBER: joeyJoyConnected: 17601 iters / 16 frames = 77004 ops/sec | hash=82B9646B
UBER: jlAudioFrameTick: 13377 iters / 16 frames = 58524 ops/sec | hash=82B9646B
UBER: jlAudioIsPlayingMod: 29041 iters / 16 frames = 127054 ops/sec | hash=82B9646B
UBER: surfaceMarkDirtyRect (via jlFillRect 32x32): 305 iters / 16 frames = 1334 ops/sec | hash=D2B9E46B
UBER: ----- end -----
UBER-CHK: ----- begin -----
UBER-CHK: edge-pixels: hash=5267FFB1
UBER-CHK: edge-drawRect: hash=BC75D707
UBER-CHK: edge-circles: hash=20674FB1
UBER-CHK: edge-lines: hash=D4F5BCA7
UBER-CHK: sprite-clipped: hash=D922E4B4
UBER-CHK: sprite-clip-roundtrip: PASS
UBER-CHK: tilePasteMono: hash=B0829554
UBER-CHK: floodFill: hash=780DC48B
UBER-CHK: drawText: hash=80B5264B
UBER-CHK: drawText-offgrid: hash=9435DA4B
UBER-CHK: palette-roundtrip: PASS
UBER-CHK: random-golden: PASS
UBER-CHK: arena-churn: PASS
UBER-CHK: sprite-from-surface: PASS
UBER-CHK: sprite-from-surface-draw: hash=5F4394C9
UBER-CHK: tilePasteMonoAll: hash=30C3A2C9
UBER-CHK: floodFillBounded: hash=EF6B6F51
UBER-CHK: ----- end -----
UBER: total wall time: 20928 ms (1465 frames @ 70 Hz)
UBER: press any key to exit

Some files were not shown because too many files have changed in this diff Show more