[iterate-4A] intro-video: correct MULSC/ADDSC/SUBSC operand addressing

The scalar-constant ALU ops (MULSC/ADDSC/SUBSC, opcodes 42-47) address
their operands through src3 in a special way, not via src_a/src_b like
ordinary scalar ops:
  temp  reg = (src3_swiz & 0x3C) | (scalar_opc & 1), component = src3_swiz & 3
  const idx = src3_reg (+256 for the PS constant bank), component = .w
The op is then temp <op> const.

We were feeding these the plain (src_a.x, src_b.x). For the movie's
YUV->RGB luma scale that made the single MULSC0 compute r0.x * r0.x = Yb^2
(a squared luma) instead of r0.x * alu[511].w = Yb * 1.1643 (the limited-
range 255/219 luma scale). The squared luma crushed shadows, so dark
regions rendered as near-pure chroma -- the intro's dark background read
purple/magenta while the bright logo looked roughly right.

With correct addressing the luma is linear: the background drops to near
black (was a purple ~(37,15,39), now ~(12,0,14)) and the logo reaches full
white -- the authentic SQUARE ENIX intro. (The earlier attempt at this
addressing failed only because the PS constant bank was still mis-indexed
before the +256 fix, so the coefficients read zero -> black.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-02 21:58:40 +02:00
parent deb9292395
commit 5573ac1c43

View File

@@ -472,28 +472,37 @@ 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()
// Scalar pipe. Most scalar ops use (src_a.x, src_b.x); ps-variants use
// the running `ps`. The scalar-constant family (MULSC/ADDSC/SUBSC,
// 42..=47) is SPECIAL: it reads one temp register and one float
// constant, both addressed through src3 (canary ucode.h
// `scalar_const_reg_op_src_temp_reg`):
// temp reg = (src3_swiz & 0x3C) | (scalar_opc & 1), component = src3_swiz & 3
// const idx = src3_reg (+256 for the PS constant bank), component = .w
// The op is then temp <op> const. Feeding it the plain (src_a.x,
// src_b.x) instead computed e.g. r0.x*r0.x (Yb²) for the YUV→RGB luma
// scale — a squared luma that crushes shadows so dark regions render as
// near-pure chroma (the intro's dark background read purple). The
// earlier attempt at this addressing failed only because the constant
// bank was still mis-indexed (pre-STEP-95): the coefficients read zero.
let (scl_src_a, scl_src_b) = if (42..=47).contains(&alu.scalar_opcode) {
let temp_reg = ((alu.src_c_swiz & 0x3C) | (alu.scalar_opcode & 1)) as u32;
let temp_comp = ['x', 'y', 'z', 'w'][(alu.src_c_swiz & 0x3) as usize];
let const_idx = alu.src_c as u32 + const_base;
(
format!("r[{temp_reg}u].{temp_comp}"),
format!("xenos_consts.alu[{const_idx}u].w"),
)
} else if alu.scalar_src_is_ps {
("ps".to_string(), format!("{}.x", b))
} else {
format!("{}.x", a)
(format!("{}.x", a), format!("{}.x", b))
};
let scl_src_b = format!("{}.x", b);
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),
None => {
return Err(reject::SCL_OP_UNSUPPORTED);
}
};
self.push(&format!("ps = {expr};"));
if alu.scalar_write_mask != 0 {