[iterate-3AE] Fix spurious WHITE TRIANGLE flashing before each splash logo

The publisher and developer splash logos rendered correctly, but a
fullscreen OPAQUE WHITE diagonal half-triangle flashed at boot, before
each logo, and persisted across the dev-logo transition — canary shows a
black background there. Readback-isolated it (env-gated frontbuffer grid
+ per-draw inventory, both removed) to the background-fill draws.

ROOT (measured, refutes the prior "saturate/interpreter/depth" guesses):
the position-only VS `0xd4c14f46` (one vfetch → oPos; exports NO color)
paired with PS `0xed732b5a` (`ocolor0 = interp0`). The iterate-3T
translator seeded `ointerp[0] = (1,1,1,1)` "so a VS that only exports
position still yields a visible non-zero color" — a debug FAKE: it
injects white that no guest value backs. So that fill's interp0 stayed
white → opaque-white fullscreen triangle. Vertex windows of a WHITE frame
and a steady BLACK frame were byte-identical; served_translated=true for
all of them and depth is disabled in the replay, so the white came purely
from the injected seed, not saturate/interp/depth.

FIX (UI-translator only, golden byte-identical):
- translator.rs: default un-exported interpolators to (0,0,0,0) instead
  of seeding interp0 white. A position-only VS now contributes nothing
  visible under its real blend (RGB=0 → black; A=0 → premult transparent),
  matching canary; every VS that really exports interp0 (the logo
  `0x03b7b020`, the color fill `0x36660986`) overwrites the seed → logos
  unaffected.
- app.rs: clear the splash frontbuffer to BLACK, not the iterate-3S navy
  placeholder `[0.04,0.04,0.06]` (never matched to the guest). The fill is
  a fullscreen Xbox-360 RectangleList drawn as a single triangle in the
  replay (4th implied corner not yet synthesized), so its uncovered half
  exposed the clear; black makes the transition uniformly black like the
  oracle. (Full RectangleList→rectangle expansion is a separate follow-up.)

READBACK (env-gated, removed): white-heavy frames 200+ → 0; navy frames
240 → 0; transition frames uniformly black; the publisher logo (white
text + red dots) and the developer logos (colored, on black) still
render. Determinism: changes feed only the UI translator/clear; n50m
--gpu-inline --stable-digest byte-identical 2× and matches the committed
golden (--expect exit 0). cargo test --workspace 686 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-19 14:09:54 +02:00
parent 3f8d3b6f1c
commit c62a355418
2 changed files with 41 additions and 5 deletions

View File

@@ -250,12 +250,30 @@ impl EmitCtx {
// iterate-3T: real export model. Xenos export index 62 = oPos; // iterate-3T: real export model. Xenos export index 62 = oPos;
// indices 0..15 = interpolators. We hold position + 8 // indices 0..15 = interpolators. We hold position + 8
// interpolator vec4s; `emit_export` writes the right slot keyed // interpolator vec4s; `emit_export` writes the right slot keyed
// on the export index. Seed interp0 to white so a VS that only // on the export index.
// exports position still yields a visible (non-zero) color. //
// iterate-3AE (WHITE-TRIANGLE ROOT): interpolators a VS does NOT
// export must default to ZERO, not white. The old `ointerp[0] =
// (1,1,1,1)` was an iterate-3T debug convenience ("so a VS that
// only exports position still yields a visible non-zero color")
// — but it is a FAKE: it injects white that no guest value backs.
// The transition/background draws use the position-only VS
// `0xd4c14f46` (one vfetch → oPos; it exports NO color) paired
// with PS `0xed732b5a` (`ocolor0 = interp0`). With the white
// seed, interp0 stayed (1,1,1,1) → the fullscreen fill rendered
// OPAQUE WHITE (the diagonal half-triangle artifact that flashed
// before each splash logo and persisted across the dev-logo
// transition). Canary shows a black background there because the
// un-exported interpolator carries no white. Default to
// (0,0,0,0): a position-only VS now contributes nothing visible
// under its real (opaque or premultiplied) blend, matching
// canary, while every VS that really exports interp0 (the logo
// `0x03b7b020`, the `0x36660986` color fill) overwrites this seed
// and is unaffected. RGB=0 → black fill; A=0 → premultiplied
// overlays stay transparent.
self.push("var opos: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 1.0);"); self.push("var opos: vec4<f32> = vec4<f32>(0.0, 0.0, 0.0, 1.0);");
self.push("var ointerp: array<vec4<f32>, 8>;"); self.push("var ointerp: array<vec4<f32>, 8>;");
self.push("for (var i = 0u; i < 8u; i = i + 1u) { ointerp[i] = vec4<f32>(0.0, 0.0, 0.0, 1.0); }"); self.push("for (var i = 0u; i < 8u; i = i + 1u) { ointerp[i] = vec4<f32>(0.0, 0.0, 0.0, 0.0); }");
self.push("ointerp[0] = vec4<f32>(1.0, 1.0, 1.0, 1.0);");
} }
Stage::Pixel => { Stage::Pixel => {
// iterate-3T: the PS reads interpolator i from general register // iterate-3T: the PS reads interpolator i from general register

View File

@@ -369,7 +369,25 @@ impl ApplicationHandler<SwapEvent> for App {
.map(|s| s.frame_index) .map(|s| s.frame_index)
.unwrap_or(0); .unwrap_or(0);
if frame_idx != self.last_xenos_swap_frame { if frame_idx != self.last_xenos_swap_frame {
rs.clear_frontbuffer([0.04, 0.04, 0.06, 1.0]); // iterate-3AE: clear to BLACK, matching canary's
// splash background. The old navy `[0.04,0.04,0.06]`
// was an iterate-3S debug placeholder never matched
// to the guest. The splash background-fill draw is a
// full-screen Xbox-360 RectangleList (3 verts → a HW
// rectangle covering the whole screen); the UI replay
// draws it as a single triangle (the 4th implied
// corner isn't synthesized), so only the diagonal
// half is covered. With a navy clear the uncovered
// half showed a navy diagonal in the brief
// pre/inter-logo transition frames (where that fill
// is the only coverage). Canary's background there is
// black, and the guest's fill itself resolves to
// black, so a black clear makes the uncovered half
// match — the transition is uniformly black like the
// oracle. (Full RectangleList→rectangle expansion is
// the deeper fix and a separate follow-up; under a
// black clear the half-coverage is invisible.)
rs.clear_frontbuffer([0.0, 0.0, 0.0, 1.0]);
self.last_xenos_swap_frame = frame_idx; self.last_xenos_swap_frame = frame_idx;
} }
let delta = (draws_total - already) as u32; let delta = (draws_total - already) as u32;