Working on performance. Still.

This commit is contained in:
Scott Duensing 2026-07-07 01:11:34 -05:00
parent 24b88c3ba2
commit 813bf595bc
27 changed files with 1530 additions and 546 deletions

View file

@ -64,6 +64,21 @@ Sprite ceilings are 2.5-4x above current aligned numbers (ST via
movem long-RMW + all 16 phases; Amiga via the BLITTER, which also movem long-RMW + all 16 phases; Amiga via the BLITTER, which also
frees the CPU -- its compiled-CPU path caps at ~1,350). frees the CPU -- its compiled-CPU path caps at ~1,350).
**ERRATUM 2026-07-07: every ST "meas" number above (and every ST
number anywhere dated before 2026-07-07) was captured with ~49.5% of
the CPU consumed by the idle 12.3 kHz Timer-A audio ISR that
jlpAudioInit started unconditionally (fixed in src/atarist/audio.c:
demand-driven pump; PERF.md artifact #4). Honest ST values are ~2x the
table above: tile paste 3,203/s (gap to ceiling 4.0-4.8x, not ~10x),
aligned sprite draw 1,188/s (gap 1.9-2.3x), gameFrame 19 fps. The
probe also measured the true ST layer split for tile paste: port op
~1,094 cyc + dirty mark ~804 + wrapper/call ~590 = 2,484 cyc/call,
within ~11% of instruction-count models (the remainder is ST 4-cycle
bus rounding) -- the cycle-model methodology is validated. Amiga
layer split from the same probe: full call ~3,120 cyc, port op alone
~1,719 (chip-RAM contention), steady-state mark ~970. Amiga/DOS/IIgs
audited for the same bug class: clean (~1.8% / ~0.5% / 0% idle).**
### Item 2 RESULT -- alignment-cliff code truth (supersedes all memory) ### Item 2 RESULT -- alignment-cliff code truth (supersedes all memory)
SPRITE_SHIFT_INDEX (spriteInternal.h:24-30): Amiga x&7, ST SPRITE_SHIFT_INDEX (spriteInternal.h:24-30): Amiga x&7, ST
@ -181,9 +196,77 @@ perf-directive memory with the outcome.
the bottom-line number; the program ends when this is at/near the bottom-line number; the program ends when this is at/near
the port's refresh-limited ceiling or a documented structural the port's refresh-limited ceiling or a documented structural
bound. bound.
- [ ] Phase 1 (68k all-shift emitters + save/restore emitters; - [~] Phase 1 DRAW SIDE COMPLETE 2026-07-06 (steps 1-4 of the
constraints (a)-(e) in the Phase-0 item-2 section above) adversary-reconciled order; save/restore = step 5 still open):
- [ ] Phase 2 step 1 fused per-port shift ladder (ST 16/x&15, Amiga 8, chunky 2;
IIgs cache fields #ifdef'd) -- gated 49/49 x4 bit-identical;
step 2 plan-based jlSpriteCompile (measure-all, uint16-cap greedy,
draws tier 1 / save+restore atomic pairs tier 2, arena-full degrade
to SPRITE_DEGRADE_DRAW_MASK, 68k-only even-size guard); step 3 Amiga
all-shift JIT (accumulator merge, shift-0 byte-identical, host-
verified); step 4 ST 16-phase draw (spriteWalk.s masked-movem walker
+ per-phase 22B thunk + 12B/group/row pre-shifted tables). GATE:
49/49 x4; unaligned ST 40->500 (12.5x), Amiga 75->803 (10.7x);
sweep16 ST 2->32, Amiga 5->50; unaligned now ~82-84% of aligned on
both. BUGS FOUND BY THE GATES: even-size guard demoted odd-sized
IIgs/DOS routines to the interpreter (fixed: 68k-only); Amiga bench
task stack was the shell 4KB default -- the deeper compile path
overflowed it nondeterministically (fixed: stack 32768 in
bench-amiga.sh + emitter accumulators to file statics).
Steps 5+6 COMPLETE 2026-07-06 -- **PHASE 1 DONE**, final gate 49/49
x4. Step 5: group-window save/restore emitters both ports (2 width
classes/sprite, IIgs-MVN-style; ST raw-span move.l chains, Amiga
plane-run move.l/move.w), format-law gates (SPRITE_SAVE_CLASS /
SPRITE_RESTORE_CLASS / SPRITE_BACKUP_PTR_OK in spriteInternal.h;
restore derives class from RECORDED geometry, (bx&15)==0 term is
correctness-critical), ST HAL raw-span interpreted branch (save +
restore switched atomically incl. the 16x16 asm reroute), planar
metadata-probe widening, JOEY_SPRITE_BACKUP_BYTES capacity macro +
all 4 example owners migrated and 2-aligned. SaveAndDraw's combined
path fires on 68k at every x for the first time. Step 6:
DEFAULT_CODEGEN_BYTES 64KB Amiga/ST; doc sweep done. RESULTS
(floor-relative): Amiga Save 276% / Restore 236% / SaveAndDraw 160%,
ST 203% / 163% / 107%; Amiga gameFrame 18 -> 23 fps. Host-verified
emitted streams (objdump); every intermediate gate 49/49 x4.
- [x] INTERLUDE 2026-07-07 (pre-Phase-2, found by the Phase-2 V1
probe): ST idle-audio ISR tax discovered and fixed -- see the
Phase-0 ERRATUM above and PERF.md artifact #4. All four ports
re-gated 49/49 after a power-failure clean-rebuild recovery AND
after the audio fix (pixels untouched). The Phase-2+ gate baselines
are now (millis clock, post-fix): jlSpriteDraw unaligned ST 992 /
Amiga 803 / IIgs 845 / DOS 5118; sweep16 ST 64 / Amiga 50; jlTilePaste
single ST 3203 / Amiga 2253 / IIgs 1741 / DOS 9806; map 10x5 (x50 =
tiles/s) ST 3100 / Amiga 2300 / IIgs 1750 / DOS 10000; gameFrame
ST 19 / Amiga 23 / IIgs 10 / DOS 18 fps. Floor parity: the ST has
ZERO below-floor cells for the first time; remaining below-floor is
Amiga-only (circle family + unaligned 95% / sweep16 89%) + DOS
audioFrameTick (gates nothing).
- [~] Phase 2 IN PROGRESS 2026-07-07. Recon: 6-agent workflow +
merged synthesis (12 ranked items R1-R12) + calibrated-probe layer
split (see ERRATUM above). Step A LANDED (gate 49/49 x4):
plane-pointer inlining via new src/amiga/amigaPlanar.h +
src/atarist/stPlanar.h internal headers (jlpSurfacePlanePtr stanza
deleted -- its only callers were spriteCompile.c's 68k dispatchers),
dead-validation cleanup in both 68k HALs. Amiga sprite ops +10-23%
(SaveUnder 1503->1853), ST +3.5-5.2%; Amiga unaligned draw now ABOVE
floor (903 vs IIgs 845). Step B LANDED (gate 49/49 x4): whole-tile
dirty mark specialized in tile.c (band = [bx*2, bx*2+1] with
compile-time proof, 8-row widen unrolled ~480 cyc vs 804 measured;
IIgs keeps its asm marker). Tile rows: ST +9-15%, Amiga +13-18%,
DOS +24-29%, map row ST 72 / Amiga 53 / DOS 258; tileSnap (no mark)
flat everywhere = clean control. Step C LANDED (gate 49/49 x4):
jlDrawRect raw-edge fills + ONE mark (band-interval proof in
draw.c) -- ST +25% / Amiga +40% / DOS +22% on the drawRect row; the
x2 widen unroll in surfaceMarkDirtyRows is PER-PORT by measurement:
it lifted Amiga sprite rows +4-6% but cost the ST -5% in BOTH the
pointer-walking and counted forms (gcc-mint materializes 4 live
pointers either way; simple loop re-measured exactly at ST
baseline; DOS flat) -- so Amiga unrolls, everyone else keeps the
simple loop (documented at the #if in surface.c).
REMAINING: R4a 68k tile call-chain flatten + R8 Amiga unroll check;
Step D IIgs glue (R2/R5/R6/R10 fused asm entries -- biggest IIgs
upside, ~2x its tile rows; own session recommended); R9 palette/SCB
memcmp->flags on 68k/DOS presents (hygiene).
- [ ] Phase 3 - [ ] Phase 3
- [ ] Phase 4 - [ ] Phase 4
- [ ] Phase 5 - [ ] Phase 5

216
PERF.md
View file

@ -40,6 +40,68 @@ walkers as pixel-perfect at all 14 previously-untested x phases.
-- e.g. jlAudioFrameTick 50223 -> 27612 -- run-to-run MAME variance on -- e.g. jlAudioFrameTick 50223 -> 27612 -- run-to-run MAME variance on
cheap ops; hashes are unaffected and those rows gate nothing.) cheap ops; hashes are unaffected and those rows gate nothing.)
**2026-07-06, NATIVE-PERF Phase 1 (draw side) LANDED:** all-shift
compiled sprite DRAW on both 68k ports. Amiga emits pre-shifted JIT
variants for all 8 bit phases; the ST emits per-phase thunk + data
tables driven by a shared masked-movem walker (spriteWalk.s) for all
16 intra-group phases, with the dispatcher's ST shift contract changed
to x & 15. jlSpriteCompile is now plan-based: it measures every
(shift, op) variant, greedily fits them under the uint16 offset cap
(draws first, save/restore as atomic pairs), and degrades to the
byte-aligned phases when the arena is too small instead of losing the
sprite. RESULT -- the alignment cliff is CLOSED: `jlSpriteDraw
unaligned` ST 40 -> 500 ops/sec (12.5x), Amiga 75 -> 803 (10.7x);
sweep16 ST 2 -> 32 sweeps/sec (512 draws/sec), Amiga 5 -> 50 (800
draws/sec). Unaligned now performs like aligned on both ports (ST 82% /
Amiga 84% of their aligned rows), and every one of the 49 golden
hashes still matches -- the compiled paths are bit-identical to the
interpreted walkers they replace, on all four ports. (Found along the
way: the Amiga bench task stack was ~4 KB -- one deep call chain from
overflow -- now 32 KB in bench-amiga.sh; the emitters' accumulators are
file statics.)
**Phase 1 step 5 (save/restore) ALSO LANDED, completing Phase 1:**
compiled save/restore on both 68k ports operate on the 16-px-group
window (two width classes per sprite, like the IIgs MVN pair), the
backup format is a pure function of the recorded geometry (identical
for compiled and interpreted producers/consumers -- the ST HAL gained a
raw-span interpreted branch, switched atomically with restore), and the
gates route by SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS with an odd-
buffer-pointer demotion (68000 word ops need even addresses). The
combined jlSpriteSaveAndDraw path now fires on 68k at EVERY x for the
first time. RESULTS (floor-relative): Amiga SaveUnder 104% -> 276%,
RestoreUnder 98% -> 236%, SaveAndDraw 94% -> 160%; ST SaveUnder
158% -> 203%, RestoreUnder 122% -> 163%, SaveAndDraw 87% -> 107%;
Amiga gameFrame 18 -> 23 fps. Backup capacity contract:
JOEY_SPRITE_BACKUP_BYTES in sprite.h (window worst case; all in-repo
examples migrated + 2-aligned). DEFAULT_CODEGEN_BYTES is 64 KB on
Amiga/ST (full variant sets). All 49 golden hashes held throughout.
**2026-07-07, THE ST COLUMN DOUBLED -- idle-audio ISR tax found and
fixed:** every previous ST capture ran with ~49.5% of the CPU silently
consumed by the 12.3 kHz Timer-A PWM ISR that jlpAudioInit started
unconditionally -- it streamed buffer silence to the YM even with
nothing playing, and UBER inits audio before its timed run. Found by
a calibrated probe (known-cycle dbra spin + per-layer direct-call rows
under the bench Hatari config): every probe row slowed by exactly
1.98x once audio was initialized, and the probe's un-taxed tilePaste
cycles/call matched the instruction-count model that UBER's number
mysteriously doubled. Fix: src/atarist/audio.c Timer A is now
demand-driven -- installed on the first PlayMod/PlaySfx, parked again
once the mix buffer drains back to silence with nothing live
(drain-to-silence handshake in jlpAudioFrameTick; StopMod parks
immediately when no SFX is riding the pump). Probe-verified: idle rows
match the never-initialized rows exactly, the tax correctly returns
while an SFX drains, and it vanishes after the drain. Full ST
re-bench: 49/49 hashes bit-identical (pixels untouched) and EVERY row
~2x: gameFrame 9 -> 19 fps, tilePaste 1,574 -> 3,203/s, sprite draw
607 -> 1,188/s, unaligned draw 508 -> 992/s. The other three ports
were audited for the same bug class and are clean: Amiga ~1.8% idle
tax (PTPlayer rides Paula DMA), DOS ~0.5% (SB auto-init DMA, ~11 Hz
half-buffer IRQ), IIgs 0% (no handler installed until PlayMod). ALL
pre-2026-07-07 ST comparisons -- including the Phase-0 ceiling gaps
and the Phase-1 relative results -- understated the ST by ~2x.
Correctness statement backing this table: all four ports produce Correctness statement backing this table: all four ports produce
BYTE-IDENTICAL pixels on every measured op and every correctness BYTE-IDENTICAL pixels on every measured op and every correctness
check -- **49/49** surface hashes match the frozen goldens in check -- **49/49** surface hashes match the frozen goldens in
@ -51,8 +113,8 @@ fillCircle/tileFill, #87 IIgs circle outline) are all fixed.
## Measurement honesty (read before trusting any number) ## Measurement honesty (read before trusting any number)
Three clock artifacts have shaped this table's history. Two are fixed, Four measurement artifacts have shaped this table's history. Three are
one is inherent -- know them before comparing rows: fixed, one is inherent -- know them before comparing rows:
1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench 1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench
loop called jlFrameCount() between every op; on the IIgs that was a loop called jlFrameCount() between every op; on the IIgs that was a
@ -86,55 +148,66 @@ one is inherent -- know them before comparing rows:
per-op `UBER-CLK:` log line reports frame-vs-millis as a standing 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 cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw
503->585, jlFillRect 80x80 97->115); DOS/IIgs/Amiga unchanged. 503->585, jlFillRect 80x80 97->115); DOS/IIgs/Amiga unchanged.
4. **ST idle-audio ISR tax -- FOUND and FIXED 2026-07-07; the ST
column below is the post-fix capture.** jlpAudioInit started the
12.3 kHz Timer-A PWM ISR at init and left it running forever, even
silent (~320 cycles/tick = ~49.5% of the CPU), and UBER inits audio
before timing anything. Every ST capture before 2026-07-07 is ~2x
pessimistic. Timer A is demand-driven now (runs only while a mod or
SFX is audible), so the benches measure the op and an idle game
gets the whole CPU. Amiga/DOS/IIgs audited clean for the same bug
class (~1.8% / ~0.5% / 0% idle cost).
## Baseline (2026-07-05): absolute ops/sec per port ## Baseline (2026-07-07): absolute ops/sec per port
The IIgs is the reference and the perf floor (every other port must The IIgs is the reference and the perf floor (every other port must
meet or beat it -- project directive). Non-IIgs cells show absolute meet or beat it -- project directive). Non-IIgs cells show absolute
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
as below-floor. `tools/uber-perf-table` emits this table directly; as below-floor. `tools/uber-perf-table` emits this table directly;
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
default cuts off the final checks). default cuts off the final checks). Capture provenance: ST 2026-07-07
(post-audio-fix); IIgs/Amiga/DOS 2026-07-06 (recovery-gate rerun,
unaffected by the ST fix).
| Op | IIgs (ops/sec) | Amiga (ops/sec, % of IIGS) | Atari ST (ops/sec, % of IIGS) | DOS (ops/sec, % of IIGS) | | Op | IIgs (ops/sec) | Amiga (ops/sec, % of IIGS) | Atari ST (ops/sec, % of IIGS) | DOS (ops/sec, % of IIGS) |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| jlSurfaceClear | 33 | 75 (227%) | 44 (133%) | 66 (200%) | | jlSurfaceClear | 33 | 75 (227%) | 89 (270%) | 67 (203%) |
| jlPaletteSet | 1561 | 11503 (737%) | 5256 (337%) | 29809 (1910%) | | jlPaletteSet | 1561 | 11453 (734%) | 10255 (657%) | 29828 (1911%) |
| jlScbSetRange | 163 | 3953 (2425%) | 2418 (1483%) | 8495 (5212%) | | jlScbSetRange | 163 | 4003 (2456%) | 4744 (2910%) | 8491 (5209%) |
| jlDrawPixel | 3179 | 5853 (184%) | 3384 (106%) | 23757 (747%) | | jlDrawPixel | 3179 | 5853 (184%) | 6645 (209%) | 23689 (745%) |
| jlDrawLine H | 1142 | 1703 (149%) | 1341 (117%) | 5708 (500%) | | jlDrawLine H | 1142 | 1703 (149%) | 2660 (233%) | 5708 (500%) |
| jlDrawLine V | 99 | 269 (272%) | 132 (133%) | 470 (475%) | | jlDrawLine V | 99 | 269 (272%) | 265 (268%) | 456 (461%) |
| jlDrawLine diag | 42 | **40 (95%)** | 43 (102%) | 109 (260%) | | jlDrawLine diag | 42 | **40 (95%)** | 86 (205%) | 108 (257%) |
| jlDrawRect 100x100 | 89 | 203 (228%) | 108 (121%) | 377 (424%) | | jlDrawRect 100x100 | 89 | 203 (228%) | 216 (243%) | 378 (425%) |
| jlDrawCircle r=16 | 303 | **285 (94%)** | **184 (61%)** | 449 (148%) | | jlDrawCircle r=16 | 303 | **285 (94%)** | 359 (118%) | 461 (152%) |
| jlDrawCircle r=80 | 70 | **63 (90%)** | **42 (60%)** | 98 (140%) | | jlDrawCircle r=80 | 70 | **61 (87%)** | 83 (119%) | 98 (140%) |
| jlFillRect 16x16 | 605 | 1153 (191%) | 610 (101%) | 3013 (498%) | | jlFillRect 16x16 | 605 | 1153 (191%) | 1211 (200%) | 3017 (499%) |
| jlFillRect 80x80 | 110 | 269 (245%) | 120 (109%) | 414 (376%) | | jlFillRect 80x80 | 110 | 269 (245%) | 236 (215%) | 412 (375%) |
| jlFillRect 320x200 | 21 | 66 (314%) | 40 (190%) | 58 (276%) | | jlFillRect 320x200 | 21 | 66 (314%) | 78 (371%) | 59 (281%) |
| jlFillCircle r=40 | 42 | **31 (74%)** | 77 (183%) | 156 (371%) | | jlFillCircle r=40 | 42 | **31 (74%)** | 155 (369%) | 156 (371%) |
| jlSamplePixel | 4815 | 7253 (151%) | 4862 (101%) | 46397 (964%) | | jlSamplePixel | 4815 | 7253 (151%) | 9603 (199%) | 46266 (961%) |
| jlTileFill | 1981 | 2303 (116%) | **1603 (81%)** | 9313 (470%) | | jlTileFill | 1981 | 2253 (114%) | 3144 (159%) | 9301 (470%) |
| jlTileCopy | 1441 | 1853 (129%) | **1341 (93%)** | 8764 (608%) | | jlTileCopy | 1441 | 1853 (129%) | 2720 (189%) | 8764 (608%) |
| jlTileCopyMasked | 623 | 953 (153%) | 689 (111%) | 3147 (505%) | | jlTileCopyMasked | 623 | 953 (153%) | 1366 (219%) | 3147 (505%) |
| jlTilePaste | 1741 | 2303 (132%) | **1574 (90%)** | 9806 (563%) | | jlTilePaste | 1741 | 2253 (129%) | 3203 (184%) | 9834 (565%) |
| jlTileSnap | 2520 | 3503 (139%) | 2907 (115%) | 19247 (764%) | | jlTileSnap | 2520 | 3503 (139%) | 5751 (228%) | 19290 (765%) |
| jlSpriteSaveUnder | 545 | 567 (104%) | 849 (156%) | 9229 (1693%) | | jlSpriteSaveUnder | 545 | 1503 (276%) | 2196 (403%) | 9248 (1697%) |
| jlSpriteDraw | 902 | 953 (106%) | **607 (67%)** | 6218 (689%) | | jlSpriteDraw | 902 | 953 (106%) | 1188 (132%) | 6234 (691%) |
| jlSpriteRestoreUnder | 566 | **553 (98%)** | 689 (122%) | 5933 (1048%) | | jlSpriteRestoreUnder | 510 | 1203 (236%) | 1662 (326%) | 5925 (1162%) |
| jlSpriteSaveAndDraw | 376 | **332 (88%)** | **328 (87%)** | 4271 (1136%) | | jlSpriteSaveAndDraw | 376 | 603 (160%) | 803 (214%) | 4271 (1136%) |
| jlStagePresent full | 67 | 359 (536%) | **55 (82%)** | 360 (537%) | | jlStagePresent full | 67 | 359 (536%) | 125 (187%) | 360 (537%) |
| jlInputPoll | 229 | 4003 (1748%) | 1400 (611%) | 3134 (1369%) | | jlInputPoll | 228 | 4003 (1756%) | 2788 (1223%) | 3148 (1381%) |
| jlKeyDown | 10191 | 39753 (390%) | 22403 (220%) | 77800 (763%) | | jlKeyDown | 10191 | 39303 (386%) | 45286 (444%) | 77548 (761%) |
| jlKeyPressed | 11432 | 38003 (332%) | 22344 (195%) | 74977 (656%) | | jlKeyPressed | 11389 | 38053 (334%) | 45286 (398%) | 75092 (659%) |
| jlMouseX | 20138 | 97703 (485%) | 58314 (290%) | 155140 (770%) | | jlMouseX | 20214 | 96703 (478%) | 117981 (584%) | 155372 (769%) |
| joeyJoyConnected | 10191 | 38903 (382%) | 21870 (215%) | 77819 (764%) | | joeyJoyConnected | 10191 | 39303 (386%) | 43440 (426%) | 77548 (761%) |
| jlAudioFrameTick | 27612 | 56453 (204%) | **11732 (42%)** | 59162 (214%) | | jlAudioFrameTick | 27509 | 55853 (203%) | 40637 (148%) | **11527 (42%)** |
| jlAudioIsPlayingMod | 11029 | 56503 (512%) | 29870 (271%) | 127794 (1159%) | | jlAudioIsPlayingMod | 17627 | 56453 (320%) | 60441 (343%) | 127708 (725%) |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 389 (120%) | 1478 (458%) | | surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 774 (240%) | 1472 (456%) |
| jlSpriteDraw unaligned | 842 | **75 (9%)** | **40 (5%)** | 5115 (607%) | | jlSpriteDraw unaligned | 845 | **803 (95%)** | 992 (117%) | 5118 (606%) |
| jlSpriteDraw sweep16 | 56 | **5 (9%)** | **2 (4%)** | 353 (630%) | | jlSpriteDraw sweep16 | 56 | **50 (89%)** | 64 (114%) | 363 (648%) |
| jlTilePaste map 10x5 | 35 | 46 (131%) | **32 (91%)** | 200 (571%) | | jlTilePaste map 10x5 | 35 | 46 (131%) | 62 (177%) | 200 (571%) |
| gameFrame composite | 10 | 18 (180%) | **9 (90%)** | 18 (180%) | | gameFrame composite | 10 | 23 (230%) | 19 (190%) | 18 (180%) |
> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves > **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 > 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both
@ -148,42 +221,25 @@ default cuts off the final checks).
## Reading notes ## Reading notes
* **Sprite rows measure different code per port.** DOS runs COMPILED * **Sprite rows measure COMPILED code on all four ports** since
sprites (finding #74 fixed 2026-07-05). Amiga/ST run their NATIVE-PERF Phase 1 (2026-07-06): the 68k ports JIT all-shift draw
interpreted planar paths (compiled save/restore emitters are stubs variants plus group-window save/restore (previously stubs that fell
by design, Phase 4 #27). The IIgs now runs COMPILED sprites too to interpreters), IIgs codegen re-enabled via finding #77, DOS via
(finding #77 fixed 2026-07-06; JOEY_IIGS_SPRITE_CODEGEN back on) -- finding #74. The interpreted walkers remain only as arena-full
the IIgs sprite rows jumped 5-25x and the huge non-IIgs ratios on degrade paths -- and are proven bit-identical to the compiled code
these rows collapsed accordingly, which re-exposes a real 68k sprite by the 49-hash gate.
gap (see below). * **The below-floor cells are the standing punch list**, and after the
* **The below-floor cells are the standing punch list** (ST column 2026-07-07 audio-tax fix the ST has NONE -- every remaining
corrected +17% by the 2026-07-06 clock fix, artifact #3 above -- many below-floor graphics cell is the Amiga's: the circle family (diag
ST cells crossed above the floor; what remains is genuinely below). line 95%, circle r=16 94%, r=80 87%, fillCircle 74% -- planar 4-plane
ST circles (57-58%) are the worst honest graphics gap -- pure-68000 RMW tax) and the two sprite workload rows a hair under floor
constraint, no blitter allowed (STF baseline). The IIgs circle-outline (unaligned 95%, sweep16 89%; chip-RAM display-DMA contention that the
case is a known structural loss (3-register 65816 vs 16-register 68000 fast-RAM-code ST does not pay). DOS jlAudioFrameTick (42%) is the SB
Bresenham; see feedback memory). With IIgs sprites now compiled, a half-buffer silence refill on an otherwise-trivial call -- it gates
68k sprite gap re-opened: ST jlSpriteDraw 67%, ST jlSpriteSaveAndDraw nothing. Floor parity is effectively DONE; the open work is
87%, ST jlTileFill 77%, and Amiga jlSpriteRestoreUnder 97% + ceiling-relative (NATIVE-PERF-PLAN.md Phase 2+), where every port
jlSpriteSaveAndDraw 88% remain below the IIgs floor (they were masked still runs 2-5x below hand-coded technique on tile/sprite glue.
while the IIgs interpreted; ST jlSpriteRestoreUnder is now 119%, above Amiga jlFillCircle r=40 (74%) was probed
floor, after the clock fix). A 2026-07-06 shared fix -- inlining the
per-row widen in surfaceMarkDirtyRows (removing an 8x function call in
every multi-row stage op's dirty mark) -- lifted all ST multi-row ops
~2-5% (tileFill 1516->1574, spriteDraw 585->607, tileCopy 1307->1366,
fillRect16 to 100%), the ST/Amiga counterpart to the IIgs asm marker. 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 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 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 number (34 vs 31, within the coarse 11-iter sample), so the per-span
@ -192,9 +248,9 @@ default cuts off the final checks).
overlap): the true lever is a scanline rewrite (one span per row, no overlap): the true lever is a scanline rewrite (one span per row, no
overlap), which would roughly halve the byte writes and stay 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 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 50 Hz frame counter, a coarse measurement. (The old note here blaming
model-vs-measured gap shows on ST sprite-draw, so part of these two a "4-5x model-vs-measured gap" partly on measurement was vindicated
cells may be measurement, not silicon. 2026-07-07: on the ST that gap was the idle-audio tax, artifact #4.)
* **jlRandom has no timed row.** Phase 8 (#43) rewrote its xorshift32 * **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 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). call on the 65816; 32-bit shifts there are per-bit helper loops).

View file

@ -61,13 +61,13 @@
/* SaveUnder rounds x down to platform alignment: 8 px on Amiga /* SaveUnder rounds x down to platform alignment: 8 px on Amiga
* planar, 2 px on chunky. Add 4 bytes/row of slack to cover the * planar, 2 px on chunky. Add 4 bytes/row of slack to cover the
* worst case on either. */ * worst case on either. */
#define EGO_BACKUP_BYTES (((EGO_W >> 1) + 4) * EGO_H) #define EGO_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(EGO_W / 8, EGO_H / 8)
#define CURSOR_W 12 #define CURSOR_W 12
#define CURSOR_H 12 #define CURSOR_H 12
#define CURSOR_TILES_X 2 #define CURSOR_TILES_X 2
#define CURSOR_TILES_Y 2 #define CURSOR_TILES_Y 2
#define CURSOR_BACKUP_BYTES (((CURSOR_TILES_X * 8) >> 1) + 4) * (CURSOR_TILES_Y * 8) #define CURSOR_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(CURSOR_TILES_X, CURSOR_TILES_Y)
#define ITEM_ICON_W 16 #define ITEM_ICON_W 16
#define ITEM_ICON_H 16 #define ITEM_ICON_H 16
@ -204,7 +204,7 @@ static RoomT gRooms[ROOM_COUNT];
static jlSpriteT *gEgo[DIR_COUNT][EGO_FRAMES]; static jlSpriteT *gEgo[DIR_COUNT][EGO_FRAMES];
static uint8_t gEgoTiles[DIR_COUNT][EGO_FRAMES][EGO_TILE_BYTES]; static uint8_t gEgoTiles[DIR_COUNT][EGO_FRAMES][EGO_TILE_BYTES];
static uint8_t gEgoBackup[EGO_BACKUP_BYTES]; static uint8_t gEgoBackup[EGO_BACKUP_BYTES] __attribute__((aligned(2)));
static jlSpriteBackupT gEgoBak; static jlSpriteBackupT gEgoBak;
static bool gEgoBakValid; static bool gEgoBakValid;
@ -213,7 +213,7 @@ static jlSpriteT *gCursor[VERB_COUNT];
static jlSpriteT *gItemSprite[ITEM_COUNT]; /* index 0 = unused */ static jlSpriteT *gItemSprite[ITEM_COUNT]; /* index 0 = unused */
static jlSpriteT *gLampLitSprite; /* lit-state inventory icon */ static jlSpriteT *gLampLitSprite; /* lit-state inventory icon */
static uint8_t gLampLitTiles[2 * 2 * TILE_BYTES]; static uint8_t gLampLitTiles[2 * 2 * TILE_BYTES];
static uint8_t gCursorBackup[CURSOR_BACKUP_BYTES]; static uint8_t gCursorBackup[CURSOR_BACKUP_BYTES] __attribute__((aligned(2)));
static jlSpriteBackupT gCursorBak; static jlSpriteBackupT gCursorBak;
static bool gCursorBakValid; static bool gCursorBakValid;

View file

@ -107,7 +107,8 @@
#define ST_PASSENGER_CEL_BOARD0 2 /* maps to C64 ptr $C6 */ #define ST_PASSENGER_CEL_BOARD0 2 /* maps to C64 ptr $C6 */
#define ST_PASSENGER_CEL_SPARKLE 8 /* C64 ptr $D9 -- 3rd sparkle cel */ #define ST_PASSENGER_CEL_SPARKLE 8 /* C64 ptr $D9 -- 3rd sparkle cel */
#define ST_SPRITE_BACKUP_BYTES (((ST_TAXI_W_PX >> 1) + 4) * ST_TAXI_H_PX) // Library-wide worst case (16-px-group window on planar ports).
#define ST_SPRITE_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(ST_TAXI_W_PX / 8, ST_TAXI_H_PX / 8)
typedef struct { typedef struct {
@ -119,16 +120,16 @@ typedef struct {
jlSpriteT *warpCels[ST_WARP_CEL_COUNT]; jlSpriteT *warpCels[ST_WARP_CEL_COUNT];
jlSpriteT *flameCels[ST_FLAME_CEL_COUNT]; jlSpriteT *flameCels[ST_FLAME_CEL_COUNT];
jlSpriteBackupT flameBackup; jlSpriteBackupT flameBackup;
uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES]; uint8_t flameBackupMem[ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
bool flameHasBackup; bool flameHasBackup;
jlSpriteT *passengerCels[ST_PASSENGER_CEL_COUNT]; jlSpriteT *passengerCels[ST_PASSENGER_CEL_COUNT];
jlSurfaceT *fontSurface; // glyph surface, built from font.tbk at load jlSurfaceT *fontSurface; // glyph surface, built from font.tbk at load
uint16_t asciiMap[128]; uint16_t asciiMap[128];
bool fontReady; bool fontReady;
jlSpriteBackupT taxiBackup; jlSpriteBackupT taxiBackup;
uint8_t taxiBackupMem[ST_SPRITE_BACKUP_BYTES]; uint8_t taxiBackupMem[ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
jlSpriteBackupT passengerBackup[ST_MAX_PASSENGERS]; jlSpriteBackupT passengerBackup[ST_MAX_PASSENGERS];
uint8_t passengerBackupMem[ST_MAX_PASSENGERS][ST_SPRITE_BACKUP_BYTES]; uint8_t passengerBackupMem[ST_MAX_PASSENGERS][ST_SPRITE_BACKUP_BYTES] __attribute__((aligned(2)));
bool taxiHasBackup; bool taxiHasBackup;
bool passengerHasBackup[ST_MAX_PASSENGERS]; bool passengerHasBackup[ST_MAX_PASSENGERS];
// Tilemap repaint gating: the static playfield art only needs to // Tilemap repaint gating: the static playfield art only needs to

View file

@ -19,7 +19,9 @@
// for chunky 4bpp (1 extra byte/row worst case), 8 px for planar // for chunky 4bpp (1 extra byte/row worst case), 8 px for planar
// 4-plane (4 extra bytes/row worst case -- one per plane). The +4 // 4-plane (4 extra bytes/row worst case -- one per plane). The +4
// covers the planar case and is a no-op overhead on chunky. // covers the planar case and is a no-op overhead on chunky.
#define BALL_BACKUP_BYTES (((BALL_W >> 1) + 4) * BALL_H) // Sized by the library-wide worst case (16-px-group window on the
// planar ports); see JOEY_SPRITE_BACKUP_BYTES in joey/sprite.h.
#define BALL_BACKUP_BYTES JOEY_SPRITE_BACKUP_BYTES(BALL_W / 8, BALL_H / 8)
#define BALL_PALETTE_IDX 0 #define BALL_PALETTE_IDX 0
@ -50,7 +52,7 @@ static const uint8_t gBallAuthored[BALL_H * (BALL_W / 2)] = {
}; };
static uint8_t gBallTiles[BALL_TILE_BYTES]; static uint8_t gBallTiles[BALL_TILE_BYTES];
static uint8_t gBallBackup[BALL_BACKUP_BYTES]; static uint8_t gBallBackup[BALL_BACKUP_BYTES] __attribute__((aligned(2)));
// Repack from authored "wide row" layout to tile-major jlSpriteT // Repack from authored "wide row" layout to tile-major jlSpriteT

View file

@ -113,7 +113,7 @@ static const char *gCurName = "(none)";
static jlSurfaceT *gStage = NULL; static jlSurfaceT *gStage = NULL;
static jlSpriteT *gSprite = NULL; static jlSpriteT *gSprite = NULL;
static jlSpriteBackupT gBackup; static jlSpriteBackupT gBackup;
static unsigned char gBackupBytes[256]; static unsigned char gBackupBytes[JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
static jlTileT gTileScratch; static jlTileT gTileScratch;
@ -127,7 +127,7 @@ static jlTileT gTileMapB;
#define UBER_FRAME_SPRITES 3 #define UBER_FRAME_SPRITES 3
static jlSpriteBackupT gFrameBackups[UBER_FRAME_SPRITES]; static jlSpriteBackupT gFrameBackups[UBER_FRAME_SPRITES];
static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][256]; static unsigned char gFrameBackupBytes[UBER_FRAME_SPRITES][JOEY_SPRITE_BACKUP_BYTES(2, 2)] __attribute__((aligned(2)));
// Current timed-op number (1-based); mirrored into the mailbox. Lives // Current timed-op number (1-based); mirrored into the mailbox. Lives
@ -1106,9 +1106,10 @@ static bool __attribute__((noinline)) setupSprite(void) {
static bool __attribute__((noinline)) initJoeyLib(void) { static bool __attribute__((noinline)) initJoeyLib(void) {
jlConfigT config; jlConfigT config;
/* 32 KB fits the 8 pre-shifted DRAW variants the Amiga planar /* 32 KB fits one 16x16 sprite's full compiled set on either 68k
* compiled sprite emitter generates. UL on the multiply because * port (Amiga: 8 pre-shifted DRAW variants ~15-18 KB; ST: 16-phase
* a 16-bit int overflows on 32 * 1024. */ * thunk+table DRAWs ~12 KB; plus ~1-2 KB of save/restore classes).
* UL on the multiply because a 16-bit int overflows on 32 * 1024. */
config.codegenBytes = 32UL * 1024; config.codegenBytes = 32UL * 1024;
config.audioBytes = 64UL * 1024; config.audioBytes = 64UL * 1024;
return jlInit(&config); return jlInit(&config);

View file

@ -160,7 +160,6 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PIXELS // amiga: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // amiga AmigaPlanarT + chip planes, function
#define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function #define JL_HAS_SURFACE_FREE_PORT_DATA // amiga FreeMem planes, function
#define JL_HAS_SURFACE_PLANE_PTR // amiga plane base pointer, function
#define JL_HAS_FRAME_HZ // PAL 50, function #define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_COUNT // VBL-server counter, function #define JL_HAS_FRAME_COUNT // VBL-server counter, function
#define JL_HAS_WAIT_VBL // WaitTOF, function #define JL_HAS_WAIT_VBL // WaitTOF, function
@ -215,7 +214,6 @@
#define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function #define JL_HAS_SURFACE_ALLOC_PIXELS // st: NULL (planar storage), function
#define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function #define JL_HAS_SURFACE_ALLOC_PORT_DATA // st StPlanarT + interleaved buffer, function
#define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function #define JL_HAS_SURFACE_FREE_PORT_DATA // st free buffer, function
#define JL_HAS_SURFACE_PLANE_PTR // st interleaved buffer base, function
#define JL_HAS_FRAME_HZ // PAL 50, function #define JL_HAS_FRAME_HZ // PAL 50, function
#define JL_HAS_FRAME_COUNT // VBL-ISR counter, function #define JL_HAS_FRAME_COUNT // VBL-ISR counter, function
#define JL_HAS_WAIT_VBL // frame-counter wait, function #define JL_HAS_WAIT_VBL // frame-counter wait, function

View file

@ -29,16 +29,14 @@
#include "surface.h" #include "surface.h"
#include "types.h" #include "types.h"
// Sprite codegen emits per-shift variants. Chunky 4bpp ports (DOS, // Sprite codegen emits per-shift variants so smooth horizontal motion
// IIgs, Atari ST) only need 2 shifts -- pixel offset 0 (sprite/dest // at any pixel position uses pre-shifted routines without runtime
// byte boundaries align) and offset 1 (every dest byte combines two // bit-shifting. The variant count is per-platform (chunky 4bpp ports
// sprite bytes' nibbles). Planar ports (Amiga -- 8 px per plane byte) // need only x & 1; Amiga planar needs x & 7; ST word-interleaved
// need 8 shifts: one for each x % 8 alignment, so smooth horizontal // planar needs the full x & 15 intra-group phase) and lives with the
// motion at any pixel position uses pre-shifted source bytes without // dispatcher's index macro in src/core/spriteInternal.h so count and
// runtime bit-shifting. Allocate the max so routineOffsets[] has // mask cannot drift. jlSpriteT is opaque to applications, so the
// slots for every variant; chunky ports leave shifts 2..7 as // count is not part of the public ABI.
// SPRITE_NOT_COMPILED, planar ports use all 8.
#define JOEY_SPRITE_SHIFT_COUNT 8
typedef struct jlSpriteT jlSpriteT; typedef struct jlSpriteT jlSpriteT;
@ -111,18 +109,27 @@ void jlSpritePrewarm(jlSpriteT *sp);
// Off-surface portions are clipped. // Off-surface portions are clipped.
void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y); void jlSpriteDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y);
// Worst-case backup capacity for a widthTiles x heightTiles sprite at
// ANY x, on every port. The planar 68k compiled save/restore operates
// on the 16-pixel-group-aligned window covering the sprite (x rounded
// DOWN to a 16-px boundary, width UP), so the worst case is one extra
// 16-px group: (ceil(w/16) + 1) * 8 bytes per row. This also covers the
// chunky ports' (w/2 + 1) * h trivially. Give the backing array 2-byte
// alignment (e.g. uint16_t backing or __attribute__((aligned(2)))) --
// odd buffer pointers route save/restore to the slower interpreted
// paths on planar ports (68000 word ops require even addresses).
#define JOEY_SPRITE_BACKUP_BYTES(_wT, _hT) \
((uint16_t)((((((uint16_t)(_wT) * 8u) + 15u) >> 4) + 1u) * 8u * ((uint16_t)(_hT) * 8u)))
// Capture the destination region a subsequent jlSpriteDraw at the same // Capture the destination region a subsequent jlSpriteDraw at the same
// (x,y) would write to. backup->bytes must have at least // (x,y) would write to. backup->bytes must have at least
// (widthTiles*4) * (heightTiles*8) bytes of capacity for fully // JOEY_SPRITE_BACKUP_BYTES(widthTiles, heightTiles) bytes of capacity
// in-bounds draws on chunky ports (DOS, IIgs); for clipped draws only // for fully in-bounds draws; for clipped draws only the visible bytes
// the visible bytes are stored. The captured region's exact size is // are stored. The captured region's exact geometry is reported in
// reported in backup->sizeBytes. // backup->x/width/height/sizeBytes -- on planar ports (Amiga, ST) it is
// // the 16-px-aligned window containing the sprite, so the region a
// Planar ports (Amiga, Atari ST) round x DOWN to an 8-pixel plane // caller must not invalidate before restoring is the RECORDED window,
// boundary and width UP, so a sprite drawn at an odd-of-8 x captures // not just the sprite rect.
// one extra plane-byte column per row. Size backup->bytes for the
// planar worst case ((widthTiles*4) + 1) * (heightTiles*8) bytes so an
// odd-x save cannot overrun the buffer.
void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup); void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup);
// Repaint the destination region from a jlSpriteBackupT captured by a // Repaint the destination region from a jlSpriteBackupT captured by a

View file

@ -7,7 +7,7 @@ BUILD := $(REPO_DIR)/build/$(PLATFORM)
LIBDIR := $(BUILD)/lib LIBDIR := $(BUILD)/lib
BINDIR := $(BUILD)/bin BINDIR := $(BUILD)/bin
CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include -I$(REPO_DIR)/toolchains/atarist/include-shim -I$(SRC_68K) -MMD -MP CFLAGS := $(COMMON_CFLAGS) -m68000 -fomit-frame-pointer -I$(REPO_DIR)/toolchains/audio/libxmp-lite/include -I$(REPO_DIR)/toolchains/atarist/include-shim -I$(SRC_DIR)/atarist -I$(SRC_68K) -MMD -MP
LDFLAGS := LDFLAGS :=
# libxmp-lite shared with the DOS port. Built as a static archive that # libxmp-lite shared with the DOS port. Built as a static archive that

View file

@ -37,8 +37,11 @@ find "$bin_dir" -maxdepth 1 -type f -executable -exec cp -t "$work/" {} +
if [[ -d "$bin_dir/DATA" ]]; then if [[ -d "$bin_dir/DATA" ]]; then
cp -r "$bin_dir/DATA" "$work/" cp -r "$bin_dir/DATA" "$work/"
fi fi
# ':' anchors to the boot volume root. # ':' anchors to the boot volume root. 32 KB task stack: the shell
echo ":Uber" > "$work/s/startup-sequence" # default (~4 KB) left UBER one deep call chain from overflow -- the
# Phase-1 sprite-compile path found it (emitter accumulator frame +
# vsnprintf underneath = nondeterministic wandering crashes).
printf 'stack 32768\n:Uber\n' > "$work/s/startup-sequence"
log=$work/joeylog.txt log=$work/joeylog.txt

44
src/amiga/amigaPlanar.h Normal file
View file

@ -0,0 +1,44 @@
// Amiga per-surface planar storage, shared between the HAL
// (src/amiga/hal.c) and the cross-platform sprite dispatchers
// (src/codegen/spriteCompile.c) so plane-pointer access inlines
// instead of paying a cross-TU function call per plane (NATIVE-PERF
// Phase 2). Plane pointers are lifetime-immutable: portData is written
// only at surface create/destroy, so reading them needs no cache and
// no invalidation.
#ifndef JOEY_AMIGA_PLANAR_H
#define JOEY_AMIGA_PLANAR_H
#include <stdint.h>
#include <stdbool.h>
#include "surfaceInternal.h"
#define AMIGA_BITPLANES 4
#define AMIGA_BYTES_PER_ROW (SURFACE_WIDTH / 8)
#define AMIGA_PLANE_SIZE (AMIGA_BYTES_PER_ROW * SURFACE_HEIGHT)
// jlSurfaceT.portData points to this on Amiga: the 4 plane base
// pointers + stride. Every surface (including the stage, which gets
// its OWN shadow planes, NOT aliased to the displayed gPlanes) owns
// AllocMem(MEMF_CHIP) planes so the blitter can reach them; present
// copies shadow -> display planes atomically.
//
// Direct fields rather than a union because the consumer is asm /
// inline C that wants minimal indirection in the inner loop.
typedef struct {
uint8_t *planes[AMIGA_BITPLANES];
uint16_t bytesPerRow; // = AMIGA_BYTES_PER_ROW (40)
uint16_t bytesPerPlane; // = AMIGA_PLANE_SIZE (8000)
bool ownsPlanes; // true = AllocMem'd (free at destroy);
// false = aliased (don't free)
} AmigaPlanarT;
// NULL means "surface has no planar storage" -- callers use that as
// their route-to-generic / no-op gate, exactly the contract the old
// jlpSurfacePlanePtr cross-TU function provided.
static inline AmigaPlanarT *amigaSurfacePlanar(const jlSurfaceT *s) {
return (AmigaPlanarT *)s->portData;
}
#endif

View file

@ -60,30 +60,13 @@ extern struct Custom custom;
static void installVblServer(void); static void installVblServer(void);
static void removeVblServer(void); static void removeVblServer(void);
// ----- Constants ----- // ----- Constants + per-surface planar storage -----
#define AMIGA_BITPLANES 4
#define AMIGA_BYTES_PER_ROW 40
#define AMIGA_PLANE_SIZE (AMIGA_BYTES_PER_ROW * SURFACE_HEIGHT)
// ----- Per-surface planar storage (project_planar_68k_plan) -----
// //
// Phase 1 carved out the jlSurfaceT.portData hook. This struct is what // AmigaPlanarT, AMIGA_BITPLANES/BYTES_PER_ROW/PLANE_SIZE, and the
// it points to on Amiga: the 4 plane base pointers + stride. For the // inline portData accessor moved to amigaPlanar.h (NATIVE-PERF Phase
// stage, planes[i] aliases gPlanes[i] (Intuition-allocated, already // 2) so spriteCompile.c's dispatchers can read plane pointers without
// in chip RAM, already on display). For non-stage surfaces planes[i] // a cross-TU call.
// gets its own AllocMem(MEMF_CHIP) so the blitter can reach it. #include "amigaPlanar.h"
//
// Direct fields rather than a union because the consumer is asm /
// inline C that wants minimal indirection in the inner loop.
typedef struct {
uint8_t *planes[AMIGA_BITPLANES];
uint16_t bytesPerRow; // = AMIGA_BYTES_PER_ROW (40)
uint16_t bytesPerPlane; // = AMIGA_PLANE_SIZE (8000)
bool ownsPlanes; // true = AllocMem'd (free at destroy);
// false = aliased to gPlanes (don't free)
} AmigaPlanarT;
// ----- Prototypes ----- // ----- Prototypes -----
@ -1092,9 +1075,9 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
uint8_t bitVal; uint8_t bitVal;
uint8_t *dst; uint8_t *dst;
if (s == NULL || w == 0u || h == 0u) { // Callers guarantee s != NULL and positive w/h: the jlFillRect
return; // wrapper validates + clips before fillRectOnSurface, and the
} // fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1.
pd = (AmigaPlanarT *)s->portData; pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) { if (pd == NULL) {
jlpGenericFillRect(s, x, y, w, h, colorIndex); jlpGenericFillRect(s, x, y, w, h, colorIndex);
@ -2266,19 +2249,6 @@ uint8_t *jlpSurfaceAllocPixels(void) {
} }
uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
AmigaPlanarT *pd;
if (planeIdx >= AMIGA_BITPLANES) {
return NULL;
}
pd = (AmigaPlanarT *)s->portData;
if (pd == NULL) {
return NULL;
}
return pd->planes[planeIdx];
}
// Allocate the per-surface planar storage: an AmigaPlanarT plus 4 // Allocate the per-surface planar storage: an AmigaPlanarT plus 4
// off-screen chip-RAM plane buffers. The stage gets its OWN shadow // off-screen chip-RAM plane buffers. The stage gets its OWN shadow
// planes (NOT aliased to gPlanes / Intuition's BitMap) so drawing // planes (NOT aliased to gPlanes / Intuition's BitMap) so drawing

View file

@ -2,9 +2,11 @@
// running PWM through YM2149 channel-A volume register. // running PWM through YM2149 channel-A volume register.
// //
// Pipeline: // Pipeline:
// * jlpAudioInit creates an xmp_context, allocates a 2 KB mix buffer // * jlpAudioInit creates an xmp_context. Timer A is NOT started here:
// (in BSS), parks the Timer A vector at our 68k ISR (audio_isr.s), // the ISR costs ~half the CPU at 12.3 kHz, so the pump is demand-
// and starts Timer A at ~12.3 kHz (MFP prescaler 200, data 1). // driven -- installed on the first PlayMod/PlaySfx (MFP prescaler
// 200, data 1) and uninstalled once the mix buffer has drained back
// to pure silence with nothing playing.
// * jlpAudioPlayMod loads a .MOD via xmp_load_module_from_memory and // * jlpAudioPlayMod loads a .MOD via xmp_load_module_from_memory and
// calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit // calls xmp_start_player at the same 12288 Hz mono unsigned-8-bit
// output that the ISR consumes one byte at a time. // output that the ISR consumes one byte at a time.
@ -81,6 +83,13 @@ static bool gXmpStarted = false;
static bool gXmpLoaded = false; static bool gXmpLoaded = false;
static int gXmpLoopCount = 0; static int gXmpLoopCount = 0;
// Demand-driven Timer A: the 12.3 kHz ISR eats ~half the CPU, so it
// only runs while there is (or may be) audible output. gHalfSilent[i]
// records that half i was last refilled with pure silence; once both
// halves are silent and nothing is playing, frameTick stops the pump.
static bool gPumpRunning = false;
static bool gHalfSilent[2] = { false, false };
static void (*gOldTimerAVec)(void) = NULL; static void (*gOldTimerAVec)(void) = NULL;
// SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c). // SFX overlay shared with the DOS HAL (see src/core/audioSfxMix.c).
@ -178,6 +187,37 @@ static void silenceMixBuffer(void) {
} }
static bool anySfxActive(void) {
uint8_t i;
for (i = 0; i < JOEY_AUDIO_SFX_SLOTS; i++) {
if (gSfxSlots[i].active) {
return true;
}
}
return false;
}
static void pumpStart(void) {
if (gPumpRunning) {
return;
}
Supexec(installTimerA);
gPumpRunning = true;
gHalfSilent[0] = false;
gHalfSilent[1] = false;
}
static void pumpStop(void) {
if (!gPumpRunning) {
return;
}
Supexec(uninstallTimerA);
gPumpRunning = false;
}
// ----- HAL API (alphabetical) ----- // ----- HAL API (alphabetical) -----
bool jlpAudioInit(void) { bool jlpAudioInit(void) {
@ -188,7 +228,6 @@ bool jlpAudioInit(void) {
if (gXmpCtx == NULL) { if (gXmpCtx == NULL) {
return false; return false;
} }
Supexec(installTimerA);
gReady = true; gReady = true;
return true; return true;
} }
@ -198,7 +237,7 @@ void jlpAudioShutdown(void) {
if (!gReady) { if (!gReady) {
return; return;
} }
Supexec(uninstallTimerA); pumpStop();
silenceMixBuffer(); silenceMixBuffer();
if (gXmpCtx != NULL) { if (gXmpCtx != NULL) {
@ -246,6 +285,7 @@ void jlpAudioPlayMod(const uint8_t *data, uint32_t length, bool loop) {
} }
gXmpLoopCount = loop ? -1 : 0; gXmpLoopCount = loop ? -1 : 0;
gXmpStarted = true; gXmpStarted = true;
pumpStart();
} }
@ -254,6 +294,7 @@ void jlpAudioPlaySfx(uint8_t slot, const uint8_t *sample, uint32_t length, uint1
return; return;
} }
audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE); audioSfxSlotArm(&gSfxSlots[slot], sample, length, rateHz, MIX_RATE);
pumpStart();
} }
@ -262,6 +303,7 @@ void jlpAudioPlaySfxStream(uint8_t slot, jlAudioStreamFillT fill, void *ctx, uin
return; return;
} }
audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE); audioSfxSlotArmStream(&gSfxSlots[slot], fill, ctx, rateHz, MIX_RATE);
pumpStart();
} }
@ -391,6 +433,12 @@ void jlpAudioStopMod(void) {
gXmpLoaded = false; gXmpLoaded = false;
} }
silenceMixBuffer(); silenceMixBuffer();
// StopMod discards the mixed tail by design (buffer just went
// silent), so the pump can stop immediately -- unless an SFX is
// still live, in which case frameTick's drain path owns the stop.
if (!anySfxActive()) {
pumpStop();
}
} }
@ -413,7 +461,9 @@ void jlpAudioStopSfx(uint8_t slot) {
// audioSfxOverlayMix can approach that budget under load, so callers // audioSfxOverlayMix can approach that budget under load, so callers
// should run the game loop comfortably faster than ~12 Hz. // should run the game loop comfortably faster than ~12 Hz.
void jlpAudioFrameTick(void) { void jlpAudioFrameTick(void) {
if (!gReady) { bool silentFill;
if (!gReady || !gPumpRunning) {
return; return;
} }
// Underrun guard: both flags set means the producer fell a full // Underrun guard: both flags set means the producer fell a full
@ -423,22 +473,34 @@ void jlpAudioFrameTick(void) {
if (gNeedRefill[0] && gNeedRefill[1]) { if (gNeedRefill[0] && gNeedRefill[1]) {
silenceMixBuffer(); silenceMixBuffer();
} }
// silentFill is sampled BEFORE the overlay: a slot that exhausts
// during this overlay still wrote its tail into the half, so the
// half only counts as silent when nothing was live going in.
if (gNeedRefill[0]) { if (gNeedRefill[0]) {
silentFill = !gXmpStarted && !anySfxActive();
if (gXmpStarted) { if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount); xmp_play_buffer(gXmpCtx, (void *)gMixBuf, MIX_HALF, gXmpLoopCount);
} else { } else {
memset((void *)gMixBuf, 0x80, MIX_HALF); memset((void *)gMixBuf, 0x80, MIX_HALF);
} }
audioSfxOverlayMix(gMixBuf, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS); audioSfxOverlayMix(gMixBuf, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
gHalfSilent[0] = silentFill;
gNeedRefill[0] = 0; gNeedRefill[0] = 0;
} }
if (gNeedRefill[1]) { if (gNeedRefill[1]) {
silentFill = !gXmpStarted && !anySfxActive();
if (gXmpStarted) { if (gXmpStarted) {
xmp_play_buffer(gXmpCtx, (void *)(gMixBuf + MIX_HALF), MIX_HALF, gXmpLoopCount); xmp_play_buffer(gXmpCtx, (void *)(gMixBuf + MIX_HALF), MIX_HALF, gXmpLoopCount);
} else { } else {
memset((void *)(gMixBuf + MIX_HALF), 0x80, MIX_HALF); memset((void *)(gMixBuf + MIX_HALF), 0x80, MIX_HALF);
} }
audioSfxOverlayMix(gMixBuf + MIX_HALF, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS); audioSfxOverlayMix(gMixBuf + MIX_HALF, MIX_HALF, gSfxSlots, JOEY_AUDIO_SFX_SLOTS);
gHalfSilent[1] = silentFill;
gNeedRefill[1] = 0; gNeedRefill[1] = 0;
} }
// Drain-to-silence stop: both halves hold pure silence and nothing
// is playing, so the ISR would stream silence forever -- park it.
if (!gXmpStarted && gHalfSilent[0] && gHalfSilent[1] && !anySfxActive()) {
pumpStop();
}
} }

View file

@ -55,24 +55,10 @@
// ----- Per-surface planar storage (project_planar_68k_plan, ST Phase 2) ----- // ----- Per-surface planar storage (project_planar_68k_plan, ST Phase 2) -----
// //
// ST has word-interleaved planar: ONE 32000-byte buffer per surface // StPlanarT and the inline portData accessor moved to stPlanar.h
// holds all 4 planes packed 8 bytes per 16-pixel group. Per-scanline // (NATIVE-PERF Phase 2) so spriteCompile.c's dispatchers can read the
// layout (160 bytes) is 20 groups of 8 bytes, where each group holds // buffer base without a cross-TU call.
// p0_word, p1_word, p2_word, p3_word back-to-back. Compare with #include "stPlanar.h"
// Amiga's 4 separate plane buffers -- same total bytes, very different
// access pattern.
//
// The stage gets its own SHADOW planar buffer (NOT aliased to a
// displayed page) so drawing primitives don't appear until
// jlStagePresent copies the union(prev,current) dirty rows from the
// shadow into the off-screen page and flips the shifter to it. Same
// rationale as Amiga's per-stage shadow planes feeding its 2-buffer
// page flip.
typedef struct {
uint8_t *base; // 32000-byte interleaved planar buffer
uint8_t *raw; // unaligned malloc result for free()
bool ownsBuffer; // true = we malloc'd, false = aliased
} StPlanarT;
// Shifter palette registers: 16 words at $FFFF8240..$FFFF825F. // Shifter palette registers: 16 words at $FFFF8240..$FFFF825F.
#define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L) #define ST_PALETTE_REGS ((volatile uint16_t *)0xFFFF8240L)
@ -1281,9 +1267,9 @@ void jlpFillRect(jlSurfaceT *s, int16_t x, int16_t y, int16_t w, int16_t h, uint
jlpGenericFillRect(s, x, y, w, h, colorIndex); jlpGenericFillRect(s, x, y, w, h, colorIndex);
return; return;
} }
if (s == NULL || w == 0u || h == 0u) { // Callers guarantee s != NULL and positive w/h: the jlFillRect
return; // wrapper validates + clips before fillRectOnSurface, and the
} // fill-circle span loop emits spanWidth = 2x+1 >= 1 with h = 1.
pd = (StPlanarT *)s->portData; pd = (StPlanarT *)s->portData;
if (pd == NULL) { if (pd == NULL) {
return; return;
@ -1399,13 +1385,7 @@ void jlpTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const uint8_t *chunky
jlpGenericTilePaste(dst, bx, by, chunkyTile); jlpGenericTilePaste(dst, bx, by, chunkyTile);
return; return;
} }
if (dst == NULL || chunkyTile == NULL) {
return;
}
pd = (StPlanarT *)dst->portData; pd = (StPlanarT *)dst->portData;
if (pd == NULL) {
return;
}
/* Phase 10.5: jlTileT.pixels holds plane-major bytes (4 plane bytes /* Phase 10.5: jlTileT.pixels holds plane-major bytes (4 plane bytes
* per row * 8 rows = 32 bytes). Direct byte copy to the planar * per row * 8 rows = 32 bytes). Direct byte copy to the planar
* buffer; no chunky <-> planar conversion. Mirrors the sibling * buffer; no chunky <-> planar conversion. Mirrors the sibling
@ -1557,14 +1537,8 @@ void jlpTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT
jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); jlpGenericTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
return; return;
} }
if (dst == NULL || src == NULL) {
return;
}
dstPd = (StPlanarT *)dst->portData; dstPd = (StPlanarT *)dst->portData;
srcPd = (const StPlanarT *)src->portData; srcPd = (const StPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
return;
}
srcGroup = (uint16_t)((uint16_t)srcBx >> 1); srcGroup = (uint16_t)((uint16_t)srcBx >> 1);
dstGroup = (uint16_t)((uint16_t)dstBx >> 1); dstGroup = (uint16_t)((uint16_t)dstBx >> 1);
srcAddr = srcPd->base srcAddr = srcPd->base
@ -1624,13 +1598,7 @@ void jlpTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSu
jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent); jlpGenericTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparent);
return; return;
} }
if (dst == NULL || src == NULL) {
return;
}
dstPd = (StPlanarT *)dst->portData; dstPd = (StPlanarT *)dst->portData;
if (dstPd == NULL) {
return;
}
/* Phase 10.5: bulk-plane fast path. scratch holds plane-major bytes /* Phase 10.5: bulk-plane fast path. scratch holds plane-major bytes
* (4 plane bytes per row * 8 rows). For each row, build a "non- * (4 plane bytes per row * 8 rows). For each row, build a "non-
* transparent" mask = OR of (plane_byte XOR replicated transparent * transparent" mask = OR of (plane_byte XOR replicated transparent
@ -1932,9 +1900,36 @@ void jlpSpriteSavePlanes(const jlSurfaceT *s, int16_t x, int16_t y, uint16_t w,
if (pd == NULL) { if (pd == NULL) {
return; return;
} }
/* NATIVE-PERF Phase 1: group-aligned raw-span format. When x AND w
* are both 16-px multiples (the compiled save/restore window
* shape) each row is a CONTIGUOUS span of interleaved-group bytes,
* so the save is a straight per-row copy -- and, critically, the
* SAME byte format the compiled emitters read and write (backup
* format is a pure function of the recorded geometry; see
* spriteInternal.h). This branch must run BEFORE the byte-aligned
* branch: a group-aligned window previously took the byte-col asm,
* whose byte ORDER differs -- save and restore switch formats
* together, atomically, on the same predicate. */
if (((x | (int16_t)w) & 15) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
uint16_t spanBytes = (uint16_t)(w >> 1);
const uint8_t *src = pd->base + (uint16_t)y * ST_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP;
uint8_t *dst = dstPlaneBytes;
for (row = 0; row < (int16_t)h; row++) {
memcpy(dst, src, (size_t)spanBytes);
src += ST_BYTES_PER_ROW;
dst += spanBytes;
}
return;
}
/* Phase 10.5 fast path: byte-aligned, fully on-surface. /* Phase 10.5 fast path: byte-aligned, fully on-surface.
* Specialized 16x16 (the UBER ball-sprite size) skips the asm * Specialized 16x16 (the UBER ball-sprite size) skips the asm
* walker's per-row col-init + col-loop-check overhead. */ * walker's per-row col-init + col-loop-check overhead. Group-
* aligned windows never reach here (raw-span above owns them);
* this serves the x % 16 == 8 byte-aligned case. */
if ((x & 7) == 0 && (w & 7) == 0 if ((x & 7) == 0 && (w & 7) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH && x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) { && y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
@ -2001,8 +1996,29 @@ void jlpSpriteRestorePlanes(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uin
if (pd == NULL) { if (pd == NULL) {
return; return;
} }
/* Group-aligned raw-span restore: MUST mirror the save-side branch
* above exactly (same predicate, same byte order) -- see the save
* comment. A raw-span backup restored through the byte-col asm (or
* vice versa) silently scrambles planes. */
if (((x | (int16_t)w) & 15) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
uint16_t spanBytes = (uint16_t)(w >> 1);
uint8_t *dst = pd->base + (uint16_t)y * ST_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_GROUP;
const uint8_t *src = srcPlaneBytes;
for (row = 0; row < (int16_t)h; row++) {
memcpy(dst, src, (size_t)spanBytes);
dst += ST_BYTES_PER_ROW;
src += spanBytes;
}
return;
}
/* Phase 10.5 fast path: byte-aligned, fully on-surface. /* Phase 10.5 fast path: byte-aligned, fully on-surface.
* Specialized 16x16 (UBER ball-sprite) skips walker overhead. */ * Specialized 16x16 (UBER ball-sprite) skips walker overhead.
* Serves only the x % 16 == 8 case (raw-span above owns group-
* aligned windows). */
if ((x & 7) == 0 && (w & 7) == 0 if ((x & 7) == 0 && (w & 7) == 0
&& x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH && x >= 0 && (x + (int16_t)w) <= SURFACE_WIDTH
&& y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) { && y >= 0 && (y + (int16_t)h) <= SURFACE_HEIGHT) {
@ -2328,27 +2344,6 @@ uint8_t *jlpSurfaceAllocPixels(void) {
} }
// ST is word-interleaved: one buffer holds all 4 planes per group
// back-to-back. There's no per-plane base, but we overload planeIdx
// 0 to return the single buffer base so the cross-platform sprite
// dispatcher (spriteCompiledDraw) can hand it to the ST JIT
// routine, which computes plane offsets internally via d16(a0)
// chains. planeIdx >= 1 returns NULL since they don't make sense
// in interleaved layout.
uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
StPlanarT *pd;
if (s == NULL || planeIdx != 0u) {
return NULL;
}
pd = (StPlanarT *)s->portData;
if (pd == NULL) {
return NULL;
}
return pd->base;
}
// Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc // Phase 9: stage has no chunky shadow either. Cross-platform stageAlloc
// stores NULL in s->pixels and skips the chunky memset. // stores NULL in s->pixels and skips the chunky memset.
uint8_t *jlpStageAllocPixels(void) { uint8_t *jlpStageAllocPixels(void) {

51
src/atarist/spriteWalk.s Normal file
View file

@ -0,0 +1,51 @@
| ST shifted compiled-sprite walker (NATIVE-PERF Phase 1).
|
| One shared routine serves all 14 sub-byte sprite phases: the emitted
| per-phase thunk (spriteEmitInterleaved68k.c) loads groupBase into a0,
| points a1 at its pre-shifted data table, loads the row / group loop
| counts into d0/d1, and jmp's here. The data table holds, per 16-px
| group per row, three big-endian longs:
| notMask = ~M16 : ~M16 (same mask for all four plane words)
| bits01 = bp0 : bp1 (pre-shifted OR bits, planes 0..1)
| bits23 = bp2 : bp3 (planes 2..3)
| The walker RMWs each group's four interleaved plane words with two
| movem.l transfers -- addressing is linear (stride 8) at group
| granularity, which is what lets one routine serve every phase where
| byte-granular displacement patterns could not.
|
| Register ABI (private -- reached only from emitted thunks):
| a0 = groupBase (base + y*160 + (x>>4)*8; even -- movem-safe)
| a1 = data table
| d0.w = rows - 1
| d1.w = groupsPerRow - 1
| The thunk jmp's (not jsr's), so sp still holds the dispatcher's
| return address and the final rts returns straight to C. d0/d1/a0/a1
| are cdecl scratch; d3-d6/a2 are callee-save and preserved here.
|
| GAS m68k syntax, assembled by m68k-atari-mint-as. ST-only: this file
| must stay in src/atarist/ (src/m68k/*.s is wildcarded into the Amiga
| build too).
.text
.globl _stSpriteDrawShiftWalker
_stSpriteDrawShiftWalker:
movem.l %d3-%d6/%a2,-(%sp)
.LswRow:
movea.l %a0,%a2 | a2 = this row's first group
move.w %d1,%d3 | d3 = groupsPerRow - 1
.LswGrp:
movem.l (%a2),%d4-%d5 | d4 = p0w:p1w, d5 = p2w:p3w
move.l (%a1)+,%d6 | notMask long
and.l %d6,%d4
and.l %d6,%d5
or.l (%a1)+,%d4 | bits01
or.l (%a1)+,%d5 | bits23
movem.l %d4-%d5,(%a2)
addq.l #8,%a2 | next group
dbra %d3,.LswGrp
lea 160(%a0),%a0 | ST_BYTES_PER_ROW
dbra %d0,.LswRow
movem.l (%sp)+,%d3-%d6/%a2
rts

42
src/atarist/stPlanar.h Normal file
View file

@ -0,0 +1,42 @@
// Atari ST per-surface planar storage, shared between the HAL
// (src/atarist/hal.c) and the cross-platform sprite dispatchers
// (src/codegen/spriteCompile.c) so buffer-base access inlines instead
// of paying a cross-TU function call (NATIVE-PERF Phase 2). The base
// pointer is lifetime-immutable: portData is written only at surface
// create/destroy, so reading it needs no cache and no invalidation.
#ifndef JOEY_ST_PLANAR_H
#define JOEY_ST_PLANAR_H
#include <stdint.h>
#include <stdbool.h>
#include "surfaceInternal.h"
// jlSurfaceT.portData points to this on the ST. ST has word-interleaved
// planar: ONE 32000-byte buffer per surface holds all 4 planes packed
// 8 bytes per 16-pixel group. Per-scanline layout (160 bytes) is 20
// groups of 8 bytes, where each group holds p0_word, p1_word, p2_word,
// p3_word back-to-back. Compare with Amiga's 4 separate plane buffers
// -- same total bytes, very different access pattern.
//
// The stage gets its own SHADOW planar buffer (NOT aliased to a
// displayed page) so drawing primitives don't appear until
// jlStagePresent copies the union(prev,current) dirty rows from the
// shadow into the off-screen page and flips the shifter to it. Same
// rationale as Amiga's per-stage shadow planes feeding its 2-buffer
// page flip.
typedef struct {
uint8_t *base; // 32000-byte interleaved planar buffer
uint8_t *raw; // unaligned malloc result for free()
bool ownsBuffer; // true = we malloc'd, false = aliased
} StPlanarT;
// NULL means "surface has no planar storage" -- callers use that as
// their route-to-generic / no-op gate, exactly the contract the old
// jlpSurfacePlanePtr cross-TU function provided.
static inline StPlanarT *stSurfacePlanar(const jlSurfaceT *s) {
return (StPlanarT *)s->portData;
}
#endif

View file

@ -101,33 +101,93 @@ static uint32_t emitRestoreForTarget(uint8_t *out, uint32_t cap, const jlSpriteT
} }
// Sizing pass: returns total bytes the emitters will produce for // Per-compile plan: which (shift, op) variants land in the slot and
// this sprite's DRAW (per shift) + SAVE (per shift) + RESTORE (per // their exact sizes. Pass A (spritePlanBuild) measures every variant
// shift). Emitters that aren't implemented for the current platform // and greedily includes them under the uint16 routineOffsets cap; the
// return 0 here, so totalSize tracks only the ops that will actually // emit pass replays the SAME emitter arguments in the SAME order so
// land in the arena. // the byte streams are identical by construction (PERF-AUDIT #74).
static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) { typedef struct SpritePlanT {
uint16_t sizes[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
uint16_t drawMask; // bit s set = DRAW shift s included
uint16_t pairMask; // bit s set = SAVE+RESTORE pair for shift s included
uint32_t total; uint32_t total;
uint32_t written; } SpritePlanT;
uint8_t shift;
uint8_t op;
// Measure one (shift, op) variant. A variant that overflows the emit
// scratch (or is odd-sized, which would odd-align every later 68k call
// target in the slot -- address error) is recorded as 0 ("unavailable"),
// exactly like an emitter's not-implemented return: that shift falls to
// the interpreter instead of rejecting the whole sprite.
static uint16_t spritePlanMeasure(uint8_t *scratch, const jlSpriteT *sp, uint8_t shift, uint8_t op) {
uint32_t written;
total = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
switch (op) { switch (op) {
case SPRITE_OP_DRAW: written = emitDrawForTarget (scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); 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_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; case SPRITE_OP_RESTORE: written = emitRestoreForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift); break;
default: written = 0; break; default: written = 0; break;
} }
if (written == SPRITE_EMIT_OVERFLOW) { if (written == SPRITE_EMIT_OVERFLOW || written > SPRITE_EMIT_SCRATCH_BYTES) {
return SPRITE_EMIT_OVERFLOW; return 0;
} }
total += written; #if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST)
if ((written & 1u) != 0u) {
// 68k call targets must stay word-aligned: one odd routine
// would misalign every subsequent routine in the slot (address
// error on the fetch). The 68k emitters only produce even
// sizes, so this is a defensive skip. 65816/x86 code has NO
// even-length invariant -- the guard must not run there (it
// silently demoted odd-sized IIgs/DOS routines to the
// interpreter when it did).
return 0;
}
#endif
return (uint16_t)written;
}
// Pass A: measure every variant and greedily include them under the
// uint16 cap. Tier 1 = DRAW shifts ascending (they are the Phase-1
// parity-gate metric); tier 2 = SAVE+RESTORE as ATOMIC pairs per shift
// (a compiled save whose restore is absent would strand a compiled-
// format backup with an interpreted restore expecting the other format
// -- silent plane scramble; both-or-neither, always). When alignedOnly
// is set (the arena-full degrade tier) tier 1 keeps only the cheap
// byte-aligned phases in SPRITE_DEGRADE_DRAW_MASK.
static void spritePlanBuild(uint8_t *scratch, const jlSpriteT *sp, bool alignedOnly, SpritePlanT *plan) {
uint8_t shift;
uint8_t op;
uint16_t drawSize;
uint16_t saveSize;
uint16_t restSize;
plan->drawMask = 0;
plan->pairMask = 0;
plan->total = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
plan->sizes[shift][op] = spritePlanMeasure(scratch, sp, shift, op);
}
}
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
if (alignedOnly && ((SPRITE_DEGRADE_DRAW_MASK >> shift) & 1u) == 0u) {
continue;
}
drawSize = plan->sizes[shift][SPRITE_OP_DRAW];
if (drawSize != 0 && plan->total + drawSize <= 0xFFFFu) {
plan->drawMask |= (uint16_t)(1u << shift);
plan->total += drawSize;
}
}
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
saveSize = plan->sizes[shift][SPRITE_OP_SAVE];
restSize = plan->sizes[shift][SPRITE_OP_RESTORE];
if (saveSize != 0 && restSize != 0 && plan->total + saveSize + restSize <= 0xFFFFu) {
plan->pairMask |= (uint16_t)(1u << shift);
plan->total += (uint32_t)saveSize + restSize;
} }
} }
return total;
} }
@ -160,15 +220,64 @@ static uint32_t emitTotalSize(uint8_t *scratch, const jlSpriteT *sp) {
#endif #endif
// Emit pass: replay the plan's included variants in the SAME order the
// plan accumulated them (tier-1 draws ascending, then tier-2 save+
// restore pairs ascending) with the SAME emitter arguments, so every
// byte stream is identical to the sizing pass by construction. The
// emitters' bound checks are deliberately conservative (they reserve a
// worst case per row/iteration), so the scratch always gets the full
// SPRITE_EMIT_SCRATCH_BYTES cap -- an exact-fit capLeft made the last
// routine in every slot spuriously overflow (PERF-AUDIT.md #74).
// Returns false on a pass disagreement (must not happen with identical
// args, but never write past the slot).
static bool spritePlanEmit(uint8_t *scratch, jlSpriteT *sp, const SpritePlanT *plan, ArenaSlotT *slot) {
uint8_t *dst;
uint32_t offset;
uint32_t written;
uint8_t shift;
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
dst = codegenArenaBase() + slot->offset;
offset = 0;
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
if (((plan->drawMask >> shift) & 1u) == 0u) {
continue;
}
written = emitDrawForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift);
if (written != plan->sizes[shift][SPRITE_OP_DRAW] || offset >= SPRITE_NOT_COMPILED) {
return false;
}
memcpy(dst + offset, scratch, (size_t)written);
sp->routineOffsets[shift][SPRITE_OP_DRAW] = (uint16_t)offset;
offset += written;
}
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
if (((plan->pairMask >> shift) & 1u) == 0u) {
continue;
}
written = emitSaveForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift);
if (written != plan->sizes[shift][SPRITE_OP_SAVE] || offset >= SPRITE_NOT_COMPILED) {
return false;
}
memcpy(dst + offset, scratch, (size_t)written);
sp->routineOffsets[shift][SPRITE_OP_SAVE] = (uint16_t)offset;
offset += written;
written = emitRestoreForTarget(scratch, SPRITE_EMIT_SCRATCH_BYTES, sp, shift);
if (written != plan->sizes[shift][SPRITE_OP_RESTORE] || offset >= SPRITE_NOT_COMPILED) {
return false;
}
memcpy(dst + offset, scratch, (size_t)written);
sp->routineOffsets[shift][SPRITE_OP_RESTORE] = (uint16_t)offset;
offset += written;
}
return true;
}
bool jlSpriteCompile(jlSpriteT *sp) { bool jlSpriteCompile(jlSpriteT *sp) {
uint8_t *scratch; uint8_t *scratch;
uint32_t totalSize; SpritePlanT plan;
uint8_t shift;
uint8_t op;
ArenaSlotT *slot; ArenaSlotT *slot;
uint8_t *dst;
uint32_t written;
uint32_t offset;
if (sp == NULL) { if (sp == NULL) {
return false; return false;
@ -183,34 +292,24 @@ bool jlSpriteCompile(jlSpriteT *sp) {
/* #19: leave sp->slot NULL -> interpreter path. See the note above. */ /* #19: leave sp->slot NULL -> interpreter path. See the note above. */
return false; return false;
#endif #endif
/* 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); scratch = (uint8_t *)jlpBigAlloc(SPRITE_EMIT_SCRATCH_BYTES);
if (scratch == NULL) { if (scratch == NULL) {
return false; return false;
} }
totalSize = emitTotalSize(scratch, sp); spritePlanBuild(scratch, sp, false, &plan);
/* SPRITE_EMIT_OVERFLOW (0xFFFFFFFF) is > 0xFFFF too, so a single if (plan.total == 0) {
* routine that did not fit the scratch is rejected here as well. */
if (totalSize > 0xFFFFu) {
jlpBigFree(scratch);
return false;
}
if (totalSize == 0) {
/* Platforms whose emitter returns 0 for every (shift, op) have /* Platforms whose emitter returns 0 for every (shift, op) have
* no compiled bytes -- spriteCompiledDraw / SaveUnder / * no compiled bytes -- spriteCompiledDraw / SaveUnder /
* RestoreUnder would dereference a degenerate slot or chunky * RestoreUnder would dereference a degenerate slot or chunky
* shadow. Bail so sp->slot stays NULL and the dispatcher * shadow. Bail so sp->slot stays NULL and the dispatcher
* routes through the interpreted the jlpSprite*Planes ops path. */ * routes through the interpreted jlpSprite*Planes ops path. */
jlpBigFree(scratch); jlpBigFree(scratch);
return false; return false;
} }
slot = codegenArenaAlloc(totalSize); slot = codegenArenaAlloc(plan.total);
if (slot == NULL) { if (slot == NULL) {
// Fragmented arena: total free space may suffice even though no // Fragmented arena: total free space may suffice even though no
// single hole does (sprite create/destroy churn across levels). // single hole does (sprite create/destroy churn across levels).
@ -218,7 +317,24 @@ bool jlSpriteCompile(jlSpriteT *sp) {
// (codegenArenaInternal.h) once before giving up -- failure-path // (codegenArenaInternal.h) once before giving up -- failure-path
// only, zero hot-path cost (#5). // only, zero hot-path cost (#5).
codegenArenaCompact(); codegenArenaCompact();
slot = codegenArenaAlloc(totalSize); slot = codegenArenaAlloc(plan.total);
}
if (slot == NULL) {
// Degrade tier: rebuild the plan with only the cheap byte-
// aligned DRAW phases (SPRITE_DEGRADE_DRAW_MASK) + the save/
// restore pairs, so an arena too small for the full variant set
// still compiles what pre-Phase-1 builds compiled instead of
// losing the whole sprite to the interpreter. Deterministic:
// the reduced plan depends only on the sprite and the mask,
// never on fragmentation state.
spritePlanBuild(scratch, sp, true, &plan);
if (plan.total != 0) {
slot = codegenArenaAlloc(plan.total);
if (slot == NULL) {
codegenArenaCompact();
slot = codegenArenaAlloc(plan.total);
}
}
} }
if (slot == NULL) { if (slot == NULL) {
// Specific message so a swallowed jlSpritePrewarm/jlSpriteCompile // Specific message so a swallowed jlSpritePrewarm/jlSpriteCompile
@ -228,50 +344,12 @@ bool jlSpriteCompile(jlSpriteT *sp) {
return false; return false;
} }
dst = codegenArenaBase() + slot->offset; if (!spritePlanEmit(scratch, sp, &plan, slot)) {
offset = 0; memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
for (shift = 0; shift < JOEY_SPRITE_SHIFT_COUNT; shift++) {
for (op = 0; op < SPRITE_OP_COUNT; op++) {
// 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 (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 || (written != 0 && written > (uint32_t)(totalSize - offset))) {
/* The second pass disagreed with the sizing pass (must
* not happen with identical args, but never write past
* the slot). Roll back. */
codegenArenaFree(slot); codegenArenaFree(slot);
jlpBigFree(scratch); jlpBigFree(scratch);
return false; 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
* explicitly so a future change can't store an offset that
* collides with the sentinel. */
if (written == 0 || offset >= SPRITE_NOT_COMPILED) {
sp->routineOffsets[shift][op] = SPRITE_NOT_COMPILED;
} else {
sp->routineOffsets[shift][op] = (uint16_t)offset;
offset = offset + written;
}
}
}
sp->slot = slot; sp->slot = slot;
// Registry (#34): every creation path (including jlSpriteBankLoad, // Registry (#34): every creation path (including jlSpriteBankLoad,
// #32) already registered sp via spriteInitFields; this idempotent // #32) already registered sp via spriteInitFields; this idempotent
@ -501,71 +579,87 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup,
// Amiga planar dispatchers. spriteEmitPlanar68k.c emits DRAW routines // Amiga planar dispatchers. spriteEmitPlanar68k.c emits DRAW routines
// with a cdecl(p0, p1, p2, p3) signature that write directly to // with a cdecl(p0, p1, p2, p3) signature that write directly to
// bitplanes. Compute byteOff = y*40 + x/8 and pass plane[i]+byteOff // bitplanes, one pre-shifted variant per x % 8 phase (all 8 compiled
// as the 4 plane args. shift = x % 8 selects the variant; today only // since NATIVE-PERF Phase 1). Compute byteOff = y*40 + x/8 and pass
// shift 0 emits non-zero bytes, and every caller gate in // plane[i]+byteOff as the 4 plane args. Every caller gate in
// src/core/sprite.c checks routineOffsets[shift][op] != // src/core/sprite.c checks routineOffsets[shift][op] !=
// SPRITE_NOT_COMPILED before calling here (#29), so non-zero shifts // SPRITE_NOT_COMPILED before calling here (#29); anything uncompiled
// (x not 8-px-aligned) never reach this dispatcher -- they fall back // (clip, degrade-tier truncation) falls back to the interpreted
// to the interpreter, which handles arbitrary x via
// jlpSpriteDrawPlanes / jlpSpriteSavePlanes / jlpSpriteRestorePlanes. // jlpSpriteDrawPlanes / jlpSpriteSavePlanes / jlpSpriteRestorePlanes.
//
// Plane pointers come straight from portData via amigaPlanar.h's
// inline accessor (NATIVE-PERF Phase 2) -- the old jlpSurfacePlanePtr
// cross-TU call cost ~190 cycles PER PLANE, x4 per dispatch. The
// pd == NULL gate preserves the "not a planar surface -> no-op"
// contract those calls provided.
// Amiga per-plane row stride: 320 px / 8 px-per-plane-byte = 40. #include "amigaPlanar.h"
// 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, uint16_t routeOffset) { 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); typedef void (*DrawFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3);
AmigaPlanarT *pd;
uint16_t byteOff; uint16_t byteOff;
uint8_t *p0;
uint8_t *p1;
uint8_t *p2;
uint8_t *p3;
DrawFn fn; DrawFn fn;
byteOff = (uint16_t)((uint16_t)y * AMIGA_PLANE_BYTES_PER_ROW + ((uint16_t)x >> 3)); pd = amigaSurfacePlanar(dst);
p0 = jlpSurfacePlanePtr(dst, 0); if (pd == NULL) {
if (p0 == NULL) {
return; return;
} }
p1 = jlpSurfacePlanePtr(dst, 1); byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)x >> 3));
p2 = jlpSurfacePlanePtr(dst, 2);
p3 = jlpSurfacePlanePtr(dst, 3);
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(p0 + byteOff, p1 + byteOff, p2 + byteOff, p3 + byteOff); fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff);
} }
// Save/restore aren't compiled on the Amiga (spriteEmitPlanar68k.c // Compiled save/restore (NATIVE-PERF Phase 1): the emitted routines are
// returns 0 for both, so every routineOffsets[*][SPRITE_OP_SAVE / // cdecl(p0, p1, p2, p3, buf) plane-run copiers over the 16-px-group
// _RESTORE] entry is SPRITE_NOT_COMPILED). The sprite.c gates route // window (bx = x & ~15, nGroups = SPRITE_GROUPS0 + class). shift here
// all save/restore work through the interpreted jlpSpriteSavePlanes / // is the width CLASS from SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS,
// jlpSpriteRestorePlanes; these stubs exist only to satisfy the linker // not the draw bit-phase. Row displacements are baked into the
// (sprite.c references them unconditionally). // routine; the window's x phase enters only through the plane base
// // pointers (byteOff is even by construction -- bx % 16 == 0 -- so the
// FUTURE (when real Amiga save/restore emitters land): the compiled // emitted move.w/move.l are 68000-legal).
// 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 spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)src; typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf);
(void)sp; AmigaPlanarT *pd;
(void)x; int16_t bx;
(void)y; uint16_t nGroups;
(void)backup; uint16_t byteOff;
(void)shift; CopyFn fn;
(void)routeOffset;
pd = amigaSurfacePlanar(src);
if (pd == NULL) {
return;
}
bx = (int16_t)(x & ~15);
nGroups = (uint16_t)(SPRITE_GROUPS0(sp->widthPx) + shift);
byteOff = (uint16_t)((uint16_t)y * AMIGA_BYTES_PER_ROW + ((uint16_t)bx >> 3));
backup->x = bx;
backup->y = y;
backup->width = (uint16_t)(nGroups << 4);
backup->height = sp->heightPx;
backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
} }
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) { void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)dst; typedef void (*CopyFn)(uint8_t *p0, uint8_t *p1, uint8_t *p2, uint8_t *p3, uint8_t *buf);
(void)backup; AmigaPlanarT *pd;
uint16_t byteOff;
CopyFn fn;
(void)shift; (void)shift;
(void)routeOffset; pd = amigaSurfacePlanar(dst);
if (pd == NULL) {
return;
}
byteOff = (uint16_t)((uint16_t)backup->y * AMIGA_BYTES_PER_ROW + ((uint16_t)backup->x >> 3));
fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset);
fn(pd->planes[0] + byteOff, pd->planes[1] + byteOff, pd->planes[2] + byteOff, pd->planes[3] + byteOff, backup->bytes);
} }
#elif defined(JOEYLIB_PLATFORM_ATARIST) #elif defined(JOEYLIB_PLATFORM_ATARIST)
@ -577,13 +671,12 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup,
// emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at // emits up to one move.b / clr.b / andi.b+ori.b / ori.b chain at
// d16(a0). // d16(a0).
// //
// shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX): // shift selection (in spriteInternal.h SPRITE_SHIFT_INDEX): the full
// 0 : byte-aligned x with x mod 16 == 0 (first tile col high half) // intra-group phase x & 15. Phases 0/8 are the unrolled byte-aligned
// 1 : byte-aligned x with x mod 16 == 8 (first tile col low half) // JIT; the 14 sub-byte phases are thunk + pre-shifted data tables
// 2+ : non-byte-aligned x, never compiled (emitter returns 0); the // driven by stSpriteDrawShiftWalker (all compiled since NATIVE-PERF
// per-shift offset is SPRITE_NOT_COMPILED so the caller gates // Phase 1). Anything uncompiled falls back to jlpSpriteDrawPlanes via
// in src/core/sprite.c (#29) never dispatch here and fall back // the caller gates (#29).
// to jlpSpriteDrawPlanes.
// ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160 // ST word-interleaved row stride: 320 px * 4 planes / 8 bits = 160
// bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct // bytes/row. Numerically equal to SURFACE_BYTES_PER_ROW but a distinct
@ -592,17 +685,24 @@ void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup,
#define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8) #define ST_GROUP_BYTES_PER_ROW (SURFACE_WIDTH * 4 / 8)
#define ST_BYTES_PER_PLANE_GROUP 8u #define ST_BYTES_PER_PLANE_GROUP 8u
// Buffer base comes straight from portData via stPlanar.h's inline
// accessor (NATIVE-PERF Phase 2); the pd == NULL gate preserves the
// "not a planar surface -> no-op" contract the old jlpSurfacePlanePtr
// cross-TU call provided.
#include "stPlanar.h"
void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) { void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t y, uint16_t routeOffset) {
typedef void (*DrawFn)(uint8_t *groupBase); typedef void (*DrawFn)(uint8_t *groupBase);
uint8_t *base; StPlanarT *pd;
uint8_t *groupBase; uint8_t *groupBase;
DrawFn fn; DrawFn fn;
base = jlpSurfacePlanePtr(dst, 0); pd = stSurfacePlanar(dst);
if (base == NULL) { if (pd == NULL) {
return; return;
} }
groupBase = base groupBase = pd->base
+ (uint16_t)y * ST_GROUP_BYTES_PER_ROW + (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP; + (uint16_t)((uint16_t)x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset); fn = (DrawFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
@ -610,28 +710,59 @@ void spriteCompiledDraw(jlSurfaceT *dst, const jlSpriteT *sp, int16_t x, int16_t
} }
// Save/Restore aren't compiled on ST yet (emitter returns 0). The // Compiled save/restore (NATIVE-PERF Phase 1): the emitted routines are
// caller gates in src/core/sprite.c see SPRITE_NOT_COMPILED for // cdecl(src, dst) raw-span copiers over the 16-px-group window
// sp->routineOffsets[shift][SPRITE_OP_SAVE/_RESTORE] and route those // (bx = x & ~15, nGroups = SPRITE_GROUPS0 + class); a group window's
// ops through the interpreted jlpSpriteSavePlanes / // rows are CONTIGUOUS byte spans in the interleaved layout, so the
// jlpSpriteRestorePlanes. These stubs exist only to satisfy the // routine is a pure move.l chain with the inter-row stride baked in.
// linker. // shift here is the width CLASS, not the draw bit-phase. rowStart is
// always even (bx % 16 == 0, base is long-aligned), so move.l is
// 68000-legal.
void spriteCompiledSaveUnder(const jlSurfaceT *src, jlSpriteT *sp, int16_t x, int16_t y, jlSpriteBackupT *backup, uint8_t shift, 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)src; typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
(void)sp; StPlanarT *pd;
(void)x; int16_t bx;
(void)y; uint16_t nGroups;
(void)backup; uint8_t *rowStart;
(void)shift; CopyFn fn;
(void)routeOffset;
pd = stSurfacePlanar(src);
if (pd == NULL) {
return;
}
bx = (int16_t)(x & ~15);
nGroups = (uint16_t)(SPRITE_GROUPS0(sp->widthPx) + shift);
rowStart = pd->base
+ (uint16_t)y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)bx >> 4) * ST_BYTES_PER_PLANE_GROUP;
backup->x = bx;
backup->y = y;
backup->width = (uint16_t)(nGroups << 4);
backup->height = sp->heightPx;
backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * sp->heightPx);
fn = (CopyFn)(codegenArenaBase() + sp->slot->offset + routeOffset);
fn(rowStart, backup->bytes);
} }
void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) { void spriteCompiledRestoreUnder(jlSurfaceT *dst, const jlSpriteBackupT *backup, uint8_t shift, uint16_t routeOffset) {
(void)dst; typedef void (*CopyFn)(const uint8_t *src, uint8_t *dst);
(void)backup; StPlanarT *pd;
uint8_t *rowStart;
CopyFn fn;
(void)shift; (void)shift;
(void)routeOffset; pd = stSurfacePlanar(dst);
if (pd == NULL) {
return;
}
rowStart = pd->base
+ (uint16_t)backup->y * ST_GROUP_BYTES_PER_ROW
+ (uint16_t)((uint16_t)backup->x >> 4) * ST_BYTES_PER_PLANE_GROUP;
fn = (CopyFn)(codegenArenaBase() + backup->sprite->slot->offset + routeOffset);
fn(backup->bytes, rowStart);
} }
#else #else

