[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:
@@ -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: &[],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user