diff --git a/src/skate3_native_scene.cpp b/src/skate3_native_scene.cpp index fc8fd3e..d22c438 100644 --- a/src/skate3_native_scene.cpp +++ b/src/skate3_native_scene.cpp @@ -1177,6 +1177,17 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_occlusion_cull, true, "Skate 3", "items.") .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, "Skate 3", "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) { 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 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) { const SubmitRecord& r = records[i]; // 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) { 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)) { continue; } diff --git a/src/skate3_native_scene.h b/src/skate3_native_scene.h index 48ed2ab..9c198ff 100644 --- a/src/skate3_native_scene.h +++ b/src/skate3_native_scene.h @@ -430,6 +430,11 @@ struct FrameScene { }; PhotoFx photo_fx; std::vector 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 occl_build_skipped; }; // One frame submission record from the hook layer (see RenderMeshRecord). diff --git a/src/skate3_native_scene_gpu.cpp b/src/skate3_native_scene_gpu.cpp index b233a3e..22a361d 100644 --- a/src/skate3_native_scene_gpu.cpp +++ b/src/skate3_native_scene_gpu.cpp @@ -7143,7 +7143,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={} 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, drawn_spline, g_draws_spline.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_shadow_alternations.load(), g_char_rows_inst_served.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); } // 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 = std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()) diff --git a/src/skate3_native_scene_state.h b/src/skate3_native_scene_state.h index 5c2652f..93562c8 100644 --- a/src/skate3_native_scene_state.h +++ b/src/skate3_native_scene_state.h @@ -968,6 +968,9 @@ inline std::atomic g_occl_culled{0}; // filter removed before the guest's draw-list renderer ran (see // skate3_native_render_scene_occlusion_cull_guest). inline std::atomic 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 g_occl_build_skipped{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