Files
xenia-rs/crates/xenia-gpu/src/draw_capture.rs
MechaCat02 3559c8f6ef [iterate-4A] intro-video ROOT #3: render the YUV movie in correct color
The intro video (ADV.wmv) now plays end-to-end in correct color. Three
stacked host-render-path bugs, each masked by the prior:

#3a Multi-texture render path. The host bound a single texture slot, so
the YUV pixel shader's three plane fetches (Y 1280x720 + U/V 640x360, all
k_8) collapsed onto one texture. Expanded the Xenos pipeline to 8 tex+1
sampler slots (xenos_pipeline.rs, xenos_interp.wgsl, translator.rs
headers); each tfetch selects its texture by fetch-constant slot; the
DrawCapture textures tuple now carries the slot; render.rs uploads+binds
every plane per-draw. Also added the scalar-constant ALU ops MULSC/ADDSC/
SUBSC (42-47) the YUV->RGB shader uses.

#3b tfetch destination swizzle. decode_fetch read the tfetch dest as a
4-bit write mask (w1 & 0xF), but Xenos tfetch dword1[0:11] is a 12-bit
destination swizzle (3 bits/component: 0-3=xyzw, 4/5=const 0/1, 6/7=keep).
The result: all three plane fetches did a full-vec4 overwrite of the dest
register, so only the last plane survived. Decode the real 12-bit swizzle
(dest_swizzle) and emit per-lane writes so Y/U/V coexist in r1.x/.y/.z.

#3c Pixel-shader constant bank. Xenos splits the 512-entry float-constant
file: the vertex shader addresses c0..255 -> physical 0..255, but the
pixel shader's c0..255 map to physical 256..511. The game uploads the
YUV->RGB coefficients to physical 510/511. Our translator indexed the low
half for PS constants, reading all-zero -> R=B=Y^2, G=0 (magenta). emit_alu
now adds a const_base of 256 for pixel-stage constant reads.

Plus a bounded (FIFO, 64-entry) host texture cache: the movie streams ~3
new-VA planes per frame, and the previously-unbounded cache exhausted GPU
memory into a device-lost crash mid-playback.

Verified visually: the SQUARE ENIX logo and ADV.wmv footage render in
correct color (was magenta); the translated movie shader now reads
alu[510]/alu[511]; frame green channel is nonzero and R != B.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 20:23:56 +02:00

