1164 lines
74 KiB
Markdown
1164 lines
74 KiB
Markdown
# 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. REVISED SCOPE per the 2026-07-07 four-agent levers
|
|
recon (gap audit + composite forensics + IIgs sprite autopsy +
|
|
Amiga/audio; full reports in the session workflow transcripts):
|
|
(a) the `jlStagePresent full` row is an ARTIFACT on all four ports
|
|
(idle early-out; IIgs also SEI-inflated) -- there is NO honest
|
|
standalone present number; Phase 5 must add probe rows P1-P4
|
|
(present96 / present3win / idle present / frameNoPresent + copied-
|
|
bytes counters, IIgs cross-checked against the MAME Lua clock)
|
|
BEFORE gating composites. Forensics already attribute the composite
|
|
gap to dirty-present costs: DOS gameFrame is AT ceiling
|
|
(present-bound, ~1.7us/VGA-byte honest); Amiga present collapses
|
|
per-row bands to ONE bounding box (hal.c:761-801) and over-copies
|
|
~5x on scattered sprites (per-row bands like the ST = the fix); ST
|
|
pays ~7-9ms FIXED per dirty present in two 200-row C scans (+
|
|
possible byte-looped row memcpy -- probe decides); IIgs promotes
|
|
any >=32-word band to a full 160-byte row slam with 6 slow softswitch
|
|
toggles per row (~27-32ms for the gameFrame band) and its SEI
|
|
freezes the bench clock, so IIgs gameFrame reads HIGH -- true ~7.5-
|
|
8.5 fps, and gameFrameClean carries ~30-40ms/frame unattributed
|
|
(llvm-mos far-memcpy byte loops suspected). (b) Ceilings needing
|
|
re-derivation before gating: ST tile (movep-honest ~350 cyc/tile),
|
|
Amiga tile pair (two Phase-0 numbers disagree 18%), IIgs sprite
|
|
(paper 4,300/s assumed no derive/mark/wrapper; honest full-op
|
|
ceiling ~2,500-2,650/s with per-row marks -- autopsy modeled the
|
|
measured 2,900 cyc within 2%: body 23%, MARK PATH 30%, derive+call
|
|
17%, validation 22%). (c) Decide whether drawLine rows gate
|
|
acceptance (~3-4x everywhere, wrapper-dominated, small absolute).
|
|
- [~] Phase 6 APPROVED by Scott 2026-07-07 -- "everything except the
|
|
audio" (candidate #5 audio-active tax is PARKED pending discussion
|
|
with Scott; do NOT start audio work). Execution waves:
|
|
W0 probes (P1-P4 + counters + IIgs Lua cross-clock; baselines).
|
|
W0 ST BASELINE (2026-07-07, probe build = -DUBER_PROBE scratch UBER;
|
|
counters landed in surfaceInternal.h/surface.c + the 3 C presents):
|
|
P1 present96 = 37 ops/s = 27.0 ms(!) for the 96-row dirty present --
|
|
copied exactly 12,288 B (no over-copy), scanned 496 rows (200 union
|
|
+ 96 copy + 200 snapshot); P2 present3win = 80 ops/s = 12.5 ms for
|
|
3 tiny windows => ST dirty-present FIXED cost ~12.5 ms (suspects:
|
|
per-present SCB flatten + the two 200-row scans); copy rate ~9.5
|
|
cyc/B (~4x slower than move.l memcpy should be -- byte-loop suspicion
|
|
CONFIRMED, W2 target); P4 frameNoPresent = 54 ops/s = 18.5 ms ==
|
|
component sum 18.15 (components HONEST, no interleaving mystery).
|
|
Ledger: P4 + P1 = 45.5 ms vs gameFrame measured 45.0 -- CLOSED.
|
|
W0 AMIGA BASELINE: P1 = 50 ops/s = 20.0 ms (copied 12,288 B = solid
|
|
band, no over-copy possible); P2 = 71 ops/s = 14.1 ms with copied
|
|
5,309 B vs ~1,536 truly dirty = the bounding-box union 3.5x
|
|
OVER-COPY CONVICTED by its own counter (48-row x 26-B enclosing
|
|
rect); fixed dirty-present cost ~12 ms (same class as the ST's);
|
|
P4 = 18.9 ms == component sum 18.92 -- honest. Ledger P4 + P1 =
|
|
38.9 vs measured 40.0 -- CLOSED. Both 68k ports: the ~12 ms FIXED
|
|
per-dirty-present cost is W2's biggest single target.
|
|
W0 DOS BASELINE: P1 = 23 ops/s = 43.5 ms (copied 24,576 VGA bytes =
|
|
exactly the 96x256 expand -- hardware-rate LUT write, matches the
|
|
forensic 40-44 ms model); P2 = 305 ops/s = 3.3 ms, copied 768 B =
|
|
zero over-copy (per-row bands); P4 = 9.2 ms ~= component sum.
|
|
Ledger P4 + P1 = 52.7 vs measured 52.6 -- CLOSED PERFECTLY. DOS is
|
|
present-bound at hardware rate: NO W2 work on DOS. (Probe-run
|
|
gotchas recorded: djgpp probe EXEs need $DOS_EMBED_DPMI or they
|
|
hang; a case-colliding UBER.exe/UBER.EXE pair breaks the DOSBox
|
|
mount.)
|
|
W0 IIGS BASELINE + CLOCK VERDICT (heartbeat-window Lua cross-clock,
|
|
probe-iigs.sh in the session scratchpad): the GetTick bench clock is
|
|
HONEST post-SEI-chunking -- tick-loss ~1.00 on 40 of 42 rows
|
|
INCLUDING the dirty-present slam (the 40-row SEI chunks + CLI
|
|
windows let the latched VBL IRQ catch up); only jlSurfaceClear
|
|
still loses (1.21x, mildly inflated row). The old SEI-inflation
|
|
reading of `jlStagePresent full` 67/s was actually the same
|
|
IDLE-present early-out artifact as the other ports. HONEST dirty
|
|
present: P1 present96 = 17 ops/s = ~59 ms(!) for a 96-row band --
|
|
worse than the 34 ms full-screen PEI floor, confirming the
|
|
>=32-word full-row promotion + 6-softswitch-per-row waste; P2
|
|
present3win = 48/s = ~21 ms fixed; P4 frameNoPresent = 26/s =
|
|
38.5 ms. Ledger P4 + P1 = 97 ms vs gameFrame measured ~95 --
|
|
CLOSED. IIgs W2 target = the slam fixed cost + row promotion; W1
|
|
fusions own the P4 side. TOOLCHAIN FLAG for Scott: a -DUBER_PROBE
|
|
uber.c whose probe block reads extern uint32_t globals through a
|
|
fn-pointer wrapper (probeOp) dies before main on llvm-mos --
|
|
bisected (trivial-op probe launches, wrapper variants do not,
|
|
optnone does not help, segment count 5 vs 6 irrelevant); worked
|
|
around by compiling the wrapper out on IIgs (uber.c PROBE_ATTR
|
|
block). W0 COMPLETE: baselines x4, ledgers closed x4, goldens
|
|
re-gated 51/51 x4 after the gameFrameBody refactor.
|
|
[Power failure after W0; recovery gate re-passed 2026-07-07: clean
|
|
rebuild + iigs-disk, 51/51 x4 (Amiga solo TIMEOUT=600), headline
|
|
rows match W0 baselines exactly (IIgs draw 966 / gameFrame 10, ST
|
|
21, DOS 19, Amiga 25 ops/s). Only loss: scratchpad probe-iigs.sh
|
|
(recreatable). W1 design in progress.]
|
|
W1 ITEM "sprite-mark specialization" LANDED 2026-07-07 (design +
|
|
4-agent audit; sprite.c only): spriteMarkDirty Step-B unrolled
|
|
16/8-row widen + spriteMarkDirtyDraw/Restore fusion-seam spellings.
|
|
Gates: IIgs OBJECT-CODE BYTE-IDENTICAL (llvm-mos -O2 old-vs-new
|
|
sprite.o cmp -- provably inert there); new P5 spriteBandSum probe
|
|
row (djb2 fold of both band arrays after compiled/interpreted/
|
|
clipped/unaligned marks) IDENTICAL pre/post AND cross-port
|
|
(1345495317 on ST/Amiga/DOS); P1/P2/gameFrame copied+scanned
|
|
counters byte-identical; 51/51 hashes x3 (IIgs exempt by object
|
|
proof; DOS needed a solo re-run -- parallel-load TIMEOUT truncation
|
|
again). Bench: save control EXACTLY flat on all 3 (2611/2103/10743);
|
|
restore ST 1781->1900 (+6.7%) Amiga 1453->1603 (+10.3%) DOS
|
|
6218->7825 (+25.8%); draw ST 1283->1317 Amiga 1103->1153 DOS
|
|
6404->8147 (+27%); saveAndDraw ST 934 Amiga 803 DOS 5480.
|
|
New probe rows also added: P6 fillBandSum (fused fillRect battery,
|
|
all 4 ports -- cross-port identity gates item 2's asm mark) and a
|
|
probeOp-wrapped gameFrame counters row.
|
|
W1 ITEM "IIgs fused fillRect" LANDED 2026-07-07 (audit fixes C5
|
|
adc frm_w, C6 fillCircle spans -> NoMark twin, C7 .a16/.i16):
|
|
iigsFillRectInner converted in place to iigsFillRectMark/NoMark in
|
|
.text.iigsFusedRect with shared stageRectMark helper (packed-band
|
|
ABI: A=(max<<8)|min, X=yStart, Y=rows -- item 3's sprite entries
|
|
jsr it too), row-invariant slam-dispatch hoists (~64 -> ~30
|
|
cyc/row), fused band widen in friExit; port.h jlpFillRect(Mark) +
|
|
jlpFillRectNoMark + JL_FILL_RECT_MARK_FUSED seam; draw.c compiles
|
|
the C mark out on IIgs, jlDrawRect edges + fillCircle spans take
|
|
the NoMark twin. Gates: draw.o BYTE-IDENTICAL on ST/Amiga/DOS
|
|
(item is a proven no-op there; item-1 hash gates stand); P6
|
|
fillBandSum 2155901235 IDENTICAL IIgs-fused-asm vs all 3 C-mark
|
|
ports (odd-x/odd-w edges, x=319 column, midBytes==1, NoMark
|
|
batching all covered); IIgs 51/51; P1/P2 unchanged, P4 26 -> 28
|
|
ops/s (38.5 -> ~35.7 ms); iigs-verify PASS. IIgs bench: fillRect
|
|
16x16 602 -> 785 (+30%, floor was 740), 80x80 110 -> 133, markRect
|
|
row 323 -> 399, drawRect 100x100 73 -> 110 (+51%, way past the +1%
|
|
model -- extra win unmodeled per-call ABI+hoists on the 1px edges),
|
|
drawLine H 1142 -> 1322, fillCircle 42 FLAT (C6 NoMark proof),
|
|
gameFrame 10 -> 11. No row regressed.
|
|
W1 ITEM "IIgs fused sprite entries" LANDED 2026-07-07; the R12
|
|
fnAddr cache was BUILT, MEASURED, AND REMOVED (the emulator
|
|
decides). Landed shape: iigsSpriteDrawMark / iigsSpriteRestoreMark
|
|
in .text.iigsFusedRect (push-target-1+rtl long-call into the arena
|
|
routine, then jsr stageRectMark; args re-read from the dp-parked
|
|
JSL frame post-call -- callee preserves D/DBR per the emitter
|
|
contract); C glue spriteCompiledDrawMark / spriteCompiledRestoreMark
|
|
derive fnAddr the old 3-load way; jlSpriteSaveAndDraw pairs the
|
|
unchanged compiled save with the fused draw (the pair's ONE mark
|
|
moved into asm); save path byte-reverted to baseline.
|
|
CACHE AUTOPSY (2 variants benched): v1 epoch-checked per-(shift,op)
|
|
cache inlined at the dispatch arms -- save (cache-only, no fusion)
|
|
REGRESSED 602 -> 543 and saveAndDraw 423 -> 399; v2 moved the arms
|
|
into small noinline helpers (setupPalette precedent) -- WORSE still
|
|
(draw 1142 -> 1026: llvm-mos arg re-marshalling through a JSL
|
|
boundary costs more than the spills it saves). Conclusion recorded
|
|
in spriteDispatch.h: on llvm-mos the epoch hit path costs as much
|
|
as the 3-far-load derive it replaces; do NOT resurrect without a
|
|
compiler-level win (LLVM816-ASKS candidate: cheaper far loads /
|
|
better 32-bit value handling). Arena epoch counter removed with it.
|
|
v3 (landed) IIgs numbers: draw 966 -> 1142 (+18%), unaligned
|
|
907-ish -> 1022, restore 545 -> 662 (+21.5%), save 602 FLAT
|
|
(baseline restored exactly), saveAndDraw 423 FLAT (llvm-mos frame
|
|
effects suspected of eating the pair's fused-mark saving -- one
|
|
follow-up candidate), gameFrame 10 -> 11, gameFrameClean 18 flat.
|
|
Plan's draw 1,700-2,200 / restore 1,600+ targets assumed the cache
|
|
and validation trims: measurement says fusion alone buys +18-22%
|
|
and the remaining fat is the C validation/wrapper chain inside the
|
|
register-starved public wrappers -- next lever is moving validation
|
|
into the fused asm (contract change, W2+ candidate), not caching.
|
|
Gates: 51/51 x4; P5 spriteBandSum 1345495317 and P7 compactEpoch
|
|
hash CA9CDB56 / bandSum 3922093832 IDENTICAL IIgs vs ST/DOS
|
|
(Amiga P7 SKIPPED -- its 8-variant arena cannot fit the two probe
|
|
sprites; pre-existing sizing, ST+DOS carry the reference); P6
|
|
unchanged; new P7 probe row added (victim/survivor compaction
|
|
content+band test, all ports).
|
|
- [x] W2 COMPLETE 2026-07-08 (all five items landed + gated x4
|
|
ports; final IIgs golden: 51/51, iigs-verify PASS, gameFrame 11 ->
|
|
12, gameFrameClean 18 -> 20, jlStagePresent full 67 -> 73 as
|
|
flagged). Wave scoreboard vs W0: ST P1/P2 +97%/+206%, gameFrame
|
|
+52%, gameFrameClean +88%; Amiga P2 +27% (over-copy 4992 -> 384),
|
|
gameFrameClean +9%; IIgs P1 +18%, P2 +8%, idle base -1.3 ms; DOS
|
|
untouched by design. Details per item below.
|
|
(2026-07-08, 3-designer + audit workflow; audit caught a
|
|
display-corrupting IIgs Stage-A mis-park [C-1], an ST run-close bug
|
|
[B-1], a probe-link #ifdef hole [B-2], and the first-iteration
|
|
union carry-in that would fail exact counter gates on CORRECT code
|
|
[A-1] -- steady-delta protocol added). Implementation order: item0
|
|
probes -> Amiga bands -> ST alignment -> ST slam asm -> IIgs Stage
|
|
A -> Stage B.
|
|
ITEM 0 LANDED (probe scaffolding + IIgs Stage-0 counters): P8
|
|
presentIdle row (2 warm-ups; isolates the per-present FIXED cost),
|
|
P9 IIgs shrParity (full display-vs-stage byte parity incl. both
|
|
predicate-flip arms), P10 ST displayMismatch, P11 Amiga
|
|
presentPageSum (copper-poke-derived front planes vs shadow, incl.
|
|
settle + true-idle arms), steady-delta second pass in probeOp + the
|
|
IIgs direct-log block, counters now incremented by the IIgs present
|
|
asm (slam +160/row, MVN +count+1, scanned +200/present; stale
|
|
.a8/.i8 at peiAllDone fixed). ALL display gates PASS on the
|
|
UNCHANGED presents (probes trusted): ST 0/0/0, Amiga fails=0, IIgs
|
|
badRows=0. Steady baselines: ST P1 12288/496 P2 384/448; Amiga P1
|
|
12288/400 P2 4992/400 (the convicted bounding-box over-copy, now
|
|
measured); DOS P1 24576/200 P2 768/200; IIgs P1 15360/200 P2
|
|
384/200 P8 copied=0 + 67 ops/s (~15 ms fixed base DIRECTLY measured
|
|
-- confirms the FITTED write model: ~444 us/promoted row, so the
|
|
honest IIgs Stage A/B target is P1 ~18.5-20 ops/s, NOT the paper
|
|
32; ~85% of a 96-row band is irreducible shadowed-write cost).
|
|
ITEM "Amiga per-row bands" LANDED: merged 200-row scan with
|
|
exact-span run coalescing feeding amigaPresentFlushRun (THIN/FAT
|
|
threshold kept -- P1's single run is geometry-identical to the old
|
|
bbox), counters honest per flush. Gates: P2 steady 4992 -> 384
|
|
EXACT + scanned 400, P1 steady 12288/400 EXACT, P11 PASS on the new
|
|
present, P5/P6 identical, presentIdle steady 0/200. Bench: P2 71 ->
|
|
90 ops/s (+27%), gameFrameClean 34 -> 37 (+9%), gameFrame 25 FLAT,
|
|
idle golden row 434 vs 437 (noise). ACCEPTED DEVIATION: probe row
|
|
P1 50 -> 46 (-8%, outside the audit's +-5%) -- the added
|
|
per-dirty-row scan cost (union fold + span derive + run compare,
|
|
gcc-amigaos register pressure) exposed by the present-only tight
|
|
loop; every GOLDEN row is flat or up, and the recorded follow-up is
|
|
an Amiga fused-scan asm on the ST presentSlam pattern once item 3
|
|
lands.
|
|
ITEM "ST alignment" LANDED (surface.c + ST prev arrays aligned(2)
|
|
-- NOT the designed aligned(4): the mint a.out object format caps
|
|
alignment at 2, and 2 is all the 68000 needs for move.l; own
|
|
relink commit per audit X-2, 51/51 x4 re-gated).
|
|
ITEM "ST present fixed-cost + copy rate" LANDED (presentSlam.s,
|
|
audit fix B-1 applied: run-close lives in the fast path after both
|
|
clean-compares, NOT the group head -- full-screen stays ONE memcpy):
|
|
ONE 200-row asm pass fuses union scan + copy + snapshot; 4-row
|
|
long-compare fast path on clean groups; partial rows copy via a
|
|
jsr'd 20-group move.l chain (no memcpy dispatch under 256 B);
|
|
full-width runs still batch through mintlib memcpy's movem path.
|
|
Gates: P10 displayMismatch 0/0/0 on the new asm; steady P1
|
|
12288/200 P2 384/200 EXACT (scanned collapsed 496/448 -> 200);
|
|
fingerprints identical; 51/51; objdump checklist (no jsr _memcpy in
|
|
jlpPresent, symbolic movem masks, chain entry 0x50(a6,d1.w)
|
|
verified). Bench: P1 37 -> 73 ops/s (+97%), P2 80 -> 245 (+206%),
|
|
gameFrame 21 -> 32 (+52%), gameFrameClean 48 -> 90 (+88%),
|
|
present-full 415 vs 403 (+3%, inside the +-5% gate), presentIdle
|
|
452 ops/s. The ~12.5 ms ST fixed cost is DEAD (P2 now ~4.1 ms incl.
|
|
marks+copy).
|
|
ITEM "IIgs slam hoist + threshold" LANDED in two bisectable stages
|
|
(audit C-1 fix: Stage A forces the run entry whole-row -- runMax=79
|
|
park, 160 B/row, table offset 0 -- so hoist correctness and
|
|
entry-math correctness gate separately under P9). Landed shape:
|
|
8-bit-M TOUCHED-test scan (min==0xFF <=> clean; sentinel pin #47
|
|
updated), two-term promotion predicate W >= 8 && (max+1) <= 1.5W
|
|
(replaces >= 33 words; fixes both UNDER-promoted left-anchored
|
|
mid-width bands and OVER-promoted right-anchored ones), run
|
|
gathering clipped at the 40-row SEI chunk budget, ONE softswitch
|
|
set/teardown per RUN (descending row slam; full-width rows leave SP
|
|
on the next row's top -- zero re-park), computed-entry PEI jump
|
|
table (Stage B: pushes words runMax..0), MVN rows reload DBR from
|
|
the phb copy at 3,s (frame-layout dependency commented), cli-only
|
|
chunk teardown, $E1:9DFE reused as the switches-immune SP-offset
|
|
park (old DBR stash deleted; gPeiTempRowBase/gPeiMvnSrc + two dead
|
|
rect-blit externs deleted). Design's `bcs peiAllDone` was OUT OF
|
|
RANGE (+700 B) -- fixed with a bcc/brl pair (the one design bug the
|
|
audit missed).
|
|
Gates: P9 shrParity badRows=0 at BOTH stages (incl. both
|
|
predicate-flip arms, row 0/199 runs, chunk-edge clip, mixed
|
|
slam+MVN presents); Stage A P1 copied stayed EXACTLY 15360 (the
|
|
C-1 mis-park would have read 13824 and faked Stage B); Stage B P1
|
|
copied dropped to EXACTLY 13824 (96x144, runMax=71 -- the counter
|
|
is the over-push instrument); P2 384/200 and P8 0/200 at every
|
|
stage; fingerprints identical throughout. Bench: P1 17 -> 18
|
|
(Stage A) -> 20 ops/s (Stage B, +18%, ~59 -> ~50 ms; the fitted
|
|
write model's prediction EXACTLY -- ~85% of the band is
|
|
irreducible shadowed-write cost, the paper-model 34ms floor is
|
|
refuted by three independent W0 rows); P2 48 -> 52 (+8%); P8
|
|
fixed base 67 -> 73 ops/s (~15 -> 13.7 ms). EXPECTED GOLDEN-ROW
|
|
DELTA (audit C-4): jlStagePresent full (the idle-present artifact
|
|
row, was 67/s) improves to ~73-74/s -- this is the same honest
|
|
idle-scan cheapening P8 measures, NOT a regression; the plan's old
|
|
"67/s artifact" figure is superseded. Lua cross-clock re-run SOFT
|
|
SKIPPED: every SEI hold strictly SHRANK (worst chunk 18.3 -> ~14.6
|
|
ms modeled) and GetTick honesty was proven at the LONGER holds;
|
|
the recreated harness adds nothing the P8/P9 gates don't cover.
|
|
- [x] W3 COMPLETE 2026-07-08 (all four items landed + gated; final
|
|
Amiga probe: steady counters exact, P11 PASS, fingerprints
|
|
identical). Wave scoreboard: ST tile singles +5..+31% (snap 8359),
|
|
DOS map row 2.7x (gap to ceiling 1.24x), Amiga gameFrameClean 37 ->
|
|
65 (+76%; 34 -> 65 = +91% across W2+W3) and map row +25%.
|
|
PHASE-6 ACCEPTANCE ARTIFACTS DONE 2026-07-08: clean rebuild x4 +
|
|
51/51 x4 + PERF.md regenerated (new baseline table + the
|
|
re-derived-ceilings section, arithmetic adversarially checked -- 25
|
|
corrections applied before landing; the old ~29 ops/s IIgs
|
|
full-screen note superseded in place). Directive scorecard from the
|
|
ceilings section: PASS at <= 1.5x -- DOS map/sprite-draw/gameFrame/
|
|
P1, ST gameFrame/P1/P2 + fillRect class, Amiga tile paste/snap +
|
|
gameFrame, IIgs gameFrame/P1/P2/map. STILL OUTSIDE the bar with
|
|
recorded levers: IIgs tile/sprite singles (~2.2-3.4x -- per-call
|
|
wrapper/validation; lever = batch APIs which already PASS, or
|
|
moving validation into the fused asm), 68k sprite save/restore
|
|
(~1.9-2.3x -- Phase-1 two-class window contract; lever = 16-phase
|
|
save emitters), Amiga presents (~2.8-5.3x -- CPU copy vs blitter;
|
|
lever = blitter present, needs the async contract from plan item
|
|
8), and the UNKNOWN-bound DOS/gameFrameClean class (fixed present
|
|
base never modeled). These are the honest Phase-7 candidates.
|
|
- [~] PHASE 7 APPROVED by Scott 2026-07-08 ("Phase 7. We'll do STAXI
|
|
after the main library fixes") -- scope = the ceilings scorecard's
|
|
out-of-bar library rows; the STAXI title-anim bug is deferred
|
|
behind them. Items (design round first, W1-W3 playbook):
|
|
P7-1 68k sprite save/restore (ST 2611/1865, Amiga 2053/1603 vs the
|
|
~5.3k / 6.3-7.4k ceiling class -- find where the ~2x actually
|
|
lives: wrapper, backup metadata, window size, or dispatch).
|
|
PROBE ANSWERED 2026-07-09 (difference-method layer split; three
|
|
hash-inert UBER_PROBE rows in uber.c: spriteSaveVal = fully-
|
|
offscreen save exits via clip-fail = wrapper+validation only;
|
|
spriteRestoreVal = odd-bx backup fails the LAST validation term =
|
|
full validation, zero body; spriteSave g16 = x%16==0 class-0
|
|
window vs the golden row's x=40 class-1 = pure window-size
|
|
delta). MEASURED cyc/call -- ST (8.0 MHz): save full 3,064, g16
|
|
2,408, saveVal 1,088; restore full 4,211, restoreVal 422. Amiga
|
|
(7.09): save full 3,372, g16 2,890, saveVal 1,524; restore full
|
|
4,423, restoreVal 672. VERDICTS: (1) the BODY is at the machine
|
|
floor -- the window delta measures the copy rate directly: ST
|
|
656 cyc/128 B = 5.1 cyc/B (move.l model 5.0), Amiga 482/128 =
|
|
3.8 cyc/B; window size and body are NOT the problem. (2) the
|
|
whole gap is the C chain: wrapper+validation ~1,100 (ST) /
|
|
~1,500 (Amiga) cyc + dispatch/backup-metadata ~660-880 cyc per
|
|
call, i.e. ~55-70% of every save/restore call is glue around a
|
|
floor-speed body -- the same shape as the IIgs W1 autopsy.
|
|
(3) HONEST CEILINGS (supersede the unrevised blitter paper
|
|
6,300-7,400 class): ST save class-1 ~5,300/s (1,509 cyc = body
|
|
1,310 + call ~200), class-0 ~9,400/s, restore ~3,880/s (body +
|
|
mark ~550 + call); Amiga save ~4,400-6,100/s (body 960-1,900
|
|
depending on contention), restore ~2,900-3,700/s. True gaps are
|
|
~2.0x (ST save 2,611 vs 5,300; restore 1,900 vs 3,880), NOT the
|
|
paper 3.1-3.6x. LEVER (needs a design round + it edits shared
|
|
core sprite.c): trim/fuse the 68k wrapper-validation chain (the
|
|
W1 IIgs fused-entry pattern, or a lean fully-on-surface fast
|
|
test + metadata writes moved into the compiled routine);
|
|
potential ~+70-100% on these rows if glue drops to ~300 cyc.
|
|
ATTEMPT 1 (2026-07-11): MACRO TIGHTENING -- REVERTED, NET LOSS.
|
|
Both class macros (SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS) were
|
|
rewritten to their algebraically-minimal form (save class =
|
|
(x&15)+((widthPx+15)&15) >= 16; restore class = copyBytes>>3 -
|
|
GROUPS0 with an unsigned <=1 range test), PROVEN value-identical
|
|
over the entire input space (scratch classcheck.c: 102k save +
|
|
1.1M restore cases, 0 mismatches) and gated 51/51 on both ports.
|
|
Result: save bit-identical on ST AND Amiga (2611/2103 unchanged),
|
|
restore neutral on ST (1900) but REGRESSED Amiga 3% (1603->1553)
|
|
-- gcc-amigaos lowered the "simpler" C to +3 instructions
|
|
(objdump confirmed pre-bench: save total 120->117 with -3 frame
|
|
spills but restore 181->184). The wrapper cost is gated by the
|
|
compiler's register allocation (movem of 10-11 regs + the
|
|
interleaved isFullyOnSurface/clip validation), NOT by arithmetic
|
|
op count -- same compiler-bound wall as the IIgs fnAddr-cache
|
|
autopsy. CONCLUSION: no C-level micro-opt moves these rows; the
|
|
only real levers are STRUCTURAL and out of "trim" scope --
|
|
(a) a trusted fast-path entry that skips revalidation for a
|
|
caller that already proved on-surface (new/opt-in API surface),
|
|
or (b) fusing the restore dirty-mark into the emitted asm body
|
|
(emitter change, the Amiga/ST analogue of iigsSpriteRestoreMark).
|
|
Both are larger efforts to be scoped separately, not landed as
|
|
part of P7-1. A left-in NOTE comment in spriteInternal.h warns
|
|
against re-attempting the macro form.
|
|
|
|
SCOPE: RESTORE-MARK ASM FUSION (lever b), measured 2026-07-11.
|
|
----------------------------------------------------------------
|
|
MEASUREMENT (uber.c UBER_PROBE "probe restoreWork": an identical
|
|
class-1 restore to a NON-stage surface, where spriteMarkDirty
|
|
early-returns on s != gStage -- so it runs body+dispatch MINUS
|
|
the 16-row dirty-band widen; differenced against the golden
|
|
jlSpriteRestoreUnder row):
|
|
ST restore 4,211 cyc (1900/s), restoreWork 2,996 (2670/s)
|
|
-> MARK = 1,215 cyc = 29% of the restore call.
|
|
Amiga restore 4,423 cyc (1603/s), restoreWork 3,293 (2153/s)
|
|
-> MARK = 1,130 cyc = 26% of the restore call.
|
|
Both hash-inert (golden 51/51 held; P11 PASS / ST
|
|
displayMismatch=0 -- the work surface is never presented).
|
|
DECOMPOSITION (from the gcc -S of spriteMarkDirty.part, Amiga):
|
|
- CALL PACKAGING ~330 cyc = movem of 7 regs (prologue+epilogue
|
|
~230) + 4-arg load off stack (~50) + the caller's 4-arg push
|
|
(~48). THIS is what fusion removes.
|
|
- derive minWord=bx>>2 / maxWord=(bx+bw-1)>>2 / minPtr / maxPtr
|
|
~60 cyc -- fusion KEEPS (still needed), maybe slightly cheaper
|
|
with values already in regs.
|
|
- the 16-row conditional-RMW widen loop ~740 cyc (2 byte
|
|
cmp+maybe-store per row x16) -- IRREDUCIBLE; fusion KEEPS it
|
|
verbatim. No 68000 vectorization (per-byte conditional min/max,
|
|
no SIMD; minWord/maxWord constant across rows doesn't help).
|
|
CEILING: fusion recovers ~the call packaging => ~+330 cyc =>
|
|
ST 1900 -> ~2,060 (+8%), Amiga 1603 -> ~1,730 (+8%). A REAL
|
|
positive win (unlike attempt 1's 0%), but bounded at ~8% because
|
|
the widen loop -- 3/4 of the mark -- stays. NOT a path to the
|
|
1.5x target on its own (restore is ~2.0x).
|
|
FEASIBILITY: HIGH. Both compiled restore bodies (emitPlaneRunCopy
|
|
Amiga / emitGroupSpanCopy ST, in src/m68k/) end in `rts` and
|
|
clobber ONLY a0/a1 (Amiga saves a2-a4 itself, touches no data
|
|
reg; ST touches no callee-saved reg at all). So a shim can hold
|
|
bx/by/bw/bh + the band walk state in d2-d7/a2-a6 ACROSS the body
|
|
jsr for free. Direct 68k port of iigsSpriteRestoreMark.
|
|
DESIGN:
|
|
- Two hand-rolled asm shims (CANNOT share -- different body
|
|
ABIs): src/amiga/spriteRestoreMark.s and
|
|
src/atarist/spriteRestoreMark.s. (src/m68k/*.s compiles into
|
|
BOTH builds, so port-specific asm must live in the port dir.)
|
|
- Low-arg entry like the IIgs one: pass fnAddr + backup ptr;
|
|
the shim reads backup->{x,y,width,height,bytes}, derives the
|
|
screen dst from the PINNED stage planar base (gated on
|
|
s==gStage, so the stage's plane pointers/base are the single
|
|
source of truth -- Amiga amigaSurfacePlanar(gStage)->planes,
|
|
ST stSurfacePlanar(gStage)->base), pushes the body's cdecl
|
|
args (Amiga: p0..p3+buf; ST: buf+rowStart), jsr's the body,
|
|
cleans the stack, then widens gStageMinWord/gStageMaxWord for
|
|
rows by..by+bh-1 to [bx>>2, (bx+bw-1)>>2].
|
|
- A shared widen tail is possible (the IIgs stageRectMark model)
|
|
but the two shims differ before it; simplest is to open-code
|
|
the ~16-row loop in each (or a .macro in src/m68k included by
|
|
both). Match SURFACE_WORD_INDEX EXACTLY (x>>2 grain).
|
|
- C glue: spriteCompiledRestoreMark analogues in the Amiga/ST
|
|
branches of spriteDispatch.h (derive fnAddr, call the shim);
|
|
externs beside the IIgs ones; a fused `#elif AMIGA/ST` arm in
|
|
jlSpriteRestoreUnder's s==gStage block mirroring the IIgs arm
|
|
(sprite.c:829-838). Non-stage restores keep the current
|
|
split path (body + separate spriteMarkDirtyRestore).
|
|
RISK: the widen is a HASH-INVISIBLE contract -- an under-mark
|
|
corrupts presents while the stage hashes stay green (the exact
|
|
class of bug the phantom-row-200 present fix hit). Gates that
|
|
DO catch it already exist and must all pass: P11 presentPageSum
|
|
(Amiga), P10 displayMismatch (ST), the P5 spriteBandSum
|
|
fingerprint, and the gameFrame/gameFrameClean golden rows
|
|
(only ops that restore+present). Bit-exact arithmetic vs
|
|
SURFACE_WORD_INDEX is mandatory.
|
|
BUNDLING: the DRAW mark is the same shape and cost; a
|
|
spriteDrawMark shim sharing the widen (the IIgs iigsSpriteDrawMark
|
|
pattern) is a cheap add-on that helps the gameFrame composite
|
|
(draw+save+restore+present per frame) more than restore alone.
|
|
Scope draw fusion together if this is greenlit.
|
|
RECOMMENDATION: viable, well-understood, measured ~+8%/row -- but
|
|
LOWER ROI than this session's Amiga present asm (+45% for
|
|
comparable effort) because 3/4 of the mark is the irreducible
|
|
widen. Do it only when specifically pushing the sprite
|
|
restore/draw + gameFrame rows toward 1.5x, and bundle draw+restore
|
|
to justify the two-port asm cost. The higher-CEILING alternative
|
|
is lever (a) trusted-entry (skips the ~422-672 cyc validation
|
|
via opt-in API) -- different target, needs an API-design round.
|
|
|
|
BUILT + LANDED 2026-07-11 (restore + draw bundled). The scope's
|
|
ASM design above was WRONG about the mechanism; the win came from
|
|
a different place. Three variants were built and measured:
|
|
1. Full body+mark asm shim (per-port, IIgs-pattern): REGRESSED
|
|
both ports (ST -3%, Amiga -6%). The old body dispatch
|
|
(spriteCompiledRestoreUnder) is ALWAYS-INLINE and shares its
|
|
backup-field loads with the validation; a separate asm shim
|
|
re-loads them and adds a call layer, outweighing the mark it
|
|
fuses.
|
|
2. Lean rolled-loop asm widen call (cdecl, keeps body inlined):
|
|
REGRESSED (ST -6%, Amiga -3%). The rolled dbra loop lost to
|
|
gcc's UNROLLED indexed spriteMarkDirty; the ~380-cyc loop
|
|
overhead exceeded the ~130-cyc 7-reg movem it removed.
|
|
3. INLINE the unrolled 16-row mark directly into jlRestore for
|
|
the h==16 stage window (SPRITE_MARK_STAGE_ROWS16 macro, C
|
|
only, NO asm): WON. ST restore 1900->1996 (+5%), Amiga
|
|
1603->1703 (+6%). Both 51/51 + all parity gates.
|
|
KEY LESSON: the recoverable win was the CALL BOUNDARY itself
|
|
(jsr + frame), not the frame size -- and only INLINING removes
|
|
it. Every asm approach KEPT a call (asm can't inline into C), so
|
|
all lost. The winning change is pure C: an unrolled inline mark
|
|
guarded to s==gStage && h==16, byte-identical band arithmetic
|
|
(SURFACE_WORD_INDEX). All fusion asm (src/m68k/spriteMark68k.s +
|
|
the two port shims) was DELETED. Draw was bundled with the same
|
|
inline (jlSpriteDraw + jlSpriteSaveAndDraw): Amiga draw +4%,
|
|
saveAndDraw +6%, unaligned +5%; ST draw NEUTRAL (its masked
|
|
draw body dwarfs the mark). DOS untouched (guarded Amiga/ST).
|
|
gameFrame composite flat both ports (present-bound). Net: a
|
|
C-only, zero-new-file change; restore +5-6% both ports, Amiga
|
|
draw/saveAndDraw +4-6%, ST draw harmless. Final +8% ceiling not
|
|
reached (inline mark still runs the full unrolled widen; only
|
|
the ~call-boundary slice was recovered).
|
|
|
|
LEVER (a) TRUSTED FAST-PATH ENTRY -- BUILT + LANDED 2026-07-11,
|
|
the BIGGEST 68k sprite win yet. New opt-in public API
|
|
(jlSpriteSaveUnderTrusted / jlSpriteRestoreUnderTrusted /
|
|
jlSpriteSaveAndDrawTrusted) that SKIPS the per-call
|
|
geometry-validation chain (~11 range/alignment checks + NULL /
|
|
slot / PTR_OK gates) for a caller that pre-compiles its sprites
|
|
and keeps them on-screen. The existing safe entries are
|
|
UNCHANGED; trusted is opt-in with a documented UB-if-violated
|
|
contract, and each trusted entry FALLS BACK to its safe sibling
|
|
for anything the fast path doesn't recognise (uncompiled shift,
|
|
non-stage / non-16-row window) so a conservative caller degrades
|
|
safely. Amiga/ST-only fast path (IIgs/DOS/generic just call the
|
|
safe entry). MEASURED (uber.c probe rows vs the golden safe rows,
|
|
same geometry), ALL SIX WIN:
|
|
save ST 705->817 (+16%) Amiga 657->753 (+15%) IIgs 602->665 (+10.5%)
|
|
restore ST 529->609 (+15%) Amiga 545->609 (+12%) IIgs 662->722 (+9%)
|
|
saveAndDraw ST 257->289 (+12.5%) Amiga 273->289 (+6%) IIgs 423->455 (+7.6%)
|
|
(Amiga saveAndDraw is the smallest because its heavy masked draw
|
|
body makes validation a smaller fraction; IIgs wins are smaller
|
|
because 65816 validation is a smaller fraction of the call.) The
|
|
68k wins EXCEED the raw validation cost (~422/672 cyc) because
|
|
dropping the checks also shrank gcc's frame/spills; IIgs routes the
|
|
trusted restore/saveAndDraw into the safe FUSED MVN entries
|
|
(spriteCompiledRestoreMark / spriteCompiledDrawMark) minus
|
|
validation. Parity gates PASS on all three (P10 displayMismatch=0
|
|
ST, P11 presentPageSum Amiga, shrParity badRows=0 IIgs); DOS 51/51
|
|
+ IIgs 51/51 (safe entries UNCHANGED, golden rows identical).
|
|
Contract in joey/sprite.h. This is the expert inner-loop path
|
|
(trusted saveAndDraw this frame, trusted restore next) and the
|
|
BIGGEST sprite win of the P7-1 program, now on all 3 compiled
|
|
ports. [P7-2 IIgs "move validation into fused asm" is effectively
|
|
satisfied by this -- validation skipped at the C entry.]
|
|
P7-2 IIgs per-call wrapper (tile singles 2.2-2.5x, sprite singles
|
|
2.2-3.4x -- lever: validation/derive into the fused asm
|
|
entries; the W1 autopsy buckets are the map). SPRITE HALF DONE
|
|
2026-07-12 via the trusted API (see the P7-1 trusted block:
|
|
IIgs save/restore/saveAndDraw +7.6-10.5%). Tile singles remain.
|
|
P7-5 ST copyRect move.l chain -- LANDED 2026-07-12 (#3). jlpSurfaceCopy
|
|
Rect did a per-row memcpy CALL; the clean-buffer windows are 8
|
|
bytes/row (2 longs) x 16 x 3 = 48 memcpy(8) calls where mintlib
|
|
memcpy's frame dwarfs the copy. src/atarist/copyRect.s
|
|
(stCopyRectSpans68k, rolled move.l + per-row lea stride) replaces
|
|
the short-span case (long rows keep memcpy past a 64-byte
|
|
crossover). gameFrameClean 90 -> 120 ops/sec (+33%), 51/51 + P10
|
|
displayMismatch=0; gameFrame unchanged (save/restore, not
|
|
copyRect). The rolled asm wins here (vs the mark-fusion rolled
|
|
loop that LOST) because it deletes a CALL boundary, not a loop
|
|
body. ST-only (Amiga uses the blitter copyRect).
|
|
P7-6 IIgs copyRect override -- LANDED 2026-07-12. Same call-overhead
|
|
lever on the chunky IIgs, where llvm-mos memcpy is a slow far
|
|
byte-loop (#79). An IIgs jlpSurfaceCopyRect (src/iigs/hal.c,
|
|
JL_HAS_SURFACE_COPY_RECT) does an inline WORD copy for short spans
|
|
-- clang compiles it to `lda [dp],y / sta`, no far-memcpy call
|
|
(verified in -S). gameFrameClean 18 -> 24 ops/sec (+33%), 51/51 +
|
|
shrParity. FIRST tried in the shared generic copyRect but that
|
|
REGRESSED DOS -12% (x86 rep-movs memcpy beats an inline word loop
|
|
for 8-byte rows) -> reverted to IIgs-only; DOS keeps generic
|
|
memcpy. The lesson: this call-overhead lever is inherently
|
|
PER-PORT (depends on that port's memcpy speed vs inline stores).
|
|
P7-3 Amiga blitter present + the deferred-fence async contract
|
|
(plan item 8 revival; P1 46 vs the ~245 CPU model and beyond;
|
|
needs the fence-completeness audit + a debug force-fence
|
|
gate; P11/P8 are the safety net).
|
|
P7-4 DOS present fixed base: model it first (P8 exists now; the W0
|
|
ledger closed the COPY at hardware rate -- the ~2 ms fixed
|
|
scan/glue was never attributed), then fix only if fat shows.
|
|
MEASURED 2026-07-08 (pre-design capture): DOS P8 presentIdle =
|
|
1,137 ops/s = 0.88 ms fixed base (copied 0 / scanned 200
|
|
steady). First-pass re-verdict: P2 ceiling ~455 ops/s (0.88 +
|
|
768 B x 1.7 us) -> measured 305 = ~1.49x, PASSES the bar;
|
|
gameFrameClean needs the designer's component model.
|
|
P7-3 STAGE A+B LANDED 2026-07-09 (src/amiga/hal.c only; baseline
|
|
re-confirmed green first -- DOS/ST/Amiga 51/51 on the uncommitted W3
|
|
tree before any edit). Forensic (Amiga probe, pre-change): P8
|
|
presentIdle 661/s = 1.51 ms pure fixed scan; P2 present3win 93/s =
|
|
10.75 ms with scanned=400 (TWO 200-row passes: main scan + separate
|
|
snapshot), copied 384 B -> scan-bound; P1 present96 46/s = 21.7 ms,
|
|
copied 12,288 B -> ~11 ms CPU copy on top. STAGE A (fuse snapshot
|
|
into the scan): first attempt put the snapshot in the HOT clean-clean
|
|
path and REGRESSED (present3win 93->87, gameFrameClean 65->59) -- the
|
|
gcc-amigaos register-pressure trap the plan already recorded, now
|
|
re-confirmed with fresh numbers. FIX: snapshot only on the COLD
|
|
union-dirty rows (a row clean in both frames already holds the clean
|
|
sentinel in gPrev; hot path stays 2 loads like the original scan),
|
|
which eliminates the second 200-row pass -- scanned 400->200 exact,
|
|
present3win 93->100, present96 46->50, P11 PASS. STAGE B (blitter
|
|
present flush): route amigaPresentRectInner's copy through the
|
|
existing W3 amigaBlitterCopyPlanes (offset+modulo A->D) for runs whose
|
|
height x aligned-words >= AMIGA_PRESENT_BLIT_MIN_WORDS (128); the
|
|
96-row present96/gameFrame bands take the blitter (both plane sets are
|
|
CHIP RAM), the scattered present3win windows stay on the CPU THIN path.
|
|
Synchronous (WaitBlit before the display flip) -- NO async fence, so
|
|
the deferred-fence contract stays deferred. RESULTS (A+B combined,
|
|
vs the W3 baseline): present96 46->71 (+54%, blitter copy offload,
|
|
copied still 12,288), present3win 93->103 (+11%, scan fusion),
|
|
gameFrame 25->32 (+28%), gameFrameClean 62->71 (+9-15%),
|
|
jlStagePresent full 584->609. GATES: Amiga 51/51 x2 (Stage A, Stage
|
|
B); P11 presentPageSum PASS (fails=0) BOTH stages -- exhaustive
|
|
displayed-page-vs-shadow byte parity, the gate for the hash-invisible
|
|
present; scanned 400->200 and copied 384/12,288 exact; P8 idle
|
|
unchanged. DOS/ST byte-identical this session (hal.c is Amiga-only);
|
|
IIgs untouched (not re-gated). REMAINING Amiga-present lever: the
|
|
fused-scan present asm (ST presentSlam analogue, 4 plane buffers) for
|
|
the scan-bound P2/gameFrameClean class -- C fusion is now empirically
|
|
capped by register pressure, so the scan floor needs walking-pointer
|
|
asm; the async-fence blitter contract stays parked behind a >=32x32
|
|
row. gameFrame gap ~2.8x -> ~2.2x; still the largest present item but
|
|
materially closed. ADVERSARIAL AUDIT (2026-07-09, 3-lens + verify
|
|
workflow: blitter/DMA-race, 2-buffer-convergence/snapshot, edge/BLTSIZE
|
|
-- all 3 verify verdicts NOT_A_BUG): change correct by construction --
|
|
shadow is read-only during present (separate MEMF_CHIP alloc, not
|
|
aliased to gPlanes/B), blitter self-drains before the copper flip,
|
|
runs are row-disjoint so mixed blitter+CPU presents are safe, the
|
|
clean-clean skip is sound because min==0xFF<=>max==0x00 propagates into
|
|
gPrev, and the modBytes!=0 modulo blit is arithmetically in-bounds
|
|
(offset/modBytes even, blitWords<=20, height<=200, stays within
|
|
AMIGA_PLANE_SIZE=8000). NOTE: P11 already exercises the modulo blit --
|
|
present96 marks words 8..71 = a PARTIAL span (modBytes=8, blitWords=16,
|
|
height=96), not full-width as two lenses assumed. AUDIT FOLLOW-UPS
|
|
BOTH LANDED 2026-07-09 (gate 51/51 + P11 PASS fails=0): (1) the P11
|
|
battery gained a narrow-tall arm (16px x 96-row column -> ONE run,
|
|
blitWords 2, height 96 = 192 >= threshold, modBytes 36) exercising the
|
|
narrow modulo-blit variant present96 (wide, modBytes 8) did not -- P11
|
|
now empirically covers it, no longer correct-by-construction only; (2)
|
|
a compile-time typedef guard amigaBltSizeFieldsFit in hal.c asserts
|
|
SURFACE_HEIGHT <= 1024 (BLTSIZE 10-bit height field) and width <= 64.
|
|
P7-3 FUSED-SCAN ASM LANDED 2026-07-09 (Scott approved; KEPT after a
|
|
measure-and-decide). Hypothesis: the union scan was the Amiga present's
|
|
register-pressure-bound dominant fixed cost (four band walkers + run
|
|
state spilling on gcc-amigaos), so hand-rolling it would win big.
|
|
Executed in TWO bisectable stages: (1) C run-list refactor -- the scan
|
|
emits coalesced runs into gPresentRuns[] and a separate loop flushes
|
|
them (keeps the blitter/CPU flush + copied counter); gated 51/51 + P11
|
|
PASS, perf-neutral, proves the architecture. (2) src/amiga/presentScan.s
|
|
= hand-rolled 68000 _amigaPresentScan68k walking the 4 band arrays in
|
|
a0-a3 with NO spills (lastRow derived as y-1 at each run close to save a
|
|
register; moveq-zero-extend per the partial-register-write hazard;
|
|
4-constant + 8-byte-record compile-time ABI guards in hal.c). GATE
|
|
(all THREE correctness signals): 51/51 hashes, copied/scanned counters
|
|
BYTE-EXACT vs the C (asm produces identical runs), and P11 presentPageSum
|
|
PASS fails=0 incl. the narrow-tall arm. FIRST verdict looked marginal --
|
|
the plain asm (no fast path) moved present96 71->75 and present3win
|
|
103->106 (within noise), composites flat -- but that was a WRONG read of
|
|
WHY. A present-component skip-mask probe (temporary gAmigaPresentSkip in
|
|
hal.c + p3win noScan/noFlush/noFlip/bare rows, since removed) split
|
|
present3win's 9.4 ms: SCAN ~4.9 ms, FLUSH ~3.3 ms (chip->chip CPU copy
|
|
of the 3 tiny windows), FLIP ~0.4 ms, residual (copper-check/reset/
|
|
WaitBlit/marks/harness) ~1.2 ms; the 3 marks alone are 0.64 ms. So the
|
|
scan IS ~half the present -- the plain asm just wasn't faster than gcc
|
|
because the scan is INSTRUCTION-COUNT-bound (~16 instr x 200 rows), NOT
|
|
register-spill-bound as hypothesized. The fix is the ST presentSlam
|
|
4-row fast path my asm LACKED: at even row indices one move.l compares 4
|
|
clean-sentinel min bytes (cur AND prev) and skips the whole group when
|
|
all clean (0xFFFFFFFF+1==0), ~4x fewer instructions on the clean-row
|
|
majority (needs gPrevStageMin/MaxWord aligned(2) for the move.l).
|
|
A FIRST cut of it PASSED pixel gates (present3win 106->128 +21%, P11,
|
|
50/51) but BROKE the gameFrameClean golden row -- a bisect revert test
|
|
was definitive (fast-path build: gCleanSurface = jlSurfaceCreate() fails
|
|
5/5 incl. at 2 MB chip and low load; removed -> 51/51). ROOT CAUSE
|
|
(found on a re-audit, NOT by the first "memory-clean" read which was
|
|
WRONG): the routine's loop-exit check (cmp #200,d2) lived only in the
|
|
single-row tail .rowNext; the fast path did 'addq #4,d2 / bra .rowLoop',
|
|
so a clean group ending at row 199 landed d2 == 200, re-entered
|
|
.rowLoop, and -- being >= 197 -- fell into .oneRow and processed a
|
|
PHANTOM out-of-bounds row 200: read gStageMinWord[200] and, when that
|
|
garbage looked dirty, WROTE gPrevStageMin/MaxWord[200] one byte past the
|
|
arrays (and could push a phantom run to gPresentRuns[200]). That
|
|
corrupts a neighbouring global -- INVISIBLE to P11/hashes/counters (it
|
|
is not the stage) but it breaks a later AllocMem. LESSON worth keeping:
|
|
display/hash gates miss a heap corruption that a downstream allocation
|
|
catches; gameFrameClean (the only op allocating a 2nd surface) was an
|
|
accidental memory-integrity canary. FIX: move the 'cmp #200,d2 / bge
|
|
.scanEnd' exit to the TOP of .rowLoop so it guards BOTH re-entry paths;
|
|
.rowNext just 'addq #1,d2 / bra .rowLoop'. LANDED + GATED: 51/51 (incl.
|
|
gameFrameClean C51FE171), P11 PASS fails=0, present3win 106->128 (+21%),
|
|
present96 ->78, gameFrame 32->34, gameFrameClean 62->82, counters exact.
|
|
Correction to the W2/PERF forensic: the "~11-12 ms fixed scan"
|
|
attribution was roughly right that the scan is big, but it is
|
|
instruction-count-bound, not the spill it was assumed to be.
|
|
FLUSH LEVER LANDED 2026-07-09: the ~3.3 ms chip->chip CPU copy of the
|
|
scattered small windows was contention-bound, so the fix was simply to
|
|
lower AMIGA_PRESENT_BLIT_MIN_WORDS 128 -> 16 (the copyRect break-even) so
|
|
those runs (present3win/gameFrameClean windows are ~32 plane-words) blit
|
|
chip->chip in parallel instead of the CPU eating display-DMA contention.
|
|
The old 128 was a conservative guess, not a measurement; the component
|
|
probe justified re-testing. GATE: 51/51, P11 PASS fails=0, present3win
|
|
128 -> 153 (+20%), gameFrameClean 82 -> 90 (+10%); present96/gameFrame
|
|
flat (their bands already blit); copied/scanned exact.
|
|
OWN-ONCE FLUSH POLISH LANDED (correct, but perf-flat -- kept for the
|
|
right blitter discipline, not a speedup): the flush now OwnBlitters ONCE
|
|
per present (lazy on the first blitter run in amigaPresentRectInner,
|
|
DisownBlitter in jlpPresent's flush tail) instead of once per window, so
|
|
present3win pays 1 Own/Disown, not 3. amigaBlitterCopyPlanes split into
|
|
a body (amigaBlitterCopyPlanesOwned, no Own/Disown, used by the flush)
|
|
and a wrapper (Own+body+Disown, used by the single-blit copyRect
|
|
callers); a bool *blitOwned threads through amigaPresentFlushRun ->
|
|
RectInner; CPU-path runs interleave safely (the Owned body ends with
|
|
WaitBlit, so the blitter is idle for a following CPU copy or the flip).
|
|
GATE: 51/51, P11 PASS fails=0, present3win 153 -> 154 (WITHIN NOISE),
|
|
gameFrameClean/present96/gameFrame flat, counters exact. The gain is
|
|
negligible because after the threshold drop the flush is blitter-DMA-
|
|
bound (the copy), so the per-window Own/Disown OS calls it removes were
|
|
already a tiny fraction -- the earlier "few %" estimate was too
|
|
optimistic. No known remaining flush lever. SESSION present3win arc: 106
|
|
(asm scan) -> 128 (4-row fast path) -> 153 (flush blitter) -> 154
|
|
(own-once) = +45%.
|
|
(2026-07-08, 4-designer + audit workflow; audit fixed wrong
|
|
expected-value rows in two designs [A1/B1], merged the DOS arm into
|
|
a shared jlpTileMapPaste seam so tile.c spells the union mark
|
|
exactly twice [B3 overturn], corrected impossible Amiga probe gates
|
|
[C1/D1], and verified the ST design's dodge of the recorded
|
|
per-call-asm 4x loss against the real 632-cyc compiled bodies).
|
|
Order: ST singles -> DOS walker+seam -> Amiga blitter copyRect ->
|
|
Amiga map hoist (rider).
|
|
ITEM "ST movep singles" LANDED: three register-args leaf entries in
|
|
tileMap.s (zero movem, zero stacked args, pointers in a0/a1 via GNU
|
|
register-variable jsr shims; the shared MTR_*_ROW macros also
|
|
rebuild MP_CELL byte-identically -- map row EXACTLY 346 flat);
|
|
paste/snap guard their tile-buffer side (even; all real sources
|
|
are), copy is movep both sides (no guard); copyMasked's snap
|
|
scratch aligned(2) keeps its arm hot [audit A2]. Bench: paste 4033
|
|
-> 4773 (+18%), copy 3445 -> 3626 (+5%; the one model shortfall,
|
|
staging-bound), snap 6403 -> 8359 (+31%), copyMasked 1632 -> 1749,
|
|
map-singles 80 -> 94 (+18%); gameFrame/Clean flat per the corrected
|
|
A1 expectation; 51/51.
|
|
ITEM "DOS map walker + seam" LANDED: dosTile.h dword-ladder engine
|
|
(16 movs/cell, hoisted pixels check + row derive), dispatched via
|
|
the NEW shared tile.c arm `#elif defined(jlpTileMapPaste)` with
|
|
port.h aliases (ST stTileMapPaste / DOS dosTileMapPaste); ST and
|
|
Amiga tile.o proven BYTE-IDENTICAL pre/post (the seam is inert).
|
|
Bench: jlTileMapPaste 450 -> 1224 ops/s (2.7x; 22,500 -> 61,200
|
|
tiles/s, gap to the ~76k ceiling now 1.24x, inside the 1.5x bar);
|
|
singles/composites flat; 51/51 with the map row's own hash
|
|
52B9646B; objdump clean (no memcpy/imul/rep-movs in the walker).
|
|
ITEM "Amiga blitter copyRect" LANDED: amigaBlitterCopyPlanes gains
|
|
offsetBytes/modBytes params (whole-plane call passes 0/0 --
|
|
bit-identical program); jlpSurfaceCopyRect = modulo-driven A->D
|
|
blit per plane (mod = 40 - rowBytes, masks $FFFF by the 16-px snap
|
|
contract, BLTSIZE one blit always legal) above the 16-plane-word
|
|
break-even, CPU word-store walk below (replaces the 64-memcpy path
|
|
entirely -- BOTH arms beat it). Gates: 51/51 in BOTH arm-forced
|
|
builds (threshold 16 blitter / 0xFFFF CPU -- byte-equal arms
|
|
proven); no new blitter race (every path drains via
|
|
WaitBlit-before-Disown, the W2 present hoist stays defensive).
|
|
Bench: gameFrameClean 37 -> 65 ops/s (+76%!! -- the plan's +13-25%
|
|
and the design's 43-45 both UNDERSHOT because the old 64x2-byte
|
|
memcpy overhead was worse than modeled; CPU-arm-only build gives
|
|
56); gameFrame 25 flat.
|
|
ITEM "Amiga map C-hoist" LANDED (GO as rider per its own evidence +
|
|
audit): amigaTileMapPaste in amigaTile.h (resident a0-a3 plane
|
|
pointers, 32 move.b/cell off compile-time displacements) behind the
|
|
shared jlpTileMapPaste seam. Bench: jlTileMapPaste 153 -> 191
|
|
ops/s (+25%, above its 176-186 model); paste 3803 EXACT,
|
|
map-singles 78 flat, gameFrame flat, 51/51.
|
|
W1 IIgs fused sprite/fillRect entries + fnAddr cache + sprite-mark
|
|
specialization (all ports), W2 present-path fixes (Amiga per-row
|
|
bands, ST scan cost, IIgs slam threshold -- NOTE: present bugs are
|
|
INVISIBLE to stage hashes; each W2 change needs visual verification
|
|
per port + probe deltas), W3 ST movep single-tile ops + DOS tilemap
|
|
walker + Amiga map walker + Amiga blitter copyRect, then Phase-5
|
|
acceptance artifacts (re-derived ceilings, PERF ceilings column).
|
|
Async blitter contract (#8) stays deferred until a >=32x32 row
|
|
exists. Original ranked list follows:
|
|
1. IIgs fused sprite entries (iigsSpriteDrawMark etc., the Phase-2
|
|
D1 tile pattern): draw est 966 -> ~1,700-1,850/s (+80-90%),
|
|
crosses the 1.5x-of-honest-ceiling bar alone; + resolved-fnAddr
|
|
cache in jlSpriteT (R12 flavor, epoch-invalidated on arena
|
|
compaction) -> ~2,000-2,200/s. Same fusion for save/restore
|
|
(restore 545 -> ~1,600+). Feeds IIgs gameFrame directly.
|
|
2. IIgs fillRect fused entry (fillRect 16x16 is 6.9x off: 4,651
|
|
cyc/call vs ~676 hand; the +60-75% fusion precedent applies) --
|
|
gameFrame's 256x96 fill inherits it.
|
|
3. Present-path fixes per forensics: Amiga per-row bands (kills the
|
|
5x over-copy), ST scan-cost reduction (dirty-row bitmap/list to
|
|
skip the 2x200-row scans; check row memcpy isn't byte-looping),
|
|
IIgs narrow-band slam threshold revisit + softswitch hoisting.
|
|
DOS is done (present-bound at hardware).
|
|
4. Sprite-window dirty mark specialization (Step-B pattern for
|
|
sprites; explains the cross-port restore-lags-save asymmetry
|
|
0.58-0.91) -- one shared fix, all four ports.
|
|
5. AUDIO-ACTIVE TAX (bench measures idle; games play music!): a
|
|
playing MOD on stock ST costs >130% CPU (12.3kHz ISR ~49.5% +
|
|
libxmp LINEAR mix ~850-1,050 cyc/output sample = 130-160% --
|
|
NOT VIABLE AT ALL); even NEAREST @6144Hz ~= 65-78%. Real fix =
|
|
YM-register-native music (~1-3%). DOS mix = 40-70% of the 386SX
|
|
(NEAREST @11025 -> ~17-30%; OPL2 FM ~1-2%, adlib hooks already
|
|
in dos/audio.c). Amiga ~2-4% (Paula DMA) and IIgs ~0% mixing
|
|
(Ensoniq DOC) are fine. Retro-authentic priority: chip-native
|
|
music formats are period-correct AND fast.
|
|
6. Amiga jlSurfaceCopyRect on the blitter (mods = 40-wBytes; ~10x
|
|
the op, +13-25% gameFrameClean; WaitBlit is free at this size --
|
|
NO async contract needed). Cheap interim: inline word stores
|
|
capture ~70% in 10 lines of C.
|
|
7. ST movep for single-tile paste/copy/snap (route per-call ops
|
|
through the movep body; ~600-750 cyc/call saved, tilePaste est
|
|
+15-20%) + DOS native tilemap walker (only port without one;
|
|
batch row 2.5-3.4x off).
|
|
8. Async blitter contract (deferred-fence, internal only) -- pays
|
|
ONLY on full-surface copies (~4.5ms/call) and future >=32x32
|
|
cookie-cut sprites; needs a fence-completeness audit + debug
|
|
force-fence gate. Keep per-call sync until a >=32x32 row exists.
|
|
9. Amiga whole-map tile walker C-hoist (+15-20%, already inside
|
|
1.5x -- optional polish).
|