diff --git a/crates/xenia-gpu/src/draw_capture.rs b/crates/xenia-gpu/src/draw_capture.rs index 937ca73..ddae76f 100644 --- a/crates/xenia-gpu/src/draw_capture.rs +++ b/crates/xenia-gpu/src/draw_capture.rs @@ -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)>, + /// + /// 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)>, /// 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 diff --git a/crates/xenia-gpu/src/gpu_system.rs b/crates/xenia-gpu/src/gpu_system.rs index 03952b3..f24fd49 100644 --- a/crates/xenia-gpu/src/gpu_system.rs +++ b/crates/xenia-gpu/src/gpu_system.rs @@ -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)>, + pub last_draw_textures: Vec<(crate::texture_cache::TextureKey, u64, Vec)>, /// 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), diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index f0355b0..d33ca7c 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -3195,7 +3195,14 @@ fn vd_swap(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // the first (the UI binds a single texture today). When the last draw // used a flat (no-tfetch) shader the list is empty, so we fall back to // the legacy slot-0 probe to preserve behavior on flat-only frames. - let published = gpu_inline.last_draw_textures.first().cloned().or_else(|| { + // The legacy single-texture `publish_texture` bridge wants + // `(TextureKey, bytes)`; `last_draw_textures` now also carries the + // content version (for the per-draw host-cache re-upload). Drop it here. + let published = gpu_inline + .last_draw_textures + .first() + .map(|(k, _v, b)| (*k, b.clone())) + .or_else(|| { // Fallback: probe fetch constant slot 0 directly. Texture fetch // constants live at `CONST_BASE_FETCH + slot*6` in the register // file; read 6 dwords, decode the key, hit the CPU cache with diff --git a/crates/xenia-ui/src/render.rs b/crates/xenia-ui/src/render.rs index 921789a..37335ba 100644 --- a/crates/xenia-ui/src/render.rs +++ b/crates/xenia-ui/src/render.rs @@ -775,14 +775,20 @@ impl RenderState { .. } = self; match cap.textures.first() { - Some((key, bytes)) => { - // Stable version: identical (key,bytes) across draws - // reuse the uploaded wgpu texture (the splash artwork is - // static). A genuine content change arrives as a new key - // (base_address/dims) from the decoder. + Some((key, version, bytes)) => { + // iterate-3AD: use the decoder's real content `version` + // (from `span_max_version`) so the host cache re-uploads + // 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 land + // AFTER the first upload. With the old hardcoded + // `version_when_uploaded = 1`, the same `TextureKey` + // never re-uploaded, so the 2nd logo sampled its (then + // still-zero) atlas region as black. The real version + // increases as the guest writes, triggering re-upload. let cached = xenia_gpu::texture_cache::CachedTexture { key: *key, - version_when_uploaded: 1, + version_when_uploaded: *version, bytes: bytes.clone(), }; host_texture_cache.upload(device, queue, &cached);