More performance work.

This commit is contained in:
Scott Duensing 2026-07-07 18:31:29 -05:00
parent 3b4effb95a
commit 07f0e4f503
19 changed files with 1062 additions and 32 deletions

View file

@ -317,6 +317,90 @@ perf-directive memory with the outcome.
scratch, 13-20% of the op; + optional CopyMaskedMark fusion), R10
fused drawPixel+mark asm entry (~820 -> ~300 cyc), optional R7
saveAndDraw consolidation, optional R12 IIgs C-residue reshapes.
- [ ] Phase 3
- [ ] Phase 4
- [x] Phase 3 LANDED 2026-07-07 (goldens refrozen 49 -> 50 per the
Phase-0 procedure; final gate 50/50 x4). jlTileMapPaste in
include/joey/tile.h + tile.c: tileset + row-major uint8_t index map,
TILE_MAP_SKIP=0xFF, one validation + one dirty union (68k/DOS);
IIgs loop specialized (hoisted stage test, base + (idx<<5) tileset
indexing with a sizeof proof -- tiles[idx] cost a 32-bit multiply
per cell on the 65816 and initially made the batch SLOWER than the
loop). New golden'd row `jlTileMapPaste 10x5` (cross-identity
52B9646B x4). RESULTS tiles/s batch vs loop: ST 8,150 (+106%,
gap 1.6-1.9x) / Amiga 7,650 (+104%, gap 1.40x = INSIDE the 1.5x
target) / DOS 22,500 (+74%) / IIgs 3,000 (+2%; per-tile cost is
~93% inner JSL post-Phase-2 -- whole-map asm walker is Phase 4's
lever there). SPRITE BATCHES DESCOPED: gate measures only the
tilemap row and post-R5 sprite dispatch overhead is ~3-6%; re-add
only against a measured need + Scott's API sign-off. API shape
SIGNED OFF by Scott 2026-07-07 (tileset + row-major uint8_t map,
TILE_MAP_SKIP=0xFF, caller-owned index validity) -- the public
header contract in include/joey/tile.h is final.
- [x] Phase 4 COMPLETE 2026-07-07 (all four arms resolved; ST landed
below). IIgs WHOLE-MAP WALKER LANDED
(gate 50/50 + iigs-verify): iigsTileMapPasteMark in joeyDraw.s --
one JSL walks the index map with an incrementally-advanced dst
offset (ONE gRowOffsetLut read; +4/column, +1280/tile-row), a
22-byte stacked D-frame for the register-starved inner (map ptr,
tile-src ptr, bases, counters -- X survives the 16-store body and
carries the dst offset; Y is clobbered so the map index lives in
the frame), bank-carry-safe idx<<5 tileset add, brl on all three
wrapping branches, ONE band union tail. RESULT: IIgs jlTileMapPaste
10x5 60 -> 140 maps/s = 7,000 tiles/s (+133%), ~400 cyc/tile --
BELOW the 480-cyc single-tile hand-coded ceiling (the walker
amortizes what per-tile calls still pay). Batch row across ports:
ST 8,150 / Amiga 7,650 / IIgs 7,000 / DOS 22,500 tiles/s.
AMIGA BLITTER COOKIE-CUT SPRITE: recon complete, DEFERRED with the
design recorded -- the source side is net-new (sprite data is
fast-RAM chunky + CPU immediates; a chip-RAM pre-shift subsystem
is required), the synchronous per-call WaitBlit contract forfeits
the parallelism win below ~32x32 (all current UBER sprite rows are
16x16 = at/below the crossover), and the honest experiment needs a
new >=32x32 golden'd row measured with AND without the fence. Full
register program (BLTCON0 0x0F00|shift|0xCA cookie-cut, per-plane B,
shared A mask) + risk list in the recon (session 2026-07-07).
ST ARM LANDED 2026-07-07 (4-agent recon + adversary, then C-first
per the binding feedback): the "movem row engine" hypothesis is
DEAD -- the tile dst is a byte scatter movem cannot address, and
byte-pair word fusion loses on a barrel-shifterless 68000 (lsl.w #8
= 22 cyc > the move.b it replaces). The C incremental pair walker
(stTileMapPaste, stTile.h) measured +17% (adversary predicted the
+15-23% cap: gcc already hoists the row mulu; the 632-cyc copy body
dominates) -- that measured C ceiling justified asm, and the real
weapon is MOVEP: stTileMapPasteMovep (src/atarist/tileMap.s) does
move.l (a0)+ / movep.l d0,d16(a1) = one tile row's 4 plane bytes in
36 cyc. ST batch row 163 -> 346 ops/s = 17,300 tiles/s (2.12x, 462
cyc/tile), ABOVE the Phase-0 12,700-15,500 ceiling (it assumed
byte-move bodies). C walker stays as the odd-tileset-pointer
fallback (move.l needs an even src; jlTileT is align-1). Gate: ST
50/50 (52B9646B held) + 12-case on-emulator TILETEST proof of the
head/tail/w1/h1/skip/odd-ptr paths the golden never exercises, each
with border-ring containment. RESTORE-FROM-CLEAN-BUFFER: EXPERIMENT
RUN AND PASSED 2026-07-07 (Scott approved same day; sign-off also
ratified the jlTileMapPaste shape). jlSurfaceCopyRect is PUBLIC API
(include/joey/surface.h): same-position rect copy, clip + outward
16-px group snap UNIFORM on every port (uniform snap is what keeps
cross-port hashes identical), pixels only, dirty-marked via
surfaceMarkDirtyRect; generic chunky row-memcpy engine (DOS + both
IIgs flavors -- the IIgs stage IS plain-C writable through its
bank-$01 pixels pointer, adversary-verified), ST = strided
interleaved row memcpys, Amiga = per-plane CPU row copies (blitter
modulo variant recorded as its upgrade; CopyMemQuick unusable at
16-px grain). Documented caller hazards: snapped erase can touch
content up to 15 px outside the rect and invalidates overlapping
live sprite backups -- erase-all-then-draw-all, one mechanism per
region. STRATEGY row `gameFrameClean composite` golden'd (contract
50 -> 51, cross-identity C51FE171 x4): vs gameFrame ST 48/22 =
2.18x (bar was 1.4x) / DOS 195/19 = 10.3x / IIgs 18/10 = 1.8x /
Amiga 34/25 = 1.36x (least, as the adversary predicted: blitter
fillRect meant less repaint to delete + chip-chip copy vs fast-RAM
backup restore). A library-transparent variant stays REJECTED (the
ST present source is the composited shadow, sprites corrupt it
pre-present; a clean source means a third 32KB buffer + routing
API by another name). IIGS
PEI-SLAM: evaluated and REJECTED -- models ~+28% on the map walker
but holds SEI for the whole map (~5.6 ms at 10x5; audio ISR jitter)
and the VBL-driven bench clock cannot honestly measure SEI ops
(SEI-inflation artifact); the landed walker already beats the
480-cyc per-tile ceiling. Batch row after Phase 4: DOS 22,500 / ST
17,300 / Amiga 7,650 / IIgs 7,000 tiles/s.
- [ ] Phase 5

164
PERF.md
View file

