From fbcffc1445df6ba8fb09f25499d3f7c6fa81f5ee Mon Sep 17 00:00:00 2001 From: mchughalex <25671999+mchughalex@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:19:40 +0100 Subject: [PATCH] render: fill one-to-two frame dyn publish gaps with the previous capture --- src/skate3_native_scene.cpp | 55 +++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index b4591ce..9494e9e 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -1132,6 +1132,11 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_ring, false, "Skate 3", "frames); F7 dumps it to logs/scene_ring_.csv for " "diffing 1-2 frame artifacts no capture can catch") .lifecycle(rex::cvar::Lifecycle::kHotReload); + +REXCVAR_DEFINE_BOOL(skate3_native_render_scene_dyn_gap_fill, true, "Skate 3", + "Re-publish character/cloth/skinned items for up to two " + "missed publish frames (high-frame-rate body flicker)") + .lifecycle(rex::cvar::Lifecycle::kHotReload); namespace skate3::native_scene { namespace { @@ -8654,12 +8659,21 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } } } - // Publish-gap telemetry ("skater flickered invisible for a moment"): a - // skinned/ropa/character mesh that published recently, vanished for 1-3 - // built frames, and came back. Rate-limited log names the mesh and gap so - // the next flicker sighting is diagnosable from the log alone. + // Publish-gap telemetry and fill ("skater flickered invisible for a + // moment"): a skinned/ropa/character mesh that published recently can + // vanish from the publish stream for 1-2 built frames at high render + // rates (the body serve races the sim; more frequent the faster the + // renderer runs, so Vulkan flickered worst). Re-publish the previous + // frame's capture for up to two missed frames - a one-frame-old pose is + // imperceptible at those rates, and a real despawn stops the fill at the + // cap. The rate-limited log keeps naming mesh and gap so sightings stay + // diagnosable. { - static std::unordered_map s_last_pub; + struct PubTrack { + uint64_t frame = 0; + DrawItem item; + }; + static std::unordered_map s_last_pub; static uint64_t s_pub_frame = 0; ++s_pub_frame; if (s_last_pub.size() > 4096) { @@ -8670,12 +8684,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { (item.skinned && !item.bones.empty()))) { continue; } - auto [it, fresh] = s_last_pub.try_emplace(item.mesh, s_pub_frame); - if (fresh) { - continue; - } - const uint64_t gap = s_pub_frame - it->second; - it->second = s_pub_frame; + PubTrack& t = s_last_pub[item.mesh]; + const uint64_t gap = t.frame != 0 ? s_pub_frame - t.frame : 0; + t.frame = s_pub_frame; + t.item = item; if (gap >= 2 && gap <= 4) { g_dyn_gap.fetch_add(1, std::memory_order_relaxed); static std::atomic gap_logged{0}; @@ -8689,6 +8701,27 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) { } } } + if (REXCVAR_GET(skate3_native_render_scene_dyn_gap_fill)) { + for (auto& [mesh, t] : s_last_pub) { + if (t.frame == 0 || t.frame == s_pub_frame) { + continue; // published this frame (or placeholder) + } + const uint64_t gap = s_pub_frame - t.frame; + if (gap <= 2) { + // t.frame stays at the last REAL publish, so the fill self-limits + // to two frames and the telemetry above still sees the true gap + // when the stream resumes. + scene.items.push_back(t.item); + static std::atomic s_gap_filled{0}; + const uint64_t n = s_gap_filled.fetch_add(1, std::memory_order_relaxed); + if (n < 8 || (n & 1023u) == 0) { + REXLOG_INFO( + "native-scene: dyn gap fill mesh={:08X} age={} ropa={} fam={} (n={})", + mesh, gap, t.item.ropa ? 1 : 0, t.item.char_family, n + 1); + } + } + } + } } if (scene.items.empty()) { return;