# NATIVE-PERF-PLAN: match hand-coded native, per port Directive (Scott, 2026-07-06): floor parity against the IIgs is not the bar anymore -- "still way too slow across the board." The new target is **each port within ~1.5x of what hand-coded native technique achieves on that hardware**, proven by cycle-math ceilings and workload benchmark rows, not vibes. The IIgs-floor rule remains a secondary invariant (never ship a 68k port slower than the 65816). Why the current table lies about games: UBER measures best-case single ops (aligned sprite x, one tile, one call). Real games pay (a) the alignment cliff -- only aligned-x sprite draws are compiled; other x falls to interpreters/per-pixel walkers 3-20x slower -- and (b) a measured ~35-50% per-call wrapper tax (dispatch + validation + dirty-mark) around cores that are already at silicon parity, and (c) have no batch paths, so a tilemap repaint pays that tax per tile. Frame-budget reality check (2026-07-06 table): at 25% CPU and 50/60 Hz, ~3 sprites/frame on ST/Amiga/IIgs; a 40x25 tilemap repaint costs ~0.44-0.64s on the 68k ports. Hand-coded games on these machines did far better. Close that gap. Ground rules (inherited from PERF-AUDIT-PLAN, all still binding): - Source toolchains/env.sh before any make; delete stale binaries after .s edits; Scott owns git; style rules apply. - Every phase is hash-gated: existing goldens stay bit-identical (diff-uber-hashes 45/45 x4) unless a phase EXPLICITLY adds rows, in which case new goldens are frozen only after 4-port cross-identity. - Every phase ends with a 4-port bench + PERF.md regeneration. - Cycle models are hypotheses; the emulator measurement decides (lesson: the fillCircle asm probe and the 17% ST clock bug). ## Phase 0 -- Ceilings + honest workload rows [items 1+2 DONE 2026-07-06] ### Item 1 RESULT -- native-technique ceilings (cycle-math, emulator-class) Ops/sec, hand-coded best technique per port. "Meas" = current UBER (2026-07-06, millis clock). Gap = ceiling / measured. Full derivations with instruction-level arithmetic are in the Phase-0 workflow reports (session scratchpad); re-derive on demand -- the arithmetic styles were validated where models met measurements (ST present model 53/sec vs 56 measured; IIgs 29/sec present ceiling already in PERF.md). | Op (16x16 sprite, 8x8 tile) | ST ceiling | meas | Amiga ceiling | meas | IIgs ceiling | meas | DOS ceiling | meas | |---|---|---|---|---|---|---|---|---| | Sprite draw, arbitrary x | ~2,235-2,750 (pre-shifted movem RMW) | 585 aligned / far less unaligned | ~3,600-4,300 (blitter cookie-cut; CPU-compiled ~1,350) | 903 aligned only | 4,300 aligned / 2,490 odd (compiled stores) | 907 | ~11,000 | 6,094 | | Sprite save or restore | ~5,300 (move.l window) | 703/553 | ~6,300-7,400 (A->D blit) | 567/553 | ~2,520 (MVN) | 543/568 | ~14,500-18,000 | 8,964/5,744 | | Tile paste 8x8 | ~12,700-15,500 | 1,574 | ~10,700 | 2,253 | ~5,830 | 1,803 | ~57,000 | 9,454 | | 40-tile row repaint | 352-880 | -- | ~316 | -- | ~190 | -- | ~1,900 | -- | | Present (native answer) | page flip ~free + dirty bands (full copy ~53/s) | 56 | page flip ~free (8 copper pokes; full blit ~110/s) | 362* | dirty bands (full PEI 20-29/s; real HW ~14-32) | 68* | dirty bands (real ISA full blast 10-31 fps!) | 354* | *Present measurement artifacts found: Amiga/DOS measured presents EXCEED their physical full-copy ceilings because UBER's timed present loop never re-dirties the stage -- after the first copy it measures the dirty-union idle early-out, not a copy. IIgs 68 is the known SEI inflation. The gameFrame workload row (below) fixes this by dirtying every call. Headline: the wrapper tax is universal and dominates -- tile paste runs 3-6x (up to ~10x ST, ~90% overhead DOS) below hand-coded ceilings on EVERY port with cores already at parity; IIgs C glue measured at ~800-4,000 cycles per call (jlDrawPixel = 820 cycles for a 1-byte op). Sprite ceilings are 2.5-4x above current aligned numbers (ST via movem long-RMW + all 16 phases; Amiga via the BLITTER, which also frees the CPU -- its compiled-CPU path caps at ~1,350). **ERRATUM 2026-07-07: every ST "meas" number above (and every ST number anywhere dated before 2026-07-07) was captured with ~49.5% of the CPU consumed by the idle 12.3 kHz Timer-A audio ISR that jlpAudioInit started unconditionally (fixed in src/atarist/audio.c: demand-driven pump; PERF.md artifact #4). Honest ST values are ~2x the table above: tile paste 3,203/s (gap to ceiling 4.0-4.8x, not ~10x), aligned sprite draw 1,188/s (gap 1.9-2.3x), gameFrame 19 fps. The probe also measured the true ST layer split for tile paste: port op ~1,094 cyc + dirty mark ~804 + wrapper/call ~590 = 2,484 cyc/call, within ~11% of instruction-count models (the remainder is ST 4-cycle bus rounding) -- the cycle-model methodology is validated. Amiga layer split from the same probe: full call ~3,120 cyc, port op alone ~1,719 (chip-RAM contention), steady-state mark ~970. Amiga/DOS/IIgs audited for the same bug class: clean (~1.8% / ~0.5% / 0% idle).** ### Item 2 RESULT -- alignment-cliff code truth (supersedes all memory) SPRITE_SHIFT_INDEX (spriteInternal.h:24-30): Amiga x&7, ST (x&7)?2:((x>>3)&1), chunky (IIgs/DOS) x&1. Emitters: | Port | DRAW compiled | SAVE/RESTORE compiled | Fallback at NOT_COMPILED | |---|---|---|---| | IIgs | shifts 0+1 = EVERY x | shifts 0+1 = every x | (n/a -- no cliff) | | DOS | shifts 0+1 = EVERY x | shifts 0+1 = every x | (n/a -- no cliff) | | ST | 0,1 only (x%8==0) | NEVER (stubs) | draw: byte-wide shifted c2p walker (hal.c:1815+); save/restore: asm byte copy when x%8==0 else per-pixel pair walker | | Amiga | shift 0 ONLY (x%8==0) | NEVER (stubs) | draw: shifted c2p walker (hal.c:1491); save/restore: plane-major copy when x%8==0 else pair walker | So the cliff is 68k-ONLY: 7/8 x positions run walkers on ST/Amiga, and jlSpriteSaveAndDraw's combined fast path NEVER fires on 68k (SAVE never compiled). IIgs/DOS need nothing in Phase 1. Phase-1 design constraints discovered: (a) ST's shift-2 bucket cannot hold compiled code -- ST needs SPRITE_SHIFT_INDEX changed to x&7 (a dispatcher-contract change) plus emitter support; (b) routineOffsets are uint16_t so one sprite's compiled bytes must stay < 64KB -- 8 DRAW variants of a mixed 32x32 (~8-16KB each) blow it; needs selective emission or offset-scheme change (16x16 sprites fit fine); (c) the jlSpriteRestoreUnder gate accepts only copyBytes == bytesPerRow or +1 (spriteCompile.c:547-552) -- planar shifted saves record +4, so the gate must widen or 7/8 alignments stay interpreted even after emitters land; (d) stale doc: sprite.h:32-40 contradicts spriteInternal.h:16-23 (fix during Phase 1); (e) uber.c:972 comment claims Amiga has 8 variants -- aspirational, false today. ### Item 3 -- workload rows [IN PROGRESS] Four rows appended AFTER the surfaceMarkDirtyRect timeOp (cumulative- hash safety: correctness checks clear the stage first, so the frozen 45 stay bit-identical; Phase-7 precedent): - "jlSpriteDraw unaligned" -- x=71,y=120 (odd AND x%8==7: worst case on all four ports simultaneously). - "jlSpriteDraw sweep16" -- all 16 x phases (208..223) inside ONE call so the op is idempotent regardless of per-port iteration counts. - "jlTilePaste map 10x5" -- 50 pastes of two SNAPPED patterned tiles (tile pixels are platform-private; only snap round-trips portably). - "gameFrame composite" -- 256x96 repaint + 16 HUD tiles + 3 sprite saveAndDraw + 3 restores + present, net-idempotent per call; all sprite x mod-16-aligned so this row isolates wrapper tax + present (rows above own the alignment signal). Also fixes the present-row artifact (dirties every call). Golden contract goes 45 -> 49. Refreeze procedure: regression gate first (each port: exactly 45 match + 4 extra vs OLD golden), then cross-identity (49/49 IIgs vs each), only then freeze + regenerate PERF.md. Gate: ceilings documented (DONE above); new rows golden'd x4; existing 45 untouched. ## Phase 1 -- Kill the alignment cliff All-shift compiled sprites everywhere. ST: emit shifts covering all x%16 phases in spriteEmitInterleaved68k (pre-shifted plane bytes + masks spanning byte halves/groups; the Amiga 8-variant planar emitter is the proven template). IIgs/DOS: odd-x nibble-shifted DRAW variants (chunky). Save/restore already widen via shift where applicable -- verify per port. Arena sizing: 8x DRAW variants (Amiga precedent: 8-32KB arena; re-check DEFAULT_CODEGEN_BYTES per port). Gate: unaligned-sprite UBER rows reach ~parity with aligned rows on every port; goldens hold. ## Phase 2 -- Strip the per-op tax Plane-pointer caching (kill per-draw jlpSurfacePlanePtr x4), flattened hot-op dispatch, inline dirty marks for the remaining hot ops (extend the 2026-07-06 surfaceMarkDirtyRows inline that bought 2-5%), remove redundant validation layers (core wrapper already proves s/bounds). Gate: small-op rows (tileFill/Paste/Copy, sprite ops, fillRect small) move measurably toward Phase-0 ceilings on all ports; goldens hold. ## Phase 3 -- Batch APIs jlTileMapPaste (N tiles, one validation, one dirty union -- the jlDrawText pattern generalized) + sprite batch draw/save/restore. API shape needs Scott's sign-off before landing in public headers. Gate: tilemap workload row approaches ceiling (per-tile overhead ~0); goldens hold (new API rows get their own goldens). ## Phase 4 -- Port-native heavy weapons Amiga: blitter cookie-cut sprite draw for larger-than-16x16 objects; blitter tile/tilemap paste evaluation. ST: evaluate movem-based row engines + restore-from-clean-buffer strategy for backgrounds. IIgs: PEI-slam variants for batch paths where applicable. Gate: per-op measurements vs ceilings; keep only what beats the compiled-CPU path on the emulator (fillCircle lesson: no dead asm). ## Phase 5 -- Acceptance Every workload row within ~1.5x of its Phase-0 ceiling per port, or a documented cycle-proof that the remaining gap is structural (e.g. ST interleaved-planar RMW). PERF.md gets a ceilings column. Update the perf-directive memory with the outcome. ## Progress tracker - [x] Phase 0 COMPLETE 2026-07-06. Ceilings + code-truth in this file (above); four workload rows landed in uber.c and golden'd: regression gate exactly 45 match + 4 extras on every port, cross-identity 49/49 x4 (bonus: sweep16 validated the ST/Amiga shifted c2p walkers pixel- perfect at all 14 untested phases). Golden contract is now 49 lines. BASELINE the later phases must move (ops/sec, millis clock): jlSpriteDraw unaligned: ST 40 (5% of floor!) | Amiga 75 (9%) | IIgs 842 | DOS 5115 -- vs aligned 607/903/902/6218 = the 15x/12x 68k cliff, Phase 1's gate. jlSpriteDraw sweep16 (x16 = draws/sec): ST 32 | Amiga 80 | IIgs 896 | DOS 5648 -- real arbitrary-x sprite rate. jlTilePaste map 10x5 (x50 = tiles/sec): ST 1600 | Amiga 2300 | IIgs 1750 | DOS 10000 -- Phase 2/3's gate (ceilings say 10-15k on 68k, 57k DOS). gameFrame composite: ST 9 | Amiga 18 | IIgs 10 | DOS 18 fps -- the bottom-line number; the program ends when this is at/near the port's refresh-limited ceiling or a documented structural bound. - [~] Phase 1 DRAW SIDE COMPLETE 2026-07-06 (steps 1-4 of the adversary-reconciled order; save/restore = step 5 still open): step 1 fused per-port shift ladder (ST 16/x&15, Amiga 8, chunky 2; IIgs cache fields #ifdef'd) -- gated 49/49 x4 bit-identical; step 2 plan-based jlSpriteCompile (measure-all, uint16-cap greedy, draws tier 1 / save+restore atomic pairs tier 2, arena-full degrade to SPRITE_DEGRADE_DRAW_MASK, 68k-only even-size guard); step 3 Amiga all-shift JIT (accumulator merge, shift-0 byte-identical, host- verified); step 4 ST 16-phase draw (spriteWalk.s masked-movem walker + per-phase 22B thunk + 12B/group/row pre-shifted tables). GATE: 49/49 x4; unaligned ST 40->500 (12.5x), Amiga 75->803 (10.7x); sweep16 ST 2->32, Amiga 5->50; unaligned now ~82-84% of aligned on both. BUGS FOUND BY THE GATES: even-size guard demoted odd-sized IIgs/DOS routines to the interpreter (fixed: 68k-only); Amiga bench task stack was the shell 4KB default -- the deeper compile path overflowed it nondeterministically (fixed: stack 32768 in bench-amiga.sh + emitter accumulators to file statics). Steps 5+6 COMPLETE 2026-07-06 -- **PHASE 1 DONE**, final gate 49/49 x4. Step 5: group-window save/restore emitters both ports (2 width classes/sprite, IIgs-MVN-style; ST raw-span move.l chains, Amiga plane-run move.l/move.w), format-law gates (SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS / SPRITE_BACKUP_PTR_OK in spriteInternal.h; restore derives class from RECORDED geometry, (bx&15)==0 term is correctness-critical), ST HAL raw-span interpreted branch (save + restore switched atomically incl. the 16x16 asm reroute), planar metadata-probe widening, JOEY_SPRITE_BACKUP_BYTES capacity macro + all 4 example owners migrated and 2-aligned. SaveAndDraw's combined path fires on 68k at every x for the first time. Step 6: DEFAULT_CODEGEN_BYTES 64KB Amiga/ST; doc sweep done. RESULTS (floor-relative): Amiga Save 276% / Restore 236% / SaveAndDraw 160%, ST 203% / 163% / 107%; Amiga gameFrame 18 -> 23 fps. Host-verified emitted streams (objdump); every intermediate gate 49/49 x4. - [x] INTERLUDE 2026-07-07 (pre-Phase-2, found by the Phase-2 V1 probe): ST idle-audio ISR tax discovered and fixed -- see the Phase-0 ERRATUM above and PERF.md artifact #4. All four ports re-gated 49/49 after a power-failure clean-rebuild recovery AND after the audio fix (pixels untouched). The Phase-2+ gate baselines are now (millis clock, post-fix): jlSpriteDraw unaligned ST 992 / Amiga 803 / IIgs 845 / DOS 5118; sweep16 ST 64 / Amiga 50; jlTilePaste single ST 3203 / Amiga 2253 / IIgs 1741 / DOS 9806; map 10x5 (x50 = tiles/s) ST 3100 / Amiga 2300 / IIgs 1750 / DOS 10000; gameFrame ST 19 / Amiga 23 / IIgs 10 / DOS 18 fps. Floor parity: the ST has ZERO below-floor cells for the first time; remaining below-floor is Amiga-only (circle family + unaligned 95% / sweep16 89%) + DOS audioFrameTick (gates nothing). - [x] Phase 2 COMPLETE 2026-07-07 (final gate 49/49 x4 + iigs-verify; detail below). Steps D3 closed it: R6 TMASK_BYTE macro inline (copyMasked +9%, loop-back needed brl past 8-bit branch range) and R10 iigsDrawPixelMark fused plot+mark (jlDrawPixel 3,179 -> 5,576, +75%; batching plotters keep the unfused inner). EXIT NUMBERS: tilemap tiles/s ST 4,050 / Amiga 3,750 / IIgs 2,950 / DOS 12,900 (from 1,600 / 2,300 / 1,750 / 10,000); gameFrame ST 21 / Amiga 25 / IIgs 10 / DOS 19; IIgs tilePaste gap-to-ceiling 2.15x. Remaining per-call floor = public wrapper + one JSL/call -> Phase 3's job. Optional non-gating leftovers: R7 saveAndDraw consolidation, R12 IIgs C-residue reshapes. Recon: 6-agent workflow + merged synthesis (12 ranked items R1-R12) + calibrated-probe layer split (see ERRATUM above). Step A LANDED (gate 49/49 x4): plane-pointer inlining via new src/amiga/amigaPlanar.h + src/atarist/stPlanar.h internal headers (jlpSurfacePlanePtr stanza deleted -- its only callers were spriteCompile.c's 68k dispatchers), dead-validation cleanup in both 68k HALs. Amiga sprite ops +10-23% (SaveUnder 1503->1853), ST +3.5-5.2%; Amiga unaligned draw now ABOVE floor (903 vs IIgs 845). Step B LANDED (gate 49/49 x4): whole-tile dirty mark specialized in tile.c (band = [bx*2, bx*2+1] with compile-time proof, 8-row widen unrolled ~480 cyc vs 804 measured; IIgs keeps its asm marker). Tile rows: ST +9-15%, Amiga +13-18%, DOS +24-29%, map row ST 72 / Amiga 53 / DOS 258; tileSnap (no mark) flat everywhere = clean control. Step C LANDED (gate 49/49 x4): jlDrawRect raw-edge fills + ONE mark (band-interval proof in draw.c) -- ST +25% / Amiga +40% / DOS +22% on the drawRect row; the x2 widen unroll in surfaceMarkDirtyRows is PER-PORT by measurement: it lifted Amiga sprite rows +4-6% but cost the ST -5% in BOTH the pointer-walking and counted forms (gcc-mint materializes 4 live pointers either way; simple loop re-measured exactly at ST baseline; DOS flat) -- so Amiga unrolls, everyone else keeps the simple loop (documented at the #if in surface.c). Step E1 LANDED (gate 49/49 x4): R4a = ST/Amiga tile ops moved to always-inline per-port headers (stTile.h / amigaTile.h; jlpTile* macro-aliased in port.h before the dispatch tail, headers included at its END so the portData==NULL fallbacks see the jlpGenericTile* prototypes; ST_BYTES_PER_ROW/GROUP moved to stPlanar.h; surface68kStTileFill8x8 extern moved to stTile.h) -- ST tiles +9-14%, Amiga +5-11%, DOS flat (already single-layer). R9 = ST/Amiga/DOS presents consume gStageScbDirty/gStagePaletteDirty instead of memcmp'ing 512-712-byte cached mirrors (mirrors + Amiga memcmpLongs deleted; stageAlloc re-arms both flags for jlShutdown/jlInit cycles) -- gameFrame ST 19->21 / Amiga 23->25 / DOS 18->19 fps. Step E2 LANDED (gate 49/49 x4): R8 Amiga 8-row d16(An) unrolls in amigaTile.h (fill/copy/paste/snap; copyMasked/pasteMono stay rolled) -- tilePaste +36%, tileSnap +52%, tileFill +32%, tileCopy +28%, map row 56->75. SESSION NET (from honest post-audio-fix baselines): map row tiles/s ST 3,100->4,000 / Amiga 2,300->3,750 / DOS 10,000->12,900; gameFrame ST 19->21 / Amiga 23->25 / DOS 18->19 (and vs the pre-audio-fix world: ST gameFrame 9->21). PERF.md table regenerated from the E2-gate logs. Step D1 LANDED (IIgs gate 49/49 + make iigs-verify framebuffer check; other ports covered by the D1-gate C-side pass): R2 fused stage-tile entries iigsTileFillMark/PasteMark/CopyMark in joeyDraw.s (.text.iigsFusedTile -- ONE section for entries + the stageTileMark helper; a cross-section 16-bit jsr wild-jumped into $00CFxx twice before the section-atomicity rule was learned, see the cross-section-jsr feedback memory). One JSL now does derive($01:2000 + gRowOffsetLut[by*8] + bx*4) + op via long,X + band widen. IIgs: tileFill +64% (3,251), tilePaste +61% (2,710), tileCopy +38%, map row 35->59 (+69%), gameFrame 10->11 fps; tilePaste gap to ceiling 3.5x -> 2.15x. port.h stage-gates the three macros; tile.c's tileMarkDirtyFused knob keeps copyMasked/pasteMono on the separate marker. Step D2 LANDED (gate 49/49 x4): R5 spriteCompiled* dispatchers moved to src/core/spriteDispatch.h as always-inline, consumed only by sprite.c (extern decls removed from spriteInternal.h; compile-time machinery stays in spriteCompile.c). Sprite rows: IIgs +6-13% (SaveAndDraw 423), ST SaveUnder +15%, Amiga +5-11%, DOS +2-16%. Measurement notes recorded in PERF.md: gameFrame quantizes +/-1 fps run-to-run; DOS unaligned-draw row is bimodal (~3,800/~5,100) across identical binaries -- don't chase it. REMAINING: R6 copyMasked tmaskByte de-layering (32 jsr/call = 384 cyc + BSS 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. - [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