Working on performance. Still.
This commit is contained in:
parent
24b88c3ba2
commit
813bf595bc
27 changed files with 1530 additions and 546 deletions
|
|
@ -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
216
PERF.md
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
44
src/amiga/amigaPlanar.h
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
51
src/atarist/spriteWalk.s
Normal 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
42
src/atarist/stPlanar.h
Normal 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
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
@ -257,6 +301,8 @@ void surfaceMarkDirtyRows(uint16_t y, uint16_t yEnd, uint8_t minWord, uint8_t ma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// ----- Internal (alphabetical) -----
|
// ----- Internal (alphabetical) -----
|
||||||
|
|
|
||||||
104
src/core/tile.c
104
src/core/tile.c
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue