scene: pause-blur ease-in, portrait-window cost cuts, frame diagnostics

This commit is contained in:
mchughalex 2026-07-15 05:47:53 +01:00
parent 01de213ad2
commit 184d3aac9d
3 changed files with 164 additions and 30 deletions

View file

@ -317,12 +317,84 @@ void PaceGuestFrame() {
void OnFrameEnd(uint8_t* base) {
PaceGuestFrame();
// EMULATED-mode guest frame breakdown (emulated gameplay once regressed
// from 140 to 66 fps while native stayed at cap; the native-scene perf
// line only prints while the native renderer is active, so emulated
// stretches had no perf visibility at all). Logs every
// ~600 guest frames while the native scene is OFF: whole-frame time (the
// fps), time spent inside BuildFrameScene, and the per-frame draw/upload
// hook traffic. If frame_avg is ~15 ms but build/hooks are small, the
// cost is in the emulated GPU/CP pipeline, not our guest-thread code.
using Clock = std::chrono::steady_clock;
static Clock::time_point s_bd_prev{};
static uint64_t s_bd_frames = 0;
static double s_bd_frame_ns = 0, s_bd_frame_max = 0;
static double s_bd_build_ns = 0, s_bd_build_max = 0;
static uint64_t s_bd_draws0 = 0;
const bool emu_profile = !skate3::native_scene::Enabled();
const auto bd_now = Clock::now();
if (emu_profile && s_bd_prev.time_since_epoch().count() != 0) {
const double dt =
std::chrono::duration<double, std::nano>(bd_now - s_bd_prev).count();
s_bd_frame_ns += dt;
s_bd_frame_max = std::max(s_bd_frame_max, dt);
}
s_bd_prev = bd_now;
std::lock_guard<std::mutex> lock(g_mutex);
++g_frame_index;
const size_t mesh_count = g_current_frame.size();
const auto bd_build0 = Clock::now();
skate3::native_scene::BuildFrameScene(base, g_current_frame.data(),
g_current_frame.size());
if (emu_profile) {
const double bns = std::chrono::duration<double, std::nano>(Clock::now() -
bd_build0)
.count();
s_bd_build_ns += bns;
s_bd_build_max = std::max(s_bd_build_max, bns);
if (++s_bd_frames >= 600) {
const uint64_t draws = skate3::native_scene::DrawSequence();
// Quantum probe: a measured 1 ms sleep + the kernel's current timer
// resolution. frame avg ~15 ms with build ~0 and the GPU at idle
// wattage is the signature of every pacing sleep rounding up to the
// 15.625 ms default Windows quantum; sleep1ms ~15.6 here proves it,
// ~1-2 ms refutes it.
const auto sp0 = Clock::now();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
const double sleep_ms =
std::chrono::duration<double, std::milli>(Clock::now() - sp0).count();
double timer_res_ms = -1.0;
#if defined(_WIN32)
{
using NtQueryTimerResolutionFn = LONG(NTAPI*)(PULONG, PULONG, PULONG);
static const auto query = reinterpret_cast<NtQueryTimerResolutionFn>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryTimerResolution"));
if (query != nullptr) {
ULONG min_res = 0, max_res = 0, cur_res = 0;
if (query(&min_res, &max_res, &cur_res) == 0) {
timer_res_ms = double(cur_res) / 10000.0; // 100 ns units
}
}
}
#endif
REXLOG_INFO(
"native-render EMULATED breakdown: frame avg={:.2f}/max={:.2f}ms "
"build avg={:.3f}/max={:.3f}ms draws/frame={} sleep1ms={:.2f}ms "
"timer_res={:.2f}ms (600-frame window)",
s_bd_frame_ns / s_bd_frames / 1e6, s_bd_frame_max / 1e6,
s_bd_build_ns / s_bd_frames / 1e6, s_bd_build_max / 1e6,
(draws - s_bd_draws0) / s_bd_frames, sleep_ms, timer_res_ms);
s_bd_frames = 0;
s_bd_frame_ns = s_bd_frame_max = s_bd_build_ns = s_bd_build_max = 0;
s_bd_draws0 = draws;
}
} else {
s_bd_frames = 0;
s_bd_frame_ns = s_bd_frame_max = s_bd_build_ns = s_bd_build_max = 0;
s_bd_draws0 = skate3::native_scene::DrawSequence();
}
const int32_t log_interval = REXCVAR_GET(skate3_native_render_log_interval);
if (log_interval > 0 && g_frame_index % static_cast<uint64_t>(log_interval) == 0) {

View file

@ -3333,8 +3333,13 @@ bool PortraitRttWindowActive() {
// 56 = pause root, 24 = FMV, 17 = pause challenge map (the game FLAPS
// [0,56]<->[0,17] ~1/s while the map is open, with 17
// unclassified every flap re-armed the grace window and held mode 0,
// pinning the map screen at 62-94 fps).
if (f0 != 0 && f0 != 56 && f0 != 24 && f0 != 17) {
// pinning the map screen at 62-94 fps), 59 = skate-reel browser
// (measured A/B: the edit-skater flow pushes
// [0,56,63]->[0,56,63,15] and legitimately needs the window; the reel
// screen pushes [0,56,59] and its panels are VIDEOS served by the
// native FMV substitution, not portrait RTTs; the fail-open window
// there bought only its costs, 73 fps + 167 ms sync-compile stalls).
if (f0 != 0 && f0 != 56 && f0 != 24 && f0 != 17 && f0 != 59) {
unknown_screen = true;
}
}
@ -3365,7 +3370,7 @@ bool PortraitRttWindowActive() {
}
REXLOG_INFO(
"native-scene: FE stack changed: [{}] (portrait window {}; "
"steady-safe = 0/56/24/17)",
"steady-safe = 0/56/24/17/59)",
buf, unknown_screen ? "OPEN - unclassified screen" : "steady");
}
}
@ -4782,17 +4787,15 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
// Shader labels can be swapped in the hook; accept either slot.
if (is_hblur(g_cur_ps_obj.load(std::memory_order_relaxed)) ||
is_hblur(g_cur_vs_obj.load(std::memory_order_relaxed))) {
// Kernel scale pinned to the ucode-verified steady-state constant: the
// recorded blur draw's PS c0.x is exactly 8 (tap pitch 0.0025). The
// live bank read at this hook point returned varying/stale values
// (e.g. 0.8 vs 8 across reads) and feeding it through per frame
// pulsed the blur radius; the popup backdrop shimmered.
g_ui_blur = 8.0f;
// Kernel scale read LIVE under a plausibility gate (k0 == k1, in
// (0, 8]) so the game's own pause-open ramp animates the native blur
// like the emulated frame; the old pin-to-8 popped the backdrop to
// full blur on the first frame, and its exact-(8,8) gate on the fade
// rejected every mid-ramp c1 (the tint then snapped in late). A
// failed gate keeps the last values (stale bank mid-rewrite); raw
// 1-frame value flaps (the tinted-backdrop flicker) are absorbed by
// the render-thread smoothing at the blur pass.
g_ui_blur_seen = true;
// Fade modulate (PS c1, see g_ui_blur_color): genuinely dynamic (the
// pause fade ramps in over a few frames), so it must be read live,
// gated on the c0 kernel row reading exactly its known (8,8) staging,
// which a stale mid-update bank fails. NaN also fails the comparisons.
const uint32_t ps_bank = g_ps_bank.load(std::memory_order_relaxed);
if (ps_bank != 0) {
const float k0 = LoadGuestF32(base, ps_bank + 0);
@ -4801,11 +4804,37 @@ void OnDrawDone(uint8_t* base, uint32_t func, uint32_t r4, uint32_t r5, uint32_t
for (int a = 0; a < 3; ++a) {
c1[a] = LoadGuestF32(base, ps_bank + 16 + a * 4);
}
if (k0 == 8.0f && k1 == 8.0f && c1[0] >= 0.0f && c1[0] <= 1.0f &&
c1[1] >= 0.0f && c1[1] <= 1.0f && c1[2] >= 0.0f && c1[2] <= 1.0f) {
const bool kernel_ok = k0 == k1 && k0 > 0.0f && k0 <= 8.0f;
const bool fade_ok = c1[0] >= 0.0f && c1[0] <= 1.0f && c1[1] >= 0.0f &&
c1[1] <= 1.0f && c1[2] >= 0.0f && c1[2] <= 1.0f;
if (kernel_ok) {
g_ui_blur = k0;
if (fade_ok) {
std::memcpy(g_ui_blur_color, c1, sizeof(g_ui_blur_color));
}
}
// Open-ramp tracer (guest render thread only): the first draws
// after each blur-ON edge print the RAW kernel/fade so the game's
// actual animate-in curve lands in the log (for an exactness pass
// if the smoothed approximation reads wrong).
static int64_t s_last_draw_ns = 0;
static uint32_t s_edge_logged = 0;
static uint32_t s_total_logged = 0;
const int64_t trace_now = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
if (trace_now - s_last_draw_ns > 1'000'000'000ll) {
s_edge_logged = 0;
}
s_last_draw_ns = trace_now;
if (s_edge_logged < 20 && s_total_logged < 100) {
++s_edge_logged;
++s_total_logged;
REXLOG_INFO(
"native-scene: blur ramp k=({:.3f},{:.3f}) c1=({:.3f},{:.3f},{:.3f}) {}",
k0, k1, c1[0], c1[1], c1[2], kernel_ok ? "accepted" : "held");
}
}
}
}
// Selected-object outline capture (see g_frame_selected): the sky draw
@ -14068,16 +14097,23 @@ bool YieldForMenus(const NativeGuestOutputRenderContext& context) {
PortraitRttWindowActive());
if (want && !s_mode_forced) {
s_mode_saved = REXCVAR_GET(native_render_suppress_mode);
if (s_mode_saved != 0) {
REXCVAR_SET(native_render_suppress_mode, 0);
// Mode 3, not 0: the portrait-class RTTs (census 560-1200 during the
// window) execute, but the 1152-wide main scene + postfx band stays
// suppressed; mode 0 ran the game's whole pipeline at scaled
// resolution for the window's duration, dropping the pause menu to
// ~60 fps whenever a screen push (or its 3 s transition grace)
// opened the window.
if (s_mode_saved != 3) {
REXCVAR_SET(native_render_suppress_mode, 3);
REXLOG_INFO(
"native-scene: portrait window - suppress mode {} -> 0 (RTT "
"passes execute; restored when the window closes)",
"native-scene: portrait window - suppress mode {} -> 3 (portrait "
"RTT passes execute, scene/postfx band stays suppressed; "
"restored when the window closes)",
s_mode_saved);
}
s_mode_forced = true;
} else if (!want && s_mode_forced) {
if (s_mode_saved != 0) {
if (s_mode_saved != 3) {
REXCVAR_SET(native_render_suppress_mode, s_mode_saved);
REXLOG_INFO(
"native-scene: portrait window closed - suppress mode {} restored",
@ -17473,6 +17509,32 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
// 2D draws follow after and stay sharp.
if (scene.ui_blur > 0.0f && g_r.pso_blur != nullptr && g_r.pso_blur_blit != nullptr &&
g_r.pso_blur_down != nullptr && g_r.blur_tex[0] != nullptr) {
// Temporal smoothing of the captured radius + fade (~50 ms time
// constant, render thread): the live bank reads can land mid-rewrite
// (1-frame value flaps = the tinted-backdrop flicker), and smoothing
// FROM the off state (radius 0, white tint) supplies the pause-open
// animate-in even when the game's own ramp frames were missed by the
// capture gate. A >0.25 s gap since the last blur frame = a fresh ON
// edge (resets the state); at the render rate active frames are ~7 ms
// apart so the reset can never fire mid-popup.
static float s_blur_r = 0.0f;
static float s_blur_tint[3] = {1.0f, 1.0f, 1.0f};
static int64_t s_blur_ns = 0;
const int64_t blur_now_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(
PerfClock::now().time_since_epoch())
.count();
float blur_dt = s_blur_ns != 0 ? float(blur_now_ns - s_blur_ns) * 1e-9f : 1.0f;
if (blur_dt > 0.25f) {
s_blur_r = 0.0f;
s_blur_tint[0] = s_blur_tint[1] = s_blur_tint[2] = 1.0f;
blur_dt = 0.0f;
}
s_blur_ns = blur_now_ns;
const float blur_a = 1.0f - std::exp(-blur_dt / 0.05f);
s_blur_r += (scene.ui_blur - s_blur_r) * blur_a;
for (int a = 0; a < 3; ++a) {
s_blur_tint[a] += (scene.ui_blur_color[a] - s_blur_tint[a]) * blur_a;
}
// The guest output resource can change between frames; re-point the
// dedicated SRV slot at it each blur frame.
{
@ -17535,11 +17597,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
list.D3DSetPipelineState(g_r.pso_blur);
const float h_consts[8] = {1.0f,
0.0f,
scene.ui_blur,
s_blur_r,
0.0f,
scene.ui_blur_color[0],
scene.ui_blur_color[1],
scene.ui_blur_color[2],
s_blur_tint[0],
s_blur_tint[1],
s_blur_tint[2],
1.0f};
list.D3DSetGraphicsRoot32BitConstants(0, 8, h_consts, 0);
srv_table(g_r.blur_srv[0]);
@ -17551,11 +17613,11 @@ bool RenderScene(const NativeGuestOutputRenderContext& context, void* /*user_dat
list.D3DOMSetRenderTargets(1, &blur_rtv0, FALSE, nullptr);
const float v_consts[8] = {0.0f,
1.0f,
scene.ui_blur,
s_blur_r,
0.0f,
scene.ui_blur_color[0],
scene.ui_blur_color[1],
scene.ui_blur_color[2],
s_blur_tint[0],
s_blur_tint[1],
s_blur_tint[2],
1.0f};
list.D3DSetGraphicsRoot32BitConstants(0, 8, v_consts, 0);
srv_table(g_r.blur_srv[1]);

@ -1 +1 @@
Subproject commit 0585bf885af0022603fb06c8218b55acc8fa771d
Subproject commit b8258670da206dd2875c08ab437458a274558df0