diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 18922d6..2fde21f 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -874,6 +874,13 @@ REXCVAR_DEFINE_STRING(skate3_native_render_scene_trace_mesh, "", "Skate 3", "every worker commit touching the mesh's objects. " "Empty = off.") .lifecycle(rex::cvar::Lifecycle::kHotReload); +REXCVAR_DEFINE_STRING(skate3_native_render_scene_trace_2d, "", "Skate 3", + "Hex fetch word 1 (base|flags) of a 2D overlay texture " + "to trace through the 2D resolver ('2d-trace:' log " + "lines): per-resolve entry state, liveness probe " + "results and the serve/heal route taken, plus the " + "worker commits for its words key. Empty = off.") + .lifecycle(rex::cvar::Lifecycle::kHotReload); REXCVAR_DEFINE_INT32(skate3_native_render_scene_detail_hold, 240, "Skate 3", "Frames an item slot keeps serving its previous HIGHER-" "resolution texture after the game's material-detail " diff --git a/src/skate3_native_scene_gpu.cpp b/src/skate3_native_scene_gpu.cpp index b3d14ee..8c5d157 100644 --- a/src/skate3_native_scene_gpu.cpp +++ b/src/skate3_native_scene_gpu.cpp @@ -150,6 +150,7 @@ REXCVAR_DECLARE(int32_t, skate3_native_render_scene_ssr_steps); REXCVAR_DECLARE(int32_t, skate3_native_render_scene_warmup_budget_ms); REXCVAR_DECLARE(int32_t, skate3_native_render_scene_warmup_min_items); REXCVAR_DECLARE(std::string, skate3_native_render_scene_trace_mesh); +REXCVAR_DECLARE(std::string, skate3_native_render_scene_trace_2d); REXCVAR_DECLARE(std::string, skate3_native_render_snapshot_dir); #if (defined(REX_HAS_D3D12) && REX_HAS_D3D12) || (defined(REX_HAS_VULKAN) && REX_HAS_VULKAN) @@ -6688,6 +6689,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat REXLOG_INFO("tex-trace: mesh={:08X} {}", parsed, parsed ? "TRACING" : "off"); } + const std::string t2(REXCVAR_GET(skate3_native_render_scene_trace_2d)); + const uint32_t parsed2 = + t2.empty() ? 0u : uint32_t(std::strtoul(t2.c_str(), nullptr, 16)); + if (parsed2 != g_trace_2d_w1) { + g_trace_2d_w1 = parsed2; + REXLOG_INFO("2d-trace: w1={:08X} {}", parsed2, + parsed2 ? "TRACING" : "off"); + } } // ---- Loading -> gameplay takeover (seamless boot / map-change loads) ---- @@ -8830,6 +8839,13 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat return &g_r.white; } const uint64_t key = FetchWordsKey(fetch); + // 2D-resolver trace (trace_2d cvar, matched on fetch word 1): logs the + // per-resolve entry state + route below, and registers the words key so + // the worker-commit trace covers its heals too. + const bool tr2d = g_trace_2d_w1 != 0 && fetch[1] == g_trace_2d_w1; + if (tr2d) { + g_trace_keys.insert(key); + } // Routing (see the 2d_async_px cvar): INLINE-FIRST under the per-frame // budget; the run-copy untiler + in-place hot updates made decodes // cheap (1280x720 sub-ms; no CreateCommittedResource churn on content @@ -8856,15 +8872,24 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat if (it != g_r.tex_store.end()) { it->second.last_used_frame = frame_number; } - if (it != g_r.tex_store.end() && it->second.valid && !it->second.incomplete && + if (it != g_r.tex_store.end() && it->second.valid && frame_number >= it->second.recheck_frame && REXCVAR_GET(skate3_native_render_scene_tex_revalidate)) { // Content liveness for CPU-rewritten UI art: video frames and APT // re-rasterized tiles rewrite the same payload with the fetch words // unchanged; without the probe the words-keyed cache would freeze - // them on their first decoded content. + // them on their first decoded content. Incomplete decodes (truncated + // tiled-mip copy) re-decode here too, like the 3D route revalidator; + // the 2D-only art (menu thumbnails) has no other heal path. const uint64_t fp = SampleProbeFingerprint(base, it->second); - if (fp != 0 && fp != it->second.payload_fp) { + if ((fp != 0 && fp != it->second.payload_fp) || it->second.incomplete) { + // Rolling-capped heal telemetry for menu-class art (the map-switch + // thumbnail wrong-image reports): names the key, the route taken + // and the fingerprint transition. + static std::atomic s_2d_heal_logs{0}; + const bool log_heal = + uint64_t(px_w) * px_h >= 32768 && + s_2d_heal_logs.fetch_add(1, std::memory_order_relaxed) < 64; // Content changes decode INLINE while the per-frame budget lasts // (1-frame latency for animating UI); fullscreen-class art gets an // extended budget; serving it stale/blank is a whole-screen @@ -8873,6 +8898,14 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat inline_redecode = video || !async_ui || hot_inline_budget_ns > 0 || (fullscreen_class && hot_inline_budget_ns > -4'000'000); + if (log_heal) { + REXLOG_INFO( + "native-scene: 2D content heal {}x{} key={:016X} fp {:016X}->" + "{:016X} inc={} route={}", + px_w, px_h, key, it->second.payload_fp, fp, + it->second.incomplete ? 1 : 0, + inline_redecode ? "inline" : "async"); + } if (inline_redecode) { // Same layout = update the committed texture in place (no // CreateCommittedResource churn, SRV slot kept, tear-guarded). @@ -8884,6 +8917,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat it->second.last_change_frame = frame_number; return &it->second; } + if (tr2d) { + REXLOG_INFO("2d-trace: f{} key={:016X} inplace update REFUSED - " + "retire + fresh decode", + frame_number, key); + } RetireGuestTexture(it->second, context.device->CurrentSubmission()); g_r.tex_store.erase(it); it = g_r.tex_store.end(); // falls into the inline decode below @@ -8920,6 +8958,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat if (async_ui && !video && !inline_redecode && !inline_ok) { EnqueueWordsMiss(key, fetch, /*ui=*/true); g_2d_async_skip.fetch_add(1, std::memory_order_relaxed); + if (tr2d) { + REXLOG_INFO("2d-trace: f{} key={:016X} SKIP (async first sight)", + frame_number, key); + } return nullptr; } // Small HUD/spline art decodes inline (sub-ms; async would pop @@ -8958,8 +9000,40 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat cmd->FlushBarriers(); gt.last_used_frame = frame_number; gt.last_change_frame = frame_number; + if (tr2d) { + REXLOG_INFO("2d-trace: f{} key={:016X} INLINE decode valid={} inc={} " + "fp={:016X}", + frame_number, key, gt.valid ? 1 : 0, gt.incomplete ? 1 : 0, + gt.payload_fp); + } it = g_r.tex_store.emplace(key, gt).first; } + if (it != g_r.tex_store.end() && + (tr2d || (g_in_menus_frame.load(std::memory_order_relaxed) && + uint64_t(px_w) * px_h >= 32768))) { + // Menu thumbnail-class serve log: per buffer (fetch word 1), log when + // EITHER the served cache content (stored fp) OR the live guest + // content (probe fp) changes. A served-fp that lags the live-fp is + // exactly the stale-thumbnail bug; the buffer holds the new area's + // art while we keep serving the previous decode. + static std::mutex s_2d_log_mutex; + static std::unordered_map s_2d_seen; // w1 -> stored^live + const uint64_t live_fp = SampleProbeFingerprint(base, it->second); + const uint64_t sig = it->second.payload_fp ^ (live_fp * 3); + std::lock_guard lk(s_2d_log_mutex); + auto sit = s_2d_seen.find(fetch[1]); + if (sit == s_2d_seen.end() || sit->second != sig) { + s_2d_seen[fetch[1]] = sig; + REXLOG_INFO( + "2d-thumb: f{} w1={:08X} {}x{} key={:016X} served_fp={:016X} " + "live_fp={:016X}{} valid={} inc={} recheck=f{}", + frame_number, fetch[1], px_w, px_h, key, it->second.payload_fp, + live_fp, + (live_fp != 0 && live_fp != it->second.payload_fp) ? " STALE" : "", + it->second.valid ? 1 : 0, it->second.incomplete ? 1 : 0, + it->second.recheck_frame); + } + } return it->second.valid ? &it->second : &g_r.white; }; const uint32_t ui_region = diff --git a/src/skate3_native_scene_gpu_internal.h b/src/skate3_native_scene_gpu_internal.h index 50f1373..b891a3b 100644 --- a/src/skate3_native_scene_gpu_internal.h +++ b/src/skate3_native_scene_gpu_internal.h @@ -250,6 +250,9 @@ inline std::atomic g_skip_new{0}; // mesh's current store keys (so worker-commit events can be matched), and // the last logged per-ctx state signature (summaries log on change). inline uint32_t g_trace_mesh_addr = 0; +// 2D-resolver trace: fetch word 1 (base|flags) selected by the trace_2d +// cvar; 0 = off. Render thread only. +inline uint32_t g_trace_2d_w1 = 0; inline std::unordered_set g_trace_keys; inline std::unordered_map g_trace_sig; // Words-keyed (event-ad / streamed-artwork) serving: stale = site served @@ -258,8 +261,8 @@ inline std::unordered_map g_trace_sig; inline std::atomic g_ad_stale_served{0}; inline std::atomic g_ad_placeholder{0}; -// Fill GuestTexture::probe_addr with up to 16 of the texture's own mip-0 -// blocks: a 4x4 spread over the block grid, each resolved through the same +// Fill GuestTexture::probe_addr with up to 64 of the texture's own mip-0 +// blocks: an 8x8 spread over the block grid, each resolved through the same // tiled/linear addressing the decode uses, clamped to the guarded copy's // readable range. Probes crossing into the padded macro-row tail are skipped // rather than clamped; the tail belongs to pool neighbors. @@ -277,6 +280,20 @@ inline void BuildPayloadProbes(const rex::graphics::TextureInfo& info, uint32_t const uint32_t height = info.height + 1u; const uint32_t cols = (width + fi->block_width - 1) / fi->block_width; const uint32_t rows = (height + fi->block_height - 1) / fi->block_height; + // DXT2/3/4/5 blocks LEAD with their 8-byte alpha half, and on fully + // opaque art that half is the same constant in every block; the leading + // qword hashed identically across entirely DIFFERENT images (every + // pause-menu location photo fingerprinted alike), so in-place content + // swaps were invisible to the liveness probes. Probe the color half of + // those blocks instead. (DXT1's 8-byte block IS the color half; DXN's + // leading half is a real content channel; both keep offset 0.) + using TF = rex::graphics::xenos::TextureFormat; + const uint32_t probe_off = + (info.format == TF::k_DXT2_3 || info.format == TF::k_DXT4_5 || + info.format == TF::k_DXT2_3_AS_16_16_16_16 || + info.format == TF::k_DXT4_5_AS_16_16_16_16) + ? 8u + : 0u; for (uint32_t gy = 0; gy < 8; ++gy) { for (uint32_t gx = 0; gx < 8; ++gx) { const uint32_t bx = cols > 1 ? gx * (cols - 1) / 7 : 0; @@ -291,7 +308,8 @@ inline void BuildPayloadProbes(const rex::graphics::TextureInfo& info, uint32_t if (off + bpb > copy_size) { continue; } - out.probe_addr[out.probe_count++] = (0xA0000000u | mip0_addr) + off; + out.probe_addr[out.probe_count++] = + (0xA0000000u | mip0_addr) + off + probe_off; } } }