View file

@ -663,20 +663,27 @@ void jlDrawRect(jlSurfaceT *s, int16_t x, int16_t y, uint16_t w, uint16_t h, uin
// (boundary proof at the macro definition: w == 320 passes only // (boundary proof at the macro definition: w == 320 passes only
// x == 0; x == 319 / w == 1 passes; x == -1 fails). // x == 0; x == 319 / w == 1 passes; x == -1 fails).
if (SURFACE_RECT_ON_SURFACE(x, y, w, h)) { if (SURFACE_RECT_ON_SURFACE(x, y, w, h)) {
// Raw edge fills + ONE dirty mark for the whole outline
// (NATIVE-PERF Phase 2 R11, the fill-circle span pattern).
// Band state is provably identical to marking each edge:
// bands are [min, max] intervals per row, and the left+right
// edge words widen an interior row to exactly the same
// [wordof(x), wordof(x+w-1)] the full-width mark produces.
// Top edge. // Top edge.
fillRectOnSurface(s, x, y, (int16_t)w, 1, colorIndex); jlpFillRect(s, x, y, (int16_t)w, 1, colorIndex);
// Bottom edge. // Bottom edge.
fillRectOnSurface(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex); jlpFillRect(s, x, (int16_t)(y + (int16_t)h - 1), (int16_t)w, 1, colorIndex);
// Interior vertical edges only exist when h > 2 (an h == 2 rect // Interior vertical edges only exist when h > 2 (an h == 2 rect
// is just the two horizontal edges). Skipping them keeps h - 2 // is just the two horizontal edges). Skipping them keeps h - 2
// == 0 rects out of fillRectOnSurface, whose contract requires // == 0 rects out of jlpFillRect, whose contract requires
// positive dimensions (PERF-AUDIT #65 verifier correction). // positive dimensions (PERF-AUDIT #65 verifier correction).
if (h > 2) { if (h > 2) {
// Left edge (interior only -- top and bottom corners already drawn). // Left edge (interior only -- top and bottom corners already drawn).
fillRectOnSurface(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex); jlpFillRect(s, x, (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
// Right edge (interior only). // Right edge (interior only).
fillRectOnSurface(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex); jlpFillRect(s, (int16_t)(x + (int16_t)w - 1), (int16_t)(y + 1), 1, (int16_t)(h - 2), colorIndex);
} }
surfaceMarkDirtyRect(s, x, y, (int16_t)w, (int16_t)h);
return; return;
} }
// General (possibly clipped) case: let jlFillRect clip each edge. // General (possibly clipped) case: let jlFillRect clip each edge.

View file

@ -24,14 +24,20 @@
// request small. Requests above one 64 KB bank are clamped in // request small. Requests above one 64 KB bank are clamped in
// codegenArenaInit (#8). // codegenArenaInit (#8).
// //
// Amiga/ST/DOS: the 68k/x86 emitters are far less dense -- an // Amiga/ST: the all-shift compiled sprites (NATIVE-PERF Phase 1)
// all-opaque 32x32 costs ~6.4 KB on ST (both shifts) and a // emit one DRAW variant per x phase (8 on Amiga, 16 on ST) plus two
// substantially-mixed one ~8-16 KB by itself, so an 8 KB default // save/restore classes -- a 16x16 sprite's full set is ~13-19 KB and
// silently demoted sprites to the interpreter. 32 KB matches what // a mixed 24x24 can reach ~25-50 KB, so 32 KB starved multi-sprite
// the sprite-heavy examples (uber, sprite, spacetaxi) already pass // apps into the degrade tier (aligned-only draws). 64 KB holds a
// explicitly, and these ports have real heaps. // couple of full-variant sprites; jlSpriteCompile degrades per
// sprite rather than failing when it still runs out. These ports
// have real heaps.
//
// DOS: two chunky shifts, ~1-6 KB per sprite; 32 KB is generous.
#if defined(JOEYLIB_PLATFORM_IIGS) #if defined(JOEYLIB_PLATFORM_IIGS)
#define DEFAULT_CODEGEN_BYTES (8UL * 1024UL) #define DEFAULT_CODEGEN_BYTES (8UL * 1024UL)
#elif defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST)
#define DEFAULT_CODEGEN_BYTES (64UL * 1024UL)
#else #else
#define DEFAULT_CODEGEN_BYTES (32UL * 1024UL) #define DEFAULT_CODEGEN_BYTES (32UL * 1024UL)
#endif #endif

View file

@ -604,14 +604,10 @@ void jlpGenericSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData);
#endif #endif
#endif #endif
uint8_t *jlpGenericSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx); // (jlpSurfacePlanePtr was retired in NATIVE-PERF Phase 2: its only
#if !defined(jlpSurfacePlanePtr) // callers were spriteCompile.c's 68k dispatchers, which now read
#if defined(JL_HAS_SURFACE_PLANE_PTR) // portData directly via the inline accessors in amigaPlanar.h /
uint8_t *jlpSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx); // stPlanar.h instead of paying a cross-TU call per plane.)
#else
#define jlpSurfacePlanePtr(_s, _i) jlpGenericSurfacePlanePtr((_s), (_i))
#endif
#endif
// --- Timing services (platform-only stub generics; millisElapsed has a real default) --- // --- Timing services (platform-only stub generics; millisElapsed has a real default) ---
uint16_t jlpGenericFrameHz(void); uint16_t jlpGenericFrameHz(void);

