[iterate-3AD] Fix 2nd splash logo rendering black: re-upload evolving atlas

The publisher (SQUARE ENIX) and the 2nd developer/studio splash logo share
one K8888 atlas at physical base 0x4dbee000, sampled at different UVs. The
publisher's white text occupies the top V-bands; the developer logo's
(bluish/gold) artwork is CPU-written into the SAME surface AFTER the publisher
frame, so the atlas evolves across frames.

The UI host texture cache (`texture_cache_host::upload`) only re-uploads a
`TextureKey` when `version_when_uploaded` increases. But the per-draw bind in
`render.rs` hardcoded `version_when_uploaded = 1` for every draw, so once the
atlas was first uploaded (during the publisher frame, with only the top bands
filled) the cache pinned that partial upload. The 2nd logo, sampling a V-band
that was still zero at first-upload time, read transparent-black -> rendered
nothing (the "white-triangle / black stub" the user saw after SQUARE ENIX).

Verdict: (G) a legitimate 2nd LOGO item whose real artwork lives in the same
evolving atlas — NOT a spurious 3rd item, and NOT a geometry/shader/blend gap.
Measured via readback: the 2nd-logo geometry rasterizes correctly (3 on-screen
quads), interp1 (UV) and interp0 (color) reach the PS with real values, the
texture content at the sampled bands exists — only the bound wgpu texture was
the stale partial upload.

Fix (UI-only, deterministic core untouched):
- `gpu_system`: thread the real content `version` (from `span_max_version`)
  into `last_draw_textures` (now `(key, version, bytes)`).
- `draw_capture::DrawCapture.textures`: same 3-tuple.
- `render.rs`: use the real `version` (not a hardcoded 1) so the host cache
  re-uploads when the guest fills more of the atlas.
- `exports.rs` `vd_swap`: the legacy single-texture `publish_texture` bridge
  drops the version (`(key, _v, bytes) -> (key, bytes)`).

Readback (env-gated probe, removed before commit): after the fix the 2nd logo
renders real varied artwork (blue + gold texels in a centered strip) instead
of black. Determinism: `check -n50m --gpu-inline --stable-digest` byte-
identical to the c0c6088 baseline (captured both via git-stash). 686 tests
green. No faking — real decoded texels through the real guest draw.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-19 13:38:35 +02:00
parent c0c6088e4d
commit 3f8d3b6f1c
4 changed files with 42 additions and 10 deletions

View File

@@ -72,7 +72,16 @@ pub struct DrawCapture {
/// per-draw so the textured logo samples the real artwork instead of the
/// magenta stub. Empty for flat (no-tfetch) draws. Populated by
/// `gpu_system` after decode (left empty by `build`).
pub textures: Vec<(crate::texture_cache::TextureKey, Vec<u8>)>,
///
/// Each entry is `(key, content_version, bytes)`. iterate-3AD: the
/// `content_version` (from `span_max_version` over the texel span) lets the
/// UI host texture cache RE-UPLOAD when the guest fills more of an evolving
/// atlas. The publisher and the 2nd splash logo share one K8888 surface
/// (base `0x4dbee000`); the 2nd logo's texels are CPU-written *after* the
/// publisher's first upload. Without the real version the host cache (which
/// previously pinned `version_when_uploaded = 1`) kept the first partial
/// upload, so the 2nd logo sampled its still-zero atlas region as black.
pub textures: Vec<(crate::texture_cache::TextureKey, u64, Vec<u8>)>,
/// iterate-3Y: per-draw color/blend render state captured from the
/// register file so the host pipeline composites the way the guest
/// intends (instead of one fixed alpha-blend state). Mirrors the fields

View File

@@ -429,7 +429,7 @@ pub struct GpuSystem {
/// hardcoded slot). `vd_swap` publishes the first of these to the UI so
/// the replay binds the texture the draw actually samples. Cleared and
/// repopulated each draw; empty when the active PS issues no `tfetch`.
pub last_draw_textures: Vec<(crate::texture_cache::TextureKey, Vec<u8>)>,
pub last_draw_textures: Vec<(crate::texture_cache::TextureKey, u64, Vec<u8>)>,
/// 10 MiB shadow of the Xenos EDRAM. Written by clear-resolves and
/// (future) host-render-target readback; read by the resolve byte-copy
/// path that writes tiled pixels into guest memory. Allocated once at
@@ -1411,7 +1411,17 @@ impl GpuSystem {
let version = span_max_version(mem, key.base_address, span_bytes.max(4));
match self.texture_cache.ensure_cached(key, version, mem) {
Ok(entry) => {
self.last_draw_textures.push((entry.key, entry.bytes.clone()));
// iterate-3AD: carry the real content `version`
// (from `span_max_version`) so the UI host
// texture cache re-uploads when the guest fills
// more of an evolving atlas (e.g. the 2nd splash
// logo's texels land after the publisher's, in
// the SAME K8888 surface). Previously the UI
// pinned `version_when_uploaded = 1`, so the
// first (partial) upload stuck and later draws
// sampled the not-yet-filled region as black.
self.last_draw_textures
.push((entry.key, version, entry.bytes.clone()));
metrics::counter!(
"gpu.texture.decode",
"fmt" => format!("{:?}", key.format),