render: occlusion-cull statics hidden behind rendered geometry

This commit is contained in:
mchughalex 2026-07-22 11:23:46 +01:00
parent 13447a041f
commit 73c19c8cd3
5 changed files with 69 additions and 9 deletions

View file

@ -1167,6 +1167,16 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_perf_interval, 600, "Skate 3",
.range(60, 6000) .range(60, 6000)
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull, true, "Skate 3",
"Skip drawing static world items whose bounds are "
"provably hidden behind already-rendered geometry (depth-"
"grid test, conservative: anything unprovable still "
"draws). Shadow passes are exempt, so hidden geometry "
"keeps casting shadows. Dense areas with extended world "
"streaming spend roughly half their per-item CPU on such "
"items.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_perf_items, false, "Skate 3", REXCVAR_DEFINE_BOOL(skate3_native_render_scene_perf_items, false, "Skate 3",
"Deep per-item CPU profiling: adds a perf-items log line " "Deep per-item CPU profiling: adds a perf-items log line "
"per perf window attributing per-item cost to pipeline " "per perf window attributing per-item cost to pipeline "

View file

@ -88,6 +88,7 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_pause_native);
REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_log); REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_log);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_perf_interval); REXCVAR_DECLARE(int32_t, skate3_native_render_scene_perf_interval);
REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_items); REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_items);
REXCVAR_DECLARE(bool, skate3_native_render_scene_occlusion_cull);
REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_display_yield); REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_display_yield);
REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_grab_native); REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_grab_native);
REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_native); REXCVAR_DECLARE(bool, skate3_native_render_scene_photo_native);
@ -7138,7 +7139,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={}", "rows_inst={} pal_srv={} occl_culled={}",
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(),
@ -7172,7 +7173,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn,
g_refl_pair.load(), g_refl_flat.load(), g_refl_gate.load(), g_refl_pair.load(), g_refl_flat.load(), g_refl_gate.load(),
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_pal_served_total.load(), g_occl_culled.load());
} }
} }
@ -9580,7 +9581,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// completed slot wins, every completed slot retires. A grid that stops // completed slot wins, every completed slot retires. A grid that stops
// refreshing (AO off, loading flows) goes invalid instead of classifying // refreshing (AO off, loading flows) goes invalid instead of classifying
// against ancient depth. // against ancient depth.
if (prof_items && !g_r.occl_failed && g_r.occl_readback_ptr[1] != nullptr) { const bool occl_cull_wanted =
REXCVAR_GET(skate3_native_render_scene_occlusion_cull);
if ((prof_items || occl_cull_wanted) && !g_r.occl_failed &&
g_r.occl_readback_ptr[1] != nullptr) {
const uint64_t occl_done = context.device->CompletedSubmission(); const uint64_t occl_done = context.device->CompletedSubmission();
int newest = -1; int newest = -1;
for (int i = 0; i < 2; ++i) { for (int i = 0; i < 2; ++i) {
@ -9604,6 +9608,8 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} }
std::memcpy(g_r.occl_grid_vp, g_r.occl_vp[newest], std::memcpy(g_r.occl_grid_vp, g_r.occl_vp[newest],
sizeof(g_r.occl_grid_vp)); sizeof(g_r.occl_grid_vp));
std::memcpy(g_r.occl_grid_cam, g_r.occl_cam[newest],
sizeof(g_r.occl_grid_cam));
g_r.occl_grid_frame = frame_number; g_r.occl_grid_frame = frame_number;
g_r.occl_grid_valid = true; g_r.occl_grid_valid = true;
for (int i = 0; i < 2; ++i) { for (int i = 0; i < 2; ++i) {
@ -9691,6 +9697,27 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
REXCVAR_GET(skate3_native_render_scene_entity_fade) && REXCVAR_GET(skate3_native_render_scene_entity_fade) &&
CharFadeAlpha(it) < 0.999f; CharFadeAlpha(it) < 0.999f;
}; };
// Occlusion cull: statics provably hidden behind already-rendered
// geometry skip the color pass entirely (every scene.items consumer that
// must still see them - shadow casters, the world-shadow map, outline,
// warm-settle - iterates the list itself and is unaffected). Engaged only
// while the grid's camera matches this frame's within 1 m: the stored-
// frustum check inside ItemOccludedByGrid already keeps rotation safe
// (bounds outside the grid's view classify visible), and past 1 m of
// translation the 1-2 frame-old depth could hide content parallax has
// revealed. At gameplay speeds the grid refreshes well inside that
// budget, so the gate only stands the cull down across teleports and
// camera cuts.
bool occl_cull_active = occl_cull_wanted && debug_mode == 0 &&
g_r.occl_grid_valid;
if (occl_cull_active) {
float cam_d2 = 0.0f;
for (int a = 0; a < 3; ++a) {
const float d = scene.cam_pos[a] - g_r.occl_grid_cam[a];
cam_d2 += d * d;
}
occl_cull_active = cam_d2 < 1.0f;
}
// Opaque items draw front-to-back (bbox-center distance): early-z rejects // Opaque items draw front-to-back (bbox-center distance): early-z rejects
// occluded pixels before the heavy material PS runs. The game's sort-list // occluded pixels before the heavy material PS runs. The game's sort-list
// order is by render state, not depth; depth-write LESS_EQUAL makes the // order is by render state, not depth; depth-write LESS_EQUAL makes the
@ -9876,6 +9903,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
} }
} }
}; };
// Occlusion cull (statics only; same classifiability gate as the
// profiler): the 1 m depth margin on top of the conservative bbox test
// keeps reveal edges safe at speed - deeply hidden mass sits many
// meters behind its occluders, so the margin costs almost none of the
// win. Ring route 3 = culled.
if (occl_cull_active && !item.skinned && !item.ropa &&
!item.cloth_quads && item.bones.empty() &&
ItemOccludedByGrid(item, 1.0f)) {
g_occl_culled.fetch_add(1, std::memory_order_relaxed);
stamp_route(3);
continue;
}
// The exact ocean surface (fam 31) is an OPAQUE draw in the game (its // The exact ocean surface (fam 31) is an OPAQUE draw in the game (its
// PS outputs alpha 0 and it writes z-prepass depth); routing it through // PS outputs alpha 0 and it writes z-prepass depth); routing it through
// the sorted alpha sub-pass would composite the blended horizon sheet // the sorted alpha sub-pass would composite the blended horizon sheet

