[iterate-4A] diagnostics: XENIA_PROFILE wall-time profiler + probe/tooling snapshot
Handoff snapshot of the env-gated diagnostic scaffolding used across the intro-video RE. Kept out of the milestone commits (645feb8..5573ac1) to keep those clean; committed here so nothing is lost on handoff. New — XENIA_PROFILE wall-time profiler (crates/xenia-gpu/src/prof.rs): Coarse buckets attributing playback wall time to interpreter (step_block), kernel HLE (call_export), block decode/cache (lookup_or_build), texture decode, host draw, and present; prints periodic snapshots (every 500M guest instr, or every 500 presents) + a clean-exit report. Hot path is gated on a cached is_on() (one relaxed load) so it is zero-cost when XENIA_PROFILE is unset. Call sites: main.rs run_superblock / parallel worker (step_block, lookup_or_build, call_export), texture_cache ensure_cached, render.rs present + dispatch_xenos_draws. First profile (movie playback, headless single-thread lockstep): effective ~35 MIPS; interpreter body ~40% @ ~95-102 MIPS; texture decode 0.3% (cache works); present ~0%; the rest is per-block dispatch + scheduler plumbing (~13 instr/block over 229M blocks). Overhead-bound, not interpreter-body bound; the levers are coarser execution units (superblock chaining) and ultimately a JIT. Pre-existing read-only probe knobs (were uncommitted; env-gated, observe-only): XENIA_RET_CAPTURE_PC/_REG/_MEM, LOG_RESUMES, LOG_WAITS, LOG_SIGNAL, FORCE_TID, STARVE_LIMIT, INCUMBENT_PICK, INSTR_PER_MS, DUMP_FRAME, DUMP_WGSL, BIND_LOG, CONST_LOG, DISPATCH_REC, AUDIT_PC_TRACE. Tooling: sylph-run.sh (movie oracle loop, 180s default timeout), zq.py (DuckDB disasm/xref helper). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1246,6 +1246,17 @@ impl GpuSystem {
|
||||
let mut ds = draw_state::extract(&self.register_file, vgt, dma_base, dma_size);
|
||||
ds.vs_blob_key = self.active_vs_key;
|
||||
ds.ps_blob_key = self.active_ps_key;
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
let c254x = f32::from_bits(self.register_file.read(CONST_BASE_ALU + 254 * 4));
|
||||
let c255x = f32::from_bits(self.register_file.read(CONST_BASE_ALU + 255 * 4));
|
||||
if c254x != 0.0 || c255x != 0.0 {
|
||||
eprintln!(
|
||||
"DRAW-CONST draw={} ps={:#x} c254.x={c254x} c255.x={c255x}",
|
||||
self.stats.draws_seen,
|
||||
self.active_ps_key.unwrap_or(0)
|
||||
);
|
||||
}
|
||||
}
|
||||
let processed = primitive::process(ds.primitive, ds.vertex_count, None);
|
||||
metrics::counter!(
|
||||
"gpu.draw",
|
||||
@@ -1468,6 +1479,15 @@ impl GpuSystem {
|
||||
let v = self.read_payload(mem, 2 + i);
|
||||
self.register_file.write(base + index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
// Compact histogram-able line: type + index-range for every
|
||||
// SET_CONSTANT. `covers254` flags an ALU write hitting c254.
|
||||
eprintln!(
|
||||
"SC t={const_type} i={index} n={} covers254={}",
|
||||
count - 1,
|
||||
const_type == 0 && index <= 1016 && 1016 < index + (count - 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_SET_CONSTANT2 => {
|
||||
// payload[0] = 16-bit index; subsequent payloads write consecutive regs.
|
||||
@@ -1476,6 +1496,15 @@ impl GpuSystem {
|
||||
let v = self.read_payload(mem, 2 + i);
|
||||
self.register_file.write(index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() {
|
||||
let alu_lo = CONST_BASE_ALU;
|
||||
let alu_hi = CONST_BASE_ALU + 2048;
|
||||
eprintln!(
|
||||
"SC2 i={index:#x} n={} in_alu={}",
|
||||
count - 1,
|
||||
index >= alu_lo && index < alu_hi
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_LOAD_ALU_CONSTANT => {
|
||||
// payload[0] = source mem addr, [1] = offset_type, [2] = size_dwords
|
||||
@@ -1496,6 +1525,13 @@ impl GpuSystem {
|
||||
let v = mem.read_u32(src + i * 4);
|
||||
self.register_file.write(base + index + i, v);
|
||||
}
|
||||
if std::env::var("XENIA_CONST_LOG").is_ok() && const_type == 0 {
|
||||
eprintln!(
|
||||
"LOAD-ALU-CONST src={src:#x} idx={index} size={size_dwords} first={:?} covers254={}",
|
||||
f32::from_bits(mem.read_u32(src)),
|
||||
index <= 1016 && 1016 < index + size_dwords
|
||||
);
|
||||
}
|
||||
}
|
||||
pm4::PM4_IM_LOAD | pm4::PM4_IM_LOAD_IMMEDIATE => {
|
||||
// Canary (pm4_command_processor_implement.h:1271-1330):
|
||||
|
||||
@@ -20,6 +20,7 @@ pub mod handle;
|
||||
pub mod mmio_region;
|
||||
pub mod pm4;
|
||||
pub mod primitive;
|
||||
pub mod prof;
|
||||
pub mod register_file;
|
||||
pub mod ring_drain;
|
||||
pub mod ring_view;
|
||||
|
||||
193
crates/xenia-gpu/src/prof.rs
Normal file
193
crates/xenia-gpu/src/prof.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
//! Lightweight env-gated wall-time profiler (probe-patch, UNCOMMITTED).
|
||||
//!
|
||||
//! Attributes emulator wall time to coarse buckets so we can tell whether the
|
||||
//! movie-playback slowdown is CPU-interpreter bound, texture-decode bound, or
|
||||
//! GPU-present bound. Enabled only when `XENIA_PROFILE` is set; the hot-path
|
||||
//! cost when disabled is a single relaxed atomic add of already-measured nanos
|
||||
//! (callers still pay `Instant::now()` — acceptable at the coarse boundaries we
|
||||
//! instrument: per basic-block, per texture upload, per present).
|
||||
//!
|
||||
//! Read the buckets with `xenia_gpu::prof::report(wall_ns)` at clean shutdown.
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
static START: OnceLock<std::time::Instant> = OnceLock::new();
|
||||
|
||||
/// Lazily anchor the wall-time window (first call wins). Called from `add`.
|
||||
#[inline]
|
||||
fn mark_start() {
|
||||
START.get_or_init(std::time::Instant::now);
|
||||
}
|
||||
|
||||
/// Nanos since the profiler's first accounted event.
|
||||
pub fn wall_ns() -> u64 {
|
||||
START.get().map(|t| t.elapsed().as_nanos() as u64).unwrap_or(0)
|
||||
}
|
||||
|
||||
pub static STEP_NS: AtomicU64 = AtomicU64::new(0); // guest interpreter (step_block)
|
||||
pub static STEP_INSTR: AtomicU64 = AtomicU64::new(0); // guest instructions retired
|
||||
pub static STEP_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static TEXDEC_NS: AtomicU64 = AtomicU64::new(0); // texture decode + host upload
|
||||
pub static TEXDEC_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
pub static TEXDEC_BYTES: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static PRESENT_NS: AtomicU64 = AtomicU64::new(0); // frontbuffer present
|
||||
pub static PRESENT_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static DRAW_NS: AtomicU64 = AtomicU64::new(0); // host draw submission
|
||||
pub static DRAW_CALLS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub static KERNEL_NS: AtomicU64 = AtomicU64::new(0); // kernel HLE export dispatch
|
||||
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);
|
||||
|
||||
/// 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);
|
||||
|
||||
/// Cheap (one relaxed load + branch) enabled check for hot paths. Resolves
|
||||
/// `XENIA_PROFILE` from the environment exactly once, then caches it.
|
||||
#[inline]
|
||||
pub fn is_on() -> bool {
|
||||
match ENABLED.load(Ordering::Relaxed) {
|
||||
1 => true,
|
||||
2 => false,
|
||||
_ => {
|
||||
let on = std::env::var_os("XENIA_PROFILE").is_some();
|
||||
ENABLED.store(if on { 1 } else { 2 }, Ordering::Relaxed);
|
||||
on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn enabled() -> bool {
|
||||
is_on()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn add(counter: &AtomicU64, v: u64) {
|
||||
mark_start();
|
||||
counter.fetch_add(v, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// RAII timer: adds elapsed nanos to `ns` and bumps `calls` on drop.
|
||||
pub struct ScopeTimer {
|
||||
t0: std::time::Instant,
|
||||
ns: &'static AtomicU64,
|
||||
calls: &'static AtomicU64,
|
||||
}
|
||||
|
||||
impl ScopeTimer {
|
||||
#[inline]
|
||||
pub fn new(ns: &'static AtomicU64, calls: &'static AtomicU64) -> Self {
|
||||
mark_start();
|
||||
Self { t0: std::time::Instant::now(), ns, calls }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ScopeTimer {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
self.ns.fetch_add(self.t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
|
||||
self.calls.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
static NEXT_REPORT_INSTR: AtomicU64 = AtomicU64::new(500_000_000);
|
||||
|
||||
/// Fire a snapshot every ~500M retired guest instructions (headless runs have
|
||||
/// no present() to piggyback on and may never reach the clean-exit report).
|
||||
#[inline]
|
||||
pub fn maybe_report_by_instr() {
|
||||
if !enabled() {
|
||||
return;
|
||||
}
|
||||
let instr = STEP_INSTR.load(Ordering::Relaxed);
|
||||
let thresh = NEXT_REPORT_INSTR.load(Ordering::Relaxed);
|
||||
if instr >= thresh
|
||||
&& NEXT_REPORT_INSTR
|
||||
.compare_exchange(thresh, thresh + 500_000_000, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
report(0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the accumulated buckets against the profiler's own wall window.
|
||||
/// The argument is accepted for call-site convenience but ignored in favour
|
||||
/// of the internally-anchored window (`wall_ns()`).
|
||||
pub fn report(_ignored: u64) {
|
||||
let wall_ns = wall_ns();
|
||||
let g = |c: &AtomicU64| c.load(Ordering::Relaxed);
|
||||
let ms = |ns: u64| ns as f64 / 1e6;
|
||||
let pct = |ns: u64| {
|
||||
if wall_ns == 0 {
|
||||
0.0
|
||||
} else {
|
||||
100.0 * ns as f64 / wall_ns as f64
|
||||
}
|
||||
};
|
||||
let step_ns = g(&STEP_NS);
|
||||
let step_instr = g(&STEP_INSTR);
|
||||
let tex_ns = g(&TEXDEC_NS);
|
||||
let pres_ns = g(&PRESENT_NS);
|
||||
let draw_ns = g(&DRAW_NS);
|
||||
let mips = if step_ns == 0 {
|
||||
0.0
|
||||
} 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);
|
||||
eprintln!(
|
||||
" kernel HLE export : {:>10.1} ms {:>5.1}% ({} calls)",
|
||||
ms(kern_ns),
|
||||
pct(kern_ns),
|
||||
g(&KERNEL_CALLS)
|
||||
);
|
||||
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))
|
||||
);
|
||||
}
|
||||
@@ -652,6 +652,7 @@ impl TextureCache {
|
||||
}
|
||||
self.restale_total += 1;
|
||||
}
|
||||
let _prof_t0 = std::time::Instant::now();
|
||||
let bytes = match key.format {
|
||||
TextureFormat::K8 => decode_k8(&key, mem)?,
|
||||
TextureFormat::K8888 => decode_k8888_tiled(&key, mem)?,
|
||||
@@ -659,9 +660,37 @@ impl TextureCache {
|
||||
TextureFormat::Dxt1 => decode_dxt1_tiled(&key, mem)?,
|
||||
TextureFormat::Dxt2_3 => decode_dxt23_tiled(&key, mem)?,
|
||||
TextureFormat::Dxt4_5 => decode_dxt45_tiled(&key, mem)?,
|
||||
_ => return Err(DecodeError::UnsupportedFormat),
|
||||
_ => {
|
||||
// XENIA_TEX_REJECT_LOG diagnostic (STEP 87, read-only): the
|
||||
// intro video uploads its YUV420 planes as `k_8` linear
|
||||
// textures; if we have no decoder for the requested format we
|
||||
// reject it here and it never reaches the GPU (→ black video).
|
||||
// Rate-limited so a per-frame flood stays grep-able.
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let n = N.fetch_add(1, Ordering::Relaxed);
|
||||
if n < 120 {
|
||||
tracing::warn!(
|
||||
fmt = ?key.format,
|
||||
w = key.width,
|
||||
h = key.height,
|
||||
base = format_args!("0x{:08x}", key.base_address),
|
||||
dim = ?key.dimension,
|
||||
pitch = key.pitch_texels,
|
||||
n,
|
||||
"TEX-REJECT: unsupported texture format (no host decoder)"
|
||||
);
|
||||
}
|
||||
return Err(DecodeError::UnsupportedFormat);
|
||||
}
|
||||
};
|
||||
self.decodes_total += 1;
|
||||
{
|
||||
use crate::prof;
|
||||
prof::add(&prof::TEXDEC_NS, _prof_t0.elapsed().as_nanos() as u64);
|
||||
prof::add(&prof::TEXDEC_CALLS, 1);
|
||||
prof::add(&prof::TEXDEC_BYTES, bytes.len() as u64);
|
||||
}
|
||||
let entry = CachedTexture {
|
||||
key,
|
||||
version_when_uploaded: current_version,
|
||||
|
||||
@@ -460,6 +460,16 @@ impl EmitCtx {
|
||||
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate, const_base);
|
||||
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate, const_base);
|
||||
|
||||
if (42..=47).contains(&alu.scalar_opcode) && std::env::var("XENIA_SC_LOG").is_ok() {
|
||||
eprintln!(
|
||||
"SC-OP opc={} sa={} atmp={} asw={:#04x} sb={} btmp={} bsw={:#04x} sc={} ctmp={} csw={:#04x}",
|
||||
alu.scalar_opcode,
|
||||
alu.src_a, alu.src_a_is_temp as u8, alu.src_a_swiz,
|
||||
alu.src_b, alu.src_b_is_temp as u8, alu.src_b_swiz,
|
||||
alu.src_c, alu.src_c_is_temp as u8, alu.src_c_swiz,
|
||||
);
|
||||
}
|
||||
|
||||
// Vector pipe.
|
||||
if alu.vector_write_mask != 0 {
|
||||
let expr = vector_expr(alu.vector_opcode, &a, &b, &c)
|
||||
@@ -501,6 +511,9 @@ impl EmitCtx {
|
||||
let expr = match scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
if std::env::var("XENIA_BIND_LOG").is_ok() {
|
||||
eprintln!("SCL-UNSUPPORTED opcode={:#04x} ({})", alu.scalar_opcode, alu.scalar_opcode);
|
||||
}
|
||||
return Err(reject::SCL_OP_UNSUPPORTED);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user