diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index 91885fb..e403062 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -1167,6 +1167,16 @@ REXCVAR_DEFINE_INT32(skate3_native_render_scene_perf_interval, 600, "Skate 3", .range(60, 6000) .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", "Deep per-item CPU profiling: adds a perf-items log line " "per perf window attributing per-item cost to pipeline " diff --git a/src/skate3_native_scene_gpu.cpp b/src/skate3_native_scene_gpu.cpp index ed14e51..713799f 100644 --- a/src/skate3_native_scene_gpu.cpp +++ b/src/skate3_native_scene_gpu.cpp @@ -88,6 +88,7 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_pause_native); REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_log); 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_occlusion_cull); 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_native); @@ -7138,7 +7139,7 @@ void LogFrameStats(const FrameScene& scene, uint64_t frames, uint32_t drawn, "reused={}] dynobj[valid={} drawn={}] water[valid={} drawn={}] " "lw[ctxs={} ents={} stamp={} fade0={} resc={} fill={} pal={} rows={}] " "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, drawn_spline, g_draws_spline.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_hair_route_flips.load(), g_hair_bone_alternations.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 // refreshing (AO off, loading flows) goes invalid instead of classifying // 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(); int newest = -1; 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], 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_valid = true; 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) && 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 // 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 @@ -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 // PS outputs alpha 0 and it writes z-prepass depth); routing it through // the sorted alpha sub-pass would composite the blended horizon sheet diff --git a/src/skate3_native_scene_gpu_internal.h b/src/skate3_native_scene_gpu_internal.h index dab7b00..afc6ec7 100644 --- a/src/skate3_native_scene_gpu_internal.h +++ b/src/skate3_native_scene_gpu_internal.h @@ -642,12 +642,14 @@ struct RendererState { uint8_t* occl_readback_ptr[2] = {}; uint64_t occl_submission[2] = {}; float occl_vp[2][16] = {}; + float occl_cam[2][3] = {}; // camera position the grid's frame rendered from bool occl_pending[2] = {}; int occl_write_index = 0; bool occl_failed = false; // Newest completed grid, CPU-side (render thread only). std::vector occl_grid; float occl_grid_vp[16] = {}; + float occl_grid_cam[3] = {}; uint64_t occl_grid_frame = 0; // frame_number the grid was captured on bool occl_grid_valid = false; // HDR pipeline (hdr.hlsl): the scene renders pre-tonemap linear into a diff --git a/src/skate3_native_scene_post.cpp b/src/skate3_native_scene_post.cpp index 14d5716..1e7a9d0 100644 --- a/src/skate3_native_scene_post.cpp +++ b/src/skate3_native_scene_post.cpp @@ -58,6 +58,7 @@ // Cvars defined in skate3_native_scene.cpp. REXCVAR_DECLARE(double, skate3_menu_blur_sigma); 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(double, skate3_native_render_scene_bloom_intensity); REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee); @@ -442,12 +443,15 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context, nrhi::ResourceState::kRenderTarget); } - // Occlusion-attribution grid (perf-items profiling): while the linear - // depth is still bound as an SRV, tile-MAX reduce it into the small R32F - // grid and queue a never-waited copy into the readback ring (the - // photo-grab pattern; consumed 1-2 frames later by the item classifier - // in RenderScene). Any creation failure disables only this attribution. - if (REXCVAR_GET(skate3_native_render_scene_perf_items) && !g_r.occl_failed) { + // Occlusion grid (perf-items attribution AND the occlusion cull): while + // the linear depth is still bound as an SRV, tile-MAX reduce it into the + // small R32F grid and queue a never-waited copy into the readback ring + // (the photo-grab pattern; consumed 1-2 frames later by the item + // classifier / cull in RenderScene). Any creation failure disables only + // 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) { nrhi::Shader* rvs = device->CreateShader( MakeShaderDesc(nrhi::ShaderStage::kVertex, "ssao.hlsl", @@ -536,6 +540,7 @@ bool ApplySsaoPass(const NativeGuestOutputRenderContext& context, nrhi::ResourceState::kRenderTarget); cmd->FlushBarriers(); 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_pending[w] = true; g_r.occl_write_index = 1 - w; diff --git a/src/skate3_native_scene_state.h b/src/skate3_native_scene_state.h index cba7e1d..2aeb7dd 100644 --- a/src/skate3_native_scene_state.h +++ b/src/skate3_native_scene_state.h @@ -960,6 +960,10 @@ inline std::atomic g_vis_in_indices{0}; // indices submitted, visibl inline std::atomic g_vis_occ_indices{0}; // indices submitted, occluded items inline std::atomic g_vis_out_indices{0}; // indices submitted, out-of-frustum items inline std::atomic 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 g_occl_culled{0}; // 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_fp; // ComputeItemFingerprint on the cache-hit path