//! 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 (3–4 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, /// 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)>, /// 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)> { // 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 = 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), } }