[iterate-4A] perf: superblock budget 128->192 (movie-validated ~7% wall) + profiler attribution

Speed frontier, increment 1 (measure-first, then bank the cheap win).

Profiler (crates/xenia-gpu/src/prof.rs): extend XENIA_PROFILE with hierarchical
attribution of the single-thread lockstep loop — ROUND (per-round tax) /
PROLOGUE (worker_prologue) / RUNSB (run_superblock) top level, plus the
STEP/EPILOGUE/KERNEL/BUILD subsets. Gated zero-cost via is_on() (off-check emits
0 lines). Movie-time split: interp step_block 42%, run_superblock chain-loop
body ~30%, worker_prologue+epilogue ~22%, kernel HLE 6%, per-round tax 0.6%,
texture 0.4%. => overhead-bound; the ceiling is a JIT (attacks the ~72% interp
+ dispatch). The per-round tax is a non-lever (0.6%).

Budget (crates/xenia-app/src/main.rs SUPERBLOCK_INSTR_BUDGET 128->192): the
per-slot-visit tax (prologue+epilogue ~22%) scales with slot-visit count, and
chains are NOT break-limited here — 128->192 cuts visits 27.4M->18.9M (-31%),
128->256 -44%. Banked 192 (not 256): the movie decode pipeline is more
timing-sensitive than boot. At 256 the decode worker tid25 (0x82506588)
intermittently fails to resume (1-of-2 runs) with a weakened feeder loop
(source-read 27->5-11) — a scheduling race the coarser interleaving exposes.
192 is deterministic and safe: 3/3 runs byte-identical (source-read=21,
tid25-resume=1, ADVreads=30) at both env-override and compiled-default, ~-7%
movie wall, well below the 384 boot cliff. XENIA_SUPERBLOCK_BUDGET still
overrides for A/B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-06 19:45:23 +02:00
parent cc9ebbb5e7
commit e07f93ed0a
2 changed files with 87 additions and 51 deletions

View File