View file

@ -343,9 +343,11 @@ static void spriteRegistryRemove(jlSpriteT *sp) {
static void spriteResetCompiledState(jlSpriteT *sp) { static void spriteResetCompiledState(jlSpriteT *sp) {
sp->slot = NULL; sp->slot = NULL;
memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets)); memset(sp->routineOffsets, 0xFF, sizeof(sp->routineOffsets));
#if defined(JOEYLIB_PLATFORM_IIGS)
memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank)); memset(sp->cachedDstBank, 0xFF, sizeof(sp->cachedDstBank));
memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank)); memset(sp->cachedSrcBank, 0xFF, sizeof(sp->cachedSrcBank));
memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes)); memset(sp->cachedSizeBytes, 0, sizeof(sp->cachedSizeBytes));
#endif
} }
@ -565,12 +567,19 @@ void jlSpriteSaveAndDraw(jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y, jlS
// mark. One mark instead of two (save doesn't dirty -- it's a // mark. One mark instead of two (save doesn't dirty -- it's a
// read; only draw dirties). shift and both route offsets are // read; only draw dirties). shift and both route offsets are
// derived once here and handed to the callees (#29). // derived once here and handed to the callees (#29).
if (slot != NULL && backup->bytes != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) { if (slot != NULL && backup->bytes != NULL && SPRITE_BACKUP_PTR_OK(backup->bytes)
&& isFullyOnSurface(x, y, widthPx, heightPx)) {
uint8_t saveClass;
// DRAW routes by the x bit-phase; SAVE/RESTORE route by the
// 16-px window width class (planar) or nibble parity (chunky).
// The two indices differ on planar ports -- derive both (#29).
shift = SPRITE_SHIFT_INDEX(x); shift = SPRITE_SHIFT_INDEX(x);
saveOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE); saveClass = SPRITE_SAVE_CLASS(sp, x);
saveOffset = SPRITE_ROUTE_OFFSET(sp, saveClass, SPRITE_OP_SAVE);
drawOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW); drawOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_DRAW);
if (saveOffset != SPRITE_NOT_COMPILED && drawOffset != SPRITE_NOT_COMPILED) { if (saveOffset != SPRITE_NOT_COMPILED && drawOffset != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup, shift, saveOffset); spriteCompiledSaveUnder(s, sp, x, y, backup, saveClass, saveOffset);
spriteCompiledDraw (s, sp, x, y, drawOffset); spriteCompiledDraw (s, sp, x, y, drawOffset);
if (!COMPILED_SPRITE_WRITES_PLANES) { if (!COMPILED_SPRITE_WRITES_PLANES) {
jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes); jlpSpriteSavePlanes(s, backup->x, backup->y, backup->width, backup->height, backup->bytes);
@ -613,7 +622,6 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
uint16_t bw; uint16_t bw;
uint16_t bh; uint16_t bh;
jlSpriteT *sp; jlSpriteT *sp;
uint16_t spriteBytesPerRow;
int16_t copyBytes; int16_t copyBytes;
uint8_t shift; uint8_t shift;
@ -640,20 +648,22 @@ void jlSpriteRestoreUnder(jlSurfaceT *s, const jlSpriteBackupT *backup) {
} }
sp = backup->sprite; sp = backup->sprite;
if (sp != NULL && sp->slot != NULL && bh == sp->heightPx) { if (sp != NULL && sp->slot != NULL && bh == sp->heightPx
&& SPRITE_BACKUP_PTR_OK(backup->bytes)) {
uint16_t routeOffset; uint16_t routeOffset;
spriteBytesPerRow = (uint16_t)(sp->widthPx >> 1);
copyBytes = (int16_t)(bw >> 1); copyBytes = (int16_t)(bw >> 1);
// The compiled restore routine walks a FIXED per-row byte count // The compiled restore routine walks a FIXED per-row byte count,
// derived purely from the chosen shift (spriteBytesPerRow for // so the class must be derived from the backup's RECORDED
// shift 0, spriteBytesPerRow+1 for shift 1). A horizontally // geometry -- never from x. SPRITE_RESTORE_CLASS returns 0/1
// clipped save records a narrower backup->width / copyBytes that // only for a backup whose geometry proves the compiled format
// matches neither, so only take the fast path when copyBytes is // (chunky: copyBytes == bytesPerRow or +1, exactly the historic
// exactly one of those two compiled widths; any other width must // check; planar: 16-px-aligned bx AND bw plus a class width --
// fall through to the interpreted memcpy that honors copyBytes. // the bx term is correctness-critical, see spriteInternal.h),
if (copyBytes == (int16_t)spriteBytesPerRow || // and 0xFF for everything else (horizontal clips, walker-format
copyBytes == (int16_t)(spriteBytesPerRow + 1)) { // backups), which falls through to the interpreted paths that
shift = (copyBytes == (int16_t)spriteBytesPerRow) ? 0 : 1; // honor copyBytes.
shift = SPRITE_RESTORE_CLASS(sp, bx, bw, (uint16_t)copyBytes);
if (shift != 0xFFu) {
routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_RESTORE); routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_RESTORE);
if (routeOffset != SPRITE_NOT_COMPILED) { if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledRestoreUnder(s, backup, shift, routeOffset); spriteCompiledRestoreUnder(s, backup, shift, routeOffset);
@ -715,9 +725,13 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// bytes for SAVE at this shift. The compiled routine assumes a // bytes for SAVE at this shift. The compiled routine assumes a
// full-size, unclipped rectangle, so anything off-edge falls // full-size, unclipped rectangle, so anything off-edge falls
// through to the interpreted memcpy loop below. // through to the interpreted memcpy loop below.
if (backup->bytes != NULL && slot != NULL && isFullyOnSurface(x, y, widthPx, heightPx)) { if (backup->bytes != NULL && slot != NULL && SPRITE_BACKUP_PTR_OK(backup->bytes)
&& isFullyOnSurface(x, y, widthPx, heightPx)) {
uint16_t routeOffset; uint16_t routeOffset;
shift = SPRITE_SHIFT_INDEX(x); // Save routes by the width class (planar 16-px window / chunky
// nibble parity), NOT by the DRAW shift index -- see
// SPRITE_SAVE_CLASS in spriteInternal.h.
shift = SPRITE_SAVE_CLASS(sp, x);
routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE); routeOffset = SPRITE_ROUTE_OFFSET(sp, shift, SPRITE_OP_SAVE);
if (routeOffset != SPRITE_NOT_COMPILED) { if (routeOffset != SPRITE_NOT_COMPILED) {
spriteCompiledSaveUnder(s, sp, x, y, backup, shift, routeOffset); spriteCompiledSaveUnder(s, sp, x, y, backup, shift, routeOffset);
@ -777,6 +791,20 @@ void jlSpriteSaveUnder(const jlSurfaceT *s, jlSpriteT *sp, int16_t x, int16_t y,
// metadata so they can size their buffer for the next // metadata so they can size their buffer for the next
// SaveUnder call. RestoreUnder will refuse to operate on a // SaveUnder call. RestoreUnder will refuse to operate on a
// backup with bytes==NULL. // backup with bytes==NULL.
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST)
// Planar metadata widening: a real (buffered) unclipped save on
// these ports goes through the compiled 16-px-window path, so a
// probe-then-allocate caller must be told the WINDOW size, not
// the chunky-rounded size -- otherwise the next buffered save
// could overrun the allocation by up to one group per row.
if (dx == x && w == (int16_t)widthPx) {
uint16_t nGroups = (uint16_t)(SPRITE_GROUPS0(widthPx) + SPRITE_SAVE_CLASS(sp, x));
backup->x = (int16_t)(x & ~15);
backup->width = (uint16_t)(nGroups << 4);
backup->sizeBytes = (uint16_t)((uint16_t)(nGroups << 3) * (uint16_t)h);
}
#endif
return; return;
} }
/* Chunky save path: skip on planar ports (s->pixels NULL). /* Chunky save path: skip on planar ports (s->pixels NULL).

View file

@ -13,20 +13,32 @@
#define SPRITE_OP_RESTORE 2 #define SPRITE_OP_RESTORE 2
#define SPRITE_OP_COUNT 3 #define SPRITE_OP_COUNT 3
// Per-platform shift index used by the dispatcher. Chunky 4bpp ports // Per-platform shift-variant count + dispatcher index. Count and mask
// store one nibble per pixel pair so the only sub-byte alignment is // live in ONE ladder so they cannot drift: count == mask + 1 always.
// x % 2. Amiga planar packs 8 pixels per plane byte so all 8 // Chunky 4bpp ports (IIgs/DOS/generic) store one nibble per pixel pair
// alignments matter. ST word-interleaved planar groups 16 pixels // so the only sub-byte alignment is x & 1 -- shifts 0+1 cover EVERY x.
// per word; for byte-aligned x (x mod 8 == 0) the only meaningful // Amiga planar packs 8 pixels per plane byte so all 8 bit-phases
// distinction is high vs low byte of the plane word, which is bit // matter. ST word-interleaved planar groups 16 pixels per plane word
// 3 of x (== (x >> 3) & 1). Other shifts (x mod 8 != 0) emit 0 // AND the displacement pattern differs between a sprite starting in
// from the JIT and route to the interpreter. // the word's high byte vs low byte (+1 within a group vs +7 crossing
#if defined(JOEYLIB_PLATFORM_AMIGA) // to the next group's high half), so the full intra-group phase x & 15
// keys the variant: 16 slots, with today's byte-aligned phases at
// slots 0 (x%16==0) and 8 (x%16==8).
// SPRITE_DEGRADE_DRAW_MASK: the DRAW shifts kept by jlSpriteCompile's
// degrade tier when the arena cannot hold the full plan -- the cheap
// byte-aligned phases every port compiled before NATIVE-PERF Phase 1.
#if defined(JOEYLIB_PLATFORM_ATARIST)
#define JOEY_SPRITE_SHIFT_COUNT 16
#define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 15))
#define SPRITE_DEGRADE_DRAW_MASK 0x0101u // phases 0 and 8
#elif defined(JOEYLIB_PLATFORM_AMIGA)
#define JOEY_SPRITE_SHIFT_COUNT 8
#define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 7)) #define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 7))
#elif defined(JOEYLIB_PLATFORM_ATARIST) #define SPRITE_DEGRADE_DRAW_MASK 0x0001u // shift 0
#define SPRITE_SHIFT_INDEX(x) ((uint8_t)(((x) & 7) ? 2u : (uint8_t)(((x) >> 3) & 1u)))
#else #else
#define JOEY_SPRITE_SHIFT_COUNT 2
#define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 1)) #define SPRITE_SHIFT_INDEX(x) ((uint8_t)((x) & 1))
#define SPRITE_DEGRADE_DRAW_MASK 0x0003u // both nibble phases
#endif #endif
// Sentinel stored in routineOffsets[shift][op] when that op's emitter // Sentinel stored in routineOffsets[shift][op] when that op's emitter
@ -35,6 +47,50 @@
// is valid for the first emitted op (typically DRAW shift 0). // is valid for the first emitted op (typically DRAW shift 0).
#define SPRITE_NOT_COMPILED 0xFFFFu #define SPRITE_NOT_COMPILED 0xFFFFu
// ----- Save/restore width classes (NATIVE-PERF Phase 1) -----
//
// Compiled save/restore on the planar 68k ports operates on the
// 16-px-group-aligned window covering the sprite (bx = x & ~15,
// bw = nGroups * 16), which makes every row a contiguous raw span (ST)
// or an even-offset plane run (Amiga) -- pure move.l/move.w copies.
// Because save/restore bake no masks, the emitted body depends only on
// the group count, so ALL x phases funnel into exactly TWO class
// variants per op: class 0 = ceil(w/16) groups, class 1 = one more
// (when (x & 15) pushes the sprite across a group boundary). Save and
// restore route by these macros, NOT by SPRITE_SHIFT_INDEX -- their
// table entries live at shifts 0/1 regardless of the DRAW shift count.
//
// SPRITE_RESTORE_CLASS derives the class from the backup's RECORDED
// geometry (never from x): returns 0/1 for a compiled-format backup,
// 0xFF to route to the interpreted paths. The (bx & 15) == 0 term is
// CORRECTNESS-CRITICAL on both ports, not an optimization: a backup
// written by the byte-aligned interpreted walker at x % 16 == 8 can
// record copyBytes numerically equal to a class width while holding a
// DIFFERENT byte format (ST byte-col vs raw-span) or run length
// (Amiga), and on the ST its odd byte offset would address-error the
// compiled move.l reader. Chunky expansions are token-identical to the
// pre-Phase-1 code so IIgs/DOS dispatch is provably unchanged.
//
// SPRITE_BACKUP_PTR_OK: the compiled 68k routines move.l/move.w through
// backup->bytes, so an odd caller buffer must route BOTH ops to the
// byte-safe interpreted paths (pointer parity is stable between save
// and restore, so provenance never splits).
#if defined(JOEYLIB_PLATFORM_AMIGA) || defined(JOEYLIB_PLATFORM_ATARIST)
#define SPRITE_GROUPS0(_wPx) ((uint16_t)(((_wPx) + 15u) >> 4))
#define SPRITE_SAVE_CLASS(_sp, _x) ((uint8_t)((uint16_t)((((uint16_t)(_x) & 15u) + (_sp)->widthPx + 15u) >> 4) > SPRITE_GROUPS0((_sp)->widthPx) ? 1u : 0u))
#define SPRITE_RESTORE_CLASS(_sp, _bx, _bw, _copyBytes) \
((uint8_t)((((uint16_t)(_bx) | (uint16_t)(_bw)) & 15u) != 0u ? 0xFFu : \
((_copyBytes) == (uint16_t)(SPRITE_GROUPS0((_sp)->widthPx) << 3) ? 0u : \
((_copyBytes) == (uint16_t)((SPRITE_GROUPS0((_sp)->widthPx) + 1u) << 3) ? 1u : 0xFFu))))
#define SPRITE_BACKUP_PTR_OK(_p) ((((uintptr_t)(_p)) & 1u) == 0u)
#else
#define SPRITE_SAVE_CLASS(_sp, _x) ((uint8_t)((_x) & 1))
#define SPRITE_RESTORE_CLASS(_sp, _bx, _bw, _copyBytes) \
((uint8_t)((_copyBytes) == (uint16_t)((_sp)->widthPx >> 1) ? 0u : \
((_copyBytes) == (uint16_t)(((_sp)->widthPx >> 1) + 1u) ? 1u : 0xFFu)))
#define SPRITE_BACKUP_PTR_OK(_p) (1)
#endif
// The 4bpp nibble value treated as transparent by the interpreters // The 4bpp nibble value treated as transparent by the interpreters
// (sprite.c) and every per-CPU emitter. Single definition so the // (sprite.c) and every per-CPU emitter. Single definition so the
// transparency contract cannot drift between the compiled and // transparency contract cannot drift between the compiled and
@ -87,13 +143,15 @@ struct jlSpriteT {
ArenaSlotT *slot; ArenaSlotT *slot;
uint16_t routineOffsets[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT]; uint16_t routineOffsets[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
#if defined(JOEYLIB_PLATFORM_IIGS)
// Per-shift, per-op MVN bank-patch cache for IIgs save/restore. // Per-shift, per-op MVN bank-patch cache for IIgs save/restore.
// patchMvnBanks rewrites 16+ MVN bank operands every call, but the // patchMvnBanks rewrites 16+ MVN bank operands every call, but the
// banks themselves rarely change frame-to-frame (screen surface // banks themselves rarely change frame-to-frame (screen surface
// is fixed; backup buffer is allocated once). After the first // is fixed; backup buffer is allocated once). After the first
// patch, subsequent calls compare requested banks to the cache // patch, subsequent calls compare requested banks to the cache
// and skip the re-stamp loop. 0xFF means "never patched yet". // and skip the re-stamp loop. 0xFF means "never patched yet".
// 12 bytes per sprite. Unused on non-IIgs. // IIgs-only: no other port bank-patches, so the fields (and their
// resets in spriteResetCompiledState) compile away elsewhere.
uint8_t cachedDstBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT]; uint8_t cachedDstBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT]; uint8_t cachedSrcBank[JOEY_SPRITE_SHIFT_COUNT][SPRITE_OP_COUNT];
@ -102,6 +160,7 @@ struct jlSpriteT {
// software multiply helper (~30-50 cyc); cache hit dodges it. Filled // software multiply helper (~30-50 cyc); cache hit dodges it. Filled
// lazily on first call (0 sentinel = uncached). // lazily on first call (0 sentinel = uncached).
uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT]; uint16_t cachedSizeBytes[JOEY_SPRITE_SHIFT_COUNT];
#endif
}; };
// Compiled entry points. Implemented alongside jlSpriteCompile in // Compiled entry points. Implemented alongside jlSpriteCompile in

View file

@ -235,15 +235,59 @@ void surfaceMarkDirtyAll(const jlSurfaceT *s) {
void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) { void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t maxWord) {
uint8_t *minPtr; uint8_t *minPtr;
uint8_t *maxPtr; uint8_t *maxPtr;
uint8_t *minEnd;
// Straight-line inlined widen (no per-row helper call) with walking // Straight-line inlined widen (no per-row helper call) with walking
// pointers, so a multi-row stage op -- tile fill/paste/copy, sprite // pointers, so a multi-row stage op -- sprite draw, fillRect --
// draw, fillRect -- pays one tight loop instead of yEnd-y function // pays one tight loop instead of yEnd-y function calls. This is
// calls. This is the ST/Amiga counterpart to the IIgs asm marker; // the ST/Amiga counterpart to the IIgs asm marker. (Whole-tile
// the 8-row tile mark was ~a third of jlTileFill's cost. // marks bypass this entirely since NATIVE-PERF Phase 2 R1 --
// tile.c's tileMarkDirty unrolls its constant 8 rows inline.)
//
// Loop shape is PER-PORT, decided by measurement (Phase 2 Step C,
// 2026-07-07): the x2 unroll lifted Amiga sprite gate rows +4-6%
// but cost the ST -5% in BOTH the pointer-walking and counted
// forms (gcc-mint materializes four live pointers either way),
// and the simple loop re-measured exactly at the ST baseline.
// DOS was flat either way, so only the Amiga takes the unroll.
minPtr = &gStageMinWord[y]; minPtr = &gStageMinWord[y];
maxPtr = &gStageMaxWord[y]; maxPtr = &gStageMaxWord[y];
#if defined(JOEYLIB_PLATFORM_AMIGA)
{
uint16_t rows;
uint16_t pairs;
rows = (uint16_t)(yEnd - y);
pairs = (uint16_t)(rows >> 1);
while (pairs != 0u) {
if (minWord < minPtr[0]) {
minPtr[0] = minWord;
}
if (maxWord > maxPtr[0]) {
maxPtr[0] = maxWord;
}
if (minWord < minPtr[1]) {
minPtr[1] = minWord;
}
if (maxWord > maxPtr[1]) {
maxPtr[1] = maxWord;
}
minPtr += 2;
maxPtr += 2;
pairs--;
}
if ((rows & 1u) != 0u) {
if (minWord < *minPtr) {
*minPtr = minWord;
}
if (maxWord > *maxPtr) {
*maxPtr = maxWord;
}
}
}
#else
{
uint8_t *minEnd;
minEnd = &gStageMinWord[yEnd]; minEnd = &gStageMinWord[yEnd];
while (minPtr < minEnd) { while (minPtr < minEnd) {
if (minWord < *minPtr) { if (minWord < *minPtr) {
@ -255,6 +299,8 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma
minPtr++; minPtr++;
maxPtr++; maxPtr++;
} }
}
#endif
} }
#endif #endif

View file

@ -29,7 +29,59 @@
#endif #endif
#define TILE_BLOCK_TO_PIXEL(_b) ((uint16_t)((uint16_t)(_b) << 3)) #define TILE_BLOCK_TO_PIXEL(_b) ((uint16_t)((uint16_t)(_b) << 3))
// Prototype (helper below is the only static in this TU). // Whole-tile dirty mark (NATIVE-PERF Phase 2 R1). Every whole-tile op
// marks the compile-time-constant rect h = TILE_PIXELS_PER_SIDE with
// word band exactly [bx*2, bx*2 + 1]: 8 px per tile over 4 px per
// dirty word means SURFACE_WORD_INDEX(bx*8) == bx*2 and
// SURFACE_WORD_INDEX(bx*8 + 7) == bx*2 + 1. Deriving the band from bx
// and widening the 8 rows inline replaces the out-of-line
// surfaceMarkDirtyRows call (~800 cycles measured steady-state on the
// 68000 ports). Callers own the `s == gStage` gate (non-stage
// surfaces carry no dirty state), exactly like surfaceMarkDirtyRect.
// The IIgs keeps its single-JSL asm marker -- already the cheap path
// there.
//
// Compile-time proof of the band identity (breaks the build if either
// the tile side or the 4-px-per-word grain ever changes):
typedef char tileMarkBandProof[(SURFACE_WORD_INDEX(TILE_PIXELS_PER_SIDE) == 2) ? 1 : -1];
#ifdef JOEYLIB_PLATFORM_IIGS
#define tileMarkDirty(_pixelY, _bx) iigsMarkDirtyRowsInner((uint16_t)(_pixelY), (uint16_t)((_pixelY) + TILE_PIXELS_PER_SIDE), (uint16_t)((uint16_t)(_bx) << 1), (uint16_t)(((uint16_t)(_bx) << 1) + 1u))
#else
// Explicit 8-row unroll: gcc-6.5 -O2 keeps the equivalent loop rolled
// (8 iterations of compare + spill-reload, ~670 cycles); the d16(An)
// unroll is ~450. Same widen semantics, row by row, as the loop.
#define TILE_MARK_ROW(_i) do { \
if (minWord < minPtr[_i]) { \
minPtr[_i] = minWord; \
} \
if (maxWord > maxPtr[_i]) { \
maxPtr[_i] = maxWord; \
} \
} while (0)
static void tileMarkDirty(uint16_t pixelY, uint8_t bx) {
uint8_t *minPtr;
uint8_t *maxPtr;
uint8_t minWord;
uint8_t maxWord;
minWord = (uint8_t)(bx << 1);
maxWord = (uint8_t)(minWord + 1u);
minPtr = &gStageMinWord[pixelY];
maxPtr = &gStageMaxWord[pixelY];
TILE_MARK_ROW(0);
TILE_MARK_ROW(1);
TILE_MARK_ROW(2);
TILE_MARK_ROW(3);
TILE_MARK_ROW(4);
TILE_MARK_ROW(5);
TILE_MARK_ROW(6);
TILE_MARK_ROW(7);
}
#endif
// Prototype (drawTextUnion is the only other static in this TU).
static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds); static void drawTextUnion(uint8_t cx, uint8_t cy, uint8_t *bounds);
@ -136,9 +188,6 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlSurfaceT *fontS
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) { void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy) {
uint16_t dstPixelX;
uint16_t dstPixelY;
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
} }
@ -146,19 +195,15 @@ void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) { srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return; return;
} }
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
// Single op: machine asm/planar override or the generic chunky default. // Single op: machine asm/planar override or the generic chunky default.
jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy); jlpTileCopy(dst, dstBx, dstBy, src, srcBx, srcBy);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, if (dst == gStage) {
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); tileMarkDirty(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
}
} }
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) { void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy, uint8_t transparentIndex) {
uint16_t dstPixelX;
uint16_t dstPixelY;
if (dst == NULL || src == NULL) { if (dst == NULL || src == NULL) {
return; return;
} }
@ -166,57 +211,45 @@ void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy, const jlSur
srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) { srcBx >= TILE_BLOCKS_PER_ROW || srcBy >= TILE_BLOCKS_PER_COL) {
return; return;
} }
dstPixelX = TILE_BLOCK_TO_PIXEL(dstBx);
dstPixelY = TILE_BLOCK_TO_PIXEL(dstBy);
jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex); jlpTileCopyMasked(dst, dstBx, dstBy, src, srcBx, srcBy, transparentIndex);
surfaceMarkDirtyRect(dst, (int16_t)dstPixelX, (int16_t)dstPixelY, if (dst == gStage) {
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); tileMarkDirty(TILE_BLOCK_TO_PIXEL(dstBy), dstBx);
}
} }
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) { void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
uint16_t pixelX;
uint16_t pixelY;
if (s == NULL) { if (s == NULL) {
return; return;
} }
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return; return;
} }
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single compile-time-selected op: machine asm/planar override or the // Single compile-time-selected op: machine asm/planar override or the
// generic chunky default. // generic chunky default.
jlpTileFill(s, bx, by, colorIndex); jlpTileFill(s, bx, by, colorIndex);
surfaceMarkDirtyRect(s, (int16_t)pixelX, (int16_t)pixelY, if (s == gStage) {
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx);
}
} }
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) { void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
uint16_t pixelX;
uint16_t pixelY;
if (dst == NULL || in == NULL) { if (dst == NULL || in == NULL) {
return; return;
} }
if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) { if (bx >= TILE_BLOCKS_PER_ROW || by >= TILE_BLOCKS_PER_COL) {
return; return;
} }
pixelX = TILE_BLOCK_TO_PIXEL(bx);
pixelY = TILE_BLOCK_TO_PIXEL(by);
// Single op: machine asm/planar override or the generic chunky default. // Single op: machine asm/planar override or the generic chunky default.
jlpTilePaste(dst, bx, by, &in->pixels[0]); jlpTilePaste(dst, bx, by, &in->pixels[0]);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY, if (dst == gStage) {
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx);
}
} }
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in, uint8_t fgColor, uint8_t bgColor) { 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) { if (dst == NULL || in == NULL) {
return; return;
} }
@ -225,14 +258,13 @@ void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in,
} }
fgColor &= 0x0Fu; fgColor &= 0x0Fu;
bgColor &= 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 // Single compile-time op: planar ports colorize in their own tile format
// (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste. // (Amiga/ST); chunky ports colorize into a chunky tile then jlpTilePaste.
// Replaces the old runtime pixels==NULL planar/chunky branch. // Replaces the old runtime pixels==NULL planar/chunky branch.
jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor); jlpTilePasteMono(dst, bx, by, &in->pixels[0], fgColor, bgColor);
surfaceMarkDirtyRect(dst, (int16_t)pixelX, (int16_t)pixelY, if (dst == gStage) {
TILE_PIXELS_PER_SIDE, TILE_PIXELS_PER_SIDE); tileMarkDirty(TILE_BLOCK_TO_PIXEL(by), bx);
}
} }

View file

@ -160,9 +160,3 @@ void jlpGenericSurfaceFreePortData(jlSurfaceT *s, bool isStage, void *portData)
} }
// Bitplane base pointer: NULL on chunky ports (no planes); planar ports override.
uint8_t *jlpGenericSurfacePlanePtr(const jlSurfaceT *s, uint8_t planeIdx) {
(void)s;
(void)planeIdx;
return NULL;
}

View file

@ -27,10 +27,12 @@
// move.b d0,disp(a0) // move.b d0,disp(a0)
// matching stSpriteDrawByteAligned in src/atarist/hal.c bit-for-bit. // matching stSpriteDrawByteAligned in src/atarist/hal.c bit-for-bit.
// //
// Only shifts 0 (x mod 16 == 0) and 1 (x mod 16 == 8) are byte-aligned // The shift argument is the full intra-group phase x & 15 (16 slots on
// and compiled; other shifts return 0 so jlSpriteCompile records // the ST; see spriteInternal.h). Only the byte-aligned phases 0
// SPRITE_NOT_COMPILED and the dispatcher routes non-aligned x through // (x mod 16 == 0) and 8 (x mod 16 == 8) are compiled today; the other
// the interpreted jlpSpriteDrawPlanes walker. // 14 phases return 0 so jlSpriteCompile records SPRITE_NOT_COMPILED
// and the dispatcher routes those x through the interpreted
// jlpSpriteDrawPlanes walker (until NATIVE-PERF Phase 1 fills them in).
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -68,6 +70,35 @@
// Emitted bytes for the trailing rts. // Emitted bytes for the trailing rts.
#define EPILOGUE_BYTES 2u #define EPILOGUE_BYTES 2u
// Shifted-phase thunk opcodes (see emitShiftedDraw):
// movea.l 4(sp),a0 : 206F 0004 (groupBase -- unchanged dispatcher ABI)
// lea 16(pc),a1 : 43FA 0010 (data table at thunk+22; ext word at +6)
// move.w #imm,d0 : 303C rrrr (rows - 1)
// move.w #imm,d1 : 323C gggg (groupsPerRow - 1)
// jmp abs.l : 4EF9 hhhh llll (stSpriteDrawShiftWalker)
#define OP_LEA_16PC_A1 0x43FAu
#define LEA_PC_DISP 0x0010u
#define OP_MOVE_W_IMM_D0 0x303Cu
#define OP_MOVE_W_IMM_D1 0x323Cu
#define OP_JMP_ABSL 0x4EF9u
#define SHIFT_THUNK_BYTES 22u
#define SHIFT_GROUP_DATA_BYTES 12u // notMask long + bits01 long + bits23 long
// The shared shifted-phase walker (src/atarist/spriteWalk.s). Its
// absolute address is baked into each thunk's jmp at emit time --
// emission happens at runtime (post-TOS-relocation), and arena moves
// via codegenArenaCompact relocate only the thunk+data, whose one
// internal reference (lea 16(pc)) is PC-relative.
extern void stSpriteDrawShiftWalker(void);
// Per-row widened accumulators for emitShiftedDraw. File statics, NOT
// stack locals: the compile path runs deep and big frames overflowed
// the Amiga shell's default 4 KB task stack (same class on ST/TOS).
// Emit-time only and single-threaded, so statics are safe.
static uint8_t gStM[SURFACE_WIDTH / 8 + 1];
static uint8_t gStBp[4][SURFACE_WIDTH / 8 + 1];
// ----- Emit helpers ----- // ----- Emit helpers -----
@ -76,6 +107,123 @@
// are shared with the Amiga emitter via spriteEmitC2p.h. // are shared with the Amiga emitter via spriteEmitC2p.h.
// ----- Emit helpers (shifted phases) -----
// Emit one sub-byte phase (shift & 7 != 0): a 22-byte thunk that tail-
// jumps into the shared stSpriteDrawShiftWalker, followed by the
// phase's pre-shifted data table (SHIFT_GROUP_DATA_BYTES per group per
// row). The table is built with the SAME c2pColumn walk the aligned
// JIT uses; a source column's bits split across two adjacent widened
// byte columns (>> s into col c, << 8-s into col c+1, disjoint at the
// seam), and byte columns pack into 16-px groups two at a time with
// the intra-group half phase h = shift >> 3 offsetting the mapping.
// The walker's masked movem RMW of a group composes to exactly the
// interpreted walker's per-byte RMWs, so pixels are bit-identical.
static uint32_t emitShiftedDraw(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor;
uint32_t walker;
uint16_t wTiles;
uint16_t srcH;
uint16_t groupsPerRow;
uint16_t rowBytes;
uint8_t s;
uint8_t h;
int16_t row;
int16_t col;
int16_t g;
uint8_t p;
// The minimal on-surface x for this phase is `shift` itself, so a
// phase the gate can never dispatch gets no dead table bytes.
if ((uint16_t)shift + sp->widthPx > SURFACE_WIDTH) {
return 0u;
}
s = (uint8_t)(shift & 7u);
h = (uint8_t)(shift >> 3);
wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
groupsPerRow = (uint16_t)((uint16_t)((h + wTiles) >> 1) + 1u);
rowBytes = (uint16_t)(groupsPerRow * SHIFT_GROUP_DATA_BYTES);
cursor = 0u;
if (cursor + SHIFT_THUNK_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
walker = (uint32_t)stSpriteDrawShiftWalker;
cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
cursor = putWord(out, cursor, 0x0004u);
cursor = putWord(out, cursor, OP_LEA_16PC_A1);
cursor = putWord(out, cursor, LEA_PC_DISP);
cursor = putWord(out, cursor, OP_MOVE_W_IMM_D0);
cursor = putWord(out, cursor, (uint16_t)(srcH - 1u));
cursor = putWord(out, cursor, OP_MOVE_W_IMM_D1);
cursor = putWord(out, cursor, (uint16_t)(groupsPerRow - 1u));
cursor = putWord(out, cursor, OP_JMP_ABSL);
cursor = putWord(out, cursor, (uint16_t)(walker >> 16));
cursor = putWord(out, cursor, (uint16_t)(walker & 0xFFFFu));
for (row = 0; row < (int16_t)srcH; row++) {
uint16_t tileY = (uint16_t)((uint16_t)row >> 3);
uint16_t inTileY = (uint16_t)((uint16_t)row & 7u);
const uint8_t *tileRowBase = sp->tileData
+ (uint32_t)tileY * wTiles * TILE_BYTES
+ (uint32_t)inTileY * TILE_BYTES_PER_ROW;
if (cursor + rowBytes > cap) {
return SPRITE_EMIT_OVERFLOW;
}
// Widened per-byte-column mask/data for this row: source column
// col contributes >> s to widened col `col` and << (8-s) to
// widened col `col + 1` (indices relative to the sprite's first
// written byte column, which sits at absolute byte column h).
for (col = 0; col <= (int16_t)wTiles; col++) {
gStM[col] = 0u;
gStBp[0][col] = 0u;
gStBp[1][col] = 0u;
gStBp[2][col] = 0u;
gStBp[3][col] = 0u;
}
for (col = 0; col < (int16_t)wTiles; col++) {
const uint8_t *trp = tileRowBase + (uint32_t)col * TILE_BYTES;
uint8_t plane[4];
uint8_t opaque;
c2pColumn(trp, plane, &opaque);
if (opaque == 0u) {
continue;
}
gStM[col] = (uint8_t)(gStM[col] | (uint8_t)(opaque >> s));
gStM[col + 1] = (uint8_t)(gStM[col + 1] | (uint8_t)(opaque << (8u - s)));
for (p = 0u; p < 4u; p++) {
gStBp[p][col] = (uint8_t)(gStBp[p][col] | (uint8_t)(plane[p] >> s));
gStBp[p][col + 1] = (uint8_t)(gStBp[p][col + 1] | (uint8_t)(plane[p] << (8u - s)));
}
}
// Pack widened columns into group records. Group g's high byte
// is absolute column 2g, low byte 2g+1; absolute column a maps
// to widened index a - h (outside [0, wTiles] contributes 0).
for (g = 0; g < (int16_t)groupsPerRow; g++) {
int16_t hiJ = (int16_t)(2 * g - (int16_t)h);
int16_t loJ = (int16_t)(hiJ + 1);
uint8_t mHi = (hiJ >= 0 && hiJ <= (int16_t)wTiles) ? gStM[hiJ] : 0u;
uint8_t mLo = (loJ >= 0 && loJ <= (int16_t)wTiles) ? gStM[loJ] : 0u;
uint16_t m16 = (uint16_t)(((uint16_t)mHi << 8) | mLo);
uint16_t nm16 = (uint16_t)~m16;
cursor = putWord(out, cursor, nm16);
cursor = putWord(out, cursor, nm16);
for (p = 0u; p < 4u; p++) {
uint8_t dHi = (hiJ >= 0 && hiJ <= (int16_t)wTiles) ? gStBp[p][hiJ] : 0u;
uint8_t dLo = (loJ >= 0 && loJ <= (int16_t)wTiles) ? gStBp[p][loJ] : 0u;
cursor = putWord(out, cursor, (uint16_t)(((uint16_t)dHi << 8) | dLo));
}
}
}
return cursor;
}
// ----- Emit API ----- // ----- Emit API -----
uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
@ -86,12 +234,17 @@ uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSprite
int16_t row; int16_t row;
int16_t tileCol; int16_t tileCol;
if (shift > 1u) { // Shift is the full intra-group phase x & 15 (spriteInternal.h).
return 0u; // The byte-aligned phases 0 (x%16==0, high byte halves) and 8
// (x%16==8, low byte halves) emit the unrolled byte-RMW JIT below;
// the 14 sub-byte phases emit a thunk + pre-shifted data table
// driven by the shared stSpriteDrawShiftWalker.
if ((shift & 7u) != 0u) {
return emitShiftedDraw(out, cap, sp, shift);
} }
wTiles = sp->widthTiles; wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE); srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
xMod16 = (uint16_t)(shift * 8u); // shift 0 -> 0, shift 1 -> 8 xMod16 = shift; // phase 0 -> 0, phase 8 -> 8
cursor = 0u; cursor = 0u;
@ -188,17 +341,84 @@ uint32_t spriteEmitDrawInterleaved68k(uint8_t *out, uint32_t cap, const jlSprite
} }
// Save/restore are not compiled on the ST yet -- the interpreted // Compiled save/restore (NATIVE-PERF Phase 1): raw-span copiers over
// jlpSpriteSavePlanes / jlpSpriteRestorePlanes path already meets the // the 16-px-group window. A group window's rows are CONTIGUOUS spans
// IIgs perf floor (plane byte copies), so returning 0 routes them // of interleaved bytes, so each row is a pure move.l chain; the shift
// there. The entry points exist only for the dispatcher's uniform API. // argument is the width CLASS (0 = ceil(w/16) groups, 1 = one more --
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { // see SPRITE_SAVE_CLASS in spriteInternal.h), and only classes 0/1
(void)out; (void)cap; (void)sp; (void)shift; // emit. cdecl void copy(const uint8_t *src, uint8_t *dst): SAVE gets
// (screenRowStart, buf) -- a0 strided, a1 contiguous; RESTORE gets
// (buf, screenRowStart) -- a0 contiguous, a1 strided. Byte format is
// identical to jlpSpriteSavePlanes' raw-span branch (format law:
// pure function of the recorded geometry).
// movea.l 4(sp),a0 : 206F 0004 movea.l 8(sp),a1 : 226F 0008
// move.l (a0)+,(a1)+ : 22D8
// lea d16(a0),a0 : 41E8 dddd lea d16(a1),a1 : 43E9 dddd
#define OP_MOVEA_8SP_A1 0x226Fu
#define OP_MOVE_L_POSTI 0x22D8u
#define OP_LEA_D16_A0 0x41E8u
#define OP_LEA_D16_A1 0x43E9u
static uint32_t emitGroupSpanCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool strideOnDst) {
uint32_t cursor;
uint16_t nGroups;
uint16_t spanBytes;
uint16_t longsPerRow;
uint16_t strideFix;
uint16_t srcH;
int16_t row;
uint16_t i;
if (shift > 1u) {
return 0u; return 0u;
}
nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift);
if ((uint16_t)(nGroups << 4) > SURFACE_WIDTH) {
// Degenerate: the widened window would exceed the surface, so
// this class is unreachable through the on-surface gate. Both
// save and restore bail identically (pair provenance).
return 0u;
}
spanBytes = (uint16_t)(nGroups << 3);
longsPerRow = (uint16_t)(spanBytes >> 2);
strideFix = (uint16_t)(ST_PLANAR_STRIDE - spanBytes);
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
cursor = 0u;
if (cursor + 8u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_MOVEA_4SP_A0);
cursor = putWord(out, cursor, 0x0004u);
cursor = putWord(out, cursor, OP_MOVEA_8SP_A1);
cursor = putWord(out, cursor, 0x0008u);
for (row = 0; row < (int16_t)srcH; row++) {
if (cursor + (uint32_t)(longsPerRow << 1) + 4u + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
for (i = 0u; i < longsPerRow; i++) {
cursor = putWord(out, cursor, OP_MOVE_L_POSTI);
}
if (row < (int16_t)(srcH - 1u)) {
// Advance the strided (screen) side to the next row; the
// buffer side is contiguous and needs no fix.
cursor = putWord(out, cursor, strideOnDst ? OP_LEA_D16_A1 : OP_LEA_D16_A0);
cursor = putWord(out, cursor, strideFix);
}
}
if (cursor + EPILOGUE_BYTES > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_RTS);
return cursor;
}
uint32_t spriteEmitSaveInterleaved68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
return emitGroupSpanCopy(out, cap, sp, shift, false);
} }
uint32_t spriteEmitRestoreInterleaved68k(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) {
(void)out; (void)cap; (void)sp; (void)shift; return emitGroupSpanCopy(out, cap, sp, shift, true);
return 0u;
} }

View file

@ -18,16 +18,21 @@
// signed 16-bit displacement range for any on-surface sprite). Only // signed 16-bit displacement range for any on-surface sprite). Only
// d0/a0/a1 are cdecl scratch; a2/a3 are saved/restored via movem. // d0/a0/a1 are cdecl scratch; a2/a3 are saved/restored via movem.
// //
// ONLY shift 0 (x mod 8 == 0, byte-aligned) is compiled. For shift 0 a // ALL 8 shifts (x mod 8) are compiled (NATIVE-PERF Phase 1). At shift 0
// sprite tile column maps to exactly one plane byte (no sub-byte spill), // a sprite tile column maps to exactly one plane byte; at shift s > 0 a
// so the per-plane work mirrors the ST byte path: // column's bits split across two adjacent dest bytes (low 8-s bits of
// fully opaque (mask==0xFF): move.b #plane[p],off(a_i) (direct store) // the left byte, high s bits of the right byte), and the seam byte
// mixed: move.b off(a_i),d0; andi.b #~mask,d0; ori.b #plane[p],d0; // between two source columns receives disjoint contributions from both.
// The emitter pre-shifts and OR-merges those contributions per dest
// byte at emit time (destMask/destData accumulators), then emits one
// write group per non-empty dest byte -- the same opcode inventory the
// proven shift-0 path used:
// fully opaque (mask==0xFF): move.b #data,off(a_i) (direct store)
// mixed: move.b off(a_i),d0; andi.b #~mask,d0; [ori.b #data,d0;]
// move.b d0,off(a_i) // move.b d0,off(a_i)
// matching jlpSpriteDrawPlanes in src/amiga/hal.c for the shift-0 case. // This is algebraically identical to jlpSpriteDrawPlanes' two sequential
// Shifts 1..7 return 0 so jlSpriteCompile records SPRITE_NOT_COMPILED // seam RMWs (disjoint masks compose), so pixels are bit-for-bit the
// and the dispatcher routes non-aligned x through the interpreted // walker's -- the frozen unaligned/sweep16 golden hashes gate it.
// jlpSpriteDrawPlanes walker (which handles the two-byte sub-byte spill).
#include "joey/sprite.h" #include "joey/sprite.h"
#include "joey/surface.h" #include "joey/surface.h"
@ -84,19 +89,39 @@ static const uint16_t OP_MOVE_B_IMM_AN[AMIGA_PLANES] = { 0x117Cu, 0x137Cu, 0x157
// ----- Emit API ----- // ----- Emit API -----
// Per-row accumulators. File statics, NOT stack locals: the compile
// path runs deep (jlSpriteCompile -> planBuild -> measure -> emitter,
// with vsnprintf-depth logging possible underneath) and a ~200-byte
// stack frame here overflowed the Amiga shell's default 4 KB task
// stack nondeterministically. Emit-time only and single-threaded, so
// statics are safe.
static uint8_t gDestMask[SURFACE_WIDTH / 8 + 1];
static uint8_t gDestData[SURFACE_WIDTH / 8 + 1][AMIGA_PLANES];
uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
uint32_t cursor; uint32_t cursor;
uint16_t wTiles; uint16_t wTiles;
uint16_t srcH; uint16_t srcH;
uint16_t lastDest;
uint8_t i; uint8_t i;
int16_t row; int16_t row;
int16_t byteCol; int16_t byteCol;
int16_t d;
if (shift != 0u) { if (shift >= 8u) {
return 0u; // Amiga shift space is x & 7; defensive.
}
if (shift != 0u && sp->widthPx >= SURFACE_WIDTH) {
// A full-width sprite can never draw unaligned on-surface (the
// dispatcher gate requires x + w <= 320 with x > 0), so a
// shifted variant would be dead bytes in the arena.
return 0u; return 0u;
} }
wTiles = sp->widthTiles; wTiles = sp->widthTiles;
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE); srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
// Shift 0 writes wTiles dest bytes per row; shifted rows spill one.
lastDest = (shift == 0u) ? (uint16_t)(wTiles - 1u) : wTiles;
cursor = 0u; cursor = 0u;
@ -119,33 +144,68 @@ uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp
+ (uint32_t)inTileY * TILE_BYTES_PER_ROW; + (uint32_t)inTileY * TILE_BYTES_PER_ROW;
uint16_t rowOff = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE); uint16_t rowOff = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE);
// Phase 1: accumulate this row's per-dest-byte mask/data. A
// source column at shift s contributes its low 8-s bits to dest
// byte c (>> s) and its high s bits to dest byte c+1 (<< 8-s);
// the seam byte's two contributions occupy disjoint bit ranges,
// so OR-accumulation reproduces the walker's two sequential
// RMWs exactly.
for (d = 0; d <= (int16_t)lastDest; d++) {
gDestMask[d] = 0u;
gDestData[d][0] = 0u;
gDestData[d][1] = 0u;
gDestData[d][2] = 0u;
gDestData[d][3] = 0u;
}
for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) { for (byteCol = 0; byteCol < (int16_t)wTiles; byteCol++) {
const uint8_t *trp = tileRowBase + (uint32_t)byteCol * TILE_BYTES; const uint8_t *trp = tileRowBase + (uint32_t)byteCol * TILE_BYTES;
uint8_t plane[4]; uint8_t plane[4];
uint8_t opaque; uint8_t opaque;
uint16_t off;
c2pColumn(trp, plane, &opaque); c2pColumn(trp, plane, &opaque);
if (opaque == 0u) { if (opaque == 0u) {
continue; continue;
} }
off = (uint16_t)(rowOff + (uint16_t)byteCol); if (shift == 0u) {
gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | opaque);
for (i = 0u; i < AMIGA_PLANES; i++) {
gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | plane[i]);
}
} else {
gDestMask[byteCol] = (uint8_t)(gDestMask[byteCol] | (uint8_t)(opaque >> shift));
gDestMask[byteCol + 1] = (uint8_t)(gDestMask[byteCol + 1] | (uint8_t)(opaque << (8u - shift)));
for (i = 0u; i < AMIGA_PLANES; i++) {
gDestData[byteCol][i] = (uint8_t)(gDestData[byteCol][i] | (uint8_t)(plane[i] >> shift));
gDestData[byteCol + 1][i] = (uint8_t)(gDestData[byteCol + 1][i] | (uint8_t)(plane[i] << (8u - shift)));
}
}
}
// Phase 2: emit one write group per non-empty dest byte.
for (d = 0; d <= (int16_t)lastDest; d++) {
uint8_t m = gDestMask[d];
uint16_t off;
if (m == 0u) {
continue;
}
off = (uint16_t)(rowOff + (uint16_t)d);
for (i = 0u; i < AMIGA_PLANES; i++) { for (i = 0u; i < AMIGA_PLANES; i++) {
if (opaque == 0xFFu) { if (m == 0xFFu) {
// Fully opaque column: store the plane byte directly. // Fully covered dest byte: store the data directly.
// move.b #plane[i],off(a_i) (6 bytes) // move.b #data,off(a_i) (6 bytes)
if (cursor + 6u > cap) { if (cursor + 6u > cap) {
return SPRITE_EMIT_OVERFLOW; return SPRITE_EMIT_OVERFLOW;
} }
cursor = putWord(out, cursor, OP_MOVE_B_IMM_AN[i]); cursor = putWord(out, cursor, OP_MOVE_B_IMM_AN[i]);
cursor = putWord(out, cursor, (uint16_t)plane[i]); cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]);
cursor = putWord(out, cursor, off); cursor = putWord(out, cursor, off);
} else { } else {
// Exact reservation (see ST emitter): a worst-case reserve // Exact reservation (see ST emitter): a worst-case reserve
// would falsely overflow the emit pass on trailing shorter // would falsely overflow the emit pass on trailing shorter
// writes. +ori.b only when this plane sets bits. // writes. +ori.b only when this plane sets bits.
uint32_t planeBytes = (plane[i] != 0u) ? 16u : 12u; uint32_t planeBytes = (gDestData[d][i] != 0u) ? 16u : 12u;
if (cursor + planeBytes > cap) { if (cursor + planeBytes > cap) {
return SPRITE_EMIT_OVERFLOW; return SPRITE_EMIT_OVERFLOW;
@ -153,13 +213,13 @@ uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp
// move.b off(a_i),d0 // move.b off(a_i),d0
cursor = putWord(out, cursor, OP_MOVE_B_AN_D0[i]); cursor = putWord(out, cursor, OP_MOVE_B_AN_D0[i]);
cursor = putWord(out, cursor, off); cursor = putWord(out, cursor, off);
// andi.b #~opaque,d0 // andi.b #~m,d0
cursor = putWord(out, cursor, OP_ANDI_B_D0); cursor = putWord(out, cursor, OP_ANDI_B_D0);
cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~opaque)); cursor = putWord(out, cursor, (uint16_t)(uint8_t)(~m));
// ori.b #plane[i],d0 (skip if no bits) // ori.b #data,d0 (skip if no bits)
if (plane[i] != 0u) { if (gDestData[d][i] != 0u) {
cursor = putWord(out, cursor, OP_ORI_B_D0); cursor = putWord(out, cursor, OP_ORI_B_D0);
cursor = putWord(out, cursor, (uint16_t)plane[i]); cursor = putWord(out, cursor, (uint16_t)gDestData[d][i]);
} }
// move.b d0,off(a_i) // move.b d0,off(a_i)
cursor = putWord(out, cursor, OP_MOVE_B_D0_AN[i]); cursor = putWord(out, cursor, OP_MOVE_B_D0_AN[i]);
@ -180,16 +240,106 @@ uint32_t spriteEmitDrawPlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp
} }
// Save/restore are not compiled on the Amiga yet -- the interpreted // Compiled save/restore (NATIVE-PERF Phase 1): plane-run copiers over
// jlpSpriteSavePlanes / jlpSpriteRestorePlanes path handles them. The // the 16-px-group window. shift is the width CLASS (0 = ceil(w/16)
// entry points exist only for the dispatcher's uniform API. // groups, 1 = one more; see SPRITE_SAVE_CLASS), so only classes 0/1
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) { // emit. cdecl void copy(p0, p1, p2, p3, buf): the dispatcher passes
(void)out; (void)cap; (void)sp; (void)shift; // each plane base already advanced to the window's first byte
// (y*40 + bx/8, even because bx % 16 == 0 and planes are AllocMem'd),
// and the routine copies run = nGroups*2 bytes per plane per row in
// [p0 run][p1 run][p2 run][p3 run] order -- byte-identical to the
// interpreted aligned walker's plane-run format (format law). Row
// displacements are baked (max 199*40 + 4 < d16 range); the buffer
// side walks contiguously via (a1)+.
// movem.l a2-a4,-(sp) : 48E7 0038 (predec mask bit-reversed)
// movem.l (sp)+,a2-a4 : 4CDF 1C00
// movea.l d16(sp),aN : a0=206F a2=246F a3=266F a4=286F a1=226F
// SAVE move.l d16(aP),(a1)+ : 22E8/22EA/22EB/22EC dddd
// SAVE move.w d16(aP),(a1)+ : 32E8/32EA/32EB/32EC dddd
// REST move.l (a1)+,d16(aP) : 2159/2559/2759/2959 dddd
// REST move.w (a1)+,d16(aP) : 3159/3559/3759/3959 dddd
#define OP_MOVEM_SAVE_A24 0x0038u
#define OP_MOVEM_REST_A24 0x1C00u
static const uint16_t OP_COPY_ARG_AN[5] = { 0x206Fu, 0x246Fu, 0x266Fu, 0x286Fu, 0x226Fu };
static const uint16_t OP_SAVE_L_AN[AMIGA_PLANES] = { 0x22E8u, 0x22EAu, 0x22EBu, 0x22ECu };
static const uint16_t OP_SAVE_W_AN[AMIGA_PLANES] = { 0x32E8u, 0x32EAu, 0x32EBu, 0x32ECu };
static const uint16_t OP_REST_L_AN[AMIGA_PLANES] = { 0x2159u, 0x2559u, 0x2759u, 0x2959u };
static const uint16_t OP_REST_W_AN[AMIGA_PLANES] = { 0x3159u, 0x3559u, 0x3759u, 0x3959u };
// Arg displacements after the 12-byte movem push (+4 return PC):
// p0/p1/p2/p3/buf at entry 4/8/12/16/20 -> 16/20/24/28/32.
#define COPY_ARG_DISP(i) (uint16_t)(16u + (uint16_t)(i) * 4u)
static uint32_t emitPlaneRunCopy(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift, bool isRestore) {
uint32_t cursor;
uint16_t nGroups;
uint16_t run;
uint16_t srcH;
int16_t row;
uint8_t p;
uint16_t i;
if (shift > 1u) {
return 0u; return 0u;
}
nGroups = (uint16_t)(((sp->widthPx + 15u) >> 4) + shift);
if ((uint16_t)(nGroups << 4) > SURFACE_WIDTH) {
// Unreachable class for this sprite width; both save and
// restore bail identically (pair provenance).
return 0u;
}
run = (uint16_t)(nGroups << 1); // bytes per plane per row
srcH = (uint16_t)(sp->heightTiles * TILE_PIXELS_PER_SIDE);
cursor = 0u;
if (cursor + 24u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_MOVEM_SAVE);
cursor = putWord(out, cursor, OP_MOVEM_SAVE_A24);
for (i = 0u; i < 5u; i++) {
cursor = putWord(out, cursor, OP_COPY_ARG_AN[i]);
cursor = putWord(out, cursor, COPY_ARG_DISP(i));
}
for (row = 0; row < (int16_t)srcH; row++) {
uint16_t rowDisp = (uint16_t)((uint16_t)row * AMIGA_PLANE_STRIDE);
// Worst case per row: 4 planes x (longs + one word) ops at
// 4 bytes each.
if (cursor + (uint32_t)(4u * ((run >> 2) + 1u) * 4u) + 6u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
for (p = 0u; p < AMIGA_PLANES; p++) {
uint16_t disp = rowDisp;
uint16_t longs = (uint16_t)(run >> 2);
for (i = 0u; i < longs; i++) {
cursor = putWord(out, cursor, isRestore ? OP_REST_L_AN[p] : OP_SAVE_L_AN[p]);
cursor = putWord(out, cursor, disp);
disp = (uint16_t)(disp + 4u);
}
if ((run & 2u) != 0u) {
cursor = putWord(out, cursor, isRestore ? OP_REST_W_AN[p] : OP_SAVE_W_AN[p]);
cursor = putWord(out, cursor, disp);
}
}
}
if (cursor + 6u > cap) {
return SPRITE_EMIT_OVERFLOW;
}
cursor = putWord(out, cursor, OP_MOVEM_REST);
cursor = putWord(out, cursor, OP_MOVEM_REST_A24);
cursor = putWord(out, cursor, OP_RTS);
return cursor;
}
uint32_t spriteEmitSavePlanar68k(uint8_t *out, uint32_t cap, const jlSpriteT *sp, uint8_t shift) {
return emitPlaneRunCopy(out, cap, sp, shift, false);
} }
uint32_t spriteEmitRestorePlanar68k(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) {
(void)out; (void)cap; (void)sp; (void)shift; return emitPlaneRunCopy(out, cap, sp, shift, true);
return 0u;
} }