diff --git a/src/native/skate3_native_diagnostics.cpp b/src/native/skate3_native_diagnostics.cpp index 3fb9b07..91f7b92 100644 --- a/src/native/skate3_native_diagnostics.cpp +++ b/src/native/skate3_native_diagnostics.cpp @@ -22,6 +22,10 @@ #include +// 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__sweep_.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"; diff --git a/src/skate3_native_debug_dialog.cpp b/src/skate3_native_debug_dialog.cpp index e807890..26f6ea2 100644 --- a/src/skate3_native_debug_dialog.cpp +++ b/src/skate3_native_debug_dialog.cpp @@ -1,5 +1,8 @@ #include "skate3_native_debug_dialog.h" +#include +#include + #include #include @@ -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);