376 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Per-draw geometry capture for the host UI's faithful-render path.
//!
//! The deterministic headless core (`check --gpu-inline`) never touches this
//! module — it is populated only when a UI bridge is installed and consumed
//! only by `crates/xenia-ui`. The goal is to hand the UI the *real* guest
//! geometry behind each `PM4_DRAW_INDX*` packet so it can rasterize the
//! actual splash vertices instead of synthetic placeholder shapes.
//!
//! What the WGSL pipeline needs to reconstruct one draw (see
//! `shaders/xenos_interp.wgsl` `vs_main` / `interpret_vertex_fetch`):
//! * the active VS/PS blob keys (already published as assets),
//! * the primitive type + the host vertex count to issue,
//! * the raw guest vertex-buffer bytes for the fetched window, and
//! * the *dword base* of that window so the shader can rebase the absolute
//! fetch-constant address into the uploaded buffer.
//!
//! The hard part is sourcing the vertex window: the VS reads a vertex-fetch
//! constant (`xe_gpu_vertex_fetch_t`) whose dword-0 carries the absolute
//! guest dword address. We parse the active VS, find its first vertex fetch,
//! read that fetch constant out of the register file, then copy a bounded
//! window of guest memory starting at the fetch base.
use xenia_memory::access::MemoryAccess;
use crate::draw_state::{IndexSize, IndexSource, PrimitiveType};
use crate::register_file::RegisterFile;
/// Texture-fetch / vertex-fetch constant region base, in register indices.
/// Each fetch constant is 6 dwords (`xe_gpu_*_fetch_t`).
const CONST_BASE_FETCH: u32 = 0x4800;
/// Upper bound (in dwords) on the vertex window we copy per draw. The splash
/// UI draws are tiny (34 verts × ≤4 dwords); 64 KiB of dwords is generous
/// slack while bounding the per-frame copy cost and the 16 MiB host buffer.
const MAX_WINDOW_DWORDS: u32 = 16 * 1024;
/// One captured draw, with enough real state for the UI to replay it through
/// the existing wgpu Xenos pipeline.
#[derive(Clone, Debug)]
pub struct DrawCapture {
/// Monotonic global draw index (matches `GpuStats::draws_seen` at capture).
pub draw_index: u32,
/// Xenos primitive-type code (see `SwapInfo::last_draw_prim` encoding).
pub prim_code: u32,
/// Host vertex count to issue (post primitive-processor rewrite).
pub host_vertex_count: u32,
/// Active VS blob key at draw time (0 = none).
pub vs_key: u32,
/// Active PS blob key at draw time (0 = none).
pub ps_key: u32,
/// Raw guest dwords of the fetched vertex window (host-endian as stored in
/// guest memory — the WGSL applies the per-format endian swap). `addr 0`
/// of this buffer corresponds to guest dword `window_base_dwords`.
pub vertex_dwords: Vec<u32>,
/// Guest dword address that maps to index 0 of `vertex_dwords`. The shader
/// subtracts this from the fetch-constant base to index `vertex_dwords`.
pub window_base_dwords: u32,
/// `true` when we successfully resolved a real vertex window. When `false`
/// the UI falls back to its procedural geometry for this draw (honest:
/// nothing faked, just "couldn't source real vertices").
pub has_real_vertices: bool,
/// iterate-3S: per-draw NDC transform derived from the guest viewport /
/// clip / VTE registers (mirrors canary `GetHostViewportInfo`). The host VS
/// converts the guest-VS position to wgpu clip space via
/// `clip.xy = pos.xy * ndc_scale + ndc_offset * pos.w`. The Y component
/// already carries the render-target → wgpu Y-flip (negated).
pub ndc_scale: [f32; 2],
pub ndc_offset: [f32; 2],
/// iterate-3T: the decoded texture(s) this draw's active pixel shader
/// samples, keyed off its real `tfetch` fetch-constant slots (the 3M
/// decoder makes these decode). Root-#3: the UI uploads + binds EACH entry
/// to its own texture slot per-draw, so a multi-plane shader (e.g. the
/// intro video's YUV Y/U/V planes) samples the right texture per fetch.
/// Empty for flat (no-tfetch) draws. Populated by `gpu_system` after
/// decode (left empty by `build`).
///
/// Each entry is `(slot, key, content_version, bytes)` where `slot` is the
/// `tfetch` fetch-constant index (0..31) the shader samples this texture
/// from. iterate-3AD: the `content_version` (from `span_max_version` over
/// the texel span) lets the UI host texture cache RE-UPLOAD when the guest
/// fills more of an evolving atlas. The publisher and the 2nd splash logo
/// share one K8888 surface (base `0x4dbee000`); the 2nd logo's texels are
/// CPU-written *after* the publisher's first upload. Without the real
/// version the host cache (which previously pinned `version_when_uploaded
/// = 1`) kept the first partial upload, so the 2nd logo sampled its
/// still-zero atlas region as black.
pub textures: Vec<(u8, crate::texture_cache::TextureKey, u64, Vec<u8>)>,
/// iterate-3Y: per-draw color/blend render state captured from the
/// register file so the host pipeline composites the way the guest
/// intends (instead of one fixed alpha-blend state). Mirrors the fields
/// canary feeds into `GetCurrentStateDescription` (D3D12
/// `pipeline_cache.cc`):
/// * `blend_control` = `RB_BLENDCONTROL0` (RT0 src/dst factors + op,
/// color and alpha). The Xbox 360 has no separate "blend enable" bit;
/// `One,Zero,Add` *is* the opaque case.
/// * `color_mask` = RT0 nibble of `RB_COLOR_MASK` (per-channel write
/// enable). When 0, canary forces `One,Zero` (no blend).
/// * `color_control` = `RB_COLORCONTROL` (alpha-test enable/func).
/// * `depth_control` = `RB_DEPTHCONTROL` (z-test enable/func/write).
pub blend_control: u32,
pub color_mask: u8,
pub color_control: u32,
pub depth_control: u32,
}
/// iterate-3S: compute the guest→host NDC XY transform for a draw, mirroring
/// canary's `draw_util.cc::GetHostViewportInfo` (the XY half). The Xbox 360 VS
/// emits a clip-space position which the HW then scales/offsets by the viewport
/// (`PA_CL_VPORT_*`, gated by `PA_CL_VTE_CNTL`) into render-target pixels, OR,
/// when clipping is disabled (`PA_CL_CLIP_CNTL.clip_disable`), the VS emits
/// render-target-pixel coordinates directly (the screen-space UI / clear case —
/// this is what Sylpheed's splash quads do). Either way we must rescale into the
/// host's [-1,1] clip space and flip Y (render-target Y-down → wgpu Y-up).
///
/// Returns `(ndc_scale[2], ndc_offset[2])` such that
/// `host_clip.xy = guest_pos.xy * ndc_scale + ndc_offset * guest_pos.w`.
/// The Y entries are pre-negated to flip into wgpu's Y-up clip space.
pub fn compute_ndc_xy(rf: &RegisterFile) -> ([f32; 2], [f32; 2]) {
const PA_CL_CLIP_CNTL: u32 = 0x2204;
const PA_SU_SC_MODE_CNTL: u32 = 0x2205;
const PA_CL_VTE_CNTL: u32 = 0x2206;
const PA_SU_VTX_CNTL: u32 = 0x2302;
const PA_CL_VPORT_XSCALE: u32 = 0x210F;
const PA_CL_VPORT_XOFFSET: u32 = 0x2110;
const PA_CL_VPORT_YSCALE: u32 = 0x2111;
const PA_CL_VPORT_YOFFSET: u32 = 0x2112;
const PA_SC_WINDOW_OFFSET: u32 = 0x2080;
const PA_SC_WINDOW_SCISSOR_BR: u32 = 0x2082;
const RB_SURFACE_INFO: u32 = 0x2000;
let clip_cntl = rf.read(PA_CL_CLIP_CNTL);
let vte = rf.read(PA_CL_VTE_CNTL);
let su_sc_mode = rf.read(PA_SU_SC_MODE_CNTL);
let su_vtx = rf.read(PA_SU_VTX_CNTL);
let fbits = |r: u32| f32::from_bits(rf.read(r));
// VTE enable bits (xenos.h PA_CL_VTE_CNTL): bit0 vport_x_scale_ena,
// bit1 vport_x_offset_ena, bit2 vport_y_scale_ena, bit3 vport_y_offset_ena.
let scale_x = if vte & (1 << 0) != 0 { fbits(PA_CL_VPORT_XSCALE) } else { 1.0 };
let off_x = if vte & (1 << 1) != 0 { fbits(PA_CL_VPORT_XOFFSET) } else { 0.0 };
let scale_y = if vte & (1 << 2) != 0 { fbits(PA_CL_VPORT_YSCALE) } else { 1.0 };
let off_y = if vte & (1 << 3) != 0 { fbits(PA_CL_VPORT_YOFFSET) } else { 0.0 };
// Render-target extent in guest pixels: clamp to the texture max (2048),
// sourced from the window scissor BR (matches canary `x_max`/`y_max`).
let br = rf.read(PA_SC_WINDOW_SCISSOR_BR);
let x_max = ((br & 0x7FFF).max(1)).min(2048) as f32;
let y_max = (((br >> 16) & 0x7FFF).max(1)).min(2048) as f32;
let _ = RB_SURFACE_INFO;
// Half-pixel + window offsets added in render-target pixels.
let mut add_x = 0.0f32;
let mut add_y = 0.0f32;
if su_sc_mode & (1 << 16) != 0 {
let wo = rf.read(PA_SC_WINDOW_OFFSET);
// 15-bit signed each (x: [14:0], y: [30:16]).
let sx = (((wo & 0x7FFF) << 1) as i32) >> 1;
let sy = ((((wo >> 16) & 0x7FFF) << 1) as i32) >> 1;
add_x += sx as f32;
add_y += sy as f32;
}
if su_vtx & 1 == 0 {
// pix_center == kD3DZero → +0.5 half-pixel offset.
add_x += 0.5;
add_y += 0.5;
}
let (s, o);
if clip_cntl & (1 << 16) != 0 {
// clip_disable: VS outputs render-target-*pixel* coords (Y-DOWN: pixel
// y=0 is the top row of the render target). Rescale the whole RT extent
// to [-1,1] and FLIP Y so pixel-top → wgpu clip-top (canary's
// huge-host-viewport path; the framebuffer→clip flip is real here).
let px2ndc_x = 2.0 / x_max;
let px2ndc_y = 2.0 / y_max;
let sx = scale_x * px2ndc_x;
let ox = (off_x - x_max * 0.5 + add_x) * px2ndc_x;
let sy = scale_y * px2ndc_y;
let oy = (off_y - y_max * 0.5 + add_y) * px2ndc_y;
// Flip Y: pixel-Y-down → wgpu clip-Y-up.
s = [sx, -sy];
o = [ox, -oy];
} else {
// iterate-3AA (DEFECT 1 ROOT): clipping enabled → the VS already emits
// *clip-space* coordinates (Y-UP: +Y is the top of the screen), exactly
// the convention the Xbox 360's D3D9 and wgpu BOTH use for clip space
// (NDC +Y → framebuffer top in each API; the framebuffer Y-direction is
// an internal viewport detail handled identically by both). A clip-space
// position is therefore portable to wgpu with NO Y-flip. The previous
// code unconditionally negated Y (the same flip the screen-space pixel
// path needs), which mirrored the publisher logo vertically: its quad is
// centered (±0.085 around 0) so the *position* stayed centered, but the
// negation swapped top↔bottom vertices while the texture V was unchanged
// → the sampled sub-rect (UV v 0.001→0.090) read bottom-up → "SQUARE
// ENIX" rendered upside down in place. Measured (readback): the red dots
// sit at 43% from the texture top but rendered at 58% from the top
// (= a clean vertical mirror); removing the flip restores them to 43%.
// Identity XY (no flip) maps guest clip-Y-up straight to wgpu clip-Y-up.
s = [1.0, 1.0];
o = [0.0, 0.0];
return (s, o);
}
(s, o)
}
/// Encode a [`PrimitiveType`] as the raw Xenos code used across the bridge.
pub fn prim_code(p: PrimitiveType) -> u32 {
match p {
PrimitiveType::None => 0,
PrimitiveType::PointList => 1,
PrimitiveType::LineList => 2,
PrimitiveType::LineStrip => 3,
PrimitiveType::TriangleList => 4,
PrimitiveType::TriangleFan => 5,
PrimitiveType::TriangleStrip => 6,
PrimitiveType::RectangleList => 8,
PrimitiveType::QuadList => 13,
PrimitiveType::Unknown(x) => x as u32,
}
}
/// Resolve the first vertex-fetch window referenced by the parsed VS.
///
/// Walks the VS instruction stream for the first `vfetch` (mini) instruction,
/// reads its fetch constant from `rf`, and copies a bounded window of guest
/// memory starting at the fetch base. Returns `(dwords, window_base_dwords)`
/// or `None` if the VS has no vertex fetch or the constant is malformed.
fn resolve_vertex_window(
parsed_vs: &crate::ucode::ParsedShader,
rf: &RegisterFile,
mem: &dyn MemoryAccess,
) -> Option<(Vec<u32>, u32)> {
// iterate-3W (GPUBUG-109): the instruction block packs ALU and fetch
// instructions identically (96 bits / 3 dwords each); ONLY the owning
// `Exec` control-flow clause's `sequence` bitmap (2 bits per instruction,
// bit[2*i]=fetch/ALU) tells them apart. The previous blind triple-walk
// decoded ALU triples as fetches → garbage fetch-constant indices and a
// bogus `type==3` guard, never reaching the real vertex fetch. Walk the CF
// exec clauses exactly as the translator does (`translator.rs::emit_exec`)
// and take the FIRST sequence-flagged *vertex* fetch.
let instrs = &parsed_vs.instructions;
let mut const_off: Option<u32> = None;
'clauses: for clause in &parsed_vs.cf {
let crate::ucode::control_flow::ControlFlowInstruction::Exec {
address,
count,
sequence,
..
} = *clause
else {
continue;
};
for i in 0..(count as usize) {
// bit[2*i] of the sequence bitmap: 1 = fetch, 0 = ALU.
if (sequence >> (i * 2)) & 1 == 0 {
continue;
}
let base = (address as usize + i) * 3;
if base + 2 >= instrs.len() {
break;
}
if let crate::ucode::fetch::FetchInstruction::Vertex(vf) =
crate::ucode::fetch::decode_fetch([instrs[base], instrs[base + 1], instrs[base + 2]])
{
const_off = Some(vf.const_reg_offset());
break 'clauses;
}
}
}
// iterate-3X (GPUBUG-110): vertex fetch constants are addressed by
// `const_index * 3 + const_index_sel` (canary `ucode.h:700` —
// `VertexFetchInstruction::fetch_constant_index`), NOT by `const_index`
// alone. The register region packs 3 two-dword vertex-fetch constants per
// 6-dword group, so the constant lives at
// `0x4800 + const_index*6 + const_index_sel*2`. The previous decode dropped
// `const_index_sel` and read sub-slot 0 (`fc*6`), which for the publisher
// logo (`const_index=31, sel=2`) held `0x00000001` (an unused slot) instead
// of the real vertex-buffer base at sub-slot 2 (`0x48BE`). That made
// `has_real_vertices=false` → the logo fell to the procedural fullscreen
// magenta fallback. (Refutes iterate-3W's "geometry is auto-generated from
// vertex_id" — measured: the real fetch constant is a 4-vertex QuadList
// buffer at `0x0adf60f0`.)
let const_reg = CONST_BASE_FETCH + const_off?;
let dword0 = rf.read(const_reg);
let dword1 = rf.read(const_reg + 1);
// address:30 at bits[31:2] of dword0 (in bytes once masked). The fetch
// constant carries a guest *physical* dword address — canary reads the
// vertex buffer via `Memory::TranslatePhysical(fetch.address * 4)`
// (`draw_util.cc:961`). On the Xbox 360 the physical range is mirrored at
// several virtual windows; ours only maps the cached-physical window at
// `0x4000_0000` (`gpu_system::physical_to_backing`). Reading the bare low
// address (`0x0adf_xxxx`) hits an unmapped VA and returns zeros, so rebase
// a low physical base onto the mapped `0x4000_0000` alias when the raw VA
// is not itself mapped. `window_base_dwords` keeps the *original* base so
// the shader's rebase against the (unmodified) fetch-constant address still
// indexes the uploaded window correctly.
let base_bytes = dword0 & 0xFFFF_FFFC;
if base_bytes == 0 {
return None;
}
let read_base = if mem.translate(base_bytes).is_some() {
base_bytes
} else if base_bytes < 0x2000_0000 && mem.translate(base_bytes | 0x4000_0000).is_some() {
base_bytes | 0x4000_0000
} else {
base_bytes
};
// size:24 at bits[25:2] of dword1, in dwords. Clamp to our window cap.
let size_dwords = ((dword1 >> 2) & 0x00FF_FFFF).clamp(1, MAX_WINDOW_DWORDS);
let window_base_dwords = base_bytes >> 2;
let mut dwords = Vec::with_capacity(size_dwords as usize);
for i in 0..size_dwords {
let addr = read_base.wrapping_add(i * 4);
if addr < read_base {
break; // wrap guard
}
// `read_u32` composes big-endian bytes into the u32 value; the WGSL's
// `gpu_swap` expects the *raw little-endian dword* as it sits in guest
// memory, so undo the BE composition with `swap_bytes`.
dwords.push(mem.read_u32(addr).swap_bytes());
}
if dwords.is_empty() {
return None;
}
Some((dwords, window_base_dwords))
}
/// Build a [`DrawCapture`] for one draw. Best-effort: when the vertex window
/// can't be resolved, `has_real_vertices` is `false` and the UI falls back to
/// procedural geometry (never fabricated pixels).
#[allow(clippy::too_many_arguments)]
pub fn build(
draw_index: u32,
primitive: PrimitiveType,
host_vertex_count: u32,
_index_source: IndexSource,
_index_size: IndexSize,
vs_key: u32,
ps_key: u32,
parsed_vs: Option<&crate::ucode::ParsedShader>,
rf: &RegisterFile,
mem: &dyn MemoryAccess,
) -> DrawCapture {
let (vertex_dwords, window_base_dwords, has_real) = match parsed_vs
.and_then(|vs| resolve_vertex_window(vs, rf, mem))
{
Some((d, base)) => (d, base, true),
None => (Vec::new(), 0, false),
};
let (ndc_scale, ndc_offset) = compute_ndc_xy(rf);
// iterate-3Y: capture RT0 color/blend/depth render state. Registers per
// canary `registers.h`: RB_BLENDCONTROL0=0x2201, RB_COLOR_MASK=0x2104
// (RT0 = bits[3:0]), RB_COLORCONTROL=0x2202, RB_DEPTHCONTROL=0x2200.
const RB_BLENDCONTROL_0: u32 = 0x2201;
const RB_COLOR_MASK: u32 = 0x2104;
const RB_COLORCONTROL: u32 = 0x2202;
const RB_DEPTHCONTROL: u32 = 0x2200;
DrawCapture {
draw_index,
prim_code: prim_code(primitive),
host_vertex_count,
vs_key,
ps_key,
vertex_dwords,
window_base_dwords,
has_real_vertices: has_real,
ndc_scale,
ndc_offset,
textures: Vec::new(),
blend_control: rf.read(RB_BLENDCONTROL_0),
color_mask: (rf.read(RB_COLOR_MASK) & 0xF) as u8,
color_control: rf.read(RB_COLORCONTROL),
depth_control: rf.read(RB_DEPTHCONTROL),
}
}