@@ -45,6 +45,16 @@ pub static KERNEL_CALLS: AtomicU64 = AtomicU64::new(0);
pub static BUILD_NS: AtomicU64 = AtomicU64::new(0); // block decode / cache lookup
pub static BUILD_CALLS: AtomicU64 = AtomicU64::new(0);
// Top-level lockstep-loop attribution (sums to ~100% with idle/GPU-pacer):
pub static ROUND_NS: AtomicU64 = AtomicU64::new(0); // per-round tax (clock/timestamp/isr/schedule)
pub static ROUND_CALLS: AtomicU64 = AtomicU64::new(0);
pub static PROLOGUE_NS: AtomicU64 = AtomicU64::new(0); // worker_prologue (⊇ KERNEL)
pub static PROLOGUE_CALLS: AtomicU64 = AtomicU64::new(0);
pub static RUNSB_NS: AtomicU64 = AtomicU64::new(0); // run_superblock (⊇ STEP + chain BUILD + EPILOGUE)
pub static RUNSB_CALLS: AtomicU64 = AtomicU64::new(0);
pub static EPILOGUE_NS: AtomicU64 = AtomicU64::new(0); // worker_epilogue (subset of RUNSB)
pub static EPILOGUE_CALLS: AtomicU64 = AtomicU64::new(0);
/// Cached on/off state so the per-block hot path never touches the
/// environment. 0 = uninitialised, 1 = on, 2 = off.
static ENABLED: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
@@ -142,52 +152,43 @@ pub fn report(_ignored: u64) {
} else {
step_instr as f64 / (step_ns as f64 / 1e3) // instr / us = MIPS
};
eprintln!("=== XENIA_PROFILE (wall {:.1} ms) ===", ms(wall_ns));
eprintln!(
" interp step_block : {:>10.1} ms {:>5.1}% ({} calls, {} instr, {:.1} MIPS)",
ms(step_ns),
pct(step_ns),
g(&STEP_CALLS),
step_instr,
mips
);
eprintln!(
" texture decode+up : {:>10.1} ms {:>5.1}% ({} calls, {} MiB)",
ms(tex_ns),
pct(tex_ns),
g(&TEXDEC_CALLS),
g(&TEXDEC_BYTES) / (1024 * 1024)
);
eprintln!(
" host draw submit : {:>10.1} ms {:>5.1}% ({} calls)",
ms(draw_ns),
pct(draw_ns),
g(&DRAW_CALLS)
);
let kern_ns = g(&KERNEL_NS);
let build_ns = g(&BUILD_NS);
let round_ns = g(&ROUND_NS);
let prologue_ns = g(&PROLOGUE_NS);
let runsb_ns = g(&RUNSB_NS);
let epilogue_ns = g(&EPILOGUE_NS);
// Derived leftovers (honest lumping — see comments):
// runsb_other = chain-loop body (arithmetic/stop-checks) + in-chain block lookups
// prologue_other = block lookup (first block) + scheduler bookkeeping
let runsb_other = runsb_ns.saturating_sub(step_ns).saturating_sub(epilogue_ns);
let prologue_other = prologue_ns.saturating_sub(kern_ns);
let line = |label: &str, ns: u64, extra: &str| {
eprintln!(" {:<20}{:>10.1} ms {:>5.1}% {}", label, ms(ns), pct(ns), extra);
};
eprintln!("=== XENIA_PROFILE (wall {:.1} ms) ===", ms(wall_ns));
eprintln!(" -- TOP LEVEL (single-thread lockstep loop; sums ~100%) --");
line("per-round tax", round_ns, &format!("({} rounds)", g(&ROUND_CALLS)));
line("worker_prologue", prologue_ns, &format!("({} visits)", g(&PROLOGUE_CALLS)));
line("run_superblock", runsb_ns, &format!("({} visits)", g(&RUNSB_CALLS)));
let top = round_ns + prologue_ns + runsb_ns;
eprintln!(
" kernel HLE export : {:>10.1} ms {:>5.1}% ({} calls)",
ms(kern_ns),
pct(kern_ns),
g(&KERNEL_CALLS)
" ---- top accounted {:.1}% ; idle/GPU-pacer/misc {:.1}%",
pct(top),
pct(wall_ns.saturating_sub(top))
);
eprintln!(
" block decode/cache: {:>10.1} ms {:>5.1}% ({} calls)",
ms(build_ns),
pct(build_ns),
g(&BUILD_CALLS)
);
eprintln!(
" frontbuffer present: {:>9.1} ms {:>5.1}% ({} calls)",
ms(pres_ns),
pct(pres_ns),
g(&PRESENT_CALLS)
);
let accounted = step_ns + tex_ns + draw_ns + pres_ns + kern_ns + build_ns;
eprintln!(
" ---- accounted {:.1}% ; remainder (locks/kernel/scheduler/idle) {:.1}%",
pct(accounted),
pct(wall_ns.saturating_sub(accounted))
eprintln!(" -- SUB-ATTRIBUTION --");
line(
"interp step_block",
step_ns,
&format!("[in run_superblock] ({} calls, {} instr, {:.1} MIPS)", g(&STEP_CALLS), step_instr, mips),
);
line("worker_epilogue", epilogue_ns, "[in run_superblock]");
line("run_superblock other", runsb_other, "[chain-loop body + in-chain lookups]");
line("kernel HLE export", kern_ns, &format!("[in worker_prologue] ({} calls)", g(&KERNEL_CALLS)));
line("worker_prologue other", prologue_other, "[first-block lookup + sched bookkeeping]");
line("block decode/cache", build_ns, &format!("[split across prologue+runsb] ({} calls)", g(&BUILD_CALLS)));
line("texture decode+up", tex_ns, &format!("({} calls, {} MiB)", g(&TEXDEC_CALLS), g(&TEXDEC_BYTES) / (1024 * 1024)));
line("host draw submit", draw_ns, &format!("({} calls)", g(&DRAW_CALLS)));
line("frontbuffer present", pres_ns, &format!("({} calls)", g(&PRESENT_CALLS)));
}