@ -102,6 +102,110 @@ 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.
**2026-07-07, NATIVE-PERF PHASE 4 (IIgs arm) LANDED -- whole-map asm
walker:** iigsTileMapPasteMark (joeyDraw.s) replaces the IIgs
stage-arm per-tile loop of jlTileMapPaste with ONE JSL that walks the
index map: dst offset advances incrementally (+4/column, +1280/tile
row) from a single gRowOffsetLut read, the register-starved inner
lives in a 22-byte stacked D-frame (X survives the 16-store body and
carries the dst offset; the map index sits in the frame because Y is
clobbered), the idx << 5 tileset add carries into the bank byte, all
three wrapping branches are brl, and ONE band union closes the call.
Gated 50/50 + make iigs-verify. RESULT: IIgs batch row 60 -> 140
maps/s = 7,000 tiles/s (+133%), ~400 cyc/tile -- BELOW the 480-cyc
single-tile hand-coded ceiling, because the walker amortizes the
JSL/derive/mark that even a hand-coded per-tile call pays. Batch row
across all ports: ST 8,150 / Amiga 7,650 / IIgs 7,000 / DOS 22,500
tiles/sec (the morning's per-call loop numbers were 1,600 / 2,300 /
1,750 / 10,000). The Amiga blitter cookie-cut sprite was recon'd and
DEFERRED with a full design on record: the synchronous WaitBlit the
hash contract requires erases the parallelism win at <= 16x16 (every
current sprite row), and the chip-RAM pre-shifted source data is a
net-new subsystem -- the honest experiment is a >= 32x32 golden'd row
measured with and without the fence.
**2026-07-07, NATIVE-PERF PHASE 4 CLOSED -- clean-buffer experiment
PASSED, jlSurfaceCopyRect is public API:** Scott signed off the
jlTileMapPaste shape and approved the clean-buffer experiment; both
landed same-day. jlSurfaceCopyRect(dst, src, x, y, w, h) is a
SAME-POSITION rect copy (clip, then snap outward to the 16-px group
grid on every port -- uniform snap keeps cross-port hashes
byte-identical): core wrapper + dirty mark in surface.c, generic
chunky row-memcpy engine (serves DOS and both IIgs surface flavors --
the IIgs stage pixels pointer reaches bank $01 through ordinary
large-model stores, recon-verified against the sprite-restore
precedent), ST override = strided interleaved row memcpys (the
raw-span restore recipe), Amiga override = per-plane CPU row copies
(CopyMemQuick is unusable at 16-px grain: word alignment only;
the modulo-driven blitter variant is the recorded production
upgrade). The STRATEGY row `gameFrameClean composite` (erase 3
sprite windows from a pristine clean surface + 3 draws + present; NO
per-frame background repaint, NO save-under -- the static-background
pattern, not an op-for-op gameFrame speedup) measures, vs gameFrame:
**ST 48 vs 22 = 2.18x (acceptance bar was 1.4x), DOS 195 vs 19 =
10.3x, IIgs 18 vs 10 = 1.8x, Amiga 34 vs 25 = 1.36x** (Amiga gains
least, exactly as the adversarial recon predicted: its fillRect was
already blitter-fast, and clean->stage is a chip-to-chip CPU copy
against display DMA -- the blitter rect variant is its lever).
GOLDEN CONTRACT now **51 lines**: regression gate exactly 50 match +
1 extra per port, 4-port cross-identity of the new row (hash
C51FE171 x4), then freeze. Found along the way: `make all` does NOT
regenerate joey.2mg, so bench-iigs.sh silently benched the previous
UBER (clean 50/50, new row absent) until `make iigs-disk` -- recorded
in the stale-binary memory.
**2026-07-07, NATIVE-PERF PHASE 4 (ST arm) LANDED -- movep.l map
engine:** the ST jlTileMapPaste arm now dispatches to
stTileMapPasteMovep (src/atarist/tileMap.s): per tile row, one
`move.l (a0)+` + `movep.l d0,d16(a1)` writes all 4 interleaved plane
bytes in 36 cycles vs ~80 for the four compiled move.b -- movep IS
the half-group byte-scatter pattern (the classic ST c2p trick), where
movem is useless (byte-granular dst) and byte-pair word fusion loses
(lsl.w #8 costs 22 cyc on a barrel-shifterless 68000, priced during
recon). Cells walk in even/odd pairs sharing one group pointer
(+8/pair, all row offsets compile-time d16); odd bx = head cell, odd
width = tail cell. The C pair walker (stTileMapPaste in stTile.h,
+17% alone -- adversarial recon correctly predicted the pure-C cap at
+15-23% because gcc already hoists the mulu and the 632-cyc copy body
dominates) stays as the odd-tileset-pointer fallback: `move.l (a0)+`
needs an even source, jlTileT is align-1, and the guard is one AND
per call. RESULT: ST batch row 163 -> 346 ops/sec = **17,300
tiles/s** (2.12x, 462 cyc/tile) -- ABOVE the Phase-0 12,700-15,500
"hand-coded ceiling", which under-counted by assuming byte-move
bodies. Gate: ST 50/50 bit-identical (hash 52B9646B held through
both engines) + a 12-case on-emulator proof (TILETEST, session
scratchpad) covering odd-bx head, odd-width tail, w=1 columns, h=1,
checkerboard/end/all skips, and both odd-pointer fallback paths,
each with a border-ring containment check. Batch row across ports:
DOS 22,500 / ST 17,300 / Amiga 7,650 / IIgs 7,000 tiles/s. IIgs
PEI-slam map variant was EVALUATED and REJECTED: models ~+28%
(~312 cyc/tile) but requires SEI for the whole map (~5.6 ms
interrupts-off at 10x5, audio ISR jitter) AND the VBL-driven bench
clock cannot honestly measure SEI ops (PERF artifact: SEI inflation)
-- the walker already beats the 480-cyc per-tile ceiling without it.
**2026-07-07, NATIVE-PERF PHASE 3 LANDED -- jlTileMapPaste batch API:**
one public entry pastes a wTiles x hTiles rectangle from a tileset +
row-major uint8_t index map (TILE_MAP_SKIP = 0xFF leaves a cell
untouched): one validation, one dirty union (68k/DOS; the IIgs
stage-fused entries mark per pasted tile, and its loop hoists the
stage test + does tileset indexing as base + (idx << 5) -- the
natural tiles[idx] spelling cost a 32-bit indexed multiply per cell
on the 65816 and erased the batch win until specialized). GOLDEN
CONTRACT now **50 lines**: refreeze followed the Phase-0 procedure
(regression exactly 49 match + 1 extra per port, then 4-port
cross-identity of the new `jlTileMapPaste 10x5` row -- all four hash
52B9646B -- then freeze). RESULTS, tiles/sec batch vs per-call loop:
ST 8,150 vs 4,000 (+106%, gap-to-ceiling 1.6-1.9x); Amiga 7,650 vs
3,750 (+104%, gap 1.40x -- INSIDE the ~1.5x program target); DOS
22,500 vs 12,900 (+74%, 2.5x); IIgs 3,000 vs 2,950 (+2% -- its
Phase-2 fused entries already deleted the per-tile wrapper, so the
remaining cost is ~93% inner JSL; a whole-map IIgs asm walker is the
Phase-4 lever). Sprite batch APIs were DESCOPED from Phase 3: the
phase gate measures only the tilemap row, and post-R5 sprite dispatch
overhead is ~3-6% -- not worth new public surface without a measured
need.
**2026-07-07, NATIVE-PERF PHASE 2 COMPLETE -- Steps D3 (R6 + R10)
close the phase:** (R6) the IIgs masked-copy inner's 4 x 8 jsr/rts
round-trips to tmaskByte were inlined via the TMASK_BYTE assembler
@ -244,49 +348,53 @@ meet or beat it -- project directive). Non-IIgs cells show absolute
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
as below-floor. `tools/uber-perf-table` emits this table directly;
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
default cuts off the final checks). Capture provenance: ST 2026-07-07
(post-audio-fix); IIgs/Amiga/DOS 2026-07-06 (recovery-gate rerun,
unaffected by the ST fix).
default cuts off the final checks). Capture provenance: all four
ports 2026-07-07, the 51-line golden freeze gate (jlSurfaceCopyRect +
gameFrameClean experiment; ST column includes the movep.l map engine;
Amiga run with TIMEOUT=600; IIgs from a fresh `make iigs-disk` image
-- `make all` alone leaves joey.2mg stale).
| Op | IIgs (ops/sec) | Amiga (ops/sec, % of IIGS) | Atari ST (ops/sec, % of IIGS) | DOS (ops/sec, % of IIGS) |
| --- | --- | --- | --- | --- |
| jlSurfaceClear | 33 | 75 (227%) | 88 (267%) | 66 (200%) |
| jlPaletteSet | 1561 | 11403 (730%) | 10388 (665%) | 29809 (1910%) |
| jlScbSetRange | 163 | 3953 (2425%) | 4833 (2965%) | 8495 (5212%) |
| jlPaletteSet | 1561 | 11503 (737%) | 10388 (665%) | 29809 (1910%) |
| jlScbSetRange | 163 | 4003 (2456%) | 4833 (2965%) | 8495 (5212%) |
| jlDrawPixel | 5576 | 5853 (105%) | 6522 (117%) | 23687 (425%) |
| jlDrawLine H | 1142 | 1753 (154%) | 2670 (234%) | 5708 (500%) |
| jlDrawLine V | 99 | 303 (306%) | 261 (264%) | 468 (473%) |
| jlDrawLine diag | 42 | **40 (95%)** | 88 (210%) | 109 (260%) |
| jlDrawRect 100x100 | 73 | 285 (390%) | 279 (382%) | 454 (622%) |
| jlDrawCircle r=16 | 304 | **285 (94%)** | 366 (120%) | 447 (147%) |
| jlDrawRect 100x100 | 73 | 285 (390%) | 279 (382%) | 470 (644%) |
| jlDrawCircle r=16 | 304 | **285 (94%)** | 359 (118%) | 447 (147%) |
| jlDrawCircle r=80 | 70 | **63 (90%)** | 83 (119%) | 98 (140%) |
| jlFillRect 16x16 | 602 | 1153 (192%) | 1203 (200%) | 3013 (500%) |
| jlFillRect 80x80 | 110 | 285 (259%) | 240 (218%) | 414 (376%) |
| jlFillRect 16x16 | 602 | 1153 (192%) | 1225 (203%) | 3027 (503%) |
| jlFillRect 80x80 | 110 | 285 (259%) | 236 (215%) | 414 (376%) |
| jlFillRect 320x200 | 21 | 68 (324%) | 77 (367%) | 58 (276%) |
| jlFillCircle r=40 | 42 | **31 (74%)** | 155 (369%) | 156 (371%) |
| jlSamplePixel | 4797 | 7253 (151%) | 9603 (200%) | 46397 (967%) |
| jlTileFill | 3251 | 3753 (115%) | 3855 (119%) | 11840 (364%) |
| jlTileCopy | 1988 | 2903 (146%) | 3445 (173%) | 10913 (549%) |
| jlSamplePixel | 4797 | 7253 (151%) | 9603 (200%) | 46251 (964%) |
| jlTileFill | 3251 | 3703 (114%) | 3855 (119%) | 11840 (364%) |
| jlTileCopy | 1988 | 2953 (149%) | 3445 (173%) | 10913 (549%) |
| jlTileCopyMasked | 681 | 1003 (147%) | 1632 (240%) | 3391 (498%) |
| jlTilePaste | 2710 | 3803 (140%) | 4033 (149%) | 12545 (463%) |
| jlTilePaste | 2710 | 3803 (140%) | 4033 (149%) | 12601 (465%) |
| jlTileSnap | 2530 | 6003 (237%) | 6403 (253%) | 19247 (761%) |
| jlSpriteSaveUnder | 602 | 2103 (349%) | 2611 (434%) | 10743 (1785%) |
| jlSpriteDraw | 966 | 1103 (114%) | 1283 (133%) | 6390 (661%) |
| jlSpriteRestoreUnder | 545 | 1453 (267%) | 1781 (327%) | 6246 (1146%) |
| jlSpriteSaveAndDraw | 423 | 753 (178%) | 892 (211%) | 4656 (1101%) |
| jlStagePresent full | 67 | 540 (806%) | 467 (697%) | 1132 (1690%) |
| jlInputPoll | 228 | 4003 (1756%) | 2841 (1246%) | 3148 (1381%) |
| jlKeyDown | 10191 | 39753 (390%) | 44448 (436%) | 77738 (763%) |
| jlKeyPressed | 11389 | 38003 (334%) | 44448 (390%) | 74907 (658%) |
| jlMouseX | 20214 | 97703 (483%) | 117920 (583%) | 155685 (770%) |
| joeyJoyConnected | 10191 | 38903 (382%) | 44260 (434%) | 77748 (763%) |
| jlAudioFrameTick | 27509 | 57003 (207%) | 39885 (145%) | 59372 (216%) |
| jlAudioIsPlayingMod | 11071 | 56403 (509%) | 60441 (546%) | 128214 (1158%) |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 788 (244%) | 1476 (457%) |
| jlSpriteDraw unaligned | 906 | **903 (100%)** | 1070 (118%) | 3837 (424%) |
| jlSpriteDraw | 966 | 1103 (114%) | 1283 (133%) | 6404 (663%) |
| jlSpriteRestoreUnder | 545 | 1453 (267%) | 1781 (327%) | 6218 (1141%) |
| jlSpriteSaveAndDraw | 423 | 803 (190%) | 892 (211%) | 4668 (1104%) |
| jlStagePresent full | 67 | 534 (797%) | 467 (697%) | 1137 (1697%) |
| jlInputPoll | 228 | 4053 (1778%) | 2788 (1223%) | 3148 (1381%) |
| jlKeyDown | 10191 | 39353 (386%) | 45286 (444%) | 77526 (761%) |
| jlKeyPressed | 11389 | 38353 (337%) | 45286 (398%) | 75119 (660%) |
| jlMouseX | 20214 | 96753 (479%) | 117981 (584%) | 155281 (768%) |
| joeyJoyConnected | 10191 | 38903 (382%) | 44260 (434%) | 77537 (761%) |
| jlAudioFrameTick | 27509 | 56353 (205%) | 40637 (148%) | 59211 (215%) |
| jlAudioIsPlayingMod | 11071 | 56453 (510%) | 60441 (546%) | 128004 (1156%) |
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 774 (240%) | 1476 (457%) |
| jlSpriteDraw unaligned | 906 | **903 (100%)** | 1070 (118%) | 5215 (576%) |
| jlSpriteDraw sweep16 | 60 | **56 (93%)** | 68 (113%) | 362 (603%) |
| jlTilePaste map 10x5 | 59 | 78 (132%) | 80 (136%) | 259 (439%) |
| gameFrame composite | 10 | 25 (250%) | 21 (210%) | 19 (190%) |
| jlTilePaste map 10x5 | 59 | 78 (132%) | 80 (136%) | 258 (437%) |
| gameFrame composite | 10 | 25 (250%) | 22 (220%) | 19 (190%) |
| jlTileMapPaste 10x5 | 140 | 153 (109%) | 346 (247%) | 450 (321%) |
| gameFrameClean composite | 18 | 34 (189%) | 48 (267%) | 195 (1083%) |
> **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

