render: skip guest draw-list dispatch for occlusion-culled statics
This commit is contained in:
parent
73c19c8cd3
commit
2aef1222a0
5 changed files with 182 additions and 3 deletions
|
|
@ -8,9 +8,11 @@
|
||||||
|
|
||||||
#include "generated/skate3_init.h"
|
#include "generated/skate3_init.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -31,6 +33,7 @@ REXCVAR_DEFINE_INT32(skate3_native_render_log_interval, 0, "Skate 3",
|
||||||
.range(0, 100000)
|
.range(0, 100000)
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_log);
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_perf_log);
|
||||||
|
REXCVAR_DECLARE(bool, skate3_native_render_scene_occlusion_cull_guest);
|
||||||
REXCVAR_DEFINE_DOUBLE(skate3_guest_fps_cap, 0.0, "Skate 3",
|
REXCVAR_DEFINE_DOUBLE(skate3_guest_fps_cap, 0.0, "Skate 3",
|
||||||
"Pace the guest render loop to this frame rate (0 = uncapped). The "
|
"Pace the guest render loop to this frame rate (0 = uncapped). The "
|
||||||
"guest produces frames at irregular 2-9 ms intervals; the display "
|
"guest produces frames at irregular 2-9 ms intervals; the display "
|
||||||
|
|
@ -122,6 +125,88 @@ void OnSceneDrawList(uint8_t* base, uint32_t view, uint32_t sort_vec, uint32_t f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Guest-side occlusion dispatch filter ---------------------------------
|
||||||
|
// The native renderer suppresses the guest's emulated draws, so for world
|
||||||
|
// statics the sorted-list dispatch below (material setup, command-packet
|
||||||
|
// building) produces nothing anyone consumes - yet it is the guest render
|
||||||
|
// thread's dominant per-item cost. For MeshContexts the render-side
|
||||||
|
// occlusion cull proved hidden last frame (published by RenderScene), the
|
||||||
|
// hook compacts the entry segment before invoking the guest dispatcher and
|
||||||
|
// restores it afterwards. Capture above has already recorded every entry,
|
||||||
|
// so scene.items, the shadow caster caches, and all state capture stay
|
||||||
|
// complete; the only guest work skipped is packet-building for draws that
|
||||||
|
// were both suppressed and occlusion-culled anyway. Guest render thread
|
||||||
|
// only.
|
||||||
|
struct OcclDispatchFilter {
|
||||||
|
uint64_t stamp = ~0ull; // g_frame_index of the ctx snapshot
|
||||||
|
std::vector<uint32_t> ctxs; // sorted culled-ctx snapshot
|
||||||
|
std::vector<uint8_t> saved; // original segment bytes for restore
|
||||||
|
uint32_t saved_addr = 0;
|
||||||
|
uint32_t saved_bytes = 0;
|
||||||
|
bool active = false; // a filtered dispatch is in flight
|
||||||
|
};
|
||||||
|
OcclDispatchFilter g_occl_filter;
|
||||||
|
|
||||||
|
// Compacts entries[first..first+count) in place, dropping culled ctxs, and
|
||||||
|
// returns the kept count. Returns `count` unchanged (nothing saved) when
|
||||||
|
// filtering is off, stale, empty, or nothing matched.
|
||||||
|
uint32_t FilterSceneDrawList(uint8_t* base, uint32_t sort_vec, uint32_t first,
|
||||||
|
uint32_t count) {
|
||||||
|
OcclDispatchFilter& f = g_occl_filter;
|
||||||
|
if (f.active || count == 0 || count > 100000 ||
|
||||||
|
!REXCVAR_GET(skate3_native_render_scene_occlusion_cull_guest)) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
if (f.stamp != g_frame_index) {
|
||||||
|
f.stamp = g_frame_index;
|
||||||
|
skate3::native_scene::CopyOcclusionCulledCtxs(f.ctxs);
|
||||||
|
}
|
||||||
|
if (f.ctxs.empty()) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
const uint32_t entries = REX_LOAD_U32(sort_vec);
|
||||||
|
if (entries == 0) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
const uint32_t seg = entries + first * 8;
|
||||||
|
f.saved.assign(base + seg, base + seg + size_t(count) * 8);
|
||||||
|
uint32_t kept = 0;
|
||||||
|
uint32_t skipped = 0;
|
||||||
|
for (uint32_t i = 0; i < count; ++i) {
|
||||||
|
const uint8_t* src = f.saved.data() + size_t(i) * 8;
|
||||||
|
uint32_t ctx_be;
|
||||||
|
std::memcpy(&ctx_be, src + 4, 4);
|
||||||
|
const uint32_t mesh_context = __builtin_bswap32(ctx_be);
|
||||||
|
if (mesh_context != 0 &&
|
||||||
|
std::binary_search(f.ctxs.begin(), f.ctxs.end(), mesh_context)) {
|
||||||
|
++skipped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (kept != i) {
|
||||||
|
std::memcpy(base + seg + size_t(kept) * 8, src, 8);
|
||||||
|
}
|
||||||
|
++kept;
|
||||||
|
}
|
||||||
|
if (skipped == 0) {
|
||||||
|
f.saved.clear();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
f.active = true;
|
||||||
|
f.saved_addr = seg;
|
||||||
|
f.saved_bytes = count * 8;
|
||||||
|
skate3::native_scene::AddGuestOcclSkipped(skipped);
|
||||||
|
return kept;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RestoreSceneDrawList(uint8_t* base) {
|
||||||
|
OcclDispatchFilter& f = g_occl_filter;
|
||||||
|
if (!f.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::memcpy(base + f.saved_addr, f.saved.data(), f.saved_bytes);
|
||||||
|
f.active = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Non-indexed cloth patch draws (see native_scene::CaptureClothDraw). The
|
// Non-indexed cloth patch draws (see native_scene::CaptureClothDraw). The
|
||||||
// synthetic "context" key is the dynamic buffer object, stable per garment.
|
// synthetic "context" key is the dynamic buffer object, stable per garment.
|
||||||
void OnClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6, uint32_t r7) {
|
void OnClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6, uint32_t r7) {
|
||||||
|
|
@ -319,6 +404,18 @@ extern "C" REX_FUNC(sub_827FAF50) {
|
||||||
if (skate3::native_render::Enabled()) {
|
if (skate3::native_render::Enabled()) {
|
||||||
skate3::native_render::OnSceneDrawList(base, ctx.r3.u32, ctx.r4.u32, ctx.r5.u32,
|
skate3::native_render::OnSceneDrawList(base, ctx.r3.u32, ctx.r4.u32, ctx.r5.u32,
|
||||||
ctx.r6.u32);
|
ctx.r6.u32);
|
||||||
|
// Guest-side occlusion dispatch filter: capture above saw every entry;
|
||||||
|
// the dispatcher only gets the ones the occlusion cull did not prove
|
||||||
|
// hidden. The segment is restored after the call (the list object
|
||||||
|
// outlives this dispatch).
|
||||||
|
const uint32_t kept = skate3::native_render::FilterSceneDrawList(
|
||||||
|
base, ctx.r4.u32, ctx.r5.u32, ctx.r6.u32);
|
||||||
|
if (kept != ctx.r6.u32) {
|
||||||
|
ctx.r6.u64 = kept;
|
||||||
|
__imp__sub_827FAF50(ctx, base);
|
||||||
|
skate3::native_render::RestoreSceneDrawList(base);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
__imp__sub_827FAF50(ctx, base);
|
__imp__sub_827FAF50(ctx, base);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_guest, true,
|
||||||
|
"Skate 3",
|
||||||
|
"Also skip the GUEST engine's draw-list dispatch for "
|
||||||
|
"items the occlusion cull proved hidden (the dispatch "
|
||||||
|
"builds command packets the native renderer suppresses "
|
||||||
|
"anyway, and it is the frame-time-critical cost). "
|
||||||
|
"Capture still sees every item, so shadows and scene "
|
||||||
|
"state are unaffected. Requires "
|
||||||
|
"skate3_native_render_scene_occlusion_cull.")
|
||||||
|
.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 "
|
||||||
|
|
@ -2580,6 +2591,10 @@ bool BuildItemGeometry(uint8_t* base, uint32_t ctx, DrawItem& item) {
|
||||||
if (prof) {
|
if (prof) {
|
||||||
g_pw_bi_fetch.Add(PerfNsSince(fetch_t0));
|
g_pw_bi_fetch.Add(PerfNsSince(fetch_t0));
|
||||||
}
|
}
|
||||||
|
// Identity for per-instance consumers (the occlusion cull's guest-side
|
||||||
|
// dispatch filter keys on it; the dynamic capture path overwrites it with
|
||||||
|
// its own value).
|
||||||
|
item.ctx = ctx;
|
||||||
|
|
||||||
// Culled island draw list from the context.
|
// Culled island draw list from the context.
|
||||||
const uint32_t draw_count = REX_LOAD_U16(ctx + kCtxDrawCountU16);
|
const uint32_t draw_count = REX_LOAD_U16(ctx + kCtxDrawCountU16);
|
||||||
|
|
|
||||||
|
|
@ -523,6 +523,16 @@ float CharFadeAlpha(const DrawItem& item);
|
||||||
// per-item profiling attribution.
|
// per-item profiling attribution.
|
||||||
bool ItemOutsideFrustum(const DrawItem& it, const float vp[16], float margin);
|
bool ItemOutsideFrustum(const DrawItem& it, const float vp[16], float margin);
|
||||||
|
|
||||||
|
// Occlusion-cull handoff for the guest-side dispatch filter (defined in
|
||||||
|
// skate3_native_scene_gpu.cpp): copies the most recent render frame's
|
||||||
|
// culled-static MeshContext set (sorted ascending) into `out`, clearing it
|
||||||
|
// instead when the last publish is older than a quarter second (native
|
||||||
|
// render idle: menus, loading). Returns true when `out` is non-empty.
|
||||||
|
bool CopyOcclusionCulledCtxs(std::vector<uint32_t>& out);
|
||||||
|
// Counts sort-list entries the guest-side filter kept away from the guest
|
||||||
|
// dispatcher (frame-stats telemetry).
|
||||||
|
void AddGuestOcclSkipped(uint32_t n);
|
||||||
|
|
||||||
// Called from the cProcessArenaAsset::RegisterTexture hook: guid -> guest
|
// Called from the cProcessArenaAsset::RegisterTexture hook: guid -> guest
|
||||||
// renderengine::Texture object.
|
// renderengine::Texture object.
|
||||||
void OnRegisterTexture(uint64_t guid, uint32_t texture);
|
void OnRegisterTexture(uint64_t guid, uint32_t texture);
|
||||||
|
|
|
||||||
|
|
@ -7139,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={} occl_culled={}",
|
"rows_inst={} pal_srv={} occl_culled={} guest_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(),
|
||||||
|
|
@ -7173,7 +7173,8 @@ 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_occl_culled.load());
|
g_pal_served_total.load(), g_occl_culled.load(),
|
||||||
|
g_occl_guest_skipped.load());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -7473,6 +7474,36 @@ bool ItemOccludedByGrid(const DrawItem& it, float depth_margin) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Culled-ctx publication for the guest-side dispatch filter: the classify
|
||||||
|
// loop collects the MeshContexts of statics it occlusion-culled and swaps
|
||||||
|
// them in here once per rendered frame (empty when the cull is inactive, so
|
||||||
|
// the guest filter clears within a frame). The timestamp lets the consumer
|
||||||
|
// expire the set when the native render idles.
|
||||||
|
std::mutex g_occl_pub_mutex;
|
||||||
|
std::vector<uint32_t> g_occl_pub_ctxs;
|
||||||
|
std::atomic<int64_t> g_occl_pub_ms{0};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool CopyOcclusionCulledCtxs(std::vector<uint32_t>& out) {
|
||||||
|
const int64_t now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
if (now_ms - g_occl_pub_ms.load(std::memory_order_relaxed) > 250) {
|
||||||
|
out.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::lock_guard<std::mutex> lock(g_occl_pub_mutex);
|
||||||
|
out = g_occl_pub_ctxs;
|
||||||
|
return !out.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddGuestOcclSkipped(uint32_t n) {
|
||||||
|
g_occl_guest_skipped.fetch_add(n, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) {
|
bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_data*/) {
|
||||||
if (!SceneEnabled() ||
|
if (!SceneEnabled() ||
|
||||||
(context.backend != NativeGuestOutputBackend::kD3D12 &&
|
(context.backend != NativeGuestOutputBackend::kD3D12 &&
|
||||||
|
|
@ -9718,6 +9749,10 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
}
|
}
|
||||||
occl_cull_active = cam_d2 < 1.0f;
|
occl_cull_active = cam_d2 < 1.0f;
|
||||||
}
|
}
|
||||||
|
std::vector<uint32_t> culled_ctxs;
|
||||||
|
if (occl_cull_active) {
|
||||||
|
culled_ctxs.reserve(scene.items.size() / 2);
|
||||||
|
}
|
||||||
// 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
|
||||||
|
|
@ -9907,11 +9942,17 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// profiler): the 1 m depth margin on top of the conservative bbox test
|
// profiler): the 1 m depth margin on top of the conservative bbox test
|
||||||
// keeps reveal edges safe at speed - deeply hidden mass sits many
|
// keeps reveal edges safe at speed - deeply hidden mass sits many
|
||||||
// meters behind its occluders, so the margin costs almost none of the
|
// meters behind its occluders, so the margin costs almost none of the
|
||||||
// win. Ring route 3 = culled.
|
// win. Ring route 3 = culled. Live-submitted culled ctxs (never
|
||||||
|
// retained ones: a re-appended item's ctx may already be freed, and a
|
||||||
|
// reused address would filter an unrelated new instance) feed the
|
||||||
|
// guest-side dispatch filter via the publication below.
|
||||||
if (occl_cull_active && !item.skinned && !item.ropa &&
|
if (occl_cull_active && !item.skinned && !item.ropa &&
|
||||||
!item.cloth_quads && item.bones.empty() &&
|
!item.cloth_quads && item.bones.empty() &&
|
||||||
ItemOccludedByGrid(item, 1.0f)) {
|
ItemOccludedByGrid(item, 1.0f)) {
|
||||||
g_occl_culled.fetch_add(1, std::memory_order_relaxed);
|
g_occl_culled.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
if (!item.retained && item.ctx != 0) {
|
||||||
|
culled_ctxs.push_back(item.ctx);
|
||||||
|
}
|
||||||
stamp_route(3);
|
stamp_route(3);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -9934,6 +9975,18 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
opaque_items.emplace_back(view_dist2(item), &item);
|
opaque_items.emplace_back(view_dist2(item), &item);
|
||||||
stamp_route(1);
|
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.
|
||||||
|
{
|
||||||
|
const int64_t now_ms =
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
std::sort(culled_ctxs.begin(), culled_ctxs.end());
|
||||||
|
std::lock_guard<std::mutex> lock(g_occl_pub_mutex);
|
||||||
|
g_occl_pub_ctxs.swap(culled_ctxs);
|
||||||
|
g_occl_pub_ms.store(now_ms, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
if (REXCVAR_GET(skate3_native_render_scene_sort_opaque) && debug_mode == 0) {
|
if (REXCVAR_GET(skate3_native_render_scene_sort_opaque) && debug_mode == 0) {
|
||||||
std::stable_sort(opaque_items.begin(), opaque_items.end(),
|
std::stable_sort(opaque_items.begin(), opaque_items.end(),
|
||||||
[](const auto& a, const auto& b) { return a.first < b.first; });
|
[](const auto& a, const auto& b) { return a.first < b.first; });
|
||||||
|
|
|
||||||
|
|
@ -964,6 +964,10 @@ inline std::atomic<uint64_t> g_vis_out_retained{0}; // out-of-frustum draws fro
|
||||||
// skate3_native_render_scene_occlusion_cull cvar; reported on the frame
|
// skate3_native_render_scene_occlusion_cull cvar; reported on the frame
|
||||||
// stats line like the other cumulative counters there).
|
// stats line like the other cumulative counters there).
|
||||||
inline std::atomic<uint64_t> g_occl_culled{0};
|
inline std::atomic<uint64_t> g_occl_culled{0};
|
||||||
|
// Session-cumulative count of sort-list entries the guest-side dispatch
|
||||||
|
// filter removed before the guest's draw-list renderer ran (see
|
||||||
|
// skate3_native_render_scene_occlusion_cull_guest).
|
||||||
|
inline std::atomic<uint64_t> g_occl_guest_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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue