diff --git a/src/skate3_draw_distance.cpp b/src/skate3_draw_distance.cpp index db6aa9b..e48a03e 100644 --- a/src/skate3_draw_distance.cpp +++ b/src/skate3_draw_distance.cpp @@ -416,6 +416,26 @@ extern "C" REX_FUNC(sub_8247B3A0) { (list_mode != 0 && list_count != 0) || cell_size > 4096) { return; } + // Loading screens and the frontend stream one world set out and another + // in, then destroy the departing collection objects. Any probe record + // kept in the gather output here stays loaded past that teardown and its + // content leaks into the arriving map (the differ cannot unload a record + // whose collection object is already gone - the ProcessEntries sanitizer + // below drops such descriptors without an unload). Leave the output + // untouched so probe cells unload through the same differ passes as the + // departing vanilla set while their collections are still live, and drop + // the caches: their neighbourhood is the departing map's. The in-game + // pause menu keeps the world resident and reports false; it merges the + // cache below instead. + if (skate3::native_scene::LoadingOrFrontendActive()) { + if (debug && (g_probe_cache[0].count > 0 || g_probe_cache[1].count > 0)) { + REXLOG_INFO( + "draw_distance: stream probe caches dropped (loading/frontend)"); + } + g_probe_cache[0] = {}; + g_probe_cache[1] = {}; + return; + } ProbeRecord records[192]; size_t record_count = 0; @@ -499,8 +519,12 @@ extern "C" REX_FUNC(sub_8247B3A0) { const bool set_resident = desc_total > 0 && desc_loaded >= desc_total; const bool in_gameplay = rex::kernel::guest_presence::GameplayContextValue() == 1; - if (cache_valid || (cache_usable && cache.count > 0 && - (!set_resident || !in_gameplay))) { + // An empty vanilla gather is the game clearing this focus (teardown or a + // transient); never inject on top of it - the differ is about to unload + // everything the focus held. + if (vanilla_count > 0 && + (cache_valid || (cache_usable && cache.count > 0 && + (!set_resident || !in_gameplay)))) { // The neighbourhood cannot have changed (cell membership is quantized // to the grid): merge the cached probe results instead of re-running // the gather fan. Cached distances are slightly stale; they are only @@ -525,10 +549,10 @@ extern "C" REX_FUNC(sub_8247B3A0) { ++record_count; } } - } else if (!set_resident || !in_gameplay) { - // Initial load, a fresh focus, or outside gameplay (loading screen / - // frontend): let the vanilla set finish streaming first; the fan - // starts on a later cycle. + } else if (vanilla_count == 0 || !set_resident || !in_gameplay) { + // Initial load, a fresh focus, an emptied focus, or outside gameplay: + // let the vanilla set finish streaming first; the fan starts on a + // later cycle. } else { // Full probe fan: every grid cell within the probe radius, stepped in // cell multiples. The 7x7 cap bounds the fan at 48 gather calls; the diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index acca094..15750c3 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -6092,6 +6092,23 @@ bool FreecamGuestPose(float out_pos[3]) { return true; } +bool LoadingOrFrontendActive() { + if (rex::kernel::guest_presence::GameplayContextValue() != 0) { + return false; + } + // Same publish-staleness window as YieldForMenus: the pause menu keeps + // the world resubmitting perspective scenes behind it, loading screens + // and the frontend stop publishing. + const int64_t last_ns = g_last_publish_ns.load(std::memory_order_relaxed); + if (last_ns < 0) { + return true; + } + const int64_t now_ns = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); + return now_ns - last_ns >= 300'000'000; +} + // Guest render thread only. Returns true when vp_out/cam_out hold a // re-timed pose for `now`; false = keep the raw guest pose (no history yet, // or a cut/teleport snapped). diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index 7923352..02d3784 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -500,6 +500,15 @@ void GetCapturedSunDir(float out[3]); // SetViewMatrix override in skate3_native_scene.cpp. bool FreecamGuestPose(float out_pos[3]); +// True while the guest sits on a loading screen or in the frontend: the +// presence context is out of gameplay AND the world has stopped publishing +// perspective scenes. The in-game pause menu keeps publishing behind the +// menu and reports false. Mirrors the pause-vs-loading distinction in +// YieldForMenus (same 300 ms publish-staleness window). With scene capture +// disabled entirely, publishes never arrive and every out-of-gameplay +// context reports true (the conservative side). +bool LoadingOrFrontendActive(); + // The game's per-entity spawn/streaming fade as carried by the item's // validated character-lighting capture (peds c21.x, defaultcharacter c13.x, // cacstamp c22.x, vehicle body c20.x, hair strand scale). 1.0 for items