View file

@ -354,6 +354,19 @@ static void op_spriteDrawSweep(void) {
#define UBER_TILEMAP_W 10
#define UBER_TILEMAP_H 5
// Batch twin of op_tileMapPaste: the SAME 10x5 alternating map in ONE
// jlTileMapPaste call (NATIVE-PERF Phase 3) at a disjoint spot two
// block-rows below, so both rows keep independent pixel signatures.
// gTileSet[0/1] are struct copies of gTileMapA/B made at setup.
#define UBER_TILEMAP_B_Y0 (UBER_TILEMAP_Y0 + UBER_TILEMAP_H + 1)
static jlTileT gTileSet[2];
static uint8_t gTileMapIndices[UBER_TILEMAP_W * UBER_TILEMAP_H];
static void op_tileMapPasteBatch(void) {
jlTileMapPaste(gStage, UBER_TILEMAP_X0, UBER_TILEMAP_B_Y0, UBER_TILEMAP_W, UBER_TILEMAP_H, gTileSet, gTileMapIndices);
}
static void op_tileMapPaste(void) {
uint8_t tx;
uint8_t ty;
@ -392,8 +405,10 @@ static void op_tileMapPaste(void) {
static const int16_t gFrameSpriteX[UBER_FRAME_SPRITES] = { 48, 144, 240 };
static const int16_t gFrameSpriteY[UBER_FRAME_SPRITES] = { 80, 96, 112 };
static void op_gameFrame(void) {
int16_t i;
// The frame's static content (play-region fill + HUD tile row) --
// shared between op_gameFrame's per-call repaint and the one-time
// clean-surface paint of op_gameFrameClean.
static void paintFrameStatic(void) {
uint8_t t;
jlFillRect(gStage, UBER_FRAME_RX, UBER_FRAME_RY, UBER_FRAME_RW, UBER_FRAME_RH, UBER_FRAME_BG);
@ -404,6 +419,13 @@ static void op_gameFrame(void) {
jlTilePaste(gStage, (uint8_t)(UBER_FRAME_TILE_TX + t), UBER_FRAME_TILE_TY, &gTileMapB);
}
}
}
static void op_gameFrame(void) {
int16_t i;
paintFrameStatic();
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
jlSpriteSaveAndDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i], &gFrameBackups[i]);
}
@ -414,6 +436,31 @@ static void op_gameFrame(void) {
}
// Clean-buffer twin of op_gameFrame (NATIVE-PERF Phase 4 STRATEGY
// row -- it measures the clean-buffer pattern for static-background
// games, not an op-for-op speedup of gameFrame). The background +
// HUD are painted ONCE into gCleanSurface before the timed window,
// so the per-frame body is 3 window erases from the clean copy + 3
// sprite draws + present: the per-frame background repaint and the
// per-sprite save-under disappear. Erase-all-then-draw-all per the
// jlSurfaceCopyRect contract. End state every call = sprites drawn
// over clean background, stable from call 1, so the post-window
// hash is iteration-count independent.
static jlSurfaceT *gCleanSurface = NULL;
static void op_gameFrameClean(void) {
int16_t i;
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
jlSurfaceCopyRect(gStage, gCleanSurface, gFrameSpriteX[i], gFrameSpriteY[i], 16, 16);
}
for (i = 0; i < UBER_FRAME_SPRITES; i++) {
jlSpriteDraw(gStage, gSprite, gFrameSpriteX[i], gFrameSpriteY[i]);
}
jlStagePresent();
}
// ----- Build the ball sprite procedurally -----
#define BALL_TILES_X 2
@ -1026,12 +1073,40 @@ static void __attribute__((noinline)) runAllTests(void) {
jlFillRect(gStage, 8, 72, 8, 8, 9);
jlDrawLine(gStage, 8, 72, 15, 79, 1);
jlTileSnap(gStage, 1, 9, &gTileMapB);
gTileSet[0] = gTileMapA;
gTileSet[1] = gTileMapB;
{
uint8_t tx;
uint8_t ty;
for (ty = 0; ty < UBER_TILEMAP_H; ty++) {
for (tx = 0; tx < UBER_TILEMAP_W; tx++) {
gTileMapIndices[ty * UBER_TILEMAP_W + tx] = (uint8_t)((tx + ty) & 1u);
}
}
}
gFrameBackups[0].bytes = gFrameBackupBytes[0];
gFrameBackups[1].bytes = gFrameBackupBytes[1];
gFrameBackups[2].bytes = gFrameBackupBytes[2];
timeOp("jlTilePaste map 10x5", op_tileMapPaste);
timeOp("gameFrame composite", op_gameFrame);
timeOp("jlTileMapPaste 10x5", op_tileMapPasteBatch);
// Clean-buffer strategy row (appended last per the cumulative-
// hash safety rule): paint the frame's static content, snapshot
// the WHOLE stage into the clean surface (clean == stage
// everywhere makes the snapped erase windows hash-safe by
// construction), then time erase+draw+present.
gCleanSurface = jlSurfaceCreate();
if (gCleanSurface != NULL) {
paintFrameStatic();
jlSurfaceCopy(gCleanSurface, gStage);
timeOp("gameFrameClean composite", op_gameFrameClean);
jlSurfaceDestroy(gCleanSurface);
gCleanSurface = NULL;
} else {
jlLogF("UBER: gameFrameClean composite: SKIPPED (surface alloc failed)\n");
}
jlLogF("UBER: ----- end -----\n");
}

