[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:
MechaCat02
2026-07-03 07:43:53 +02:00
parent 5573ac1c43
commit 2c883e9d5e
13 changed files with 800 additions and 14 deletions

View File

@@ -600,6 +600,8 @@ impl RenderState {
if count == 0 {
return;
}
let _prof_g =
xenia_gpu::prof::ScopeTimer::new(&xenia_gpu::prof::DRAW_NS, &xenia_gpu::prof::DRAW_CALLS);
let _span = tracing::debug_span!(
"ui.xenos.dispatch",
count,
@@ -881,6 +883,36 @@ impl RenderState {
cap.ps_key,
rstate,
);
// Log only the "interesting" draws — multi-texture or any non-K8888
// (e.g. the movie's k_8 YUV planes) — so the boot's single-K8888
// quads don't flood the cap before the movie composites.
let interesting = cap.textures.len() > 1
|| cap.textures.iter().any(|(_, k, ..)| {
!matches!(k.format, xenia_gpu::texture_cache::TextureFormat::K8888)
});
if std::env::var("XENIA_BIND_LOG").is_ok() && interesting {
use std::sync::atomic::{AtomicUsize, Ordering};
static N: AtomicUsize = AtomicUsize::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
if n < 200 {
let slots: Vec<u8> = cap.textures.iter().map(|(s, ..)| *s).collect();
let fmts: Vec<String> = cap
.textures
.iter()
.map(|(_, k, ..)| format!("{:?}", k.format))
.collect();
eprintln!(
"BIND-LOG ps={:#x} vs={:#x} ntex={} slots={:?} fmts={:?} translated={} verts={}",
cap.ps_key,
cap.vs_key,
cap.textures.len(),
slots,
fmts,
served_translated,
cap.host_vertex_count,
);
}
}
if served_translated {
self.xenos_dispatches_translator =
self.xenos_dispatches_translator.saturating_add(1);
@@ -901,6 +933,26 @@ impl RenderState {
self.xenos_draws_rendered = self
.xenos_draws_rendered
.saturating_add(captures.len() as u64);
// Frontbuffer readback for offline color verification (XENIA_DUMP_FRAME).
// Counts frames that contained a movie (k_8 YUV) draw and dumps the
// composited frontbuffer at a few fade-in stages so the YUV→RGB output
// can be inspected as a raw RGBA image without needing a live viewer.
if std::env::var("XENIA_DUMP_FRAME").is_ok() {
use std::sync::atomic::{AtomicUsize, Ordering};
static MOVIE_FRAMES: AtomicUsize = AtomicUsize::new(0);
let has_movie = captures.iter().any(|c| {
c.textures.iter().any(|(_, k, _, _)| {
matches!(k.format, xenia_gpu::texture_cache::TextureFormat::K8)
})
});
if has_movie {
let n = MOVIE_FRAMES.fetch_add(1, Ordering::Relaxed);
if n % 20 == 0 && n <= 2000 {
let dir = "/tmp/claude-1000/-home-fabi-RE---Project-Sylpheed/c5711e5b-8a9c-410c-860d-662365e450a4/scratchpad";
self.dump_frontbuffer(&format!("{dir}/fb_movie_{n:04}.raw"));
}
}
}
self.real_geometry_draws = self
.real_geometry_draws
.saturating_add(real_count as u64);
@@ -915,6 +967,64 @@ impl RenderState {
real_count
}
/// Diagnostic: copy the current frontbuffer back to the CPU and write it
/// as tight RGBA8 bytes (`width*height*4`) to `path`. Blocks on the GPU.
/// Used to verify rendered colors offline (e.g. the intro-video YUV→RGB).
fn dump_frontbuffer(&self, path: &str) {
let (w, h) = self.frontbuffer_size;
if w == 0 || h == 0 {
return;
}
let bpp = 4u32;
let unpadded = w * bpp;
let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
let padded = unpadded.div_ceil(align) * align;
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("frontbuffer readback"),
size: (padded * h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("fb-dump") });
encoder.copy_texture_to_buffer(
wgpu::ImageCopyTexture {
texture: &self.frontbuffer_tex,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::ImageCopyBuffer {
buffer: &buffer,
layout: wgpu::ImageDataLayout {
offset: 0,
bytes_per_row: Some(padded),
rows_per_image: Some(h),
},
},
wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(encoder.finish()));
let slice = buffer.slice(..);
slice.map_async(wgpu::MapMode::Read, |_| {});
self.device.poll(wgpu::Maintain::Wait);
let data = slice.get_mapped_range();
let mut out = Vec::with_capacity((unpadded * h) as usize);
for row in 0..h as usize {
let s = row * padded as usize;
out.extend_from_slice(&data[s..s + unpadded as usize]);
}
drop(data);
buffer.unmap();
let _ = std::fs::write(path, &out);
eprintln!("FRONTBUFFER-DUMP {w}x{h} -> {path} ({} bytes)", out.len());
}
/// Count of distinct translator pipelines compiled so far. Surfaced
/// on the HUD as `xlated=N` to make "is P7 working?" observable.
pub fn translated_pipeline_count(&self) -> usize {
@@ -1072,8 +1182,19 @@ impl RenderState {
pass.draw(0..self.hud_vertex_count, 0..1);
}
}
let _prof_t0 = std::time::Instant::now();
self.queue.submit(std::iter::once(encoder.finish()));
frame.present();
{
use xenia_gpu::prof;
prof::add(&prof::PRESENT_NS, _prof_t0.elapsed().as_nanos() as u64);
let n = prof::PRESENT_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
// Periodic snapshot so a SIGTERM'd (timed-out) movie run still
// yields a profile — the clean-exit report may never be reached.
if prof::enabled() && n % 500 == 0 {
prof::report(0);
}
}
Ok(())
}
}
@@ -1107,6 +1228,9 @@ fn ensure_translated_pipeline(
"reason" => reason,
)
.increment(1);
if std::env::var("XENIA_BIND_LOG").is_ok() {
eprintln!("TRANSLATE-REJECT vs={vs_key:#x} ps={ps_key:#x} stage=vs reason={reason}");
}
return false;
}
};
@@ -1119,10 +1243,18 @@ fn ensure_translated_pipeline(
"reason" => reason,
)
.increment(1);
if std::env::var("XENIA_BIND_LOG").is_ok() {
eprintln!("TRANSLATE-REJECT vs={vs_key:#x} ps={ps_key:#x} stage=ps reason={reason}");
}
return false;
}
};
let wgsl = combine_stages(&vs_body, &ps_body);
if let Ok(dir) = std::env::var("XENIA_DUMP_WGSL") {
let path = format!("{dir}/wgsl_vs{vs_key:#x}_ps{ps_key:#x}.wgsl");
let _ = std::fs::write(&path, &wgsl);
eprintln!("DUMP-WGSL wrote {path}");
}
xenos_pipeline.insert_translated(device, vs_key, ps_key, &wgsl)
}
@@ -1143,6 +1275,7 @@ fn make_frontbuffer(device: &wgpu::Device, w: u32, h: u32) -> (wgpu::Texture, wg
// this texture instead of only consuming CPU-side raw scrapes.
usage: wgpu::TextureUsages::TEXTURE_BINDING
| wgpu::TextureUsages::COPY_DST
| wgpu::TextureUsages::COPY_SRC
| wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
});