467 lines
28 KiB
Markdown
467 lines
28 KiB
Markdown
# JoeyLib cross-port performance (UBER benchmark)
|
|
|
|
Regenerated 2026-07-05 (PERF-AUDIT-PLAN.md Phase 8, #25b) from
|
|
same-day captures of all four ports. This supersedes the 2026-07-04
|
|
Phase 0 baseline; every phase of the audit fix plan (0-8) has landed,
|
|
so these are the numbers the library actually ships with.
|
|
|
|
**2026-07-06 update:** the IIgs sprite column is now COMPILED. Finding
|
|
#77 (the loader/segment-BSS + malloc-heap reservation that forced the
|
|
codegen arena into the app's own BSS) was fixed toolchain-side and
|
|
JOEY_IIGS_SPRITE_CODEGEN was re-enabled; the four IIgs sprite rows were
|
|
re-captured (bench-iigs.sh) and jumped 5-25x (SaveUnder 104->543,
|
|
Draw 48->907, RestoreUnder 110->568, SaveAndDraw 15->376 ops/sec),
|
|
bit-identical to the interpreter goldens (diff-uber-hashes 45/45). The
|
|
ST and DOS columns are unchanged from the 2026-07-05 capture.
|
|
|
|
Also 2026-07-06: the Amiga `jlpFillRect` partial-width path was moved
|
|
onto the blitter (BLTAFWM/BLTALWM hardware edge masks + masked minterm),
|
|
which lifted `jlFillRect 16x16` 567->1153, `80x80` 71->269 (both from
|
|
below-floor to well above it), and -- because they build on fillRect --
|
|
`jlDrawLine V` 144->269 and `jlDrawRect 100x100` 122->203, all
|
|
hash-identical (Amiga 45/45). The one remaining Amiga below-floor cell
|
|
is `jlFillCircle r=40` (74%); a whole-circle asm routine was tried and
|
|
did NOT help (per-span dispatch is not the cost -- the 8-way-symmetry
|
|
overlap fill dominates), so it stays on the C span walker for now.
|
|
|
|
**2026-07-06, NATIVE-PERF Phase 0:** four WORKLOAD rows were added (see
|
|
NATIVE-PERF-PLAN.md) and the goldens refrozen at **49 lines** after a
|
|
clean regression gate (45 match + exactly 4 extras per port) and full
|
|
4-port cross-identity. The new rows measure what games actually do, and
|
|
they expose the real problem the per-op rows hid: `jlSpriteDraw
|
|
unaligned` runs at **5% (ST) / 9% (Amiga)** of the IIgs floor -- a
|
|
15x/12x cliff vs the aligned row, because only x%8==0 draws are
|
|
compiled on the 68k ports (see the plan's code-truth table). The
|
|
`gameFrame composite` row is the bottom line: **ST 9 / Amiga 18 /
|
|
IIgs 10 / DOS 18 frames/sec** for a modest game frame. Bonus finding:
|
|
cross-identity of the sweep16 row validated the ST/Amiga shifted c2p
|
|
walkers as pixel-perfect at all 14 previously-untested x phases.
|
|
(Re-capture note: IIgs input/audio rows wobbled vs the prior MAME run
|
|
-- e.g. jlAudioFrameTick 50223 -> 27612 -- run-to-run MAME variance on
|
|
cheap ops; hashes are unaffected and those rows gate nothing.)
|
|
|
|
**2026-07-06, NATIVE-PERF Phase 1 (draw side) LANDED:** all-shift
|
|
compiled sprite DRAW on both 68k ports. Amiga emits pre-shifted JIT
|
|
variants for all 8 bit phases; the ST emits per-phase thunk + data
|
|
tables driven by a shared masked-movem walker (spriteWalk.s) for all
|
|
16 intra-group phases, with the dispatcher's ST shift contract changed
|
|
to x & 15. jlSpriteCompile is now plan-based: it measures every
|
|
(shift, op) variant, greedily fits them under the uint16 offset cap
|
|
(draws first, save/restore as atomic pairs), and degrades to the
|
|
byte-aligned phases when the arena is too small instead of losing the
|
|
sprite. RESULT -- the alignment cliff is CLOSED: `jlSpriteDraw
|
|
unaligned` ST 40 -> 500 ops/sec (12.5x), Amiga 75 -> 803 (10.7x);
|
|
sweep16 ST 2 -> 32 sweeps/sec (512 draws/sec), Amiga 5 -> 50 (800
|
|
draws/sec). Unaligned now performs like aligned on both ports (ST 82% /
|
|
Amiga 84% of their aligned rows), and every one of the 49 golden
|
|
hashes still matches -- the compiled paths are bit-identical to the
|
|
interpreted walkers they replace, on all four ports. (Found along the
|
|
way: the Amiga bench task stack was ~4 KB -- one deep call chain from
|
|
overflow -- now 32 KB in bench-amiga.sh; the emitters' accumulators are
|
|
file statics.)
|
|
|
|
**Phase 1 step 5 (save/restore) ALSO LANDED, completing Phase 1:**
|
|
compiled save/restore on both 68k ports operate on the 16-px-group
|
|
window (two width classes per sprite, like the IIgs MVN pair), the
|
|
backup format is a pure function of the recorded geometry (identical
|
|
for compiled and interpreted producers/consumers -- the ST HAL gained a
|
|
raw-span interpreted branch, switched atomically with restore), and the
|
|
gates route by SPRITE_SAVE_CLASS / SPRITE_RESTORE_CLASS with an odd-
|
|
buffer-pointer demotion (68000 word ops need even addresses). The
|
|
combined jlSpriteSaveAndDraw path now fires on 68k at EVERY x for the
|
|
first time. RESULTS (floor-relative): Amiga SaveUnder 104% -> 276%,
|
|
RestoreUnder 98% -> 236%, SaveAndDraw 94% -> 160%; ST SaveUnder
|
|
158% -> 203%, RestoreUnder 122% -> 163%, SaveAndDraw 87% -> 107%;
|
|
Amiga gameFrame 18 -> 23 fps. Backup capacity contract:
|
|
JOEY_SPRITE_BACKUP_BYTES in sprite.h (window worst case; all in-repo
|
|
examples migrated + 2-aligned). DEFAULT_CODEGEN_BYTES is 64 KB on
|
|
Amiga/ST (full variant sets). All 49 golden hashes held throughout.
|
|
|
|
**2026-07-07, THE ST COLUMN DOUBLED -- idle-audio ISR tax found and
|
|
fixed:** every previous ST capture ran with ~49.5% of the CPU silently
|
|
consumed by the 12.3 kHz Timer-A PWM ISR that jlpAudioInit started
|
|
unconditionally -- it streamed buffer silence to the YM even with
|
|
nothing playing, and UBER inits audio before its timed run. Found by
|
|
a calibrated probe (known-cycle dbra spin + per-layer direct-call rows
|
|
under the bench Hatari config): every probe row slowed by exactly
|
|
1.98x once audio was initialized, and the probe's un-taxed tilePaste
|
|
cycles/call matched the instruction-count model that UBER's number
|
|
mysteriously doubled. Fix: src/atarist/audio.c Timer A is now
|
|
demand-driven -- installed on the first PlayMod/PlaySfx, parked again
|
|
once the mix buffer drains back to silence with nothing live
|
|
(drain-to-silence handshake in jlpAudioFrameTick; StopMod parks
|
|
immediately when no SFX is riding the pump). Probe-verified: idle rows
|
|
match the never-initialized rows exactly, the tax correctly returns
|
|
while an SFX drains, and it vanishes after the drain. Full ST
|
|
re-bench: 49/49 hashes bit-identical (pixels untouched) and EVERY row
|
|
~2x: gameFrame 9 -> 19 fps, tilePaste 1,574 -> 3,203/s, sprite draw
|
|
607 -> 1,188/s, unaligned draw 508 -> 992/s. The other three ports
|
|
were audited for the same bug class and are clean: Amiga ~1.8% idle
|
|
tax (PTPlayer rides Paula DMA), DOS ~0.5% (SB auto-init DMA, ~11 Hz
|
|
half-buffer IRQ), IIgs 0% (no handler installed until PlayMod). ALL
|
|
pre-2026-07-07 ST comparisons -- including the Phase-0 ceiling gaps
|
|
and the Phase-1 relative results -- understated the ST by ~2x.
|
|
|
|
**2026-07-07, NATIVE-PERF PHASE 4 (IIgs arm) LANDED -- whole-map asm
|
|
walker:** iigsTileMapPasteMark (joeyDraw.s) replaces the IIgs
|
|
stage-arm per-tile loop of jlTileMapPaste with ONE JSL that walks the
|
|
index map: dst offset advances incrementally (+4/column, +1280/tile
|
|
row) from a single gRowOffsetLut read, the register-starved inner
|
|
lives in a 22-byte stacked D-frame (X survives the 16-store body and
|
|
carries the dst offset; the map index sits in the frame because Y is
|
|
clobbered), the idx << 5 tileset add carries into the bank byte, all
|
|
three wrapping branches are brl, and ONE band union closes the call.
|
|
Gated 50/50 + make iigs-verify. RESULT: IIgs batch row 60 -> 140
|
|
maps/s = 7,000 tiles/s (+133%), ~400 cyc/tile -- BELOW the 480-cyc
|
|
single-tile hand-coded ceiling, because the walker amortizes the
|
|
JSL/derive/mark that even a hand-coded per-tile call pays. Batch row
|
|
across all ports: ST 8,150 / Amiga 7,650 / IIgs 7,000 / DOS 22,500
|
|
tiles/sec (the morning's per-call loop numbers were 1,600 / 2,300 /
|
|
1,750 / 10,000). The Amiga blitter cookie-cut sprite was recon'd and
|
|
DEFERRED with a full design on record: the synchronous WaitBlit the
|
|
hash contract requires erases the parallelism win at <= 16x16 (every
|
|
current sprite row), and the chip-RAM pre-shifted source data is a
|
|
net-new subsystem -- the honest experiment is a >= 32x32 golden'd row
|
|
measured with and without the fence.
|
|
|
|
**2026-07-07, NATIVE-PERF PHASE 4 CLOSED -- clean-buffer experiment
|
|
PASSED, jlSurfaceCopyRect is public API:** Scott signed off the
|
|
jlTileMapPaste shape and approved the clean-buffer experiment; both
|
|
landed same-day. jlSurfaceCopyRect(dst, src, x, y, w, h) is a
|
|
SAME-POSITION rect copy (clip, then snap outward to the 16-px group
|
|
grid on every port -- uniform snap keeps cross-port hashes
|
|
byte-identical): core wrapper + dirty mark in surface.c, generic
|
|
chunky row-memcpy engine (serves DOS and both IIgs surface flavors --
|
|
the IIgs stage pixels pointer reaches bank $01 through ordinary
|
|
large-model stores, recon-verified against the sprite-restore
|
|
precedent), ST override = strided interleaved row memcpys (the
|
|
raw-span restore recipe), Amiga override = per-plane CPU row copies
|
|
(CopyMemQuick is unusable at 16-px grain: word alignment only;
|
|
the modulo-driven blitter variant is the recorded production
|
|
upgrade). The STRATEGY row `gameFrameClean composite` (erase 3
|
|
sprite windows from a pristine clean surface + 3 draws + present; NO
|
|
per-frame background repaint, NO save-under -- the static-background
|
|
pattern, not an op-for-op gameFrame speedup) measures, vs gameFrame:
|
|
**ST 48 vs 22 = 2.18x (acceptance bar was 1.4x), DOS 195 vs 19 =
|
|
10.3x, IIgs 18 vs 10 = 1.8x, Amiga 34 vs 25 = 1.36x** (Amiga gains
|
|
least, exactly as the adversarial recon predicted: its fillRect was
|
|
already blitter-fast, and clean->stage is a chip-to-chip CPU copy
|
|
against display DMA -- the blitter rect variant is its lever).
|
|
GOLDEN CONTRACT now **51 lines**: regression gate exactly 50 match +
|
|
1 extra per port, 4-port cross-identity of the new row (hash
|
|
C51FE171 x4), then freeze. Found along the way: `make all` does NOT
|
|
regenerate joey.2mg, so bench-iigs.sh silently benched the previous
|
|
UBER (clean 50/50, new row absent) until `make iigs-disk` -- recorded
|
|
in the stale-binary memory.
|
|
|
|
**2026-07-07, NATIVE-PERF PHASE 4 (ST arm) LANDED -- movep.l map
|
|
engine:** the ST jlTileMapPaste arm now dispatches to
|
|
stTileMapPasteMovep (src/atarist/tileMap.s): per tile row, one
|
|
`move.l (a0)+` + `movep.l d0,d16(a1)` writes all 4 interleaved plane
|
|
bytes in 36 cycles vs ~80 for the four compiled move.b -- movep IS
|
|
the half-group byte-scatter pattern (the classic ST c2p trick), where
|
|
movem is useless (byte-granular dst) and byte-pair word fusion loses
|
|
(lsl.w #8 costs 22 cyc on a barrel-shifterless 68000, priced during
|
|
recon). Cells walk in even/odd pairs sharing one group pointer
|
|
(+8/pair, all row offsets compile-time d16); odd bx = head cell, odd
|
|
width = tail cell. The C pair walker (stTileMapPaste in stTile.h,
|
|
+17% alone -- adversarial recon correctly predicted the pure-C cap at
|
|
+15-23% because gcc already hoists the mulu and the 632-cyc copy body
|
|
dominates) stays as the odd-tileset-pointer fallback: `move.l (a0)+`
|
|
needs an even source, jlTileT is align-1, and the guard is one AND
|
|
per call. RESULT: ST batch row 163 -> 346 ops/sec = **17,300
|
|
tiles/s** (2.12x, 462 cyc/tile) -- ABOVE the Phase-0 12,700-15,500
|
|
"hand-coded ceiling", which under-counted by assuming byte-move
|
|
bodies. Gate: ST 50/50 bit-identical (hash 52B9646B held through
|
|
both engines) + a 12-case on-emulator proof (TILETEST, session
|
|
scratchpad) covering odd-bx head, odd-width tail, w=1 columns, h=1,
|
|
checkerboard/end/all skips, and both odd-pointer fallback paths,
|
|
each with a border-ring containment check. Batch row across ports:
|
|
DOS 22,500 / ST 17,300 / Amiga 7,650 / IIgs 7,000 tiles/s. IIgs
|
|
PEI-slam map variant was EVALUATED and REJECTED: models ~+28%
|
|
(~312 cyc/tile) but requires SEI for the whole map (~5.6 ms
|
|
interrupts-off at 10x5, audio ISR jitter) AND the VBL-driven bench
|
|
clock cannot honestly measure SEI ops (PERF artifact: SEI inflation)
|
|
-- the walker already beats the 480-cyc per-tile ceiling without it.
|
|
|
|
**2026-07-07, NATIVE-PERF PHASE 3 LANDED -- jlTileMapPaste batch API:**
|
|
one public entry pastes a wTiles x hTiles rectangle from a tileset +
|
|
row-major uint8_t index map (TILE_MAP_SKIP = 0xFF leaves a cell
|
|
untouched): one validation, one dirty union (68k/DOS; the IIgs
|
|
stage-fused entries mark per pasted tile, and its loop hoists the
|
|
stage test + does tileset indexing as base + (idx << 5) -- the
|
|
natural tiles[idx] spelling cost a 32-bit indexed multiply per cell
|
|
on the 65816 and erased the batch win until specialized). GOLDEN
|
|
CONTRACT now **50 lines**: refreeze followed the Phase-0 procedure
|
|
(regression exactly 49 match + 1 extra per port, then 4-port
|
|
cross-identity of the new `jlTileMapPaste 10x5` row -- all four hash
|
|
52B9646B -- then freeze). RESULTS, tiles/sec batch vs per-call loop:
|
|
ST 8,150 vs 4,000 (+106%, gap-to-ceiling 1.6-1.9x); Amiga 7,650 vs
|
|
3,750 (+104%, gap 1.40x -- INSIDE the ~1.5x program target); DOS
|
|
22,500 vs 12,900 (+74%, 2.5x); IIgs 3,000 vs 2,950 (+2% -- its
|
|
Phase-2 fused entries already deleted the per-tile wrapper, so the
|
|
remaining cost is ~93% inner JSL; a whole-map IIgs asm walker is the
|
|
Phase-4 lever). Sprite batch APIs were DESCOPED from Phase 3: the
|
|
phase gate measures only the tilemap row, and post-R5 sprite dispatch
|
|
overhead is ~3-6% -- not worth new public surface without a measured
|
|
need.
|
|
|
|
**2026-07-07, NATIVE-PERF PHASE 2 COMPLETE -- Steps D3 (R6 + R10)
|
|
close the phase:** (R6) the IIgs masked-copy inner's 4 x 8 jsr/rts
|
|
round-trips to tmaskByte were inlined via the TMASK_BYTE assembler
|
|
macro (identical body; the row loop-back became brl -- the expanded
|
|
body exceeds 8-bit branch range): jlTileCopyMasked 625 -> 681 (+9%,
|
|
exactly the 384-cycle model). (R10) the stage arm of jlDrawPixel is
|
|
one fused JSL (iigsDrawPixelMark: single-row widen + nibble RMW plot
|
|
against the pinned stage base): jlDrawPixel 3,179 -> 5,576 (+75%, 502
|
|
cyc/call incl. wrapper vs the measured 880 before); the batching
|
|
plotters (lines/circles, plotPixelNoMark) keep the unfused inner and
|
|
were flat, as were tiles/sprites. Final gate 49/49 x4 + make
|
|
iigs-verify. PHASE-2 EXIT vs its gate ("small-op rows move measurably
|
|
toward Phase-0 ceilings on all ports; goldens hold"): tilemap row
|
|
tiles/sec ST 1,600 -> 4,050 / Amiga 2,300 -> 3,750 / IIgs 1,750 ->
|
|
2,950 / DOS 10,000 -> 12,900; gameFrame ST 9 -> 21 / Amiga 18 -> 25 /
|
|
IIgs 10 / DOS 18 -> 19; IIgs tilePaste ceiling gap 3.5x -> 2.15x, ST
|
|
2.6x. The remaining per-call floor is the public wrapper + one
|
|
JSL/call -- Phase 3 batching territory. Optional leftovers R7
|
|
(saveAndDraw consolidation) and R12 (IIgs C-residue reshapes) are
|
|
recorded in the plan, not phase-gating.
|
|
|
|
**2026-07-07, NATIVE-PERF Phase 2 Step D2 LANDED -- sprite dispatch
|
|
flattening (R5):** the spriteCompiled* runtime dispatchers moved from
|
|
spriteCompile.c into spriteDispatch.h as always-inline functions
|
|
consumed only by sprite.c, deleting the cross-TU call per compiled
|
|
sprite op and letting the compiler share the slot/field loads the
|
|
caller already did. Gated 49/49 x4. Sprite rows: IIgs +6-13%
|
|
(SaveAndDraw 376 -> 423, Draw 906 -> 966), ST SaveUnder +15% (2,611),
|
|
Amiga +5-11% (SaveUnder 2,053), DOS SaveUnder +16% (10,743). Two
|
|
measurement footnotes: gameFrame wobbles +/-1 fps between runs
|
|
(single-digit iters per 16-frame window -- quantization, not signal;
|
|
ST has bounced 21/22 across three consecutive gates), and DOS
|
|
`jlSpriteDraw unaligned` is BIMODAL across runs of identical binaries
|
|
(~3,800 vs ~5,100; seen 07-06 and again today; the all-phase sweep16
|
|
row is stable at ~362, so per-phase behavior is fine -- likely DOSBox
|
|
dynamic-core state; do not chase single-run deltas on that row).
|
|
|
|
**2026-07-07, NATIVE-PERF Phase 2 Step D1 LANDED -- IIgs fused tile
|
|
entries (R2):** jlTileFill/Copy/Paste on the stage now run ONE JSL
|
|
each: new joeyDraw.s entries (iigsTile*Mark, section
|
|
.text.iigsFusedTile) derive the destination in asm from the pinned
|
|
$01:2000 stage base + gRowOffsetLut, write via long,X with the stage
|
|
literal folded into each store, and widen the dirty band via a shared
|
|
stageTileMark helper -- replacing C-side pointer math plus two JSLs
|
|
(op + marker). Gated 49/49 (pixels) AND make iigs-verify (framebuffer
|
|
-- the mark half is hash-blind, so the display check is load-bearing).
|
|
RESULTS: IIgs tileFill 1,981 -> 3,251 (+64%), tilePaste 1,687 -> 2,710
|
|
(+61%), tileCopy 1,441 -> 1,988 (+38%), map row 35 -> 59 (+69% = 2,950
|
|
tiles/s), gameFrame 10 -> 11 fps; unfused controls (copyMasked, snap,
|
|
sprites) flat. IIgs tilePaste is now ~1,030 cyc/call vs the 480-cyc
|
|
hand-coded ceiling (gap 2.15x, was 3.5x). Toolchain landmine found en
|
|
route: a 16-bit jsr between .section blocks can cross OMF segment
|
|
banks (no link error, wild jump into $00CFxx) -- helpers must share
|
|
their callers' section or be jsl/rtl; see the cross-section-jsr
|
|
feedback memory.
|
|
|
|
**2026-07-07, NATIVE-PERF Phase 2 Steps A-E LANDED (per-op tax):**
|
|
five hash-gated steps, each 49/49 x4. (A) plane-pointer access
|
|
inlined into the 68k compiled-sprite dispatchers (amigaPlanar.h /
|
|
stPlanar.h; the jlpSurfacePlanePtr cross-TU call -- ~190 cyc x4 per
|
|
Amiga dispatch -- deleted): Amiga sprite ops +10-23%. (B) whole-tile
|
|
dirty marks specialized (band = [bx*2, bx*2+1] with compile-time
|
|
proof, unrolled 8-row widen): tiles +9-28% on 68k/DOS. (C) jlDrawRect
|
|
raw-edge fills + ONE band-identical mark (+22-40%); the multi-row
|
|
widen unroll is Amiga-only by measurement (gcc-mint compiled it WORSE,
|
|
ST -5% -- see the per-port #if in surface.c). (D=R9) presents consume
|
|
the core gStageScbDirty/gStagePaletteDirty flags instead of
|
|
memcmp'ing 512-712-byte cached mirrors: gameFrame +1-2 fps on
|
|
ST/Amiga/DOS. (E) ST/Amiga tile ops moved into always-inline per-port
|
|
headers (stTile.h / amigaTile.h), deleting the second cdecl layer
|
|
(ST tiles +9-14%, Amiga +5-11%), then the Amiga row loops unrolled to
|
|
d16(An) form (tilePaste +36%, tileSnap +52%). NET SESSION MOVEMENT on
|
|
the tile gate row (tiles/sec, map 10x5): ST 1,600 -> 4,000; Amiga
|
|
2,300 -> 3,750; DOS 10,000 -> 12,900. gameFrame: ST 9 -> 21, Amiga
|
|
23 -> 25, DOS 18 -> 19 fps (IIgs untouched at 10 -- its fused-asm
|
|
glue is Phase 2 Step D, still open). The present-row jumps (ST 467,
|
|
DOS 1,137) are the dirty-union idle early-out running flag-gated;
|
|
gameFrame is the honest present signal.
|
|
|
|
Correctness statement backing this table: all four ports produce
|
|
BYTE-IDENTICAL pixels on every measured op and every correctness
|
|
check -- **49/49** surface hashes match the frozen goldens in
|
|
tests/goldens/uber/ (37 timed ops + 12 hashed checks; the PASS/FAIL
|
|
checks -- palette round-trip, PRNG golden sequence, sprite clip
|
|
round-trip, arena churn, sprite-from-surface -- pass everywhere).
|
|
The Phase 0-era divergences (#75 tilePasteMono three-way, #78 ST
|
|
fillCircle/tileFill, #87 IIgs circle outline) are all fixed.
|
|
|
|
## Measurement honesty (read before trusting any number)
|
|
|
|
Four measurement artifacts have shaped this table's history. Three are
|
|
fixed, one is inherent -- know them before comparing rows:
|
|
|
|
1. **Per-iteration poll tax -- FIXED (Phase 0).** The pre-audit bench
|
|
loop called jlFrameCount() between every op; on the IIgs that was a
|
|
GetTick toolbox chain (~600-800 cycles), so every small-op number
|
|
carried the harness cost, not the op cost. Ops now run in adaptive
|
|
batches per poll. Small-op numbers jumped 2-10x on every port when
|
|
this landed; never compare against pre-Phase-0 captures.
|
|
2. **SEI tick loss -- INHERENT on IIgs, rows tagged below.** The
|
|
benchmark clock is jlFrameCount = the VBL-interrupt-driven GetTick.
|
|
An op that holds SEI while it runs suppresses those interrupts, the
|
|
clock loses ticks, and the op over-reports. The three SEI-holding
|
|
rows -- `jlSurfaceClear`, `jlFillRect 320x200`, and
|
|
`jlStagePresent full` -- are CLOCK-UNRELIABLE in the IIgs column.
|
|
The physics check: a full-screen op moves 32000 bytes at ~6
|
|
cyc/word, so ~29 ops/sec is the hard ceiling at 2.8 MHz. The
|
|
present row's 68 and the clear row's 35 sit above it -- inflated;
|
|
the honest values are at or below the ceiling.
|
|
3. **ST frame-rate miscalibration -- FOUND and FIXED 2026-07-06; the
|
|
ST column below is the corrected (millis-clock) capture.** Before the
|
|
fix, ops/sec was `iters * jlFrameHz() / actualFrames` and the ST's
|
|
`jlpFrameHz()` hardcoded 50 -- but Hatari runs the emulated ST at
|
|
~60 Hz VBL, so every ST number was scaled by 50/60 ~= 0.83 (~17%
|
|
pessimistic). Caught by cross-checking against UBER's independent
|
|
Timer-C 200 Hz millis clock (refresh-independent), which ran ~16-22%
|
|
faster; the Amiga's two clocks agreed exactly (FS-UAE = PAL 50 Hz),
|
|
confirming it was ST-only. Two fixes landed: (a) UBER now computes
|
|
ops/sec from `jlMillisElapsed` (refresh-independent on all four
|
|
ports; identical to the old formula on IIgs where millis is
|
|
GetTick-derived), and (b) ST `jlpFrameHz()` now reads the shifter
|
|
resolution + sync-mode registers to return the true 50/60/71 Hz. The
|
|
per-op `UBER-CLK:` log line reports frame-vs-millis as a standing
|
|
cross-check (they now agree). ST cells rose ~17% (e.g. jlSpriteDraw
|
|
503->585, jlFillRect 80x80 97->115); DOS/IIgs/Amiga unchanged.
|
|
4. **ST idle-audio ISR tax -- FOUND and FIXED 2026-07-07; the ST
|
|
column below is the post-fix capture.** jlpAudioInit started the
|
|
12.3 kHz Timer-A PWM ISR at init and left it running forever, even
|
|
silent (~320 cycles/tick = ~49.5% of the CPU), and UBER inits audio
|
|
before timing anything. Every ST capture before 2026-07-07 is ~2x
|
|
pessimistic. Timer A is demand-driven now (runs only while a mod or
|
|
SFX is audible), so the benches measure the op and an idle game
|
|
gets the whole CPU. Amiga/DOS/IIgs audited clean for the same bug
|
|
class (~1.8% / ~0.5% / 0% idle cost).
|
|
|
|
## Baseline (2026-07-07): absolute ops/sec per port
|
|
|
|
The IIgs is the reference and the perf floor (every other port must
|
|
meet or beat it -- project directive). Non-IIgs cells show absolute
|
|
ops/sec plus percentage of the IIgs number; sub-100% cells are bolded
|
|
as below-floor. `tools/uber-perf-table` emits this table directly;
|
|
regenerate after any re-capture (Amiga needs TIMEOUT=600 -- the
|
|
default cuts off the final checks). Capture provenance: all four
|
|
ports 2026-07-07, the 51-line golden freeze gate (jlSurfaceCopyRect +
|
|
gameFrameClean experiment; ST column includes the movep.l map engine;
|
|
Amiga run with TIMEOUT=600; IIgs from a fresh `make iigs-disk` image
|
|
-- `make all` alone leaves joey.2mg stale).
|
|
|
|
| Op | IIgs (ops/sec) | Amiga (ops/sec, % of IIGS) | Atari ST (ops/sec, % of IIGS) | DOS (ops/sec, % of IIGS) |
|
|
| --- | --- | --- | --- | --- |
|
|
| jlSurfaceClear | 33 | 75 (227%) | 88 (267%) | 66 (200%) |
|
|
| jlPaletteSet | 1561 | 11503 (737%) | 10388 (665%) | 29809 (1910%) |
|
|
| jlScbSetRange | 163 | 4003 (2456%) | 4833 (2965%) | 8495 (5212%) |
|
|
| jlDrawPixel | 5576 | 5853 (105%) | 6522 (117%) | 23687 (425%) |
|
|
| jlDrawLine H | 1142 | 1753 (154%) | 2670 (234%) | 5708 (500%) |
|
|
| jlDrawLine V | 99 | 303 (306%) | 261 (264%) | 468 (473%) |
|
|
| jlDrawLine diag | 42 | **40 (95%)** | 88 (210%) | 109 (260%) |
|
|
| jlDrawRect 100x100 | 73 | 285 (390%) | 279 (382%) | 470 (644%) |
|
|
| jlDrawCircle r=16 | 304 | **285 (94%)** | 359 (118%) | 447 (147%) |
|
|
| jlDrawCircle r=80 | 70 | **63 (90%)** | 83 (119%) | 98 (140%) |
|
|
| jlFillRect 16x16 | 602 | 1153 (192%) | 1225 (203%) | 3027 (503%) |
|
|
| jlFillRect 80x80 | 110 | 285 (259%) | 236 (215%) | 414 (376%) |
|
|
| jlFillRect 320x200 | 21 | 68 (324%) | 77 (367%) | 58 (276%) |
|
|
| jlFillCircle r=40 | 42 | **31 (74%)** | 155 (369%) | 156 (371%) |
|
|
| jlSamplePixel | 4797 | 7253 (151%) | 9603 (200%) | 46251 (964%) |
|
|
| jlTileFill | 3251 | 3703 (114%) | 3855 (119%) | 11840 (364%) |
|
|
| jlTileCopy | 1988 | 2953 (149%) | 3445 (173%) | 10913 (549%) |
|
|
| jlTileCopyMasked | 681 | 1003 (147%) | 1632 (240%) | 3391 (498%) |
|
|
| jlTilePaste | 2710 | 3803 (140%) | 4033 (149%) | 12601 (465%) |
|
|
| jlTileSnap | 2530 | 6003 (237%) | 6403 (253%) | 19247 (761%) |
|
|
| jlSpriteSaveUnder | 602 | 2103 (349%) | 2611 (434%) | 10743 (1785%) |
|
|
| jlSpriteDraw | 966 | 1103 (114%) | 1283 (133%) | 6404 (663%) |
|
|
| jlSpriteRestoreUnder | 545 | 1453 (267%) | 1781 (327%) | 6218 (1141%) |
|
|
| jlSpriteSaveAndDraw | 423 | 803 (190%) | 892 (211%) | 4668 (1104%) |
|
|
| jlStagePresent full | 67 | 534 (797%) | 467 (697%) | 1137 (1697%) |
|
|
| jlInputPoll | 228 | 4053 (1778%) | 2788 (1223%) | 3148 (1381%) |
|
|
| jlKeyDown | 10191 | 39353 (386%) | 45286 (444%) | 77526 (761%) |
|
|
| jlKeyPressed | 11389 | 38353 (337%) | 45286 (398%) | 75119 (660%) |
|
|
| jlMouseX | 20214 | 96753 (479%) | 117981 (584%) | 155281 (768%) |
|
|
| joeyJoyConnected | 10191 | 38903 (382%) | 44260 (434%) | 77537 (761%) |
|
|
| jlAudioFrameTick | 27509 | 56353 (205%) | 40637 (148%) | 59211 (215%) |
|
|
| jlAudioIsPlayingMod | 11071 | 56453 (510%) | 60441 (546%) | 128004 (1156%) |
|
|
| surfaceMarkDirtyRect (via jlFillRect 32x32) | 323 | 853 (264%) | 774 (240%) | 1476 (457%) |
|
|
| jlSpriteDraw unaligned | 906 | **903 (100%)** | 1070 (118%) | 5215 (576%) |
|
|
| jlSpriteDraw sweep16 | 60 | **56 (93%)** | 68 (113%) | 362 (603%) |
|
|
| jlTilePaste map 10x5 | 59 | 78 (132%) | 80 (136%) | 258 (437%) |
|
|
| gameFrame composite | 10 | 25 (250%) | 22 (220%) | 19 (190%) |
|
|
| jlTileMapPaste 10x5 | 140 | 153 (109%) | 346 (247%) | 450 (321%) |
|
|
| gameFrameClean composite | 18 | 34 (189%) | 48 (267%) | 195 (1083%) |
|
|
|
|
> **IIgs full-screen ceiling (~29 ops/sec).** A full-screen op moves
|
|
> 32000 bytes = 16000 word-stores x 6 cyc (STA long,X / PEI, both
|
|
> 3 cyc/byte) / 2.8 MHz ~= 34 ms ~= 29 ops/sec maximum. So
|
|
> `jlSurfaceClear`, `jlFillRect 320x200`, and `jlStagePresent full`
|
|
> cannot legitimately exceed ~29; a higher reading means the benchmark
|
|
> clock was perturbed. The clock is `jlFrameCount` = the VBL-interrupt-
|
|
> driven GetTick, so an op that holds `SEI` while it runs can over-
|
|
> report its ops/sec. Treat full-screen IIgs numbers above ~29 as
|
|
> inflated and cross-check against the cycle model.
|
|
|
|
## Reading notes
|
|
|
|
* **Sprite rows measure COMPILED code on all four ports** since
|
|
NATIVE-PERF Phase 1 (2026-07-06): the 68k ports JIT all-shift draw
|
|
variants plus group-window save/restore (previously stubs that fell
|
|
to interpreters), IIgs codegen re-enabled via finding #77, DOS via
|
|
finding #74. The interpreted walkers remain only as arena-full
|
|
degrade paths -- and are proven bit-identical to the compiled code
|
|
by the 49-hash gate.
|
|
* **The below-floor cells are the standing punch list**, and after the
|
|
2026-07-07 audio-tax fix the ST has NONE -- every remaining
|
|
below-floor graphics cell is the Amiga's: the circle family (diag
|
|
line 95%, circle r=16 94%, r=80 87%, fillCircle 74% -- planar 4-plane
|
|
RMW tax) and the two sprite workload rows a hair under floor
|
|
(unaligned 95%, sweep16 89%; chip-RAM display-DMA contention that the
|
|
fast-RAM-code ST does not pay). DOS jlAudioFrameTick (42%) is the SB
|
|
half-buffer silence refill on an otherwise-trivial call -- it gates
|
|
nothing. Floor parity is effectively DONE; the open work is
|
|
ceiling-relative (NATIVE-PERF-PLAN.md Phase 2+), where every port
|
|
still runs 2-5x below hand-coded technique on tile/sprite glue.
|
|
Amiga jlFillCircle r=40 (74%) was probed
|
|
2026-07-06 with a full whole-circle asm routine (Bresenham + inline
|
|
4-plane long-fill span): it held 45/45 hashes but did NOT move the
|
|
number (34 vs 31, within the coarse 11-iter sample), so the per-span
|
|
C dispatch is NOT the bottleneck -- the asm was reverted. The real cost
|
|
is the midpoint fill's 8-way symmetry filling each row 2-4x (idempotent
|
|
overlap): the true lever is a scanline rewrite (one span per row, no
|
|
overlap), which would roughly halve the byte writes and stay
|
|
hash-identical. NB: this op is timed at batch=1 (~29 ms each) by the
|
|
50 Hz frame counter, a coarse measurement. (The old note here blaming
|
|
a "4-5x model-vs-measured gap" partly on measurement was vindicated
|
|
2026-07-07: on the ST that gap was the idle-audio tax, artifact #4.)
|
|
* **jlRandom has no timed row.** Phase 8 (#43) rewrote its xorshift32
|
|
update in 16-bit halves on the IIgs only (~2-3x fewer cycles per
|
|
call on the 65816; 32-bit shifts there are per-bit helper loops).
|
|
Output is bit-identical -- host-proven over the golden sequence and
|
|
a 50M-state sweep, and gated by UBER's random-golden check on all
|
|
four ports.
|
|
* **Input predicates stay functions** (Phase 8 #40 decision): the
|
|
post-clock-fix numbers (12K-77K ops/sec) put call overhead nowhere
|
|
near a real frame budget; macro forms in the public header were
|
|
rejected as not worth the API exposure.
|
|
* **IIgs libc memset/memcpy remain ~4x slow** (finding #79,
|
|
toolchain-owned): llvm-mos runtime byte loops vs ORCA's MVN. The
|
|
jlScbSetRange and jlPaletteSet IIgs rows carry that tax; library-side
|
|
mitigations landed (#16 unrolled word copy, #21), the libc fix is
|
|
flagged to the llvm816 session.
|
|
|
|
## History
|
|
|
|
Pre-2026-07-04 tables (ORCA-era IIgs column, artifact-inflated DOS
|
|
numbers, un-batched loops) live in git history only -- every
|
|
conclusion drawn from them was re-derived from Phase 0 onward. The
|
|
Phase 0 baseline (2026-07-04) and per-phase deltas are recorded in
|
|
PERF-AUDIT-PLAN.md's progress tracker; headline gains since Phase 0:
|
|
IIgs interpreted spriteDraw 18 -> 48 ops/sec (Phase 4), IIgs
|
|
tilePasteMono asm 3.35x (Phase 7 #3), ST flood plane hooks 5.6x
|
|
(Phase 7 #4), DOS paletteSet +92% / tileSnap +42% (Phases 2/5),
|
|
DOS compiled sprites 284 -> ~6100 ops/sec (#74).
|