View file

@ -152,6 +152,7 @@
#define JL_HAS_FLOOD_WALK_PLANES // amiga planar flood walk-out, function
#define JL_HAS_FLOOD_SCAN_ROW_PLANES // amiga planar flood scan-row, function
#define JL_HAS_SURFACE_COPY_PLANES // amiga blitter plane copy, function
#define JL_HAS_SURFACE_COPY_RECT // amiga per-plane rect row copy, function
#define JL_HAS_SAMPLE_PIXEL // amiga planar nibble decode, function
#define JL_HAS_SURFACE_HASH // amiga planar hash, function
#define JL_HAS_SURFACE_LOAD_FILE // amiga planar load, function
@ -206,6 +207,7 @@
#define JL_HAS_FLOOD_WALK_PLANES // st planar flood walk-out, function
#define JL_HAS_FLOOD_SCAN_ROW_PLANES // st planar flood scan-row, function
#define JL_HAS_SURFACE_COPY_PLANES // st planar buffer memcpy, function
#define JL_HAS_SURFACE_COPY_RECT // st interleaved rect row memcpy, function
#define JL_HAS_SAMPLE_PIXEL // st planar nibble decode, function
#define JL_HAS_SURFACE_HASH // st planar hash, function
#define JL_HAS_SURFACE_LOAD_FILE // st planar load, function

View file

@ -50,6 +50,23 @@ jlSurfaceT *jlStageGet(void);
// surfaces.
void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src);
// Copy a pixel rectangle from src into dst at the SAME position (no
// repositioning, so planar ports never shift bits). The rect is
// clipped to the surface, then widened OUTWARD to the 16-pixel group
// grid ON EVERY PORT -- the copied window is x rounded down and
// x + w rounded up to multiples of 16 (same window rule as
// jlSpriteSaveUnder), so up to 15 extra pixels on each side are
// copied. Two consequences the caller owns: a snapped erase can
// clobber on-screen content up to 15 px outside the requested rect,
// and it silently invalidates any live jlSpriteBackupT whose
// recorded window it touches. Copies pixels only: SCBs and palettes
// are untouched. The intended use is restoring background
// rectangles from a pristine copy of the scene (the clean-buffer
// pattern) instead of per-sprite save/restore -- erase ALL windows,
// then redraw ALL sprites; do not mix with backup-restore in the
// same region.
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h);
// Persist a surface to disk in target-native form: 32000 bytes of
// pixels followed by 200 bytes of SCB followed by 512 bytes of $0RGB
// palette table (16 palettes x 16 colors, native uint16_t). Returns

View file

@ -132,4 +132,23 @@ void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
// Map entry value that leaves a cell untouched (nothing pasted, the
// destination shows through). Mirrors TILE_NO_GLYPH's role in
// jlDrawText.
#define TILE_MAP_SKIP ((uint8_t)0xFFu)
// Paste a wTiles x hTiles rectangle of tiles in ONE call: the
// validation, dispatch, and dirty union are paid once for the whole
// map instead of per tile (the jlDrawText batching pattern
// generalized, NATIVE-PERF Phase 3). `tiles` is the tileset; `map`
// holds wTiles * hTiles tileset indices in row-major order
// (map[row * wTiles + col]). An index of TILE_MAP_SKIP leaves that
// cell untouched; any other out-of-tileset index is the CALLER'S
// contract to avoid (the batch does not know the tileset length and
// does not range-check it). The whole rectangle must fit on the
// surface -- a map whose edge crosses TILE_BLOCKS_PER_ROW/COL is
// rejected as a no-op, matching the per-tile ops' silent-reject
// convention. wTiles or hTiles of 0 is a no-op.
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map);
#endif

View file

