[iterate-3Z] Fix logo color (yellow->white): k_8_8_8_8 vfetch + vfetch field/stride/saturate

Defect 2 of the three render-fidelity defects vs the canary oracle (the
publisher "SQUARE ENIX" logo rendered YELLOW instead of WHITE). Root,
measured by readback (env-gated probes, removed): the logo PS multiplies
the sampled texture by the interpolated vertex COLOR; the K8888 texture
itself decodes correctly (67,667 white texels + 2,087 red — the red dots —
zero yellow), so the yellow came from the vertex-color attribute decode.

Four coupled, canary-faithful fixes (all UI-translator/capture only — the
deterministic headless core is untouched; n50m --gpu-inline --stable-digest
golden byte-identical, exit 0):

- GPUBUG-112 (translator vfetch): VertexFormat 6 = k_8_8_8_8 (4x u8
  normalized, 1 dword), NOT k_16_16 (which is 25) per canary xenos.h:643.
  The logo color stream is k_8_8_8_8; decoding it as k_16_16 read only 2 of
  4 channels and forced BLUE = 0 -> white texture x (R,G,0) = yellow. Now
  unpacks all four 8-bit channels (canary spirv_shader_translator_fetch.cc
  k_8_8_8_8 packed_offsets 0/8/16/24); added k_16_16 (format 25) too.

- GPUBUG-113 (ucode/fetch): vfetch is_signed / is_normalized / is_mini_fetch
  bit positions were wrong (read bits 24/25, which sit inside exp_adjust).
  Per canary ucode.h:757-758,764: signed=fomat_comp_all (w1 bit12),
  normalized=(num_format_all==0) (w1 bit13), mini_fetch (w1 bit30).

- GPUBUG-114 (translator vfetch): a vfetch_mini reuses the address AND
  STRIDE of the preceding full vfetch of the same stream (canary
  ucode.h:733); its own stride field is 0. Track the last full stride per
  fetch-const and inherit it so a mini color/UV attribute indexes by the
  real vertex stride, not its tight dword count.

- GPUBUG-115 (translator PS export): saturate the color export to [0,1]
  before the UNORM render-target write, mirroring canary
  spirv_shader_translator.cc:3607 ("Saturate, flushing NaN to 0"). Without
  it an out-of-range guest color writes garbage to the sRGB target.

Verified by env-gated frontbuffer readback (copy_texture_to_buffer, removed
before commit): the logo now renders WHITE text + RED dots (bbox centered
~y322-389), zero yellow anywhere. Workspace tests green (added 4: k_8_8_8_8
4-channel unpack, mini-fetch stride inheritance, vfetch bit decode, PS
saturate). Determinism: golden byte-identical.

Remaining (defects 1 & 3, see memory iterate-3Z): logo orientation and the
ed732b5a fullscreen background fill (renders ~white, canary shows black) —
both localized but not yet cleanly resolved; plan in the memory file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-18 20:58:21 +02:00
parent 2a992db47b
commit f6f3aac673
2 changed files with 170 additions and 22 deletions

View File

@@ -48,13 +48,19 @@ pub struct VertexFetch {
/// three vfetches sharing one fetch-constant read different attributes
/// instead of all reading offset 0.
pub offset: u32,
/// iterate-3T: `is_signed` (dword2 bit 24 in canary) — selects signed vs
/// unsigned interpretation of packed integer formats.
/// `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,
/// iterate-3T: `is_normalized` canary inverts it: dword2 bit 25 set means
/// the value is taken as an *integer* (un-normalized); clear means
/// normalized to [0,1] / [-1,1]. We store the normalized sense directly.
/// `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],
}
@@ -121,8 +127,13 @@ pub fn decode_fetch(words: [u32; 3]) -> FetchInstruction {
format: ((w1 >> 16) & 0x3F) as u8,
stride: (w2 & 0xFF) as u8,
offset: (w2 >> 8) & 0xFF,
is_signed: ((w1 >> 24) & 1) != 0,
is_normalized: ((w1 >> 25) & 1) == 0,
// 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 {
@@ -183,6 +194,31 @@ mod tests {
}
}
#[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.