View file

@ -642,12 +642,14 @@ struct RendererState {
uint8_t* occl_readback_ptr[2] = {}; uint8_t* occl_readback_ptr[2] = {};
uint64_t occl_submission[2] = {}; uint64_t occl_submission[2] = {};
float occl_vp[2][16] = {}; float occl_vp[2][16] = {};
float occl_cam[2][3] = {}; // camera position the grid's frame rendered from
bool occl_pending[2] = {}; bool occl_pending[2] = {};
int occl_write_index = 0; int occl_write_index = 0;
bool occl_failed = false; bool occl_failed = false;
// Newest completed grid, CPU-side (render thread only). // Newest completed grid, CPU-side (render thread only).
std::vector<float> occl_grid; std::vector<float> occl_grid;
float occl_grid_vp[16] = {}; float occl_grid_vp[16] = {};
float occl_grid_cam[3] = {};
uint64_t occl_grid_frame = 0; // frame_number the grid was captured on uint64_t occl_grid_frame = 0; // frame_number the grid was captured on
bool occl_grid_valid = false; bool occl_grid_valid = false;
// HDR pipeline (hdr.hlsl): the scene renders pre-tonemap linear into a // HDR pipeline (hdr.hlsl): the scene renders pre-tonemap linear into a

View file

@ -58,6 +58,7 @@
// Cvars defined in skate3_native_scene.cpp. // Cvars defined in skate3_native_scene.cpp.
REXCVAR_DECLARE(double, skate3_menu_blur_sigma); REXCVAR_DECLARE(double, skate3_menu_blur_sigma);
REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_items); REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_items);
REXCVAR_DECLARE(bool, skate3_native_render_scene_occlusion_cull);
REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom); REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom);
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity); REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee); REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee);
@ -442,12 +443,15 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
nrhi::ResourceState::kRenderTarget); nrhi::ResourceState::kRenderTarget);
} }
// Occlusion-attribution grid (perf-items profiling): while the linear // Occlusion grid (perf-items attribution AND the occlusion cull): while
// depth is still bound as an SRV, tile-MAX reduce it into the small R32F // the linear depth is still bound as an SRV, tile-MAX reduce it into the
// grid and queue a never-waited copy into the readback ring (the // small R32F grid and queue a never-waited copy into the readback ring
// photo-grab pattern; consumed 1-2 frames later by the item classifier // (the photo-grab pattern; consumed 1-2 frames later by the item
// in RenderScene). Any creation failure disables only this attribution. // classifier / cull in RenderScene). Any creation failure disables only
if (REXCVAR_GET(skate3_native_render_scene_perf_items) && !g_r.occl_failed) { // these consumers.
if ((REXCVAR_GET(skate3_native_render_scene_perf_items) ||
REXCVAR_GET(skate3_native_render_scene_occlusion_cull)) &&
!g_r.occl_failed) {
if (g_r.pso_occl_reduce == nullptr) { if (g_r.pso_occl_reduce == nullptr) {
nrhi::Shader* rvs = device->CreateShader( nrhi::Shader* rvs = device->CreateShader(
MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl", MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl",
@ -536,6 +540,7 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context,
nrhi::ResourceState::kRenderTarget); nrhi::ResourceState::kRenderTarget);
cmd->FlushBarriers(); cmd->FlushBarriers();
std::memcpy(g_r.occl_vp[w], scene.view_proj, sizeof(g_r.occl_vp[w])); std::memcpy(g_r.occl_vp[w], scene.view_proj, sizeof(g_r.occl_vp[w]));
std::memcpy(g_r.occl_cam[w], scene.cam_pos, sizeof(g_r.occl_cam[w]));
g_r.occl_submission[w] = device->CurrentSubmission(); g_r.occl_submission[w] = device->CurrentSubmission();
g_r.occl_pending[w] = true; g_r.occl_pending[w] = true;
g_r.occl_write_index = 1 - w; g_r.occl_write_index = 1 - w;

View file

@ -960,6 +960,10 @@ inline std::atomic<uint64_t> g_vis_in_indices{0}; // indices submitted, visibl
inline std::atomic<uint64_t> g_vis_occ_indices{0}; // indices submitted, occluded items inline std::atomic<uint64_t> g_vis_occ_indices{0}; // indices submitted, occluded items
inline std::atomic<uint64_t> g_vis_out_indices{0}; // indices submitted, out-of-frustum items inline std::atomic<uint64_t> g_vis_out_indices{0}; // indices submitted, out-of-frustum items
inline std::atomic<uint64_t> g_vis_out_retained{0}; // out-of-frustum draws from retention inline std::atomic<uint64_t> g_vis_out_retained{0}; // out-of-frustum draws from retention
// Session-cumulative count of statics skipped by the occlusion cull (the
// skate3_native_render_scene_occlusion_cull cvar; reported on the frame
// stats line like the other cumulative counters there).
inline std::atomic<uint64_t> g_occl_culled{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