diff --git a/crates/xenia-gpu/src/draw_capture.rs b/crates/xenia-gpu/src/draw_capture.rs index 90db985..0f73e88 100644 --- a/crates/xenia-gpu/src/draw_capture.rs +++ b/crates/xenia-gpu/src/draw_capture.rs @@ -200,7 +200,7 @@ fn resolve_vertex_window( // 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 fetch_const: Option = None; + let mut const_off: Option = None; 'clauses: for clause in &parsed_vs.cf { let crate::ucode::control_flow::ControlFlowInstruction::Exec { address, @@ -223,14 +223,27 @@ fn resolve_vertex_window( if let crate::ucode::fetch::FetchInstruction::Vertex(vf) = crate::ucode::fetch::decode_fetch([instrs[base], instrs[base + 1], instrs[base + 2]]) { - fetch_const = Some(vf.fetch_const); + const_off = Some(vf.const_reg_offset()); break 'clauses; } } } - let fc = fetch_const? as u32; - let dword0 = rf.read(CONST_BASE_FETCH + fc * 6); - let dword1 = rf.read(CONST_BASE_FETCH + fc * 6 + 1); + // 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)` diff --git a/crates/xenia-gpu/src/translator.rs b/crates/xenia-gpu/src/translator.rs index 63a7ad4..6a4864e 100644 --- a/crates/xenia-gpu/src/translator.rs +++ b/crates/xenia-gpu/src/translator.rs @@ -519,7 +519,12 @@ impl EmitCtx { // iterate-3T: per-attribute dword offset within the vertex (vfetches // sharing one fetch constant read different attributes). let attr_off = vf.offset; - let fetch_const = (vf.raw[0] >> 5) & 0x1F; + // iterate-3X (GPUBUG-110): index the fetch-constant region by the full + // `const_index*3 + const_index_sel` mapping (canary `ucode.h:700`), + // packed as `const_index*6 + sel*2` dwords. The previous expression + // `(vf.raw[0] >> 5) & 0x1F` read the *src_reg* bits, not the const + // index — wrong for the endian term and the no-window fallback base. + let const_off = vf.const_reg_offset(); let src_reg = vf.src_register & 0x7F; let dst_reg = vf.dest_register & 0x7F; // is_signed selects [-1,1] vs [0,1] for normalized integer formats. @@ -573,7 +578,7 @@ impl EmitCtx { // every quad collapsed to ~one pixel at the origin. Index from 0 when a // real window is present (`vertex_base_dwords != 0`); only the // synthetic/no-window fallback consults the uniform fetch constant. - let endian_term = format!("xenos_consts.fetch[{}u] & 0x3u", fetch_const * 2 + 1); + let endian_term = format!("xenos_consts.fetch[{}u] & 0x3u", const_off + 1); // For packed-16 we read one dword into `w16` (post endian-swap) and the // `lane()` exprs above unpack the two halfwords. let w16_decl = if pack == Pack::Norm16x2 { @@ -594,7 +599,7 @@ impl EmitCtx { {w16_decl}\ r[{dst_reg}u] = vec4({l0}, {l1}, {l2}, {l3}); \ }} }}", - fc0_idx = fetch_const * 2, + fc0_idx = const_off, l0 = lane(0), l1 = lane(1), l2 = lane(2), @@ -960,6 +965,7 @@ mod tests { let mut ctx = EmitCtx::new(Stage::Vertex); let vf = crate::ucode::fetch::VertexFetch { fetch_const: 0, + const_index_sel: 0, src_register: 0, dest_register: 0, dest_write_mask: 0xF, diff --git a/crates/xenia-gpu/src/ucode/fetch.rs b/crates/xenia-gpu/src/ucode/fetch.rs index 1edfed8..2e5a11e 100644 --- a/crates/xenia-gpu/src/ucode/fetch.rs +++ b/crates/xenia-gpu/src/ucode/fetch.rs @@ -17,8 +17,16 @@ pub enum FetchInstruction { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct VertexFetch { - /// Vertex fetch constant index (0..=95). + /// Vertex fetch *const_index* (5 bits, w0[20:24]). The full fetch-constant + /// index is `const_index * 3 + const_index_sel` (canary `ucode.h:700`); use + /// [`VertexFetch::const_reg_offset`] for the register-region dword offset. pub fetch_const: u8, + /// iterate-3X (GPUBUG-110): `const_index_sel` (2 bits, w0[25:26]) — selects + /// one of the 3 two-dword vertex-fetch constants packed in each 6-dword + /// register group. Dropping this read sub-slot 0 of the group, missing the + /// real vertex-buffer base for shaders that use sub-slot 1/2 (the publisher + /// logo uses `const_index=31, sel=2`). + pub const_index_sel: u8, /// Source register index (vertex index in r#). pub src_register: u8, /// Destination register for the fetched value. @@ -50,6 +58,17 @@ pub struct VertexFetch { pub raw: [u32; 3], } +impl VertexFetch { + /// Dword offset of this fetch's 2-dword constant within the fetch-constant + /// register region (`CONST_BASE_FETCH`). Vertex fetch constants are packed + /// 3 per 6-dword group: `const_index * 6 + const_index_sel * 2` + /// (canary `ucode.h:700` `fetch_constant_index = const_index*3 + sel`, + /// each constant 2 dwords). + pub fn const_reg_offset(&self) -> u32 { + self.fetch_const as u32 * 6 + self.const_index_sel as u32 * 2 + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct TextureFetch { /// Texture fetch constant index (0..=31). @@ -91,6 +110,7 @@ pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction { match opcode { op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch { fetch_const: ((w0 >> 20) & 0x1F) as u8, + const_index_sel: ((w0 >> 25) & 0x3) as u8, src_register: ((w0 >> 5) & 0x3F) as u8, dest_register: ((w0 >> 12) & 0x3F) as u8, dest_write_mask: (w1 & 0xF) as u8, @@ -137,6 +157,32 @@ mod tests { } } + #[test] + fn vertex_fetch_const_index_sel_and_reg_offset() { + // iterate-3X (GPUBUG-110): the real publisher-logo vfetch (w0 = + // 0x2DF82000) encodes const_index=31, const_index_sel=2. Its fetch + // constant lives at dword offset `31*6 + 2*2 = 190` (reg 0x48BE), not + // `31*6 = 186` (reg 0x48BA, which held the unused 0x1 slot). Dropping + // the sel field made the logo geometry resolve as "no vertex buffer". + let v = decode_fetch([0x2DF8_2000, 0, 0]); + match v { + FetchInstruction::Vertex(vf) => { + assert_eq!(vf.fetch_const, 31, "const_index"); + assert_eq!(vf.const_index_sel, 2, "const_index_sel"); + assert_eq!(vf.const_reg_offset(), 190, "reg offset = 31*6 + 2*2"); + } + other => panic!("expected Vertex, got {other:?}"), + } + // sel=0 collapses to the legacy `fetch_const*6` offset (back-compat). + let v0 = decode_fetch([0u32 | (5 << 20), 0, 0]); + if let FetchInstruction::Vertex(vf) = v0 { + assert_eq!(vf.const_index_sel, 0); + assert_eq!(vf.const_reg_offset(), 30); + } else { + panic!("expected Vertex"); + } + } + #[test] fn decode_texture_fetch() { // opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4. diff --git a/crates/xenia-ui/src/render.rs b/crates/xenia-ui/src/render.rs index 2942a8f..47c07ea 100644 --- a/crates/xenia-ui/src/render.rs +++ b/crates/xenia-ui/src/render.rs @@ -742,11 +742,16 @@ impl RenderState { return 0; } let mut real_count = 0u32; - let mut encoder = self - .device - .create_command_encoder(&wgpu::CommandEncoderDescriptor { - label: Some("xenos capture replay"), - }); + // iterate-3X (GPUBUG-111): each captured draw uploads its OWN vertex + // window + per-draw constants + shader via `queue.write_buffer`. In + // wgpu all `write_buffer` calls staged before a single `queue.submit` + // are applied *before any* command in that submit executes — so a single + // encoder for the whole batch made every draw read only the LAST draw's + // vertex buffer / uniforms (the splash logo quad sampled the fullscreen + // background quad's vertices → nothing rendered where the logo was). + // Submit ONE encoder PER draw so each draw's writes land before its own + // pass. The frontbuffer uses `LoadOp::Load`, so per-draw submits still + // composite over each other exactly like before. for cap in captures { // iterate-3T: bind this draw's REAL decoded texture (keyed off the // active PS's tfetch slot, attached in `gpu_system`) so the textured @@ -831,6 +836,11 @@ impl RenderState { ndc_scale: if cap.has_real_vertices { cap.ndc_scale } else { [0.0, 0.0] }, ndc_offset: if cap.has_real_vertices { cap.ndc_offset } else { [0.0, 0.0] }, }; + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("xenos capture replay (per-draw)"), + }); if use_translated && let Some(p) = self.xenos_pipeline.translated_pipeline(cap.vs_key, cap.ps_key) { @@ -853,8 +863,8 @@ impl RenderState { self.xenos_dispatches_interpreter = self.xenos_dispatches_interpreter.saturating_add(1); } + self.queue.submit(std::iter::once(encoder.finish())); } - self.queue.submit(std::iter::once(encoder.finish())); self.xenos_draws_rendered = self .xenos_draws_rendered .saturating_add(captures.len() as u64);