[iterate-3X] Real splash logo geometry renders: fix vertex-fetch const_index_sel + per-draw submit
Two readback-proven root-cause fixes make the publisher-logo QuadList draw land its REAL captured vertex buffer (the texture was already correct from 3V). REFUTES iterate-3W's "logo geometry is auto-generated from vertex_id": the logo IS sourced from a 4-vertex QuadList buffer at guest physical 0x0adf60f0 (measured), it was just resolved at the wrong fetch-constant register. GPUBUG-110 (vertex fetch const_index_sel dropped). The Xenos vertex-fetch instruction encodes const_index (w0[20:24]) AND const_index_sel (w0[25:26]); the full constant index is const_index*3 + const_index_sel (canary ucode.h:700), packed 3 two-dword constants per 6-dword register group. ucode/fetch.rs decoded only const_index and read sub-slot 0 (fc*6). The logo vfetch is const_index=31, sel=2 -> the real base lives at reg 0x48BE, but ours read 0x48BA which held an unused 0x00000001 (base=0,size=0) slot. So resolve_vertex_window returned None -> has_real_vertices=false -> the logo fell to the procedural fullscreen magenta fallback. Fix: decode const_index_sel, add VertexFetch::const_reg_offset() = const_index*6 + sel*2, and use it in both draw_capture.rs (capture) and translator.rs (the WGSL endian term + no-window fallback base; the old expression there read the src_reg bits, not the const index). Measured: logo now resolves a 24-dword (4 verts x stride 6) window, base 0x0adf60f0. GPUBUG-111 (single batch encoder = last-draw-wins vertex data). In wgpu every queue.write_buffer staged before a single queue.submit is applied before ANY command in that submit runs. dispatch_xenos_captures recorded the whole batch into one encoder + one submit, so every draw read only the LAST draw's vertex buffer / per-draw uniforms. The logo quad therefore sampled the trailing fullscreen background quad's vertices and rasterized nothing where the logo was. Fix: submit one encoder per draw (frontbuffer LoadOp::Load composites identically). Measured (env-gated readback, removed): with this fix the logo draw in isolation renders real varied texels (e.g. (225,17,22)/(255,255,0)) in a centered strip (~20k px), vs 100% navy before. Determinism: all changes are UI-side (xenia-ui replay) or the UI translator / capture path (frame_captures None in headless); the fetch.rs field addition is purely additive and does not change any existing decoded value. Verified the deterministic core unchanged: check -n50M --gpu-inline --stable-digest exit 0 and all 136 metric counters byte-identical across two runs. All temp probes removed. cargo test --workspace green; new regression test vertex_fetch_const_index_sel_and_reg_offset. Known remaining (next iterate): a fullscreen flat QuadList (ps 0x03b79081, vertex color green, no texture) and other textureless draws overpaint the logo in the full composite (their per-draw blend/alpha render state is not yet replayed, and draw order alternates bg/logo). The logo artwork renders correctly in isolation; the composite is not yet clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -200,7 +200,7 @@ fn resolve_vertex_window(
|
|||||||
// exec clauses exactly as the translator does (`translator.rs::emit_exec`)
|
// exec clauses exactly as the translator does (`translator.rs::emit_exec`)
|
||||||
// and take the FIRST sequence-flagged *vertex* fetch.
|
// and take the FIRST sequence-flagged *vertex* fetch.
|
||||||
let instrs = &parsed_vs.instructions;
|
let instrs = &parsed_vs.instructions;
|
||||||
let mut fetch_const: Option<u8> = None;
|
let mut const_off: Option<u32> = None;
|
||||||
'clauses: for clause in &parsed_vs.cf {
|
'clauses: for clause in &parsed_vs.cf {
|
||||||
let crate::ucode::control_flow::ControlFlowInstruction::Exec {
|
let crate::ucode::control_flow::ControlFlowInstruction::Exec {
|
||||||
address,
|
address,
|
||||||
@@ -223,14 +223,27 @@ fn resolve_vertex_window(
|
|||||||
if let crate::ucode::fetch::FetchInstruction::Vertex(vf) =
|
if let crate::ucode::fetch::FetchInstruction::Vertex(vf) =
|
||||||
crate::ucode::fetch::decode_fetch([instrs[base], instrs[base + 1], instrs[base + 2]])
|
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;
|
break 'clauses;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let fc = fetch_const? as u32;
|
// iterate-3X (GPUBUG-110): vertex fetch constants are addressed by
|
||||||
let dword0 = rf.read(CONST_BASE_FETCH + fc * 6);
|
// `const_index * 3 + const_index_sel` (canary `ucode.h:700` —
|
||||||
let dword1 = rf.read(CONST_BASE_FETCH + fc * 6 + 1);
|
// `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
|
// address:30 at bits[31:2] of dword0 (in bytes once masked). The fetch
|
||||||
// constant carries a guest *physical* dword address — canary reads the
|
// constant carries a guest *physical* dword address — canary reads the
|
||||||
// vertex buffer via `Memory::TranslatePhysical(fetch.address * 4)`
|
// vertex buffer via `Memory::TranslatePhysical(fetch.address * 4)`
|
||||||
|
|||||||
@@ -519,7 +519,12 @@ impl EmitCtx {
|
|||||||
// iterate-3T: per-attribute dword offset within the vertex (vfetches
|
// iterate-3T: per-attribute dword offset within the vertex (vfetches
|
||||||
// sharing one fetch constant read different attributes).
|
// sharing one fetch constant read different attributes).
|
||||||
let attr_off = vf.offset;
|
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 src_reg = vf.src_register & 0x7F;
|
||||||
let dst_reg = vf.dest_register & 0x7F;
|
let dst_reg = vf.dest_register & 0x7F;
|
||||||
// is_signed selects [-1,1] vs [0,1] for normalized integer formats.
|
// 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
|
// every quad collapsed to ~one pixel at the origin. Index from 0 when a
|
||||||
// real window is present (`vertex_base_dwords != 0`); only the
|
// real window is present (`vertex_base_dwords != 0`); only the
|
||||||
// synthetic/no-window fallback consults the uniform fetch constant.
|
// 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
|
// For packed-16 we read one dword into `w16` (post endian-swap) and the
|
||||||
// `lane()` exprs above unpack the two halfwords.
|
// `lane()` exprs above unpack the two halfwords.
|
||||||
let w16_decl = if pack == Pack::Norm16x2 {
|
let w16_decl = if pack == Pack::Norm16x2 {
|
||||||
@@ -594,7 +599,7 @@ impl EmitCtx {
|
|||||||
{w16_decl}\
|
{w16_decl}\
|
||||||
r[{dst_reg}u] = vec4<f32>({l0}, {l1}, {l2}, {l3}); \
|
r[{dst_reg}u] = vec4<f32>({l0}, {l1}, {l2}, {l3}); \
|
||||||
}} }}",
|
}} }}",
|
||||||
fc0_idx = fetch_const * 2,
|
fc0_idx = const_off,
|
||||||
l0 = lane(0),
|
l0 = lane(0),
|
||||||
l1 = lane(1),
|
l1 = lane(1),
|
||||||
l2 = lane(2),
|
l2 = lane(2),
|
||||||
@@ -960,6 +965,7 @@ mod tests {
|
|||||||
let mut ctx = EmitCtx::new(Stage::Vertex);
|
let mut ctx = EmitCtx::new(Stage::Vertex);
|
||||||
let vf = crate::ucode::fetch::VertexFetch {
|
let vf = crate::ucode::fetch::VertexFetch {
|
||||||
fetch_const: 0,
|
fetch_const: 0,
|
||||||
|
const_index_sel: 0,
|
||||||
src_register: 0,
|
src_register: 0,
|
||||||
dest_register: 0,
|
dest_register: 0,
|
||||||
dest_write_mask: 0xF,
|
dest_write_mask: 0xF,
|
||||||
|
|||||||
@@ -17,8 +17,16 @@ pub enum FetchInstruction {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct VertexFetch {
|
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,
|
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#).
|
/// Source register index (vertex index in r#).
|
||||||
pub src_register: u8,
|
pub src_register: u8,
|
||||||
/// Destination register for the fetched value.
|
/// Destination register for the fetched value.
|
||||||
@@ -50,6 +58,17 @@ pub struct VertexFetch {
|
|||||||
pub raw: [u32; 3],
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct TextureFetch {
|
pub struct TextureFetch {
|
||||||
/// Texture fetch constant index (0..=31).
|
/// Texture fetch constant index (0..=31).
|
||||||
@@ -91,6 +110,7 @@ pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
|
|||||||
match opcode {
|
match opcode {
|
||||||
op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch {
|
op::VERTEX_FETCH => FetchInstruction::Vertex(VertexFetch {
|
||||||
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
||||||
|
const_index_sel: ((w0 >> 25) & 0x3) as u8,
|
||||||
src_register: ((w0 >> 5) & 0x3F) as u8,
|
src_register: ((w0 >> 5) & 0x3F) as u8,
|
||||||
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
||||||
dest_write_mask: (w1 & 0xF) 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]
|
#[test]
|
||||||
fn decode_texture_fetch() {
|
fn decode_texture_fetch() {
|
||||||
// opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4.
|
// opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4.
|
||||||
|
|||||||
@@ -742,11 +742,16 @@ impl RenderState {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let mut real_count = 0u32;
|
let mut real_count = 0u32;
|
||||||
let mut encoder = self
|
// iterate-3X (GPUBUG-111): each captured draw uploads its OWN vertex
|
||||||
.device
|
// window + per-draw constants + shader via `queue.write_buffer`. In
|
||||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
// wgpu all `write_buffer` calls staged before a single `queue.submit`
|
||||||
label: Some("xenos capture replay"),
|
// 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 {
|
for cap in captures {
|
||||||
// iterate-3T: bind this draw's REAL decoded texture (keyed off the
|
// iterate-3T: bind this draw's REAL decoded texture (keyed off the
|
||||||
// active PS's tfetch slot, attached in `gpu_system`) so the textured
|
// 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_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] },
|
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
|
if use_translated
|
||||||
&& let Some(p) = self.xenos_pipeline.translated_pipeline(cap.vs_key, cap.ps_key)
|
&& 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 =
|
||||||
self.xenos_dispatches_interpreter.saturating_add(1);
|
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
|
self.xenos_draws_rendered = self
|
||||||
.xenos_draws_rendered
|
.xenos_draws_rendered
|
||||||
.saturating_add(captures.len() as u64);
|
.saturating_add(captures.len() as u64);
|
||||||
|
|||||||
Reference in New Issue
Block a user