More performance work
This commit is contained in:
parent
a0931e8d16
commit
6110f714af
105 changed files with 8397 additions and 2481 deletions
141
LLVM816-ASKS.md
Normal file
141
LLVM816-ASKS.md
Normal 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
750
PERF-AUDIT-PLAN.md
Normal 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
1425
PERF-AUDIT.md
Normal file
File diff suppressed because it is too large
Load diff
244
PERF.md
244
PERF.md
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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.
BIN
examples/spacetaxi/generated/amiga/tiles/tilebank0.tbk
Normal file
BIN
examples/spacetaxi/generated/amiga/tiles/tilebank0.tbk
Normal file
Binary file not shown.
Binary file not shown.
BIN
examples/spacetaxi/generated/atarist/tiles/tilebank0.tbk
Normal file
BIN
examples/spacetaxi/generated/atarist/tiles/tilebank0.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/font.tbk
Normal file
BIN
examples/spacetaxi/generated/iigs/font.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level01.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level01.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level02.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level02.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level03.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level03.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level04.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level04.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level05.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level05.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level06.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level06.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level07.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level07.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level08.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level08.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level09.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level09.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level10.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level10.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level11.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level11.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level12.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level12.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level13.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level13.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level14.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level14.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level15.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level15.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level16.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level16.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level17.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level17.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level18.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level18.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level19.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level19.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level20.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level20.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level21.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level21.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level22.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level22.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level23.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level23.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/level24.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/level24.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/levels/title.dat
Normal file
BIN
examples/spacetaxi/generated/iigs/levels/title.dat
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/sprites/sprites.spr
Normal file
BIN
examples/spacetaxi/generated/iigs/sprites/sprites.spr
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/tiles/tbank0.tbk
Normal file
BIN
examples/spacetaxi/generated/iigs/tiles/tbank0.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/tiles/tbank1.tbk
Normal file
BIN
examples/spacetaxi/generated/iigs/tiles/tbank1.tbk
Normal file
Binary file not shown.
BIN
examples/spacetaxi/generated/iigs/tiles/tbank2.tbk
Normal file
BIN
examples/spacetaxi/generated/iigs/tiles/tbank2.tbk
Normal file
Binary file not shown.
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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.)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 $@
|
||||
|
|
|
|||
|
|
@ -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 $@
|
||||
|
|
|
|||
|
|
@ -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
252
scripts/bench-iigs.sh
Executable 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
160
scripts/bench-staxi-title.sh
Executable 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
26
scripts/check-millis.sh
Executable 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
128
scripts/check-sfxmix.sh
Executable 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
143
scripts/diag-staxi-screen.sh
Executable 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
407
src/amiga/hal.c
407
src/amiga/hal.c
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
94
src/codegen/spriteEmitChunky.h
Normal file
94
src/codegen/spriteEmitChunky.h
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
156
src/core/debug.c
156
src/core/debug.c
|
|
@ -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 */
|
||||
|
|
|
|||
626
src/core/draw.c
626
src/core/draw.c
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
168
src/core/port.h
168
src/core/port.h
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
175
src/core/tile.c
175
src/core/tile.c
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
172
src/iigs/hal.c
172
src/iigs/hal.c
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
63
src/m68k/spriteEmitC2p.h
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
37
tests/goldens/sfxmix-baseline.txt
Normal file
37
tests/goldens/sfxmix-baseline.txt
Normal file
File diff suppressed because one or more lines are too long
120
tests/goldens/uber/amiga.txt
Normal file
120
tests/goldens/uber/amiga.txt
Normal 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)
|
||||
|
||||
118
tests/goldens/uber/atarist.txt
Normal file
118
tests/goldens/uber/atarist.txt
Normal 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
118
tests/goldens/uber/dos.txt
Normal 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
Loading…
Add table
Reference in a new issue