Native renderer performance overhaul + high-fps motion smoothing

This commit is contained in:
mchughalex 2026-07-09 04:53:44 +01:00
parent 77d22e22e5
commit c613ffacc8
4 changed files with 1503 additions and 230 deletions

View file

@ -24,6 +24,10 @@ REXCVAR_DECLARE(bool, skate3_native_render_scene_tex_revalidate);
REXCVAR_DECLARE(bool, skate3_native_render_scene_mesh_revalidate); REXCVAR_DECLARE(bool, skate3_native_render_scene_mesh_revalidate);
REXCVAR_DECLARE(bool, skate3_native_render_scene_tex_mips); REXCVAR_DECLARE(bool, skate3_native_render_scene_tex_mips);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_debug); REXCVAR_DECLARE(int32_t, skate3_native_render_scene_debug);
// Smoothness / pacing.
REXCVAR_DECLARE(bool, skate3_native_render_scene_smooth_camera);
REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque);
REXCVAR_DECLARE(double, skate3_guest_fps_cap);
// SDK: emulated-draw suppression while the native output is active. // SDK: emulated-draw suppression while the native output is active.
REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws); REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws);
@ -149,6 +153,34 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
CvarCheckbox("Neon splines", REXCVAR_GET(skate3_native_render_scene_splines), CvarCheckbox("Neon splines", REXCVAR_GET(skate3_native_render_scene_splines),
"Waypoint arrows / marker beams")); "Waypoint arrows / marker beams"));
ImGui::SeparatorText("Smoothness / pacing");
REXCVAR_SET(skate3_native_render_scene_smooth_camera,
CvarCheckbox("Smooth camera + entity poses",
REXCVAR_GET(skate3_native_render_scene_smooth_camera),
"The guest updates its camera/entities on its own ~200 Hz "
"sim tick; raw poses judder at high render rates. Re-times "
"them on the host clock (1 kHz camera sampler + pose "
"interpolation, a few ms of camera latency)."));
REXCVAR_SET(skate3_native_render_scene_sort_opaque,
CvarCheckbox("Front-to-back opaque sort",
REXCVAR_GET(skate3_native_render_scene_sort_opaque),
"Early-z rejects occluded pixels before the material shading"));
{
int cap = int(REXCVAR_GET(skate3_guest_fps_cap));
if (ImGui::InputInt("Guest fps cap (0 = off)", &cap, 10, 30)) {
if (cap < 0) cap = 0;
if (cap > 1000) cap = 1000;
REXCVAR_SET(skate3_guest_fps_cap, double(cap));
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Pace guest frames on an even beat. Set a few fps below the display "
"refresh (with G-Sync/VRR this is what makes motion read as smooth; "
"uncapped, the guest's irregular frame times drive the refresh "
"directly).");
}
}
ImGui::SeparatorText("Caches (lightmap-era plumbing)"); ImGui::SeparatorText("Caches (lightmap-era plumbing)");
REXCVAR_SET( REXCVAR_SET(
skate3_native_render_scene_tex_revalidate, skate3_native_render_scene_tex_revalidate,

View file

@ -15,6 +15,7 @@
#include <fstream> #include <fstream>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread>
#include <vector> #include <vector>
#if defined(_WIN32) #if defined(_WIN32)
@ -49,6 +50,17 @@ REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_stride, 1, "Skate 3",
REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshots", "Skate 3", REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshots", "Skate 3",
"Directory for native-render guest memory snapshots and metadata") "Directory for native-render guest memory snapshots and metadata")
.lifecycle(rex::cvar::Lifecycle::kHotReload); .lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_DOUBLE(skate3_guest_fps_cap, 0.0, "Skate 3",
"Pace the guest render loop to this frame rate (0 = uncapped). The "
"guest produces frames at irregular 2-9 ms intervals; the display "
"(especially with G-Sync/VRR, which follows present times directly) "
"turns that variance into visible irregular judder that no content "
"smoothing can fix. An even cap a few fps below the display refresh "
"(e.g. 140 on a 144 Hz panel) is the standard VRR recipe: every "
"frame arrives on a steady beat. Precise pacing: coarse sleep to "
"~1.5 ms before the target, then spin.")
.range(0.0, 1000.0)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
namespace skate3::native_render { namespace skate3::native_render {
namespace { namespace {
@ -263,7 +275,42 @@ void OnClothDraw(uint8_t* base, uint32_t r4, uint32_t r5, uint32_t r6, uint32_t
g_current_frame.push_back({3, key, 0, dyn}); g_current_frame.push_back({3, key, 0, dyn});
} }
// Precise guest frame pacing (see skate3_guest_fps_cap): called on the guest
// render thread at the swap boundary. Absolute-schedule pacing (target +=
// interval) so sleep jitter never accumulates; resyncs when the guest falls
// more than one interval behind (loads, hitches).
void PaceGuestFrame() {
const double cap = REXCVAR_GET(skate3_guest_fps_cap);
static std::chrono::steady_clock::time_point s_next{};
if (cap < 1.0) {
s_next = {};
return;
}
const auto interval =
std::chrono::duration_cast<std::chrono::steady_clock::duration>(
std::chrono::duration<double>(1.0 / cap));
const auto now = std::chrono::steady_clock::now();
if (s_next.time_since_epoch().count() == 0 || now > s_next + interval) {
s_next = now + interval;
return;
}
// Coarse sleep to ~1.5 ms before the target, then spin for precision.
while (true) {
const auto remaining = s_next - std::chrono::steady_clock::now();
if (remaining <= std::chrono::steady_clock::duration::zero()) {
break;
}
if (remaining > std::chrono::milliseconds(2)) {
std::this_thread::sleep_for(remaining - std::chrono::milliseconds(2));
} else if (remaining > std::chrono::microseconds(50)) {
std::this_thread::yield();
}
}
s_next += interval;
}
void OnFrameEnd(uint8_t* base) { void OnFrameEnd(uint8_t* base) {
PaceGuestFrame();
std::lock_guard<std::mutex> lock(g_mutex); std::lock_guard<std::mutex> lock(g_mutex);
++g_frame_index; ++g_frame_index;
const size_t mesh_count = g_current_frame.size(); const size_t mesh_count = g_current_frame.size();
@ -306,6 +353,18 @@ void OnFrameEnd(uint8_t* base) {
g_frame_index, mesh_count); g_frame_index, mesh_count);
} }
f9_was_down = f9_down; f9_was_down = f9_down;
// F8: flush the native texture + mesh caches. Debug/bisect aid, and the
// reproducible worst-case decode burst for perf work (everything visible
// re-decodes at once; the decode workers should absorb it with a brief
// white/pop-in instead of a render-thread freeze).
static bool f8_was_down = false;
const bool f8_down = (GetAsyncKeyState(VK_F8) & 0x8000) != 0;
if (f8_down && !f8_was_down) {
skate3::native_scene::FlushTextureCache();
skate3::native_scene::FlushMeshCache();
REXLOG_INFO("native-render: F8, texture + mesh caches flushed");
}
f8_was_down = f8_down;
static bool f10_was_down = false; static bool f10_was_down = false;
const bool f10_down = (GetAsyncKeyState(VK_F10) & 0x8000) != 0; const bool f10_down = (GetAsyncKeyState(VK_F10) & 0x8000) != 0;
if (f10_down && !f10_was_down) { if (f10_down && !f10_was_down) {

File diff suppressed because it is too large Load diff

@ -1 +1 @@
Subproject commit 0fcbad33e66829aa61ca95c166c64f387e56fcd0 Subproject commit 96de5455f2eb87f183dfc9459780008780ee245c