render: skip rebuilding occlusion-culled statics on most frames

This commit is contained in:
mchughalex 2026-07-22 11:48:33 +01:00
parent 8f8642aec2
commit 7b0d690cf3
4 changed files with 55 additions and 3 deletions

View file

@ -1177,6 +1177,17 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull, true, "Skate 3",
"items.") "items.")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull_build, true,
"Skate 3",
"Also skip the per-frame scene-item rebuild for statics "
"the occlusion cull proved hidden, on 3 of every 4 "
"frames (staggered per instance). The every-4th-frame "
"rebuild re-enters the item in the rendered scene so "
"disocclusion is re-tested and the persistent shadow "
"caster cache stays refreshed. Requires "
"skate3_native_render_scene_occlusion_cull.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull_guest, true, REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull_guest, true,
"Skate 3", "Skate 3",
"Also skip the GUEST engine's draw-list dispatch for " "Also skip the GUEST engine's draw-list dispatch for "
@ -8261,6 +8272,21 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
if (wloop_prof) { if (wloop_prof) {
g_bi_records.fetch_add(count, std::memory_order_relaxed); g_bi_records.fetch_add(count, std::memory_order_relaxed);
} }
// Build-side occlusion skip: the render thread's culled-ctx set (the same
// sticky snapshot the guest dispatch filter uses, expiring with it when
// the native render idles or the cull's motion gate stands down). A ctx
// in it skips the whole item rebuild on 3 of every 4 frames; the
// staggered rebuild frame re-enters it in scene.items so the render side
// re-tests occlusion (disocclusion drops it from the set) and the shadow
// caster caches keep refreshing.
static std::vector<uint32_t> s_build_culled; // guest render thread only
s_build_culled.clear();
if (REXCVAR_GET(skate3_native_render_scene_occlusion_cull_build)) {
CopyOcclusionCulledCtxs(s_build_culled);
if (!s_build_culled.empty()) {
scene.occl_build_skipped.reserve(s_build_culled.size());
}
}
for (size_t i = 0; i < count; ++i) { for (size_t i = 0; i < count; ++i) {
const SubmitRecord& r = records[i]; const SubmitRecord& r = records[i];
// Primary opaque list of the chosen view only; other lists (shadow // Primary opaque list of the chosen view only; other lists (shadow
@ -8368,6 +8394,14 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
if (!seen.insert(r.a).second) { if (!seen.insert(r.a).second) {
continue; continue;
} }
if (!s_build_culled.empty() &&
((g_guest_frame + ((r.a >> 4) * 2654435761u >> 8)) & 3u) != 0u &&
std::binary_search(s_build_culled.begin(), s_build_culled.end(),
r.a)) {
scene.occl_build_skipped.push_back(r.a);
g_occl_build_skipped.fetch_add(1, std::memory_order_relaxed);
continue;
}
if (!REXCVAR_GET(skate3_native_render_scene_world_items)) { if (!REXCVAR_GET(skate3_native_render_scene_world_items)) {
continue; continue;
} }

View file

@ -430,6 +430,11 @@ struct FrameScene {
}; };
PhotoFx photo_fx; PhotoFx photo_fx;
std::vector<DrawItem> items; std::vector<DrawItem> items;
// MeshContexts the build-side occlusion skip left out of `items` this
// frame (see skate3_native_render_scene_occlusion_cull_build): the
// render thread re-publishes them as culled so the guest dispatch filter
// keeps excluding them between their staggered rebuild frames.
std::vector<uint32_t> occl_build_skipped;
}; };
// One frame submission record from the hook layer (see RenderMeshRecord). // One frame submission record from the hook layer (see RenderMeshRecord).

View file

@ -7143,7 +7143,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
"reused={}] dynobj[valid={} drawn={}] water[valid={} drawn={}] " "reused={}] dynobj[valid={} drawn={}] water[valid={} drawn={}] "
"lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] " "lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] "
"refl[pair={} flat={} gate={:#x}] flips={} alt[bones={} shadow={}] " "refl[pair={} flat={} gate={:#x}] flips={} alt[bones={} shadow={}] "
"rows_inst={} pal_srv={} occl_culled={} guest_skip={}", "rows_inst={} pal_srv={} occl_culled={} guest_skip={} build_skip={}",
frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d, frames, scene.items.size(), drawn, g_draws_2d.load(), drawn_2d,
drawn_spline, g_draws_spline.load(), drawn_spline, g_draws_spline.load(),
g_draws_2d_other.load(), g_draws_2d_dropped.load(), g_draws_2d_other.load(), g_draws_2d_dropped.load(),
@ -7178,7 +7178,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
g_hair_route_flips.load(), g_hair_bone_alternations.load(), g_hair_route_flips.load(), g_hair_bone_alternations.load(),
g_shadow_alternations.load(), g_char_rows_inst_served.load(), g_shadow_alternations.load(), g_char_rows_inst_served.load(),
g_pal_served_total.load(), g_occl_culled.load(), g_pal_served_total.load(), g_occl_culled.load(),
g_occl_guest_skipped.load()); g_occl_guest_skipped.load(), g_occl_build_skipped.load());
} }
} }
@ -9980,8 +9980,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
stamp_route(1); stamp_route(1);
} }
// Publish the culled-ctx set every rendered frame, including empty ones: // Publish the culled-ctx set every rendered frame, including empty ones:
// the guest filter must clear promptly when the cull stands down. // the guest filter must clear promptly when the cull stands down. Ctxs
// the build-side skip left out of this scene stay culled by re-inclusion
// (they could not be re-tested; their staggered rebuild frame is when
// they re-enter scene.items and get a fresh verdict) - but only while
// the cull is ACTIVE: on stand-down (debug views, the camera-motion
// gate, teleports) the set must drain so every skipped item resumes
// building on the next guest frame.
{ {
if (occl_cull_active) {
culled_ctxs.insert(culled_ctxs.end(), scene.occl_build_skipped.begin(),
scene.occl_build_skipped.end());
}
const int64_t now_ms = const int64_t now_ms =
std::chrono::duration_cast<std::chrono::milliseconds>( std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()) std::chrono::steady_clock::now().time_since_epoch())

View file

@ -968,6 +968,9 @@ inline std::atomic<uint64_t> g_occl_culled{0};
// filter removed before the guest's draw-list renderer ran (see // filter removed before the guest's draw-list renderer ran (see
// skate3_native_render_scene_occlusion_cull_guest). // skate3_native_render_scene_occlusion_cull_guest).
inline std::atomic<uint64_t> g_occl_guest_skipped{0}; inline std::atomic<uint64_t> g_occl_guest_skipped{0};
// Session-cumulative count of item rebuilds the build-side occlusion skip
// left out (see skate3_native_render_scene_occlusion_cull_build).
inline std::atomic<uint64_t> g_occl_build_skipped{0};
// Guest render thread: decomposition of the per-item build walk. // Guest render thread: decomposition of the per-item build walk.
inline PerfWindow g_pw_bi_core; // BuildItemFromMeshCached total (hit validate + misses) inline PerfWindow g_pw_bi_core; // BuildItemFromMeshCached total (hit validate + misses)
inline PerfWindow g_pw_bi_fp; // ComputeItemFingerprint on the cache-hit path inline PerfWindow g_pw_bi_fp; // ComputeItemFingerprint on the cache-hit path