@ -1454,6 +1454,45 @@ void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
}
// Same-position group-snapped rect copy (NATIVE-PERF Phase 4
// clean-buffer op). The core wrapper clipped and 16-px-snapped the
// rect; per plane each row is a short contiguous span at the same
// byte offset in both buffers. CPU row copies, not the blitter:
// amigaBlitterCopyPlanes hardcodes modulo 0 (whole contiguous rows),
// a 16-px snap gives only word alignment (CopyMemQuick needs longs),
// and typical spans (a 16x16 sprite window = 2 bytes/row/plane) are
// below any setup-cost crossover. A modulo-driven blitter variant is
// the production upgrade if this op ever moves big rects.
void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
AmigaPlanarT *dstPd;
AmigaPlanarT *srcPd;
uint8_t *d;
const uint8_t *s;
uint16_t off;
uint16_t rowBytes;
uint16_t row;
uint8_t plane;
dstPd = (AmigaPlanarT *)dst->portData;
srcPd = (AmigaPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
jlpGenericSurfaceCopyRect(dst, src, x, y, w, h);
return;
}
off = (uint16_t)(y * AMIGA_BYTES_PER_ROW + (x >> 3));
rowBytes = (uint16_t)(w >> 3);
for (plane = 0; plane < 4u; plane++) {
d = dstPd->planes[plane] + off;
s = srcPd->planes[plane] + off;
for (row = 0; row < h; row++) {
memcpy(d, s, rowBytes);
d += AMIGA_BYTES_PER_ROW;
s += AMIGA_BYTES_PER_ROW;
}
}
}
// Phase 4 planar dual-write helper: set one pixel's bit in each of
// the four shadow planes. Caller (the per-primitive walker below) has
// already validated (x, y) is on-surface. byteOff is computed once

View file

@ -1311,6 +1311,39 @@ void jlpSurfaceCopyPlanes(jlSurfaceT *dst, const jlSurfaceT *src) {
}
// Same-position group-snapped rect copy (NATIVE-PERF Phase 4
// clean-buffer op). The core wrapper clipped and 16-px-snapped the
// rect, so each row's span is contiguous interleaved bytes at the
// same offset in both buffers -- the raw-span sprite-restore recipe
// with a strided source. Offsets are multiples of 8 from long-aligned
// bases, so memcpy's long path is alignment-safe.
void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
StPlanarT *dstPd;
StPlanarT *srcPd;
uint8_t *d;
const uint8_t *s;
uint16_t off;
uint16_t rowBytes;
uint16_t row;
dstPd = (StPlanarT *)dst->portData;
srcPd = (StPlanarT *)src->portData;
if (dstPd == NULL || srcPd == NULL) {
jlpGenericSurfaceCopyRect(dst, src, x, y, w, h);
return;
}
off = (uint16_t)(y * ST_BYTES_PER_ROW + (x >> 4) * ST_BYTES_PER_GROUP);
d = dstPd->base + off;
s = srcPd->base + off;
rowBytes = (uint16_t)((w >> 4) * ST_BYTES_PER_GROUP);
for (row = 0; row < h; row++) {
memcpy(d, s, rowBytes);
d += ST_BYTES_PER_ROW;
s += ST_BYTES_PER_ROW;
}
}
// ----- Phases 4-7: per-pixel / tile / sprite / blit planar primitives -----
//
// These implementations dual-write the planar shadow alongside the

View file

@ -19,6 +19,13 @@
// all 4 planes. Declared here because stTileFill is its only caller.
extern void surface68kStTileFill8x8(uint8_t *firstGroupPtr, uint16_t mask, uint8_t color);
// 68k asm entry (tileMap.s): whole-map paste with a movep.l row body
// (36 cyc/tile-row vs ~80 for compiled move.b). Requires an EVEN
// tileset pointer (move.l source alignment); stTileMapPaste guards
// and keeps the C pair walker as the odd-pointer fallback. Declared
// here because stTileMapPaste is its only caller.
extern void stTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles, const uint8_t *map, uint16_t wTiles, uint16_t hTiles, uint16_t headOdd);
// Phase 5 tile ops. 8x8 tiles at byte position (bx, by) start at
// pixel (bx*8, by*8). 8 pixels wide always covers exactly half a
// 16-pixel group: high half (bits 15..8) when bx is even, low half
@ -93,6 +100,104 @@ static inline __attribute__((always_inline)) void stTilePaste(jlSurfaceT *dst, u
}
// Whole-map paste engine (NATIVE-PERF Phase 4). jlTileMapPaste's ST
// arm calls this ONCE per map: the portData check, the row base
// derivation, and the group walk are hoisted out of the per-cell
// loop -- the generic loop re-derives base + by*1280 + (bx>>1)*8 +
// (bx&1) and reloads portData per cell, ~250 of its ~980 cyc/tile.
// Cells advance in even/odd PAIRS sharing one group pointer (+8 per
// pair, constant displacements select the half-group), so an odd bx
// contributes a head cell and an odd width a tail cell. Pixel output
// is byte-identical to per-cell stTilePaste.
#define ST_MAP_CELL_ROW(_o, _r) \
do { \
gp[(_o) + (_r) * ST_BYTES_PER_ROW + 0] = src[(_r) * TILE_BYTES_PER_ROW + 0]; \
gp[(_o) + (_r) * ST_BYTES_PER_ROW + 2] = src[(_r) * TILE_BYTES_PER_ROW + 1]; \
gp[(_o) + (_r) * ST_BYTES_PER_ROW + 4] = src[(_r) * TILE_BYTES_PER_ROW + 2]; \
gp[(_o) + (_r) * ST_BYTES_PER_ROW + 6] = src[(_r) * TILE_BYTES_PER_ROW + 3]; \
} while (0)
#define ST_MAP_CELL(_o) \
do { \
src = tileBytes + ((uint16_t)idx << 5); \
ST_MAP_CELL_ROW(_o, 0); \
ST_MAP_CELL_ROW(_o, 1); \
ST_MAP_CELL_ROW(_o, 2); \
ST_MAP_CELL_ROW(_o, 3); \
ST_MAP_CELL_ROW(_o, 4); \
ST_MAP_CELL_ROW(_o, 5); \
ST_MAP_CELL_ROW(_o, 6); \
ST_MAP_CELL_ROW(_o, 7); \
} while (0)
static inline __attribute__((always_inline)) void stTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) {
StPlanarT *pd;
const uint8_t *tileBytes;
const uint8_t *src;
uint8_t *rowBase;
uint8_t *gp;
uint8_t idx;
uint8_t ty;
uint8_t tx;
uint8_t pairs;
uint8_t headOdd;
uint8_t tail;
tileBytes = (const uint8_t *)tiles;
pd = (StPlanarT *)dst->portData;
if (pd == NULL) {
for (ty = 0; ty < hTiles; ty++) {
for (tx = 0; tx < wTiles; tx++) {
idx = map[tx];
if (idx != TILE_MAP_SKIP) {
jlpGenericTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), tileBytes + ((uint16_t)idx << 5));
}
}
map += wTiles;
}
return;
}
rowBase = pd->base + (uint16_t)by * 8u * ST_BYTES_PER_ROW + ((uint16_t)bx >> 1) * ST_BYTES_PER_GROUP;
headOdd = (uint8_t)(bx & 1u);
if (((uintptr_t)tileBytes & 1u) == 0u) {
stTileMapPasteMovep(rowBase, tileBytes, map, wTiles, hTiles, headOdd);
return;
}
pairs = (uint8_t)((uint8_t)(wTiles - headOdd) >> 1);
tail = (uint8_t)((uint8_t)(wTiles - headOdd) & 1u);
for (ty = 0; ty < hTiles; ty++) {
gp = rowBase;
if (headOdd != 0u) {
idx = *map++;
if (idx != TILE_MAP_SKIP) {
ST_MAP_CELL(1);
}
gp += ST_BYTES_PER_GROUP;
}
for (tx = pairs; tx != 0u; tx--) {
idx = *map++;
if (idx != TILE_MAP_SKIP) {
ST_MAP_CELL(0);
}
idx = *map++;
if (idx != TILE_MAP_SKIP) {
ST_MAP_CELL(1);
}
gp += ST_BYTES_PER_GROUP;
}
if (tail != 0u) {
idx = *map++;
if (idx != TILE_MAP_SKIP) {
ST_MAP_CELL(0);
}
}
rowBase += 8u * ST_BYTES_PER_ROW;
}
}
#undef ST_MAP_CELL
#undef ST_MAP_CELL_ROW
// Planar monochrome paste. monoTile follows the cross-port mono
// contract (include/joey/tile.h + jlpGenericTilePasteMono): 32 chunky
// nibble-pair bytes, row-major, TILE_BYTES_PER_ROW bytes per row, two

