debug: F12 post-effect sliders and a one-shot post-effect comparison sweep

This commit is contained in:
mchughalex 2026-07-19 14:05:42 +01:00
parent f2aaf4081e
commit 995f690029
2 changed files with 321 additions and 0 deletions

View file

@ -22,6 +22,10 @@
#include <rex/cvar.h>
// Guest-thread shared scene state (the post-sweep's shadow-readiness gate
// reads g_shadow_have; OnCaptureFrameEnd runs on the same thread).
#include "skate3_native_scene_state.h"
#include "generated/skate3_init.h"
#include "skate3_screenshot.h"
@ -59,6 +63,22 @@ REXCVAR_DEFINE_INT32(skate3_native_render_snapshot_stride, 1, "Skate 3",
"e.g. 12 = ~12 recorded frames/sec at 144fps)")
.range(1, 32)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
// Defined in skate3_native_scene.cpp (the HDR post-effect cvars the sweep
// below drives).
REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom);
REXCVAR_DECLARE(bool, skate3_native_render_scene_haze);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_hdr_debug);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shafts);
REXCVAR_DEFINE_BOOL(
skate3_native_render_post_sweep, false, "Skate 3",
"One-shot HDR post-effect comparison sweep: once gameplay is on screen, "
"steps through baseline (bloom/shafts/haze off), each effect alone, "
"everything on, and the hdr_debug views (shaft plane / haze term / sky "
"gate), settling between steps and writing one tagged screenshot per "
"step (shot_<ts>_sweep_<step>.png), then restores the previous cvar "
"values and turns itself off. Arm via config for a hands-off capture "
"run, or from the debug dialog mid-session.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
REXCVAR_DEFINE_STRING(skate3_native_render_snapshot_dir, "native_render_snapshots", "Skate 3",
"Directory for native-render guest memory snapshots and metadata")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
@ -769,6 +789,135 @@ void OnCaptureFrameEnd(uint8_t* base, uint64_t frame_index,
}
f11_was_down = f11_down;
}
// Post-effect comparison sweep (skate3_native_render_post_sweep): one
// session, one screenshot per render state: baseline, each HDR post
// effect alone, everything on, then the hdr_debug diagnostic views.
// Gameplay detection reuses the auto-snapshot heuristic (a sustained
// full-scene mesh count); each step settles before its capture so
// hot-reload toggles and exposure have taken effect.
{
struct SweepStep {
const char* tag;
bool bloom;
bool shafts;
bool haze;
int32_t debug;
};
static constexpr SweepStep kSweepSteps[] = {
{"base", false, false, false, 0},
{"bloom", true, false, false, 0},
{"shafts", false, true, false, 0},
{"haze", false, false, true, 0},
{"all", true, true, true, 0},
{"dbg_shaftplane", true, true, true, 4},
{"dbg_hazeterm", true, true, true, 5},
};
enum class SweepState { kIdle, kWaitGameplay, kStep };
static SweepState sweep_state = SweepState::kIdle;
static uint32_t sweep_gameplay_frames = 0;
static size_t sweep_index = 0;
static uint64_t sweep_capture_frame = 0;
static bool sweep_captured = false;
static bool saved_bloom = false, saved_shafts = false, saved_haze = false;
static int32_t saved_debug = 0;
static char sweep_ts[24] = {};
// Every swept cvar is a per-frame hot read (no pipeline rebuilds), so
// settling is short. Kept to a fraction of a second per step: quick
// enough that the captures stay frame-comparable for offline A/B
// diffing, slow enough to follow by eye.
constexpr uint64_t kSweepSettleFrames = 48;
const bool sweep_armed = REXCVAR_GET(skate3_native_render_post_sweep);
const auto sweep_restore = [&] {
REXCVAR_SET(skate3_native_render_scene_bloom, saved_bloom);
REXCVAR_SET(skate3_native_render_scene_shafts, saved_shafts);
REXCVAR_SET(skate3_native_render_scene_haze, saved_haze);
REXCVAR_SET(skate3_native_render_scene_hdr_debug, saved_debug);
REXCVAR_SET(skate3_native_render_post_sweep, false);
sweep_state = SweepState::kIdle;
};
switch (sweep_state) {
case SweepState::kIdle:
if (sweep_armed) {
sweep_state = SweepState::kWaitGameplay;
sweep_gameplay_frames = 0;
REXLOG_INFO("native-render post sweep: armed, waiting for gameplay");
}
break;
case SweepState::kWaitGameplay:
if (!sweep_armed) {
sweep_state = SweepState::kIdle;
break;
}
// Gameplay AND the frame-global shadow rows captured: the sweep's
// shaft states are meaningless before the shadow pass is live, and
// right after takeover the sweep otherwise outruns its bring-up.
sweep_gameplay_frames =
(mesh_count >= 300 && g_shadow_have) ? sweep_gameplay_frames + 1
: 0;
if (sweep_gameplay_frames >= 240) {
saved_bloom = REXCVAR_GET(skate3_native_render_scene_bloom);
saved_shafts = REXCVAR_GET(skate3_native_render_scene_shafts);
saved_haze = REXCVAR_GET(skate3_native_render_scene_haze);
saved_debug = REXCVAR_GET(skate3_native_render_scene_hdr_debug);
const std::time_t t = std::time(nullptr);
std::tm tm{};
localtime_s(&tm, &t);
std::snprintf(sweep_ts, sizeof(sweep_ts), "%02d%02d%02d", tm.tm_hour,
tm.tm_min, tm.tm_sec);
sweep_index = 0;
sweep_captured = false;
sweep_capture_frame = frame_index + kSweepSettleFrames;
REXCVAR_SET(skate3_native_render_scene_bloom, kSweepSteps[0].bloom);
REXCVAR_SET(skate3_native_render_scene_shafts, kSweepSteps[0].shafts);
REXCVAR_SET(skate3_native_render_scene_haze, kSweepSteps[0].haze);
REXCVAR_SET(skate3_native_render_scene_hdr_debug, kSweepSteps[0].debug);
sweep_state = SweepState::kStep;
REXLOG_INFO("native-render post sweep: started, tag {} ({} steps)",
sweep_ts, std::size(kSweepSteps));
}
break;
case SweepState::kStep:
if (!sweep_armed) {
sweep_restore();
REXLOG_INFO("native-render post sweep: cancelled, cvars restored");
break;
}
if (!sweep_captured && frame_index >= sweep_capture_frame) {
char tag[48];
std::snprintf(tag, sizeof(tag), "%s_sweep_%s", sweep_ts,
kSweepSteps[sweep_index].tag);
skate3::screenshot::CaptureWindow(
skate3::screenshot::RememberedWindow(), tag);
REXLOG_INFO("native-render post sweep: captured {} ({}/{})", tag,
sweep_index + 1, std::size(kSweepSteps));
sweep_captured = true;
// A short hold so the (asynchronous) window grab reads this
// state's frame before the next step's cvars apply.
sweep_capture_frame = frame_index + 12;
break;
}
if (sweep_captured && frame_index >= sweep_capture_frame) {
if (++sweep_index >= std::size(kSweepSteps)) {
sweep_restore();
REXLOG_INFO(
"native-render post sweep: done, cvars restored (tag {})",
sweep_ts);
break;
}
REXCVAR_SET(skate3_native_render_scene_bloom,
kSweepSteps[sweep_index].bloom);
REXCVAR_SET(skate3_native_render_scene_shafts,
kSweepSteps[sweep_index].shafts);
REXCVAR_SET(skate3_native_render_scene_haze,
kSweepSteps[sweep_index].haze);
REXCVAR_SET(skate3_native_render_scene_hdr_debug,
kSweepSteps[sweep_index].debug);
sweep_captured = false;
sweep_capture_frame = frame_index + kSweepSettleFrames;
}
break;
}
}
#endif
if (!g_collecting && frame_index % 32 == 0) {
const std::filesystem::path trigger = SnapshotDir() / "trigger";

View file

@ -1,5 +1,8 @@
#include "skate3_native_debug_dialog.h"
#include <algorithm>
#include <cmath>
#include <imgui.h>
#include <rex/cvar.h>
@ -38,6 +41,27 @@ 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_tex_mips);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_debug);
// HDR post-effect stack (hot; skate3_native_scene.cpp).
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_hdr_debug);
REXCVAR_DECLARE(bool, skate3_native_render_scene_bloom);
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_threshold);
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_knee);
REXCVAR_DECLARE(double, skate3_native_render_scene_bloom_intensity);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shafts);
REXCVAR_DECLARE(double, skate3_native_render_scene_shafts_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_shafts_reach);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shafts_steps);
REXCVAR_DECLARE(bool, skate3_native_render_scene_haze);
REXCVAR_DECLARE(double, skate3_native_render_scene_haze_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_haze_density);
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_radius);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_intensity);
REXCVAR_DECLARE(double, skate3_native_render_scene_ssao_luma_protect);
REXCVAR_DECLARE(bool, skate3_native_render_scene_sun_override);
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_azimuth);
REXCVAR_DECLARE(double, skate3_native_render_scene_sun_elevation);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_refl_mode);
REXCVAR_DECLARE(double, skate3_native_render_scene_refl_lod);
REXCVAR_DECLARE(double, skate3_native_render_scene_refl_bias_x);
@ -66,6 +90,20 @@ bool CvarCheckbox(const char* label, bool value, const char* help = nullptr) {
return v;
}
// Float-cvar slider (the cvars are hot-reload doubles; edits apply next
// frame). Returns the possibly-changed value for REXCVAR_SET.
double CvarSlider(const char* label, double value, float lo, float hi,
const char* fmt, const char* help = nullptr) {
float v = float(value);
if (ImGui::SliderFloat(label, &v, lo, hi, fmt)) {
value = double(v);
}
if (help != nullptr && (ImGui::IsItemHovered() || ImGui::IsItemActive())) {
ImGui::SetTooltip("%s", help);
}
return value;
}
} // namespace
void NativeDebugDialog::Show() {
@ -158,6 +196,140 @@ void NativeDebugDialog::OnDraw(ImGuiIO& io) {
"World env materials cull FRONT like the game's material "
"XMLs; off = legacy cull-none (shows interior faces)"));
ImGui::SeparatorText("HDR post effects (live)");
REXCVAR_SET(skate3_native_render_scene_hdr,
CvarCheckbox("HDR intermediate",
REXCVAR_GET(skate3_native_render_scene_hdr),
"Float scene target + single host tonemap, the basis for "
"bloom, shafts and haze. Off = the classic in-material "
"tonemap (parity A/B)."));
{
int dbg = REXCVAR_GET(skate3_native_render_scene_hdr_debug);
const char* kHdrDbg[] = {"0: off", "1: bloom term",
"2: raw pre-tonemap", "3: AO plane",
"4: shaft plane", "5: haze term"};
if (ImGui::Combo("HDR debug view", &dbg, kHdrDbg, 6)) {
REXCVAR_SET(skate3_native_render_scene_hdr_debug, dbg);
}
}
REXCVAR_SET(skate3_native_render_scene_shafts,
CvarCheckbox("Volumetric sun shafts",
REXCVAR_GET(skate3_native_render_scene_shafts),
"Shadow-marched air: shadowed portions of each view ray "
"proportionally dim the light seen through them (dark "
"crepuscular shafts from buildings/trees/underpasses). "
"Fully lit air is untouched."));
REXCVAR_SET(skate3_native_render_scene_shafts_intensity,
CvarSlider("shaft intensity",
REXCVAR_GET(skate3_native_render_scene_shafts_intensity),
0.0f, 1.5f, "%.2f",
"How strongly fully shadowed air dims the scene behind it "
"(scaled by the forward-scatter phase toward the sun)."));
REXCVAR_SET(skate3_native_render_scene_shafts_reach,
CvarSlider("shaft reach (world units)",
REXCVAR_GET(skate3_native_render_scene_shafts_reach),
5.0f, 300.0f, "%.0f",
"How far in front of the camera the air is sampled. Longer "
"= more distant shadow volumes, coarser sampling."));
{
int steps = REXCVAR_GET(skate3_native_render_scene_shafts_steps);
if (ImGui::SliderInt("shaft steps", &steps, 8, 64)) {
REXCVAR_SET(skate3_native_render_scene_shafts_steps, steps);
}
}
REXCVAR_SET(skate3_native_render_scene_haze,
CvarCheckbox("Directional haze",
REXCVAR_GET(skate3_native_render_scene_haze),
"Fog-tinted sun scattering added with view distance, "
"strongest toward the sun. Off by default: the game's "
"authored sky/fog already carry the base atmosphere."));
REXCVAR_SET(skate3_native_render_scene_haze_intensity,
CvarSlider("haze intensity",
REXCVAR_GET(skate3_native_render_scene_haze_intensity),
0.0f, 0.5f, "%.3f"));
REXCVAR_SET(skate3_native_render_scene_haze_density,
CvarSlider("haze density (1/unit)",
REXCVAR_GET(skate3_native_render_scene_haze_density),
0.0f, 0.02f, "%.4f",
"How quickly the scattering saturates with distance "
"(0.005 reaches ~63% at 200 units)."));
REXCVAR_SET(skate3_native_render_scene_bloom,
CvarCheckbox("Bloom",
REXCVAR_GET(skate3_native_render_scene_bloom),
"Downsample/upsample pyramid driven by pre-tonemap "
"brightness (night lamps, neon, sun glare)."));
REXCVAR_SET(skate3_native_render_scene_bloom_threshold,
CvarSlider("bloom threshold",
REXCVAR_GET(skate3_native_render_scene_bloom_threshold),
0.0f, 1.5f, "%.2f",
"Pre-tonemap onset (1.0 = the tone curve's saturation "
"point). Below ~0.7 the sunlit day frame starts feeding "
"the pyramid and veils in glow."));
REXCVAR_SET(skate3_native_render_scene_bloom_knee,
CvarSlider("bloom knee",
REXCVAR_GET(skate3_native_render_scene_bloom_knee),
0.0f, 0.5f, "%.2f"));
REXCVAR_SET(skate3_native_render_scene_bloom_intensity,
CvarSlider("bloom intensity",
REXCVAR_GET(skate3_native_render_scene_bloom_intensity),
0.0f, 0.3f, "%.3f"));
REXCVAR_SET(skate3_native_render_scene_ssao,
CvarCheckbox("SSAO (GTAO)",
REXCVAR_GET(skate3_native_render_scene_ssao),
"Screen-space ambient occlusion: contact shading under "
"ledges, rails, vehicles, feet."));
REXCVAR_SET(skate3_native_render_scene_ssao_radius,
CvarSlider("SSAO radius (world units)",
REXCVAR_GET(skate3_native_render_scene_ssao_radius),
0.1f, 4.0f, "%.2f"));
REXCVAR_SET(skate3_native_render_scene_ssao_intensity,
CvarSlider("SSAO intensity",
REXCVAR_GET(skate3_native_render_scene_ssao_intensity),
0.0f, 3.0f, "%.2f",
"Exponent on the visibility term (1 = physical, >1 = "
"accentuated)."));
REXCVAR_SET(skate3_native_render_scene_ssao_luma_protect,
CvarSlider("SSAO sunlit protection",
REXCVAR_GET(skate3_native_render_scene_ssao_luma_protect),
0.0f, 3.0f, "%.2f",
"How strongly bright (sun-lit) surfaces resist SSAO "
"darkening (ambient-only approximation)."));
{
const bool was_on = REXCVAR_GET(skate3_native_render_scene_sun_override);
const bool now_on =
CvarCheckbox("Sun override (lighting lab)", was_on,
"Move the sun with the sliders below: dynamic CSM "
"shadows, the static world-shadow map, shadow receivers "
"and the volumetric shafts all follow. Baked lightmap "
"shade and the sky dome's painted sun stay put (game "
"content).");
if (now_on && !was_on) {
// Seed the sliders from the captured sun so enabling the override
// starts at the true position instead of jumping.
float sun[3];
skate3::native_scene::GetCapturedSunDir(sun);
const float kRad = 57.29577951f;
REXCVAR_SET(skate3_native_render_scene_sun_azimuth,
double(std::fmod(std::atan2(sun[0], sun[2]) * kRad + 360.0f,
360.0f)));
REXCVAR_SET(
skate3_native_render_scene_sun_elevation,
double(std::clamp(std::asin(std::clamp(sun[1], -1.0f, 1.0f)) * kRad,
2.0f, 88.0f)));
}
REXCVAR_SET(skate3_native_render_scene_sun_override, now_on);
}
REXCVAR_SET(skate3_native_render_scene_sun_azimuth,
CvarSlider("sun azimuth (deg)",
REXCVAR_GET(skate3_native_render_scene_sun_azimuth),
0.0f, 360.0f, "%.0f"));
REXCVAR_SET(skate3_native_render_scene_sun_elevation,
CvarSlider("sun elevation (deg)",
REXCVAR_GET(skate3_native_render_scene_sun_elevation),
2.0f, 88.0f, "%.0f",
"Low elevations give long shadows and the most visible "
"volumetric shafts."));
ImGui::SeparatorText("Reflective glass isolation (env fam 5/6/13)");
{
int mode = REXCVAR_GET(skate3_native_render_scene_refl_mode);