[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>
This commit is contained in:
@@ -112,8 +112,15 @@ struct XenosConstants {
|
||||
@group(0) @binding(3) var<storage, read> ps_ucode : array<u32>;
|
||||
@group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;
|
||||
|
||||
@group(1) @binding(0) var xenos_tex : texture_2d<f32>;
|
||||
@group(1) @binding(1) var xenos_samp : sampler;
|
||||
@group(1) @binding(0) var xenos_samp : sampler;
|
||||
@group(1) @binding(1) var xenos_tex0 : texture_2d<f32>;
|
||||
@group(1) @binding(2) var xenos_tex1 : texture_2d<f32>;
|
||||
@group(1) @binding(3) var xenos_tex2 : texture_2d<f32>;
|
||||
@group(1) @binding(4) var xenos_tex3 : texture_2d<f32>;
|
||||
@group(1) @binding(5) var xenos_tex4 : texture_2d<f32>;
|
||||
@group(1) @binding(6) var xenos_tex5 : texture_2d<f32>;
|
||||
@group(1) @binding(7) var xenos_tex6 : texture_2d<f32>;
|
||||
@group(1) @binding(8) var xenos_tex7 : texture_2d<f32>;
|
||||
|
||||
// iterate-3T: real interpolator passthrough. The Xenos VS exports up to 16
|
||||
// interpolators (export index 0..15); the PS reads interpolator i from its
|
||||
@@ -427,9 +434,20 @@ impl EmitCtx {
|
||||
// discarded, so every ALU read came back as r[low7] without
|
||||
// any swizzle / negation, dropping every shader's uniforms +
|
||||
// negative operands.
|
||||
let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate);
|
||||
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate);
|
||||
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate);
|
||||
// Xenos splits the 512-entry float-constant file into two halves: the
|
||||
// vertex shader addresses c0..255 (physical 0..255), the pixel shader
|
||||
// addresses c0..255 too but the hardware reads them from the UPPER half
|
||||
// (physical 256..511). Our snapshot is the full 512-entry file read
|
||||
// linearly, so a PS constant reference `c_n` must index `alu[256 + n]`.
|
||||
// (RE STEP 95: the movie's YUV→RGB PS reads c254/c255; the game uploads
|
||||
// the coefficients to physical 510/511 = 256+254/255.)
|
||||
let const_base = match self.stage {
|
||||
Stage::Pixel => 256u32,
|
||||
Stage::Vertex => 0,
|
||||
};
|
||||
let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate, const_base);
|
||||
let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate, const_base);
|
||||
let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate, const_base);
|
||||
|
||||
// Vector pipe.
|
||||
if alu.vector_write_mask != 0 {
|
||||
@@ -446,14 +464,26 @@ impl EmitCtx {
|
||||
// Scalar pipe. Binary ops use (src_a.x, src_b.x); ps-variants use
|
||||
// src_a.x + running ps. `scl_src_a` mirrors the interpreter's
|
||||
// `scalar_src_is_ps` selector.
|
||||
//
|
||||
// NOTE: the scalar-constant family (MULSC/ADDSC/SUBSC, 42..=47) really
|
||||
// reads a SPECIAL operand pair — a float constant at src3's index
|
||||
// (W-swizzled) and a temp register at a bit-packed index (X-swizzled),
|
||||
// per canary ucode.h:1302 / shader_translator.cc:1419. A first attempt
|
||||
// at that addressing rendered the intro video BLACK (the operands
|
||||
// resolved to zero), so it's reverted here to the plain (src_a.x,
|
||||
// src_b.x) read: the YUV→RGB result is then visible but MAGENTA-tinted
|
||||
// (wrong chroma coefficients). Correct SC-operand addressing is a
|
||||
// follow-up. See project memory STEP 92.
|
||||
let scl_src_a = if alu.scalar_src_is_ps {
|
||||
"ps".to_string()
|
||||
} else {
|
||||
format!("{}.x", a)
|
||||
};
|
||||
let scl_src_b = format!("{}.x", b);
|
||||
let expr = scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps")
|
||||
.ok_or(reject::SCL_OP_UNSUPPORTED)?;
|
||||
let expr = match scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") {
|
||||
Some(e) => e,
|
||||
None => return Err(reject::SCL_OP_UNSUPPORTED),
|
||||
};
|
||||
self.push(&format!("ps = {expr};"));
|
||||
if alu.scalar_write_mask != 0 {
|
||||
let v = "vec4<f32>(ps, ps, ps, ps)";
|
||||
@@ -674,14 +704,58 @@ impl EmitCtx {
|
||||
}
|
||||
|
||||
fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) {
|
||||
// v1: sample the single bound texture; UV = r[src].xy. P5's cache
|
||||
// publishes the `fetch_const=0` texture into `@group(1)`; slot
|
||||
// mismatch is a silent magenta for now.
|
||||
// Sample the texture bound to this fetch's constant slot; UV =
|
||||
// r[src].xy. The UI binds up to 8 planes (one per fetch-constant slot),
|
||||
// so a multi-plane shader (the intro video's YUV Y/U/V) reads the right
|
||||
// texture per fetch. Slots >= 8 fall back to slot 0 (matches the
|
||||
// interpreter's `default` arm).
|
||||
let src_reg = tf.src_register & 0x7F;
|
||||
let dst_reg = tf.dest_register & 0x7F;
|
||||
self.push(&format!(
|
||||
"r[{dst_reg}u] = textureSampleLevel(xenos_tex, xenos_samp, r[{src_reg}u].xy, 0.0);"
|
||||
));
|
||||
let slot = if (tf.fetch_const as usize) < 8 {
|
||||
tf.fetch_const as usize
|
||||
} else {
|
||||
0
|
||||
};
|
||||
// Honor the 12-bit destination swizzle (3 bits/component). Only the
|
||||
// selected lanes are written; `keep` codes preserve the prior value.
|
||||
// This is what lets the YUV video's three fetches (all → r1, each in a
|
||||
// different lane) coexist instead of clobbering each other.
|
||||
let letters = ['x', 'y', 'z', 'w'];
|
||||
let mut comps: Vec<String> = Vec::with_capacity(4);
|
||||
let mut any_write = false;
|
||||
for i in 0..4u16 {
|
||||
let code = (tf.dest_swizzle >> (i * 3)) & 0x7;
|
||||
let expr = match code {
|
||||
0..=3 => {
|
||||
any_write = true;
|
||||
format!("_tex.{}", letters[code as usize])
|
||||
}
|
||||
4 => {
|
||||
any_write = true;
|
||||
"0.0".to_string()
|
||||
}
|
||||
5 => {
|
||||
any_write = true;
|
||||
"1.0".to_string()
|
||||
}
|
||||
// 6/7 = keep: preserve the destination's current component.
|
||||
_ => format!("r[{dst_reg}u].{}", letters[i as usize]),
|
||||
};
|
||||
comps.push(expr);
|
||||
}
|
||||
if any_write {
|
||||
self.push("{");
|
||||
self.indent += 1;
|
||||
self.push(&format!(
|
||||
"let _tex = textureSampleLevel(xenos_tex{slot}, xenos_samp, r[{src_reg}u].xy, 0.0);"
|
||||
));
|
||||
self.push(&format!(
|
||||
"r[{dst_reg}u] = vec4<f32>({}, {}, {}, {});",
|
||||
comps[0], comps[1], comps[2], comps[3]
|
||||
));
|
||||
self.indent -= 1;
|
||||
self.push("}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,11 +766,11 @@ impl EmitCtx {
|
||||
/// per canary `AluInstruction::GetSwizzledComponentIndex`: for output
|
||||
/// component i, source component is `((swiz >> (2*i)) + i) & 3`.
|
||||
/// Identity swizzle is `0x00`. GPUBUG-100 / GPUBUG-101.
|
||||
fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool) -> String {
|
||||
fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool, const_base: u32) -> String {
|
||||
let base = if is_temp {
|
||||
format!("r[{}u]", (src_byte & 0x3F) as u32)
|
||||
} else {
|
||||
format!("xenos_consts.alu[{}u]", src_byte as u32)
|
||||
format!("xenos_consts.alu[{}u]", src_byte as u32 + const_base)
|
||||
};
|
||||
let s = swizzle as u32;
|
||||
let lane = |i: u32| -> char {
|
||||
@@ -802,6 +876,16 @@ fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option<String> {
|
||||
sop::SQRT => format!("select(0.0, sqrt({a}), {a} >= 0.0)"),
|
||||
sop::SIN => format!("sin({a})"),
|
||||
sop::COS => format!("cos({a})"),
|
||||
// Scalar-constant family (canary `kMulsc0..kSubsc1`): a two-source
|
||||
// scalar op `ps = src0.x OP src1.x`. Our `a`/`b` are already src0.x /
|
||||
// src1.x (see the `scl_src_a`/`scl_src_b` setup in the interpreter and
|
||||
// above), so the `c0`/`c1` variants collapse to the same arithmetic —
|
||||
// the constant-bank distinction is resolved during operand read. The
|
||||
// intro video's YUV→RGB pixel shader builds its matrix-multiply from
|
||||
// these (multiply-by-coefficient + add-offset).
|
||||
sop::MULSC0 | sop::MULSC1 => format!("({a} * {b})"),
|
||||
sop::ADDSC0 | sop::ADDSC1 => format!("({a} + {b})"),
|
||||
sop::SUBSC0 | sop::SUBSC1 => format!("({a} - {b})"),
|
||||
sop::RETAIN_PREV => prev.to_string(),
|
||||
_ => return None,
|
||||
};
|
||||
@@ -935,17 +1019,17 @@ mod tests {
|
||||
fn src_operand_decodes_temp_vs_constant_no_modifiers() {
|
||||
// GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full].
|
||||
// Identity swizzle (0x00), no negate → bare base expression.
|
||||
assert_eq!(src_operand(0x00, true, 0x00, false), "r[0u]");
|
||||
assert_eq!(src_operand(0x05, true, 0x00, false), "r[5u]");
|
||||
assert_eq!(src_operand(0x3F, true, 0x00, false), "r[63u]");
|
||||
assert_eq!(src_operand(0x00, true, 0x00, false, 0), "r[0u]");
|
||||
assert_eq!(src_operand(0x05, true, 0x00, false, 0), "r[5u]");
|
||||
assert_eq!(src_operand(0x3F, true, 0x00, false, 0), "r[63u]");
|
||||
// For temps, bits 6/7 are reserved (abs/rel) — they don't widen
|
||||
// the register index even if set. Phase D2 will consume them.
|
||||
assert_eq!(src_operand(0x80, true, 0x00, false), "r[0u]");
|
||||
assert_eq!(src_operand(0xFF, true, 0x00, false), "r[63u]");
|
||||
assert_eq!(src_operand(0x80, true, 0x00, false, 0), "r[0u]");
|
||||
assert_eq!(src_operand(0xFF, true, 0x00, false, 0), "r[63u]");
|
||||
// Constants: full 8-bit index.
|
||||
assert_eq!(src_operand(0x00, false, 0x00, false), "xenos_consts.alu[0u]");
|
||||
assert_eq!(src_operand(0x05, false, 0x00, false), "xenos_consts.alu[5u]");
|
||||
assert_eq!(src_operand(0xFF, false, 0x00, false), "xenos_consts.alu[255u]");
|
||||
assert_eq!(src_operand(0x00, false, 0x00, false, 0), "xenos_consts.alu[0u]");
|
||||
assert_eq!(src_operand(0x05, false, 0x00, false, 0), "xenos_consts.alu[5u]");
|
||||
assert_eq!(src_operand(0xFF, false, 0x00, false, 0), "xenos_consts.alu[255u]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -959,7 +1043,7 @@ mod tests {
|
||||
// We just verify the mechanics by precomputing a known case:
|
||||
// swizzle=0x00 (identity) outputs .xyzw — matched by no-swizzle
|
||||
// branch. Negate wraps in `(-…)`.
|
||||
assert_eq!(src_operand(0x05, true, 0x00, true), "(-r[5u])");
|
||||
assert_eq!(src_operand(0x05, true, 0x00, true, 0), "(-r[5u])");
|
||||
// swizzle=0xFF → for each i, ((0xFF >> (2i)) + i) & 3:
|
||||
// i=0: (3 + 0) & 3 = 3 → w
|
||||
// i=1: ((0x3F) + 1) & 3 = (63+1)&3 = 0 → x
|
||||
@@ -967,12 +1051,12 @@ mod tests {
|
||||
// i=3: ((0x03) + 3) & 3 = (3+3)&3 = 2 → z
|
||||
// Output: .wxyz
|
||||
assert_eq!(
|
||||
src_operand(0x05, true, 0xFF, false),
|
||||
src_operand(0x05, true, 0xFF, false, 0),
|
||||
"vec4<f32>(r[5u].w, r[5u].x, r[5u].y, r[5u].z)"
|
||||
);
|
||||
// Combined: negate of constant with .wxyz swizzle.
|
||||
assert_eq!(
|
||||
src_operand(0x07, false, 0xFF, true),
|
||||
src_operand(0x07, false, 0xFF, true, 0),
|
||||
"(-vec4<f32>(xenos_consts.alu[7u].w, xenos_consts.alu[7u].x, xenos_consts.alu[7u].y, xenos_consts.alu[7u].z))"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user