From 07f0e4f5031fae4e298a16a435786216fa9637db Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 7 Jul 2026 18:31:29 -0500 Subject: [PATCH] More performance work. --- NATIVE-PERF-PLAN.md | 88 +++++++++++- PERF.md | 164 ++++++++++++++++++---- examples/uber/uber.c | 79 ++++++++++- include/joey/platform.h | 2 + include/joey/surface.h | 17 +++ include/joey/tile.h | 19 +++ src/amiga/hal.c | 39 ++++++ src/atarist/hal.c | 33 +++++ src/atarist/stTile.h | 105 ++++++++++++++ src/atarist/tileMap.s | 127 +++++++++++++++++ src/core/port.h | 14 ++ src/core/surface.c | 43 ++++++ src/core/tile.c | 85 ++++++++++++ src/generic/genericSurface.c | 27 ++++ src/iigs/joeyDraw.s | 244 +++++++++++++++++++++++++++++++++ tests/goldens/uber/amiga.txt | 2 + tests/goldens/uber/atarist.txt | 2 + tests/goldens/uber/dos.txt | 2 + tests/goldens/uber/iigs.txt | 2 + 19 files changed, 1062 insertions(+), 32 deletions(-) create mode 100644 src/atarist/tileMap.s diff --git a/NATIVE-PERF-PLAN.md b/NATIVE-PERF-PLAN.md index 38bae11..23d65ec 100644 --- a/NATIVE-PERF-PLAN.md +++ b/NATIVE-PERF-PLAN.md @@ -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 diff --git a/PERF.md b/PERF.md index ee9269e..256d190 100644 --- a/PERF.md +++ b/PERF.md @@ -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 diff --git a/examples/uber/uber.c b/examples/uber/uber.c index 8741c5d..107d5e5 100644 --- a/examples/uber/uber.c +++ b/examples/uber/uber.c @@ -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"); } diff --git a/include/joey/platform.h b/include/joey/platform.h index 6c49247..d6e730c 100644 --- a/include/joey/platform.h +++ b/include/joey/platform.h @@ -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 diff --git a/include/joey/surface.h b/include/joey/surface.h index a872a37..675d9cd 100644 --- a/include/joey/surface.h +++ b/include/joey/surface.h @@ -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 diff --git a/include/joey/tile.h b/include/joey/tile.h index 3d95c82..871f73f 100644 --- a/include/joey/tile.h +++ b/include/joey/tile.h @@ -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 diff --git a/src/amiga/hal.c b/src/amiga/hal.c index 889ea09..ae16a7d 100644 --- a/src/amiga/hal.c +++ b/src/amiga/hal.c @@ -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 diff --git a/src/atarist/hal.c b/src/atarist/hal.c index 2f0c963..a8284e6 100644 --- a/src/atarist/hal.c +++ b/src/atarist/hal.c @@ -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 diff --git a/src/atarist/stTile.h b/src/atarist/stTile.h index e77f78d..41aa5ab 100644 --- a/src/atarist/stTile.h +++ b/src/atarist/stTile.h @@ -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 diff --git a/src/atarist/tileMap.s b/src/atarist/tileMap.s new file mode 100644 index 0000000..d6ff545 --- /dev/null +++ b/src/atarist/tileMap.s @@ -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 diff --git a/src/core/port.h b/src/core/port.h index 00763d6..0bc1ec0 100644 --- a/src/core/port.h +++ b/src/core/port.h @@ -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) diff --git a/src/core/surface.c b/src/core/surface.c index 41711ad..0e120e3 100644 --- a/src/core/surface.c +++ b/src/core/surface.c @@ -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; diff --git a/src/core/tile.c b/src/core/tile.c index e54befa..661ac57 100644 --- a/src/core/tile.c +++ b/src/core/tile.c @@ -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; diff --git a/src/generic/genericSurface.c b/src/generic/genericSurface.c index 036e715..a6dfec0 100644 --- a/src/generic/genericSurface.c +++ b/src/generic/genericSurface.c @@ -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 diff --git a/src/iigs/joeyDraw.s b/src/iigs/joeyDraw.s index f9d11f9..d0213f0 100644 --- a/src/iigs/joeyDraw.s +++ b/src/iigs/joeyDraw.s @@ -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 + + ; -------------------------------------------------------------------- diff --git a/tests/goldens/uber/amiga.txt b/tests/goldens/uber/amiga.txt index e55e91d..b08b241 100644 --- a/tests/goldens/uber/amiga.txt +++ b/tests/goldens/uber/amiga.txt @@ -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 diff --git a/tests/goldens/uber/atarist.txt b/tests/goldens/uber/atarist.txt index 27bd8ef..a5b452b 100644 --- a/tests/goldens/uber/atarist.txt +++ b/tests/goldens/uber/atarist.txt @@ -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 diff --git a/tests/goldens/uber/dos.txt b/tests/goldens/uber/dos.txt index 90bd482..95adc4a 100644 --- a/tests/goldens/uber/dos.txt +++ b/tests/goldens/uber/dos.txt @@ -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 diff --git a/tests/goldens/uber/iigs.txt b/tests/goldens/uber/iigs.txt index 8cc3f42..e451669 100644 --- a/tests/goldens/uber/iigs.txt +++ b/tests/goldens/uber/iigs.txt @@ -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