127
src/atarist/tileMap.s Normal file
View file

@ -0,0 +1,127 @@
| ST whole-map tile-paste engine, movep.l row body (NATIVE-PERF
| Phase 4).
|
| One call pastes a wTiles x hTiles map of 8x8 tiles. The copy body
| uses the 68000's byte-lane scatter instruction:
|
| move.l (%a0)+,%d0 | one tile row: 4 plane bytes, 12 cyc
| movep.l %d0,d16(%a1) | writes d16, d16+2, d16+4, d16+6, 24 cyc
|
| = 36 cyc per tile row vs ~80 for the four move.b the compiler emits
| (movep writes MSB first, matching the plane-major tile layout and
| the interleaved d16 0/2/4/6 dst pattern of stTilePaste). movem is
| useless here -- the dst is a byte scatter -- but movep IS this
| access pattern. Cells advance in even/odd pairs sharing one group
| pointer (+8/pair); an odd bx adds a head cell (displacements +1),
| an odd width a tail cell. Row displacements are compile-time
| constants (row r at r*160), so the group pointer only moves once
| per pair.
|
| void stTileMapPasteMovep(uint8_t *rowBase, const uint8_t *tiles,
| const uint8_t *map, uint16_t wTiles,
| uint16_t hTiles, uint16_t headOdd);
|
| rowBase = planar base + by*1280 + (bx>>1)*8 (group-aligned; the
| caller keeps the bx&1 head flag). CONTRACT: tiles must be EVEN
| (move.l source alignment on 68000); the C caller guards and falls
| back to the byte-copy engine for odd tileset pointers. Map bytes of
| 0xFF (TILE_MAP_SKIP) leave the cell untouched but still advance the
| walk. wTiles/hTiles >= 1 (jlTileMapPaste validated).
|
| cdecl args land in 4-byte slots (uint16_t value at slot+2), same
| convention as surface68kStTileFill8x8 (lineSpan.s). d2-d7/a2-a6 are
| callee-save; register lists (not hand masks) keep the predec movem
| mask correct.
|
| Register map: a0 tile src (scratch), a1 group walker, a2 map
| cursor, a3 tileset base, a4 rowBase (+1280/map row); d0 movep data,
| d1 map byte, d2 pairs/row, d3 tail flag, d4 rows left, d5 head
| flag, d6 pair countdown.
|
| 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).
.equ MP_SAVED, 32 | d2-d6/a2-a4 = 8 longs
.equ MP_OFF, (MP_SAVED + 4)
.equ MP_BASE, MP_OFF + 0
.equ MP_TILES, MP_OFF + 4
.equ MP_MAP, MP_OFF + 8
.equ MP_WTILES, MP_OFF + 12 + 2
.equ MP_HTILES, MP_OFF + 16 + 2
.equ MP_HEAD, MP_OFF + 20 + 2
| One tile cell: derive src from the map byte in d1, then 8 rows of
| move.l + movep.l at half-group displacement \off (0 = even cell,
| 1 = odd cell). Falls straight through on TILE_MAP_SKIP.
.macro MP_CELL off, skip
moveq #0,%d1
move.b (%a2)+,%d1 | map byte
cmp.b #-1,%d1 | TILE_MAP_SKIP?
beq.s \skip
lsl.w #5,%d1 | idx * sizeof(jlTileT)
lea (%a3,%d1.w),%a0 | tile src (even by contract)
move.l (%a0)+,%d0
movep.l %d0,\off+0(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+160(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+320(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+480(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+640(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+800(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+960(%a1)
move.l (%a0)+,%d0
movep.l %d0,\off+1120(%a1)
\skip:
.endm
.text
.globl _stTileMapPasteMovep
_stTileMapPasteMovep:
movem.l %d2-%d6/%a2-%a4,-(%sp)
movea.l MP_BASE(%sp),%a4 | a4 = rowBase
movea.l MP_TILES(%sp),%a3 | a3 = tileset base
movea.l MP_MAP(%sp),%a2 | a2 = map cursor
move.w MP_WTILES(%sp),%d3
move.w MP_HTILES(%sp),%d4 | d4 = rows left
move.w MP_HEAD(%sp),%d5 | d5 = 1 if bx odd
sub.w %d5,%d3
move.w %d3,%d2
lsr.w #1,%d2 | d2 = pairs per row
and.w #1,%d3 | d3 = tail cell flag
.LmpRow:
movea.l %a4,%a1 | a1 = this row's group ptr
tst.w %d5
beq.s .LmpNoHead
MP_CELL 1, .LmpHeadSkip
addq.l #8,%a1
.LmpNoHead:
move.w %d2,%d6 | pair countdown
beq.w .LmpNoPairs
subq.w #1,%d6
.LmpPair:
MP_CELL 0, .LmpEvenSkip
MP_CELL 1, .LmpOddSkip
addq.l #8,%a1
dbra %d6,.LmpPair
.LmpNoPairs:
tst.w %d3
beq.s .LmpNoTail
MP_CELL 0, .LmpTailSkip
.LmpNoTail:
lea 1280(%a4),%a4 | next tile row of the map
subq.w #1,%d4
bne.w .LmpRow
movem.l (%sp)+,%d2-%d6/%a2-%a4
rts

View file

@ -67,6 +67,7 @@ extern void iigsTileCopyMark (const uint8_t *srcRow0, uint16_t dstBx, uint16
extern void iigsTileCopyMaskedInner(uint8_t *dstRow0, const uint8_t *srcRow0, uint16_t transparent);
extern void iigsTilePasteInner (uint8_t *dstRow0, const uint8_t *srcTilePixels);
extern void iigsTilePasteMark (const uint8_t *srcTilePixels, uint16_t bx, uint16_t by);
extern void iigsTileMapPasteMark (uint16_t bx, uint16_t by, uint16_t wTiles, uint16_t hTiles, const uint8_t *tiles, const uint8_t *map);
extern void iigsTilePasteMonoInner(uint8_t *dstRow0, const uint8_t *monoTile, uint16_t fgColor, uint16_t bgColor);
extern void iigsTileSnapInner (uint8_t *dstTilePixels, const uint8_t *srcRow0);
extern void iigsFillRectInner (uint8_t *pixels, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t nibble);
@ -542,6 +543,19 @@ void jlpGenericSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src);
#endif
#endif
// Same-position group-snapped rect copy (jlSurfaceCopyRect payload).
// The generic body serves the chunky ports (incl. the IIgs stage --
// its pixels pointer reaches bank $01 through ordinary large-model
// stores); the planar ports override with their plane-layout walks.
void jlpGenericSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h);
#if !defined(jlpSurfaceCopyRect)
#if defined(JL_HAS_SURFACE_COPY_RECT)
void jlpSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h);
#else
#define jlpSurfaceCopyRect(_d, _s, _x, _y, _w, _h) jlpGenericSurfaceCopyRect((_d), (_s), (_x), (_y), (_w), (_h))
#endif
#endif
void jlpGenericSurfaceFreePixels(uint8_t *pixels);
#if !defined(jlpSurfaceFreePixels)
#if defined(JL_HAS_SURFACE_FREE_PIXELS)

View file

@ -73,6 +73,49 @@ void jlSurfaceCopy(jlSurfaceT *dst, const jlSurfaceT *src) {
}
void jlSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, int16_t x, int16_t y, uint16_t w, uint16_t h) {
int32_t left;
int32_t top;
int32_t right;
int32_t bottom;
if (dst == NULL || src == NULL || dst == src) {
return;
}
if (w == 0u || h == 0u) {
return;
}
// Clip in int32 (jlFillRect precedent), then widen OUTWARD to the
// 16-px group grid per the header contract. SURFACE_WIDTH is a
// multiple of 16, so the widened right edge never re-crosses it.
left = x;
top = y;
right = (int32_t)x + w;
bottom = (int32_t)y + h;
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (right > SURFACE_WIDTH) {
right = SURFACE_WIDTH;
}
if (bottom > SURFACE_HEIGHT) {
bottom = SURFACE_HEIGHT;
}
if (left >= right || top >= bottom) {
return;
}
left = left & ~(int32_t)15;
right = (right + 15) & ~(int32_t)15;
jlpSurfaceCopyRect(dst, src, (uint16_t)left, (uint16_t)top, (uint16_t)(right - left), (uint16_t)(bottom - top));
// Pixels-only op: the SCB/palette dirty flags are jlSurfaceCopy's
// concern, not this one's.
surfaceMarkDirtyRect(dst, (int16_t)left, (int16_t)top, (int16_t)(right - left), (int16_t)(bottom - top));
}
jlSurfaceT *jlSurfaceCreate(void) {
jlSurfaceT *s;

View file

@ -45,6 +45,10 @@
// 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];
// jlTileMapPaste indexes its tileset as base + (idx << 5); pin the
// struct size so that shift can never drift from sizeof(jlTileT).
typedef char tileMapStrideProof[(sizeof(jlTileT) == 32) ? 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))
// Fill/copy/paste FUSE the mark into their stage-arm asm entries on
@ -242,6 +246,87 @@ void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex) {
}
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, uint8_t wTiles, uint8_t hTiles, const jlTileT *tiles, const uint8_t *map) {
uint8_t tx;
uint8_t ty;
uint8_t idx;
const uint8_t *row;
if (dst == NULL || tiles == NULL || map == NULL) {
return;
}
if (wTiles == 0u || hTiles == 0u) {
return;
}
if ((uint16_t)bx + wTiles > TILE_BLOCKS_PER_ROW || (uint16_t)by + hTiles > TILE_BLOCKS_PER_COL) {
return;
}
// One validation above, one dirty union below: the per-cell loop
// pays only the jlp dispatch (inline on the 68k ports since Phase
// 2 R4a; the fused stage entry on IIgs). Tileset indexing is a
// 16-bit shift folded into the far base ONCE per cell -- the
// natural tiles[idx] spelling made the 65816 do a 32-bit indexed
// multiply per cell, which alone erased the batch win there.
row = map;
#ifdef JOEYLIB_PLATFORM_IIGS
// The stage test is hoisted OUT of the loop on the IIgs: the
// jlpTilePaste macro's per-cell (dst == gStage) ternary
// materializes a boolean (~65 cycles/cell) that a 10x5 map pays
// 50 times. The fused stage entry marks each pasted tile itself,
// so the stage arm needs no union; non-stage surfaces carry no
// dirty state.
if (dst == gStage) {
// ONE JSL for the whole map (NATIVE-PERF Phase 4): the asm
// walker amortizes the per-tile JSL + arg staging + derive +
// 8-row mark across the rectangle and issues one band union.
iigsTileMapPasteMark(bx, by, wTiles, hTiles, (const uint8_t *)tiles, map);
return;
}
for (ty = 0; ty < hTiles; ty++) {
for (tx = 0; tx < wTiles; tx++) {
idx = row[tx];
if (idx != TILE_MAP_SKIP) {
iigsTilePasteInner(&dst->pixels[SURFACE_ROW_OFFSET((uint16_t)(by + ty) << 3) + ((uint16_t)(bx + tx) << 2)], (const uint8_t *)tiles + ((uint16_t)idx << 5));
}
}
row += wTiles;
}
#elif defined(JOEYLIB_PLATFORM_ATARIST)
// Whole-map engine (NATIVE-PERF Phase 4, stTile.h): one portData
// check, one row-base derive, and a paired group-pointer walk for
// the whole rectangle instead of the per-cell re-derivation the
// generic loop pays.
(void)tx;
(void)ty;
(void)idx;
(void)row;
stTileMapPaste(dst, bx, by, wTiles, hTiles, tiles, map);
// One rectangle-union mark for the whole map. Over-marks
// TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
// pixels unchanged).
if (dst == gStage) {
surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
}
#else
for (ty = 0; ty < hTiles; ty++) {
for (tx = 0; tx < wTiles; tx++) {
idx = row[tx];
if (idx != TILE_MAP_SKIP) {
jlpTilePaste(dst, (uint8_t)(bx + tx), (uint8_t)(by + ty), (const uint8_t *)tiles + ((uint16_t)idx << 5));
}
}
row += wTiles;
}
// One rectangle-union mark for the whole map. Over-marks
// TILE_MAP_SKIP cells (safe: present copies extra CLEAN content,
// pixels unchanged).
if (dst == gStage) {
surfaceMarkDirtyRows((uint16_t)((uint16_t)by << 3), (uint16_t)(((uint16_t)by + hTiles) << 3), (uint8_t)((uint16_t)bx << 1), (uint8_t)((((uint16_t)bx + wTiles) << 1) - 1u));
}
#endif
}
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in) {
if (dst == NULL || in == NULL) {
return;

View file

@ -23,6 +23,33 @@ void jlpGenericSurfaceCopyChunky(jlSurfaceT *dst, const jlSurfaceT *src) {
}
// jlSurfaceCopyRect's chunky payload: same-position row segments. The
// core wrapper already clipped and group-snapped the rect, so x and w
// are multiples of 16 and the rect is on-surface. Guarded to a no-op
// on planar ports (pixels == NULL), whose JL_HAS_SURFACE_COPY_RECT
// override owns the plane work. The row offset is seeded once and
// walked by stride: SURFACE_ROW_OFFSET is a LUT read on the IIgs and
// a multiply elsewhere, and neither belongs in a row loop.
void jlpGenericSurfaceCopyRect(jlSurfaceT *dst, const jlSurfaceT *src, uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
uint8_t *d;
const uint8_t *s;
uint16_t rowBytes;
uint16_t row;
if (dst->pixels == NULL || src->pixels == NULL) {
return;
}
d = dst->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1);
s = src->pixels + SURFACE_ROW_OFFSET(y) + (x >> 1);
rowBytes = (uint16_t)(w >> 1);
for (row = 0; row < h; row++) {
memcpy(d, s, rowBytes);
d += SURFACE_BYTES_PER_ROW;
s += SURFACE_BYTES_PER_ROW;
}
}
// Release a per-surface chunky pixel buffer. Must mirror
// jlpGenericSurfaceAllocPixels' allocator exactly: handing a
// jlpBigAlloc pointer to free() corrupts the IIgs heap

View file

@ -1731,6 +1731,250 @@ iigsTileCopyMark:
plp
rtl
; ====================================================================
; iigsTileMapPasteMark(uint16_t bx, uint16_t by, uint16_t wTiles,
; uint16_t hTiles, const jlTileT *tiles,
; const uint8_t *map)
;
; NATIVE-PERF Phase 4: whole-map tile walker. ONE JSL walks the
; wTiles x hTiles row-major uint8_t index map and pastes every
; non-0xFF tile to the stage, then widens the dirty band once for the
; whole rectangle -- amortizing the per-tile JSL, arg staging, derive,
; and 8-row mark that iigsTilePasteMark pays per tile. The dst offset
; advances incrementally (+4 per tile column, +1280 per tile row =
; 8 SHR rows) from ONE gRowOffsetLut read at entry.
;
; ABI: bx in A; by @4,s, wTiles @6,s, hTiles @8,s, tiles @10,s/12,s,
; map @14,s/16,s on JSL entry. Frame: php+phb+phd (+4), parked
; bx (+2), then a 22-byte reserved D-frame (+22) = +28 total; D
; = SP+1 so frame slots are D-relative and args sit at D+31..43.
; X survives the 16-pair copy body (sta long,x never writes X),
; so it carries the walking dst offset; Y is clobbered per cell
; and the map index lives in the frame instead. The idx << 5
; tileset add carries into the bank byte (tmwSrc+2), so a
; tileset crossing a bank boundary still resolves.
;
; No internal jsr (the band union is inline), so the section is
; self-contained -- no cross-section 16-bit-jsr hazard. The three
; wrapping branches (skip-cell forward, column back, row back) span
; the ~150-byte copy body and use the bne-over-brl idiom.
; ====================================================================
.section .text.iigsTileMapPasteMark,"ax"
.globl iigsTileMapPasteMark
iigsTileMapPasteMark:
tmwSrc = 0 ; current tile src far ptr ([0],y)
tmwMap = 4 ; map far ptr ([4],y)
tmwRowBase = 8 ; dst offset of current tile row
tmwTilesLo = 10 ; tileset base low16
tmwTilesHi = 12 ; tileset base high16 (bank)
tmwMapIdx = 14 ; running map index (reused: yEnd for the mark)
tmwTx = 16 ; columns left this row (reused: minWord)
tmwTy = 18 ; rows left (reused: maxWord)
tmwWt = 20 ; wTiles reload value
tmwBx = 22 ; parked bx
tmwByArg = 31 ; by (orig 4,s + 28 pushed, D-relative)
tmwWtArg = 33 ; wTiles (orig 6,s)
tmwHtArg = 35 ; hTiles (orig 8,s)
tmwTLoArg = 37 ; tiles low16 (orig 10,s)
tmwTHiArg = 39 ; tiles high16 (orig 12,s)
tmwMLoArg = 41 ; map low16 (orig 14,s)
tmwMHiArg = 43 ; map high16 (orig 16,s)
php
phb
phd
rep #0x30
.a16
.i16
pha ; park bx -> D+22
tsc
sec
sbc #22
tcs ; reserve the 22-byte frame
inc a
tcd ; D = frame base (bank-0 stack)
lda tmwMLoArg
sta tmwMap+0
lda tmwMHiArg
sta tmwMap+2
lda tmwTLoArg
sta tmwTilesLo
lda tmwTHiArg
sta tmwTilesHi
lda tmwWtArg
sta tmwWt
lda tmwHtArg
sta tmwTy
stz tmwMapIdx
; row-0 dst offset = gRowOffsetLut[by*8] + bx*4 (ONCE)
lda tmwByArg
asl a
asl a
asl a
asl a
tax
lda tmwBx
asl a
asl a
clc
adc gRowOffsetLut,x
sta tmwRowBase
tmwRowTop:
lda tmwWt
sta tmwTx
lda tmwRowBase
tax ; X = dst offset, walks +4/column
tmwColTop:
ldy tmwMapIdx
sep #0x20
.a8
lda [tmwMap], y ; map byte
cmp #0xFF
bne tmwDoTile
rep #0x20
.a16
brl tmwCellNext ; skip: past the copy body's range
tmwDoTile:
rep #0x20
.a16
and #0x00FF ; idx (high byte was stale)
asl a
asl a
asl a
asl a
asl a ; idx * 32 (sizeof proof in tile.c)
clc
adc tmwTilesLo
sta tmwSrc+0
lda tmwTilesHi
adc #0 ; bank carry
sta tmwSrc+2
; 8 rows: src stride 4 via [0],y; dst stride 160 folded
; into the long,x literals (the iigsTilePasteMark body).
ldy #0
lda [tmwSrc], y
sta 0x012000+0,x
ldy #2
lda [tmwSrc], y
sta 0x012000+2,x
ldy #4
lda [tmwSrc], y
sta 0x012000+160,x
ldy #6
lda [tmwSrc], y
sta 0x012000+162,x
ldy #8
lda [tmwSrc], y
sta 0x012000+320,x
ldy #10
lda [tmwSrc], y
sta 0x012000+322,x
ldy #12
lda [tmwSrc], y
sta 0x012000+480,x
ldy #14
lda [tmwSrc], y
sta 0x012000+482,x
ldy #16
lda [tmwSrc], y
sta 0x012000+640,x
ldy #18
lda [tmwSrc], y
sta 0x012000+642,x
ldy #20
lda [tmwSrc], y
sta 0x012000+800,x
ldy #22
lda [tmwSrc], y
sta 0x012000+802,x
ldy #24
lda [tmwSrc], y
sta 0x012000+960,x
ldy #26
lda [tmwSrc], y
sta 0x012000+962,x
ldy #28
lda [tmwSrc], y
sta 0x012000+1120,x
ldy #30
lda [tmwSrc], y
sta 0x012000+1122,x
tmwCellNext:
inc tmwMapIdx ; 16-bit (M=16 on both arrival paths)
inx
inx
inx
inx ; dst offset += 4 (one tile column)
dec tmwTx
beq tmwRowDone
brl tmwColTop
tmwRowDone:
lda tmwRowBase
clc
adc #1280 ; 8 SHR rows x 160 = one tile row
sta tmwRowBase
dec tmwTy
beq tmwMark
brl tmwRowTop
tmwMark:
; One band union for the whole rectangle (over-marks
; TILE_MAP_SKIP cells -- safe, present just re-uploads
; clean pixels). Frame slots are dead now; reuse them:
; tmwMapIdx = yEnd, tmwTx = minWord, tmwTy = maxWord.
lda tmwByArg
clc
adc tmwHtArg
asl a
asl a
asl a
sta tmwMapIdx ; yEnd = (by + hTiles) * 8
lda tmwBx
asl a
sta tmwTx ; minWord = bx * 2
lda tmwBx
clc
adc tmwWtArg
asl a
dec a
sta tmwTy ; maxWord = (bx + wTiles) * 2 - 1
lda tmwByArg
asl a
asl a
asl a
tax ; X = first row = by * 8
sep #0x20
.a8
tmwMarkLoop:
lda tmwTx
cmp gStageMinWord,x
bcs tmwNoMin
sta gStageMinWord,x ; widen down
tmwNoMin:
lda tmwTy
cmp gStageMaxWord,x
bcc tmwNoMax
beq tmwNoMax
sta gStageMaxWord,x ; widen up
tmwNoMax:
inx
cpx tmwMapIdx
bcc tmwMarkLoop
rep #0x30
.a16
.i16
tsc
clc
adc #24 ; drop frame (22) + parked bx (2)
tcs
pld
plb
plp
rtl
; --------------------------------------------------------------------

View file

@ -202,3 +202,5 @@ UBER: total wall time: 165280 ms (8264 frames @ 50 Hz)
UBER: press any key to exit
UBER: jlTileMapPaste 10x5: 49 iters / 16 frames = 153 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 11 iters / 16 frames = 34 ops/sec | hash=C51FE171

View file

@ -198,3 +198,5 @@ UBER: total wall time: 289833 ms (17390 frames @ 60 Hz)
UBER: press any key to exit
UBER: jlTileMapPaste 10x5: 49 iters / 18 frames = 163 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 13 iters / 16 frames = 48 ops/sec | hash=C51FE171

View file

@ -198,3 +198,5 @@ UBER: total wall time: 23142 ms (1620 frames @ 70 Hz)
UBER: press any key to exit
UBER: jlTileMapPaste 10x5: 113 iters / 18 frames = 450 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 43 iters / 16 frames = 195 ops/sec | hash=C51FE171

View file

@ -98,3 +98,5 @@ UBER-CHK: floodFillBounded: hash=EF6B6F51
UBER-CHK: ----- end -----
UBER: total wall time: 419016 ms (25141 frames @ 60 Hz)
UBER: press any key to exit
UBER: jlTileMapPaste 10x5: 15 iters / 16 frames = 56 ops/sec | hash=52B9646B
UBER: gameFrameClean composite: 5 iters / 16 frames = 18 ops/sec | hash=C51FE171