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>
254 lines
11 KiB
Rust
254 lines
11 KiB
Rust
//! Xenos fetch (vertex + texture) instruction decoder.
|
|
//!
|
|
//! Like ALU instructions, fetches are 96 bits (3 dwords). The opcode lives
|
|
//! in the low 5 bits of word0. We split them into `VertexFetch` and
|
|
//! `TextureFetch` structurally because their operand layouts differ.
|
|
//!
|
|
//! Reference: `xenia-canary/src/xenia/gpu/ucode.h:690-877`.
|
|
|
|
/// Decoded fetch instruction.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum FetchInstruction {
|
|
Vertex(VertexFetch),
|
|
Texture(TextureFetch),
|
|
/// Unknown / minor variants we don't model yet.
|
|
Unknown { opcode: u8, raw: [u32; 3] },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct VertexFetch {
|
|
/// 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.
|
|
pub dest_register: u8,
|
|
/// 4-bit write mask.
|
|
pub dest_write_mask: u8,
|
|
/// iterate-3S (GPUBUG-107): `xenos::VertexFormat` (6 bits, dword1[16:21]).
|
|
/// Determines how many components to read and their packing. Pre-fix the
|
|
/// translator hardcoded `k_32_32_32_32_FLOAT` (4 floats, stride 4),
|
|
/// over-striding 2-float UI quads (`k_32_32_FLOAT`) → wrong/clipped
|
|
/// positions (the next vertex's X bled into .w, giving negative W → the
|
|
/// whole rectangle was clipped behind the camera).
|
|
pub format: u8,
|
|
/// Dword stride between consecutive vertices (dword2[0:7]).
|
|
pub stride: u8,
|
|
/// iterate-3T: dword offset of THIS attribute within the vertex stride
|
|
/// (dword2[16:38] in canary's `VertexFetchInstruction`; the low 23 bits).
|
|
/// A 6-dword vertex with position@0 + UV@2 + extra@3 needs this so the
|
|
/// three vfetches sharing one fetch-constant read different attributes
|
|
/// instead of all reading offset 0.
|
|
pub offset: u32,
|
|
/// `is_signed` = canary `fomat_comp_all`, word1 bit 12 (ucode.h:757) —
|
|
/// selects signed vs unsigned interpretation of packed integer formats.
|
|
/// (GPUBUG-113: was read from word1 bit 24, which is inside `exp_adjust`.)
|
|
pub is_signed: bool,
|
|
/// `is_normalized` = canary `num_format_all == 0`, word1 bit 13
|
|
/// (ucode.h:758). Set bit ⇒ integer (un-normalized); clear ⇒ normalized.
|
|
/// We store the normalized sense directly. (GPUBUG-113: was word1 bit 25.)
|
|
pub is_normalized: bool,
|
|
/// `is_mini_fetch` = canary word1 bit 30 (ucode.h:764). A mini-fetch reuses
|
|
/// the address AND STRIDE of the preceding full vfetch of the same stream;
|
|
/// its own `stride` field is 0. Required so a vfetch_mini color attribute
|
|
/// indexes by the real vertex stride instead of its tight dword count.
|
|
pub is_mini_fetch: bool,
|
|
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).
|
|
pub fetch_const: u8,
|
|
pub src_register: u8,
|
|
pub dest_register: u8,
|
|
/// Destination swizzle — 12 bits, 3 per output component (Xenos tfetch
|
|
/// dword1 bits 0..11). Per canary `ucode.h`, each 3-bit code selects:
|
|
/// 0..3 = sampled X/Y/Z/W, 4 = constant 0.0, 5 = constant 1.0,
|
|
/// 6/7 = keep (destination component unwritten).
|
|
/// The old `dest_write_mask: (w1 & 0xF)` read collapsed this to a bogus
|
|
/// 4-bit mask, so multi-plane fetches (the intro video's Y/U/V, all
|
|
/// targeting r1 but each in its own lane) each did a *full* `r1 = sample`
|
|
/// overwrite — only the last plane survived → wrong-color video.
|
|
pub dest_swizzle: u16,
|
|
/// Dimension: 0=1D, 1=2D, 2=3D/stacked, 3=cube.
|
|
pub dimension: u8,
|
|
pub raw: [u32; 3],
|
|
}
|
|
|
|
/// Opcodes (low 5 bits of word0). From `ucode.h`.
|
|
pub mod op {
|
|
pub const VERTEX_FETCH: u8 = 0x00;
|
|
pub const TEXTURE_FETCH: u8 = 0x01;
|
|
pub const GET_TEXTURE_BORDER_COLOR_FRAC: u8 = 0x16;
|
|
pub const GET_TEXTURE_COMPUTED_LOD: u8 = 0x17;
|
|
pub const GET_TEXTURE_WEIGHTS: u8 = 0x18;
|
|
pub const GET_TEXTURE_GRADIENTS: u8 = 0x19;
|
|
pub const SET_TEXTURE_LOD: u8 = 0x1A;
|
|
pub const SET_TEXTURE_GRADIENTS_HORZ: u8 = 0x1B;
|
|
pub const SET_TEXTURE_GRADIENTS_VERT: u8 = 0x1C;
|
|
}
|
|
|
|
pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
|
|
// Fetch dword0 bitfields (Xenos `ucode.h:740-749` vfetch / `844-845`
|
|
// tfetch): opcode_value:5, src_reg:6, src_reg_am:1, dst_reg:6,
|
|
// dst_reg_am:1, (fetch_valid_only|must_be_one):1, const_index:5 @ bit20,
|
|
// ... The prior decoder read `const_index` from bit 5 (which is actually
|
|
// `src_reg`), so every fetch reported the wrong fetch-constant slot — the
|
|
// logo `tfetch2D ..., tf0` was read as `tf1`, and slot 1's empty constant
|
|
// failed to decode → no texture. The texture-fetch `dimension` lives in
|
|
// dword2 bits 14..15, not dword1.
|
|
let w0 = words[0];
|
|
let w1 = words[1];
|
|
let w2 = words[2];
|
|
let opcode = (w0 & 0x1F) as u8;
|
|
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,
|
|
// dword1[16:21] = VertexFormat. dword2: stride[0:7],
|
|
// offset (in dwords) [8:?] — empirically the attribute offset of
|
|
// the textured logo VS lands in dword2[8:15] (pos@4, UV@3,
|
|
// 3-float@0 in a 6-dword vertex). signed/normalized live higher.
|
|
format: ((w1 >> 16) & 0x3F) as u8,
|
|
stride: (w2 & 0xFF) as u8,
|
|
offset: (w2 >> 8) & 0xFF,
|
|
// GPUBUG-113: canary ucode.h:757-758,764 — signed=fomat_comp_all
|
|
// (w1 bit12), normalized=(num_format_all==0) (w1 bit13),
|
|
// mini-fetch=(w1 bit30). The previous bit24/25 reads landed inside
|
|
// `exp_adjust`, so signedness/normalization were effectively random.
|
|
is_signed: ((w1 >> 12) & 1) != 0,
|
|
is_normalized: ((w1 >> 13) & 1) == 0,
|
|
is_mini_fetch: ((w1 >> 30) & 1) != 0,
|
|
raw: words,
|
|
}),
|
|
op::TEXTURE_FETCH => FetchInstruction::Texture(TextureFetch {
|
|
fetch_const: ((w0 >> 20) & 0x1F) as u8,
|
|
src_register: ((w0 >> 5) & 0x3F) as u8,
|
|
dest_register: ((w0 >> 12) & 0x3F) as u8,
|
|
dest_swizzle: (w1 & 0xFFF) as u16,
|
|
dimension: ((w2 >> 14) & 0x3) as u8,
|
|
raw: words,
|
|
}),
|
|
_ => FetchInstruction::Unknown { opcode, raw: words },
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn decode_vertex_fetch() {
|
|
// opcode=0 (vertex). Xenos dword0: src_reg@bit5, dst_reg@bit12,
|
|
// const_index@bit20. fetch_const=5, src=2, dest=7.
|
|
let w0 = 0u32 | (2 << 5) | (7 << 12) | (5 << 20);
|
|
let v = decode_fetch([w0, 0, 0]);
|
|
match v {
|
|
FetchInstruction::Vertex(vf) => {
|
|
assert_eq!(vf.fetch_const, 5);
|
|
assert_eq!(vf.src_register, 2);
|
|
assert_eq!(vf.dest_register, 7);
|
|
}
|
|
other => panic!("expected Vertex, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[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 vertex_fetch_signed_normalized_mini_bits() {
|
|
// GPUBUG-113: canary ucode.h:757-758,764 — is_signed=fomat_comp_all
|
|
// (w1 bit12), is_normalized=(num_format_all==0) (w1 bit13),
|
|
// is_mini_fetch=(w1 bit30). Validate each bit independently.
|
|
let mk = |w1: u32| match decode_fetch([0, w1, 0]) {
|
|
FetchInstruction::Vertex(vf) => vf,
|
|
_ => panic!("vertex"),
|
|
};
|
|
// No bits: unsigned, normalized, full fetch.
|
|
let v = mk(0);
|
|
assert!(!v.is_signed);
|
|
assert!(v.is_normalized);
|
|
assert!(!v.is_mini_fetch);
|
|
// bit12 → signed.
|
|
assert!(mk(1 << 12).is_signed);
|
|
// bit13 (num_format_all=1) → NOT normalized.
|
|
assert!(!mk(1 << 13).is_normalized);
|
|
// bit30 → mini fetch.
|
|
assert!(mk(1 << 30).is_mini_fetch);
|
|
// The old (wrong) bits 24/25 must NOT affect signed/normalized.
|
|
assert!(!mk(1 << 24).is_signed);
|
|
assert!(mk(1 << 25).is_normalized);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_texture_fetch() {
|
|
// opcode=1 (texture). const_index@bit20=3, src@bit5=1, dst@bit12=4.
|
|
// dimension lives in dword2 bits 14..15.
|
|
let w0 = 1u32 | (1 << 5) | (4 << 12) | (3 << 20);
|
|
let w2 = 2u32 << 14;
|
|
let t = decode_fetch([w0, 0, w2]);
|
|
match t {
|
|
FetchInstruction::Texture(tf) => {
|
|
assert_eq!(tf.fetch_const, 3);
|
|
assert_eq!(tf.src_register, 1);
|
|
assert_eq!(tf.dest_register, 4);
|
|
assert_eq!(tf.dimension, 2);
|
|
}
|
|
other => panic!("expected Texture, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_opcode_is_classified() {
|
|
let v = decode_fetch([0x16, 0, 0]); // GET_TEXTURE_BORDER_COLOR_FRAC
|
|
assert!(matches!(v, FetchInstruction::Unknown { opcode: 0x16, .. }));
|
|
}
|
|
}
|