scene: native loading screens and boot flow
This commit is contained in:
parent
302ba69241
commit
ba50240655
1 changed files with 126 additions and 15 deletions
|
|
@ -151,6 +151,22 @@ REXCVAR_DEFINE_BOOL(skate3_native_render_scene_pause_native, true, "Skate 3",
|
||||||
"also skips the pause-entry cache clears, so unpausing costs "
|
"also skips the pause-entry cache clears, so unpausing costs "
|
||||||
"nothing.")
|
"nothing.")
|
||||||
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_loading_native, true, "Skate 3",
|
||||||
|
"Render post-startup loading screens natively, black backdrop "
|
||||||
|
"plus the game's own captured 2D loading UI, instead of "
|
||||||
|
"yielding to the emulated output. The loading-screen "
|
||||||
|
"housekeeping (cache clears, registration prewarm, takeover "
|
||||||
|
"arming) is unchanged; only the presented pixels switch source. "
|
||||||
|
"The boot flow before the first gameplay stays emulated.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
|
REXCVAR_DEFINE_BOOL(skate3_native_render_scene_boot_native, true, "Skate 3",
|
||||||
|
"Extend native rendering to the game startup flow (intro videos, "
|
||||||
|
"boot frontend, the first load); i.e. drop the first-gameplay "
|
||||||
|
"prerequisite from the native menu/loading modes, and render the "
|
||||||
|
"pre-takeover boot frames as native 2D-over-black instead of "
|
||||||
|
"yielding. With this and the pause/loading modes on, the emulated "
|
||||||
|
"GPU output is never presented.")
|
||||||
|
.lifecycle(rex::cvar::Lifecycle::kHotReload);
|
||||||
REXCVAR_DEFINE_BOOL(skate3_native_render_snapshot_all_draws, false, "Skate 3",
|
REXCVAR_DEFINE_BOOL(skate3_native_render_snapshot_all_draws, false, "Skate 3",
|
||||||
"Record the draw stream on every recorded frame instead of 2 of every "
|
"Record the draw stream on every recorded frame instead of 2 of every "
|
||||||
"60 (large .draws.bin; for targeted investigations)")
|
"60 (large .draws.bin; for targeted investigations)")
|
||||||
|
|
@ -6311,6 +6327,10 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (!SceneEnabled()) {
|
if (!SceneEnabled()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Published every frame (not just on world submissions): boot/menu frames
|
||||||
|
// carry only 2D, and the render thread's 2D texture decodes need the
|
||||||
|
// guest base from the very first natively rendered boot frame.
|
||||||
|
g_guest_base.store(base, std::memory_order_relaxed);
|
||||||
++g_guest_frame; // paces the world-item cache revalidation
|
++g_guest_frame; // paces the world-item cache revalidation
|
||||||
// Perf telemetry: guest frame interval + this frame's capture-hook cost.
|
// Perf telemetry: guest frame interval + this frame's capture-hook cost.
|
||||||
static PerfClock::time_point s_last_frame_tp{};
|
static PerfClock::time_point s_last_frame_tp{};
|
||||||
|
|
@ -6376,7 +6396,6 @@ void BuildFrameScene(uint8_t* base, const SubmitRecord* records, size_t count) {
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_guest_base.store(base, std::memory_order_relaxed);
|
|
||||||
|
|
||||||
// Multiple SceneRenderViews can submit per frame (main, shadow cascades,
|
// Multiple SceneRenderViews can submit per frame (main, shadow cascades,
|
||||||
// reflections). Pick the perspective one (proj[2][3] == 1 in row-vector
|
// reflections). Pick the perspective one (proj[2][3] == 1 in row-vector
|
||||||
|
|
@ -12947,12 +12966,25 @@ void PrewarmCommit(const NativeGuestOutputRenderContext& context,
|
||||||
.count()));
|
.count()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set by YieldForMenus, consumed by RenderScene in the same call (render
|
||||||
|
// thread only): this frame is a NATIVE loading-screen frame; no world scene
|
||||||
|
// exists (or it is the previous map's stale one), so RenderScene renders a
|
||||||
|
// black backdrop + the captured 2D loading UI instead of the world.
|
||||||
|
bool g_loading_native_frame = false;
|
||||||
|
// Render thread only: the previous rendered frame was a native loading
|
||||||
|
// frame. Used to HOLD the loading visuals through the post-load takeover
|
||||||
|
// gate window; the emulated output was suppressed all through the native
|
||||||
|
// loading screen, so yielding there would flash the stale pre-load frame.
|
||||||
|
bool g_loading_hold = false;
|
||||||
|
|
||||||
// Menus / pause / loading yield gate (see the comment at the call site):
|
// Menus / pause / loading yield gate (see the comment at the call site):
|
||||||
// returns true when RenderScene must yield this frame to the emulated
|
// returns true when RenderScene must yield this frame to the emulated
|
||||||
// output. Handles the cache clears on entry and the takeover re-arm +
|
// output. Handles the cache clears on entry and the takeover re-arm +
|
||||||
// loading-screen pipeline build / prewarm commit while yielded.
|
// loading-screen pipeline build / prewarm commit while in a load, whether
|
||||||
|
// the loading pixels themselves render emulated (yield) or natively
|
||||||
|
// (g_loading_native_frame).
|
||||||
bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
static bool s_yielding = false;
|
static bool s_in_loading = false;
|
||||||
static bool s_seen_gameplay = false;
|
static bool s_seen_gameplay = false;
|
||||||
static bool s_pause_native = false;
|
static bool s_pause_native = false;
|
||||||
const bool in_menus = rex::graphics::ultrawide_debug::Skate3GameplayContextValue() == 0;
|
const bool in_menus = rex::graphics::ultrawide_debug::Skate3GameplayContextValue() == 0;
|
||||||
|
|
@ -12967,8 +12999,12 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
// go stale (a load was picked from the pause menu, or the game stops
|
// go stale (a load was picked from the pause menu, or the game stops
|
||||||
// redrawing the world), this degrades to the yield path below within
|
// redrawing the world), this degrades to the yield path below within
|
||||||
// ~300 ms, cache clears and all.
|
// ~300 ms, cache clears and all.
|
||||||
|
// boot_native lifts the first-gameplay prerequisite from both native menu
|
||||||
|
// modes: a boot-frontend 3D backdrop renders like a pause backdrop, and
|
||||||
|
// everything else (videos, menus, the first load) renders as 2D-over-black.
|
||||||
|
const bool boot_native = REXCVAR_GET(skate3_native_render_scene_boot_native);
|
||||||
bool pause_native = false;
|
bool pause_native = false;
|
||||||
if (in_menus && s_seen_gameplay &&
|
if (in_menus && (s_seen_gameplay || boot_native) &&
|
||||||
REXCVAR_GET(skate3_native_render_scene_pause_native)) {
|
REXCVAR_GET(skate3_native_render_scene_pause_native)) {
|
||||||
const int64_t last_ns = g_last_publish_ns.load(std::memory_order_relaxed);
|
const int64_t last_ns = g_last_publish_ns.load(std::memory_order_relaxed);
|
||||||
const int64_t now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
const int64_t now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
|
|
@ -12996,13 +13032,22 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
g_draws_2d_dropped.load(std::memory_order_relaxed));
|
g_draws_2d_dropped.load(std::memory_order_relaxed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const bool yield = in_menus && !pause_native;
|
const bool in_loading = in_menus && !pause_native;
|
||||||
if (yield != s_yielding) {
|
// Loading screens themselves render natively too (black + the captured 2D
|
||||||
s_yielding = yield;
|
// loading UI) when enabled, everything after the first gameplay. The
|
||||||
if (yield) {
|
// housekeeping below runs for the loading STATE either way; only the
|
||||||
|
// yield decision changes.
|
||||||
|
const bool loading_native =
|
||||||
|
in_loading && (s_seen_gameplay || boot_native) &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_loading_native);
|
||||||
|
g_loading_native_frame = loading_native;
|
||||||
|
if (in_loading != s_in_loading) {
|
||||||
|
s_in_loading = in_loading;
|
||||||
|
if (in_loading) {
|
||||||
REXLOG_INFO(
|
REXLOG_INFO(
|
||||||
"native-scene: menus/loading - yielding to emulated output "
|
"native-scene: menus/loading - {} (presence context)",
|
||||||
"(presence context)");
|
loading_native ? "rendering the loading screen NATIVELY"
|
||||||
|
: "yielding to emulated output");
|
||||||
// Arena addresses are reused across map loads: let the next load's
|
// Arena addresses are reused across map loads: let the next load's
|
||||||
// registrations re-queue meshes (and re-stage textures) at reused
|
// registrations re-queue meshes (and re-stage textures) at reused
|
||||||
// addresses, and drop the cached item cores built from them.
|
// addresses, and drop the cached item cores built from them.
|
||||||
|
|
@ -13025,7 +13070,7 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
g_prewarm_dropped.load(std::memory_order_relaxed), queued);
|
g_prewarm_dropped.load(std::memory_order_relaxed), queued);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (yield) {
|
if (in_loading) {
|
||||||
// Re-arm the takeover gate for the next stretch of gameplay (map
|
// Re-arm the takeover gate for the next stretch of gameplay (map
|
||||||
// load, unpause).
|
// load, unpause).
|
||||||
g_warmup_armed.store(true, std::memory_order_relaxed);
|
g_warmup_armed.store(true, std::memory_order_relaxed);
|
||||||
|
|
@ -13043,6 +13088,11 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
|
||||||
if (!g_r.failed && g_r.pso == nullptr) {
|
if (!g_r.failed && g_r.pso == nullptr) {
|
||||||
EnsurePipeline(context);
|
EnsurePipeline(context);
|
||||||
}
|
}
|
||||||
|
if (loading_native) {
|
||||||
|
// RenderScene renders this frame (black + 2D loading UI) and runs
|
||||||
|
// the prewarm commit itself with the loading budget.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// THE loading-screen heavy lifting runs on the prewarm decode WORKER
|
// THE loading-screen heavy lifting runs on the prewarm decode WORKER
|
||||||
// POOL (a serial render-thread drain both tanked the loading spinner
|
// POOL (a serial render-thread drain both tanked the loading spinner
|
||||||
// to ~13 fps and still left 1500 of a map's ~2600 meshes undecoded at
|
// to ~13 fps and still left 1500 of a map's ~2600 meshes undecoded at
|
||||||
|
|
@ -13632,8 +13682,39 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
.count());
|
.count());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool loading_native = g_loading_native_frame;
|
||||||
|
// Render the takeover-gate window (warmup armed, no fresh substantial
|
||||||
|
// scene yet) as a native loading frame instead of yielding, in two cases:
|
||||||
|
// (a) g_loading_hold: the frames right after a native loading screen
|
||||||
|
// (the presence context flips to gameplay before the first fresh
|
||||||
|
// scene, and the emulated output was suppressed all through the load,
|
||||||
|
// so yielding would flash its stale pre-load content);
|
||||||
|
// (b) boot_native: the whole startup flow (intro videos, boot frontend)
|
||||||
|
// runs with the gate armed and no scene published, and should render
|
||||||
|
// natively as 2D-over-black rather than fall back to emulated.
|
||||||
|
if (!loading_native &&
|
||||||
|
(g_loading_hold || REXCVAR_GET(skate3_native_render_scene_boot_native)) &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_warmup_budget_ms) > 0 &&
|
||||||
|
g_warmup_armed.load(std::memory_order_relaxed)) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_scene_mutex);
|
||||||
|
const bool ready =
|
||||||
|
g_scene && g_scene->generation >= g_warmup_fresh_generation &&
|
||||||
|
g_scene->items.size() >=
|
||||||
|
size_t(REXCVAR_GET(skate3_native_render_scene_warmup_min_items));
|
||||||
|
loading_native = !ready;
|
||||||
|
}
|
||||||
|
g_loading_hold = loading_native;
|
||||||
std::shared_ptr<const FrameScene> scene_ptr;
|
std::shared_ptr<const FrameScene> scene_ptr;
|
||||||
{
|
if (loading_native) {
|
||||||
|
// Native loading screen: there is no current world scene (g_scene holds
|
||||||
|
// the PREVIOUS map's stale one); render an empty scene, i.e. a black
|
||||||
|
// backdrop, and let the 2D overlay tail replay the game's live loading
|
||||||
|
// UI (Publish2dDraws publishes every guest frame, loads included).
|
||||||
|
// Value-initialized: zero items, no shadow/blur/outline, generation 0.
|
||||||
|
static const std::shared_ptr<const FrameScene> s_loading_scene =
|
||||||
|
std::make_shared<const FrameScene>();
|
||||||
|
scene_ptr = s_loading_scene;
|
||||||
|
} else {
|
||||||
std::lock_guard<std::mutex> lock(g_scene_mutex);
|
std::lock_guard<std::mutex> lock(g_scene_mutex);
|
||||||
if (!g_scene || g_scene->items.empty()) {
|
if (!g_scene || g_scene->items.empty()) {
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -13689,7 +13770,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// for a frame instead of freezing the takeover frame.
|
// for a frame instead of freezing the takeover frame.
|
||||||
const int32_t warmup_ms = REXCVAR_GET(skate3_native_render_scene_warmup_budget_ms);
|
const int32_t warmup_ms = REXCVAR_GET(skate3_native_render_scene_warmup_budget_ms);
|
||||||
bool settling = false;
|
bool settling = false;
|
||||||
if (warmup_ms > 0 && REXCVAR_GET(skate3_native_render_scene_debug) == 0) {
|
// The takeover gates judge REAL scenes only; a native loading frame
|
||||||
|
// renders its empty scene deliberately (the gates re-run as usual once
|
||||||
|
// the presence context flips back to gameplay).
|
||||||
|
if (warmup_ms > 0 && !loading_native &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_debug) == 0) {
|
||||||
if (g_warmup_armed.load(std::memory_order_relaxed)) {
|
if (g_warmup_armed.load(std::memory_order_relaxed)) {
|
||||||
if (scene.generation < g_warmup_fresh_generation ||
|
if (scene.generation < g_warmup_fresh_generation ||
|
||||||
scene.items.size() <
|
scene.items.size() <
|
||||||
|
|
@ -13746,7 +13831,9 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
// EnqueueWordsMiss), so make sure they exist even on a session that never
|
// EnqueueWordsMiss), so make sure they exist even on a session that never
|
||||||
// showed a loading screen with the pipeline up.
|
// showed a loading screen with the pipeline up.
|
||||||
EnsurePrewarmWorkers();
|
EnsurePrewarmWorkers();
|
||||||
PrewarmCommit(context, frame_number);
|
// Native loading frames take the loading-screen commit budget; the heavy
|
||||||
|
// decode lifting behind the load is unchanged from the yielded path.
|
||||||
|
PrewarmCommit(context, frame_number, /*loading=*/loading_native);
|
||||||
// Content-store LRU: superseded words states (old mip levels, pre-demote
|
// Content-store LRU: superseded words states (old mip levels, pre-demote
|
||||||
// detail sets, one-shot UI art) age out once nothing routes to them.
|
// detail sets, one-shot UI art) age out once nothing routes to them.
|
||||||
EvictTexStore(frame_number, command_processor->GetCurrentSubmission());
|
EvictTexStore(frame_number, command_processor->GetCurrentSubmission());
|
||||||
|
|
@ -13778,7 +13865,12 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
|
|
||||||
const bool use_depth = debug_mode != 4;
|
const bool use_depth = debug_mode != 4;
|
||||||
const D3D12_CPU_DESCRIPTOR_HANDLE dsv = g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart();
|
const D3D12_CPU_DESCRIPTOR_HANDLE dsv = g_r.dsv_heap->GetCPUDescriptorHandleForHeapStart();
|
||||||
const FLOAT clear_color[4] = {0.25f, 0.35f, 0.55f, 1.0f};
|
// Loading frames clear to black (the game's loading UI composes over
|
||||||
|
// black); real scenes keep the sky-ish debug clear that shows through
|
||||||
|
// undecoded holes.
|
||||||
|
const FLOAT clear_color[4] = {loading_native ? 0.0f : 0.25f,
|
||||||
|
loading_native ? 0.0f : 0.35f,
|
||||||
|
loading_native ? 0.0f : 0.55f, 1.0f};
|
||||||
list.D3DClearRenderTargetView(scene_rtv, clear_color, 0, nullptr);
|
list.D3DClearRenderTargetView(scene_rtv, clear_color, 0, nullptr);
|
||||||
if (use_depth) {
|
if (use_depth) {
|
||||||
list.D3DClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
|
list.D3DClearDepthStencilView(dsv, D3D12_CLEAR_FLAG_DEPTH, 1.0f, 0, 0, nullptr);
|
||||||
|
|
@ -15314,6 +15406,25 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
|
||||||
key *= 1099511628211ull;
|
key *= 1099511628211ull;
|
||||||
}
|
}
|
||||||
auto it = find_words_texture(key);
|
auto it = find_words_texture(key);
|
||||||
|
if (it != g_r.tex_store.end() && it->second.valid && !it->second.incomplete &&
|
||||||
|
frame_number >= it->second.recheck_frame &&
|
||||||
|
REXCVAR_GET(skate3_native_render_scene_tex_revalidate)) {
|
||||||
|
// Content liveness for CPU-rewritten UI art: intro/menu VIDEO frames
|
||||||
|
// (rw::movie software decode) rewrite the same payload every tick with
|
||||||
|
// the fetch words unchanged; the words-keyed cache would freeze
|
||||||
|
// playback on its first decoded frame. Probe the payload; on change,
|
||||||
|
// retire + re-decode inline (the fresh entry's recheck_frame of 0
|
||||||
|
// keeps a playing video on a per-frame cadence). Static art settles at
|
||||||
|
// the same 16-frame cadence as the world store's revalidation.
|
||||||
|
const uint64_t fp = SampleProbeFingerprint(base, it->second);
|
||||||
|
if (fp != 0 && fp != it->second.payload_fp) {
|
||||||
|
RetireGuestTexture(it->second, command_processor->GetCurrentSubmission());
|
||||||
|
g_r.tex_store.erase(it);
|
||||||
|
it = g_r.tex_store.end();
|
||||||
|
} else {
|
||||||
|
it->second.recheck_frame = frame_number + 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (it == g_r.tex_store.end()) {
|
if (it == g_r.tex_store.end()) {
|
||||||
// HUD/spline art decodes inline (small; async would flash UI elements
|
// HUD/spline art decodes inline (small; async would flash UI elements
|
||||||
// white on first sight). The big streamed posters go through
|
// white on first sight). The big streamed posters go through
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue