skate3recomp-multiplayer/src/skate3_native_debug_dialog.cpp

1259 lines
56 KiB
C++

#include "skate3_native_debug_dialog.h"
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include <imgui.h>
#include <rex/cvar.h>
#include <rex/graphics/native_guest_renderer.h>
#include <rex/logging.h>
#include <rex/ui/presenter.h>
#include "skate3_native_scene.h"
REXCVAR_DEFINE_BOOL(skate3_native_render_mode_indicator, false, "Skate 3",
"Small top-right corner readout of which renderer produced the "
"last presented frame: NATIVE (native scene renderer) or "
"EMULATED (Xenos GPU emulation; menus/loading yields, F5 off). "
"Shows automatically while the native scene renderer is "
"switched off, regardless of this setting.")
.lifecycle(rex::cvar::Lifecycle::kHotReload);
// Hook-layer master (boot-time), shown read-only.
REXCVAR_DECLARE(bool, skate3_native_render);
// Hot-reload feature gates (skate3_native_scene.cpp).
REXCVAR_DECLARE(bool, skate3_native_render_scene);
REXCVAR_DECLARE(bool, skate3_native_render_scene_lightmaps);
REXCVAR_DECLARE(bool, skate3_native_render_scene_macro);
REXCVAR_DECLARE(bool, skate3_native_render_scene_decals);
REXCVAR_DECLARE(bool, skate3_native_render_scene_transparents);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadows);
REXCVAR_DECLARE(bool, skate3_native_render_scene_backface_cull);
REXCVAR_DECLARE(bool, skate3_native_render_scene_2d);
REXCVAR_DECLARE(bool, skate3_native_render_scene_splines);
REXCVAR_DECLARE(bool, skate3_native_render_scene_quadlists);
REXCVAR_DECLARE(bool, skate3_native_render_scene_world_items);
REXCVAR_DECLARE(bool, skate3_native_render_scene_dynamic_items);
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_fade);
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_identity);
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_gap_fill);
REXCVAR_DECLARE(bool, skate3_native_render_scene_lw_palette);
REXCVAR_DECLARE(bool, skate3_native_render_scene_retain_offscreen);
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);
// Image quality (hot; skate3_native_scene.cpp).
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_msaa);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shadow_tile);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_shadow_static_size);
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssao_full_res);
REXCVAR_DECLARE(bool, skate3_native_render_scene_hdr_packed);
// 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_shadow_static_casters);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_strength);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_radius);
REXCVAR_DECLARE(bool, skate3_native_render_scene_shadow_pcss);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_sun_deg);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_pcss_max_m);
REXCVAR_DECLARE(double, skate3_native_render_scene_shadow_static_bias);
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);
REXCVAR_DECLARE(double, skate3_native_render_scene_refl_bias_y);
REXCVAR_DECLARE(bool, skate3_native_render_scene_refl_bias_auto);
// Smoothness / pacing.
REXCVAR_DECLARE(bool, skate3_native_render_scene_smooth_camera);
REXCVAR_DECLARE(double, skate3_native_render_scene_smooth_camera_filter_ms);
REXCVAR_DECLARE(bool, skate3_native_render_scene_sort_opaque);
REXCVAR_DECLARE(double, skate3_guest_fps_cap);
REXCVAR_DECLARE(int32_t, skate3_native_render_scene_synthetic_pan);
REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_rate);
REXCVAR_DECLARE(double, skate3_native_render_scene_synthetic_pan_amp);
// SDK: emulated-draw suppression while the native output is active.
REXCVAR_DECLARE(bool, native_render_suppress_emulated_draws);
REXCVAR_DECLARE(bool, skate3_native_render_scene_showcase);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_hold);
REXCVAR_DECLARE(double, skate3_native_render_scene_showcase_wipe);
REXCVAR_DECLARE(std::string, skate3_native_render_scene_showcase_order);
REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam);
REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_speed);
REXCVAR_DECLARE(double, skate3_native_render_scene_freecam_look_speed);
REXCVAR_DECLARE(bool, skate3_native_render_scene_freecam_capture_input);
REXCVAR_DECLARE(bool, skate3_native_render_scene_ssr);
// Draw distance (hot; skate3_draw_distance.cpp).
REXCVAR_DECLARE(double, skate3_draw_distance_scale);
REXCVAR_DECLARE(double, skate3_lod_distance_scale);
REXCVAR_DECLARE(double, skate3_draw_distance_stream_probe);
namespace skate3 {
namespace {
bool CvarCheckbox(const char* label, bool value, const char* help = nullptr) {
bool v = value;
ImGui::Checkbox(label, &v);
if (help != nullptr && ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", help);
}
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;
}
// Combo over a fixed value list (for quality steps that are not a
// continuum: sample counts, map sizes). Returns the possibly-changed value;
// out-of-list current values snap to the nearest entry's label without
// writing back until the user picks one.
int32_t ValueCombo(const char* label, int32_t value, const int32_t* values,
const char* const* labels, int count,
const char* help = nullptr) {
int index = 0;
for (int i = 1; i < count; ++i) {
const int32_t di = values[i] > value ? values[i] - value : value - values[i];
const int32_t db = values[index] > value ? values[index] - value
: value - values[index];
if (di < db) {
index = i;
}
}
if (ImGui::Combo(label, &index, labels, count)) {
value = values[index];
}
if (help != nullptr && ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", help);
}
return value;
}
// ---- Max quality (photo mode) ---------------------------------------------
// One switch that pushes every quality knob to its maximum for
// capture/recording (showcase runs, promo footage, screenshots) and
// restores the previous values when switched off. Session-scoped: nothing
// is persisted. Aesthetic calibrations (bloom/shaft/haze strengths, SSAO
// tuning) keep their values: this maximizes fidelity, not effect strength.
// SSR stays as-is (experimental, image-quality issues) and quad-list
// particles stay off (no sprite textures).
struct MaxQualityState {
bool active = false;
// Saved values, restored on switch-off.
int32_t msaa;
int32_t shadow_tile;
int32_t static_size;
int32_t shafts_steps;
bool ssao_full_res;
bool hdr_packed;
bool hdr;
bool bloom;
bool shafts;
bool ssao;
bool shadows;
bool static_casters;
bool pcss;
double draw_scale;
double lod_scale;
double stream_probe;
};
MaxQualityState s_max_quality;
void SetMaxQuality(bool enable) {
MaxQualityState& s = s_max_quality;
if (enable == s.active) {
return;
}
if (enable) {
s.msaa = REXCVAR_GET(skate3_native_render_scene_msaa);
s.shadow_tile = REXCVAR_GET(skate3_native_render_scene_shadow_tile);
s.static_size = REXCVAR_GET(skate3_native_render_scene_shadow_static_size);
s.shafts_steps = REXCVAR_GET(skate3_native_render_scene_shafts_steps);
s.ssao_full_res = REXCVAR_GET(skate3_native_render_scene_ssao_full_res);
s.hdr_packed = REXCVAR_GET(skate3_native_render_scene_hdr_packed);
s.hdr = REXCVAR_GET(skate3_native_render_scene_hdr);
s.bloom = REXCVAR_GET(skate3_native_render_scene_bloom);
s.shafts = REXCVAR_GET(skate3_native_render_scene_shafts);
s.ssao = REXCVAR_GET(skate3_native_render_scene_ssao);
s.shadows = REXCVAR_GET(skate3_native_render_scene_shadows);
s.static_casters =
REXCVAR_GET(skate3_native_render_scene_shadow_static_casters);
s.pcss = REXCVAR_GET(skate3_native_render_scene_shadow_pcss);
s.draw_scale = REXCVAR_GET(skate3_draw_distance_scale);
s.lod_scale = REXCVAR_GET(skate3_lod_distance_scale);
s.stream_probe = REXCVAR_GET(skate3_draw_distance_stream_probe);
REXCVAR_SET(skate3_native_render_scene_msaa, 8);
REXCVAR_SET(skate3_native_render_scene_shadow_tile, 4096);
REXCVAR_SET(skate3_native_render_scene_shadow_static_size, 8192);
REXCVAR_SET(skate3_native_render_scene_shafts_steps, 64);
REXCVAR_SET(skate3_native_render_scene_ssao_full_res, true);
REXCVAR_SET(skate3_native_render_scene_hdr_packed, false); // RGBA16F
REXCVAR_SET(skate3_native_render_scene_hdr, true);
REXCVAR_SET(skate3_native_render_scene_bloom, true);
REXCVAR_SET(skate3_native_render_scene_shafts, true);
REXCVAR_SET(skate3_native_render_scene_ssao, true);
REXCVAR_SET(skate3_native_render_scene_shadows, true);
REXCVAR_SET(skate3_native_render_scene_shadow_static_casters, true);
REXCVAR_SET(skate3_native_render_scene_shadow_pcss, true);
// The largest steps the settings menu offers (tested territory).
REXCVAR_SET(skate3_draw_distance_scale, 5.0);
REXCVAR_SET(skate3_lod_distance_scale, 5.0);
REXCVAR_SET(skate3_draw_distance_stream_probe, 300.0);
} else {
REXCVAR_SET(skate3_native_render_scene_msaa, s.msaa);
REXCVAR_SET(skate3_native_render_scene_shadow_tile, s.shadow_tile);
REXCVAR_SET(skate3_native_render_scene_shadow_static_size, s.static_size);
REXCVAR_SET(skate3_native_render_scene_shafts_steps, s.shafts_steps);
REXCVAR_SET(skate3_native_render_scene_ssao_full_res, s.ssao_full_res);
REXCVAR_SET(skate3_native_render_scene_hdr_packed, s.hdr_packed);
REXCVAR_SET(skate3_native_render_scene_hdr, s.hdr);
REXCVAR_SET(skate3_native_render_scene_bloom, s.bloom);
REXCVAR_SET(skate3_native_render_scene_shafts, s.shafts);
REXCVAR_SET(skate3_native_render_scene_ssao, s.ssao);
REXCVAR_SET(skate3_native_render_scene_shadows, s.shadows);
REXCVAR_SET(skate3_native_render_scene_shadow_static_casters,
s.static_casters);
REXCVAR_SET(skate3_native_render_scene_shadow_pcss, s.pcss);
REXCVAR_SET(skate3_draw_distance_scale, s.draw_scale);
REXCVAR_SET(skate3_lod_distance_scale, s.lod_scale);
REXCVAR_SET(skate3_draw_distance_stream_probe, s.stream_probe);
}
s.active = enable;
}
REXCVAR_DEFINE_INT32(
skate3_native_render_scene_maxq_cycle, 0, "Skate 3",
"DEBUG: auto-toggle MAX QUALITY every N native-scene frames (0 = off). "
"Stress-tests the hot pipeline/target/shadow-map rebuild path; pair "
"with Vulkan validation layers when hunting stale bindings.")
.range(0, 1000000)
.lifecycle(rex::cvar::Lifecycle::kHotReload);
void DrawMaxQualityToggle() {
bool on = s_max_quality.active;
if (ImGui::Checkbox("MAX QUALITY (photo mode)", &on)) {
SetMaxQuality(on);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Push every quality setting to its maximum for recording: MSAA 8x,\n"
"4096 shadow tiles, 8192 static sun map, full-res SSAO, RGBA16F\n"
"HDR, max shaft steps, 5x draw/LOD distance, 300 m streaming, all\n"
"quality features on. GPU-heavy; previous values restore when\n"
"unchecked. Applies live (a one-frame pipeline rebuild).");
}
if (s_max_quality.active) {
ImGui::SameLine();
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "ACTIVE");
}
}
// ---- Showcase setup window ------------------------------------------------
// Editor for skate3_native_render_scene_showcase_order: one row per build-up
// layer, reorderable, each optionally joined to the previous row's wipe. The
// cvar string is the single source of truth ("tok,tok+tok,-tok": ',' starts
// a new wipe, '+' joins, '-' disables in place); the rows parse from it
// every frame and any edit writes it straight back, so the sequencer, this
// window and hand edits in config all stay consistent.
bool s_showcase_setup_open = false;
struct ShowcaseRow {
const skate3::native_scene::ShowcaseLayer* layer;
bool enabled;
bool joined;
};
std::vector<ShowcaseRow> ParseShowcaseRows(const std::string& order) {
using skate3::native_scene::kShowcaseLayers;
std::vector<ShowcaseRow> rows;
std::string tok;
bool cur_joined = false; // the separator before the accumulating token
const auto flush = [&]() {
while (!tok.empty() && (tok.front() == ' ' || tok.front() == '\t')) {
tok.erase(tok.begin());
}
while (!tok.empty() && (tok.back() == ' ' || tok.back() == '\t')) {
tok.pop_back();
}
bool enabled = true;
if (!tok.empty() && tok[0] == '-') {
enabled = false;
tok.erase(tok.begin());
}
for (char& ch : tok) {
if (ch >= 'A' && ch <= 'Z') {
ch = char(ch + 32);
}
}
for (const auto& layer : kShowcaseLayers) {
if (tok != layer.token) {
continue;
}
bool present = false;
for (const auto& row : rows) {
present = present || row.layer == &layer;
}
if (!present) {
rows.push_back({&layer, enabled, !rows.empty() && cur_joined});
}
break;
}
tok.clear();
};
for (char ch : order) {
if (ch == ',' || ch == '+') {
flush();
cur_joined = ch == '+';
} else {
tok += ch;
}
}
flush();
// Layers missing from the string append disabled, so they can always be
// re-enabled from the window.
for (const auto& layer : kShowcaseLayers) {
bool present = false;
for (const auto& row : rows) {
present = present || row.layer == &layer;
}
if (!present) {
rows.push_back({&layer, false, false});
}
}
return rows;
}
std::string BuildShowcaseOrder(const std::vector<ShowcaseRow>& rows) {
std::string out;
for (size_t k = 0; k < rows.size(); ++k) {
if (k > 0) {
out += rows[k].joined ? '+' : ',';
}
if (!rows[k].enabled) {
out += '-';
}
out += rows[k].layer->token;
}
return out;
}
// Mirrors the sequencer's availability rules (TickShowcase): the material
// looks need nothing; shadow/post layers need their feature (post gates
// live in the HDR tonemap).
bool ShowcaseLayerAvailable(uint32_t bit) {
const bool hdr_on = REXCVAR_GET(skate3_native_render_scene_hdr);
switch (bit) {
case 8u:
return REXCVAR_GET(skate3_native_render_scene_shadows);
case 16u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssao);
case 32u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_ssr);
case 64u:
return hdr_on && (REXCVAR_GET(skate3_native_render_scene_shafts) ||
REXCVAR_GET(skate3_native_render_scene_haze));
case 128u:
return hdr_on && REXCVAR_GET(skate3_native_render_scene_bloom);
default:
return true;
}
}
// Drone-camera controls (same cvars everywhere the section is embedded;
// edits apply live).
void DrawFreecamControls() {
ImGui::SeparatorText("Drone camera");
bool freecam = REXCVAR_GET(skate3_native_render_scene_freecam);
if (ImGui::Checkbox("Free-fly drone cam (End)", &freecam)) {
REXCVAR_SET(skate3_native_render_scene_freecam, freecam);
}
ImGui::SameLine();
bool capture = REXCVAR_GET(skate3_native_render_scene_freecam_capture_input);
if (ImGui::Checkbox("capture game input", &capture)) {
REXCVAR_SET(skate3_native_render_scene_freecam_capture_input, capture);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"While flying, keep keyboard/controller input away from the game so\n"
"the fly keys don't also steer the skater.");
}
REXCVAR_SET(skate3_native_render_scene_freecam_speed,
CvarSlider("fly speed (m/s)",
REXCVAR_GET(skate3_native_render_scene_freecam_speed),
0.5f, 60.0f, "%.1f",
"Base fly speed; Shift = 4x, Ctrl = 0.2x"));
REXCVAR_SET(
skate3_native_render_scene_freecam_look_speed,
CvarSlider("look speed (deg/s)",
REXCVAR_GET(skate3_native_render_scene_freecam_look_speed),
20.0f, 240.0f, "%.0f", "Arrow-key look rate"));
ImGui::TextDisabled(
"WASD fly, E/Space up, Q/C down, arrows or right-mouse drag look,\n"
"Z/X zoom, Shift fast, Ctrl slow.");
}
void DrawShowcaseSetupWindow(bool* p_open) {
using skate3::native_scene::kShowcaseLayers;
ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Showcase Setup", p_open, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
return;
}
const bool running = REXCVAR_GET(skate3_native_render_scene_showcase);
if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Ctrl+Shift+B)",
ImVec2(-1.0f, 0.0f))) {
REXCVAR_SET(skate3_native_render_scene_showcase, !running);
}
REXCVAR_SET(skate3_native_render_scene_showcase_hold,
CvarSlider("stage hold (s)",
REXCVAR_GET(skate3_native_render_scene_showcase_hold),
0.0f, 10.0f, "%.1f",
"Seconds each layer holds fullscreen between wipes"));
REXCVAR_SET(skate3_native_render_scene_showcase_wipe,
CvarSlider("wipe duration (s)",
REXCVAR_GET(skate3_native_render_scene_showcase_wipe),
0.5f, 10.0f, "%.1f",
"Seconds each split wipe takes to cross the screen"));
ImGui::SeparatorText("Layer order");
ImGui::TextDisabled(
"Every run starts from clay geometry. \"join\" reveals a layer\n"
"inside the previous layer's wipe instead of its own.");
std::vector<ShowcaseRow> rows =
ParseShowcaseRows(REXCVAR_GET(skate3_native_render_scene_showcase_order));
bool changed = false;
for (int k = 0; k < int(rows.size()); ++k) {
ImGui::PushID(k);
bool en = rows[k].enabled;
if (ImGui::Checkbox("##on", &en)) {
rows[k].enabled = en;
changed = true;
}
ImGui::SameLine();
ImGui::BeginDisabled(k == 0);
if (ImGui::ArrowButton("up", ImGuiDir_Up) && k > 0) {
std::swap(rows[k - 1], rows[k]);
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(k + 1 == int(rows.size()));
if (ImGui::ArrowButton("down", ImGuiDir_Down) && k + 1 < int(rows.size())) {
std::swap(rows[k], rows[k + 1]);
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
ImGui::BeginDisabled(k == 0);
bool joined = k > 0 && rows[k].joined;
if (ImGui::Checkbox("join", &joined)) {
rows[k].joined = joined;
changed = true;
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ShowcaseLayerAvailable(rows[k].layer->bit)) {
ImGui::TextUnformatted(rows[k].layer->label);
} else {
ImGui::TextDisabled("%s (feature off)", rows[k].layer->label);
}
ImGui::PopID();
}
if (changed) {
if (!rows.empty()) {
rows[0].joined = false;
}
REXCVAR_SET(skate3_native_render_scene_showcase_order,
BuildShowcaseOrder(rows));
}
if (ImGui::Button("Reset to default order")) {
REXCVAR_SET(skate3_native_render_scene_showcase_order,
std::string(skate3::native_scene::kShowcaseOrderDefault));
}
// Live preview of the resulting run, mirrors the sequencer's step
// builder (cumulative layer masks; materials subsumes albedo/lighting;
// a final full-render step covers whatever the list leaves out).
ImGui::SeparatorText("Run preview");
uint32_t avail_mask = 0;
for (const auto& layer : kShowcaseLayers) {
if (ShowcaseLayerAvailable(layer.bit)) {
avail_mask |= layer.bit;
}
}
int step_no = 1;
ImGui::Text("%d. clay geometry", step_no++);
uint32_t cum = 0;
size_t k = 0;
while (k < rows.size()) {
uint32_t add = 0;
std::string label;
size_t j = k;
do {
const ShowcaseRow& row = rows[j];
if (row.enabled && (avail_mask & row.layer->bit) != 0 &&
(cum & row.layer->bit) == 0 && (add & row.layer->bit) == 0) {
add |= row.layer->bit;
if (!label.empty()) {
label += " & ";
}
label += row.layer->label;
}
++j;
} while (j < rows.size() && rows[j].joined);
if (add != 0) {
cum |= add;
ImGui::Text("%d. + %s", step_no++, label.c_str());
}
k = j;
}
const bool covered = (cum & 4u) != 0 && ((avail_mask & ~cum) & 0xF8u) == 0u;
if (!covered) {
ImGui::Text("%d. full render", step_no++);
}
DrawFreecamControls();
ImGui::End();
}
// ---- F12 menu sections ----------------------------------------------------
// One CollapsingHeader per intent group. The groups the menu is actually
// opened for (capture tools, image quality) default open; tuning and
// diagnostics stay collapsed until needed.
void DrawShowcaseCaptureSection() {
DrawMaxQualityToggle();
{
const bool running = REXCVAR_GET(skate3_native_render_scene_showcase);
if (ImGui::Button(running ? "Cancel showcase" : "Start showcase (Ctrl+Shift+B)")) {
REXCVAR_SET(skate3_native_render_scene_showcase, !running);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Strip the frame to clay geometry, then rebuild it layer by\n"
"layer, each layer revealed by a live vertical split wiping\n"
"across the screen. Layer order, grouping and pacing are edited\n"
"in the setup window.");
}
ImGui::SameLine();
if (ImGui::Button("Setup...")) {
s_showcase_setup_open = true;
}
}
DrawFreecamControls();
}
void DrawImageQualitySection() {
{
static const int32_t kMsaaValues[] = {1, 2, 4, 8};
static const char* const kMsaaLabels[] = {"Off (1x)", "2x", "4x", "8x"};
REXCVAR_SET(skate3_native_render_scene_msaa,
ValueCombo("MSAA", REXCVAR_GET(skate3_native_render_scene_msaa),
kMsaaValues, kMsaaLabels, 4,
"Scene multisampling. Distant thin geometry "
"(railings, wires) shimmers without it. Applies "
"live (pipeline rebuild)."));
}
{
static const int32_t kTileValues[] = {0, 512, 1024, 2048, 4096};
static const char* const kTileLabels[] = {"Auto (render scale)", "512",
"1024", "2048", "4096"};
REXCVAR_SET(
skate3_native_render_scene_shadow_tile,
ValueCombo("shadow tile size",
REXCVAR_GET(skate3_native_render_scene_shadow_tile),
kTileValues, kTileLabels, 5,
"Dynamic-shadow cascade tile resolution. Auto matches the "
"emulated renderer's crispness at the current Resolution "
"Scale; 512 = the softer original-console look."));
}
{
static const int32_t kStaticValues[] = {1024, 2048, 4096, 8192};
static const char* const kStaticLabels[] = {"1024", "2048", "4096",
"8192"};
REXCVAR_SET(
skate3_native_render_scene_shadow_static_size,
ValueCombo("static sun map size",
REXCVAR_GET(skate3_native_render_scene_shadow_static_size),
kStaticValues, kStaticLabels, 4,
"Static sun-shadow map resolution per cascade tile (the "
"map is three tiles)."));
}
REXCVAR_SET(skate3_native_render_scene_ssao_full_res,
CvarCheckbox("Full-res SSAO",
REXCVAR_GET(skate3_native_render_scene_ssao_full_res),
"Evaluate SSAO at full output resolution instead "
"of half. ~4x the AO cost for slightly sharper "
"contact shadows."));
REXCVAR_SET(skate3_native_render_scene_hdr_packed,
CvarCheckbox("Packed HDR format (R11G11B10)",
REXCVAR_GET(skate3_native_render_scene_hdr_packed),
"Halves scene-pass color bandwidth vs RGBA16F at "
"slightly lower precision in very dark "
"gradients."));
}
void DrawLightingPostSection() {
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)."));
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_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_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."));
}
void DrawShadowsSection() {
REXCVAR_SET(skate3_native_render_scene_shadows,
CvarCheckbox("Dynamic shadows",
REXCVAR_GET(skate3_native_render_scene_shadows),
"Native CSM: skater/NPC/prop shadows onto the world"));
REXCVAR_SET(skate3_native_render_scene_shadow_static_casters,
CvarCheckbox(
"Static sun shadows",
REXCVAR_GET(skate3_native_render_scene_shadow_static_casters),
"Native static sun-shadow map: buildings, trees, rails and "
"placed props cast live shadows from a dedicated "
"sun-aligned depth map: shade on characters and props, "
"and shadows that follow a moved sun. The game's own "
"dynamic shadow cascades are untouched."));
REXCVAR_SET(
skate3_native_render_scene_shadow_static_strength,
CvarSlider(
"static shadow strength",
REXCVAR_GET(skate3_native_render_scene_shadow_static_strength),
0.0f, 1.0f, "%.2f",
"How dark static-geometry shadows get (characters/props always "
"cast at full strength). Lower if live static shade fights the "
"baked lighting."));
REXCVAR_SET(
skate3_native_render_scene_shadow_static_radius,
CvarSlider(
"static shadow radius (m)",
REXCVAR_GET(skate3_native_render_scene_shadow_static_radius), 40.0f,
600.0f, "%.0f",
"Far-cascade half-extent of the static sun-shadow map (mid and "
"inner cascades cover 1/2 and 1/6 at 2x/6x density). Larger "
"reaches farther at lower far-cascade density."));
REXCVAR_SET(skate3_native_render_scene_shadow_pcss,
CvarCheckbox(
"Soft shadows (PCSS)",
REXCVAR_GET(skate3_native_render_scene_shadow_pcss),
"Contact-hardening filter: crisp where a shadow touches "
"its caster, progressively softer with caster height."));
REXCVAR_SET(skate3_native_render_scene_shadow_pcss_sun_deg,
CvarSlider(
"sun angular size (deg)",
REXCVAR_GET(skate3_native_render_scene_shadow_pcss_sun_deg),
0.1f, 8.0f, "%.1f",
"Penumbra growth per meter of caster height (the real sun "
"is ~0.53 deg)."));
REXCVAR_SET(skate3_native_render_scene_shadow_pcss_max_m,
CvarSlider(
"max penumbra (m)",
REXCVAR_GET(skate3_native_render_scene_shadow_pcss_max_m),
0.05f, 5.0f, "%.2f"));
REXCVAR_SET(
skate3_native_render_scene_shadow_static_bias,
CvarSlider("static receive bias (m)",
REXCVAR_GET(skate3_native_render_scene_shadow_static_bias),
0.0f, 0.5f, "%.3f",
"Raise if static geometry shows self-shadow acne (stipple "
"on sunlit ground/walls); lower if static shadows visibly "
"detach from their casters."));
}
void DrawWorldShadingSection() {
REXCVAR_SET(skate3_native_render_scene_lightmaps,
CvarCheckbox("Lightmaps", REXCVAR_GET(skate3_native_render_scene_lightmaps),
"Baked lighting atlas sample x2 on world materials"));
REXCVAR_SET(skate3_native_render_scene_macro,
CvarCheckbox("Macro overlay", REXCVAR_GET(skate3_native_render_scene_macro),
"Large-scale grime/crack multiply (ground/wall weathering)"));
REXCVAR_SET(skate3_native_render_scene_decals,
CvarCheckbox("Decal art composite",
REXCVAR_GET(skate3_native_render_scene_decals),
"Graffiti/paint art lerped over environment.decal sections"));
REXCVAR_SET(skate3_native_render_scene_transparents,
CvarCheckbox("Transparent sub-pass",
REXCVAR_GET(skate3_native_render_scene_transparents),
"environment.transparent items (mist sheets, glass, fences)"));
REXCVAR_SET(skate3_native_render_scene_backface_cull,
CvarCheckbox("Backface cull (game parity)",
REXCVAR_GET(skate3_native_render_scene_backface_cull),
"World env materials cull FRONT like the game's material "
"XMLs; off = legacy cull-none (shows interior faces)"));
}
void DrawSceneContentSection() {
REXCVAR_SET(skate3_native_render_scene_world_items,
CvarCheckbox("World items", REXCVAR_GET(skate3_native_render_scene_world_items),
"Static geometry from the world sort lists"));
REXCVAR_SET(skate3_native_render_scene_dynamic_items,
CvarCheckbox("Dynamic items",
REXCVAR_GET(skate3_native_render_scene_dynamic_items),
"Characters, movable props, cloth (RenderMesh/world-path "
"captures)"));
REXCVAR_SET(
skate3_native_render_scene_lw_fade,
CvarCheckbox("LW entity fade (store)",
REXCVAR_GET(skate3_native_render_scene_lw_fade),
"Serve NPC/traffic fade alpha from the LivingWorld entity "
"itself (per-instance store) instead of the per-draw "
"captured constant row; fixes opaque mid-air spawns, "
"missing fade-ins and clone alpha blinks"));
REXCVAR_SET(
skate3_native_render_scene_lw_identity,
CvarCheckbox("LW entity identity (pose rings)",
REXCVAR_GET(skate3_native_render_scene_lw_identity),
"Key NPC/traffic pose-smoothing rings by the game's own "
"per-instance MeshContext instead of (mesh, occurrence) "
"pairing; clone reshuffles can no longer mispair "
"(teleport/slide class)"));
REXCVAR_SET(
skate3_native_render_scene_lw_gap_fill,
CvarCheckbox("LW gap fill (1-2 frame republish)",
REXCVAR_GET(skate3_native_render_scene_lw_gap_fill),
"Republish a live NPC whose MeshContext skipped this "
"frame's submit records (the 1-3 frame publish GAPs that "
"read as blinks/small teleports)"));
REXCVAR_SET(
skate3_native_render_scene_lw_palette,
CvarCheckbox("LW authoritative caster palettes",
REXCVAR_GET(skate3_native_render_scene_lw_palette),
"Replace GUESSED ortho caster-bank palettes on "
"edge-of-view vehicles with the entity's own packed "
"palette from the pack writer (the mangle/transform "
"class)"));
REXCVAR_SET(skate3_native_render_scene_quadlists,
CvarCheckbox("Quad-list particles",
REXCVAR_GET(skate3_native_render_scene_quadlists),
"Non-indexed quad-list captures (particle systems; off by "
"default: render as white squares without sprite textures)"));
REXCVAR_SET(
skate3_native_render_scene_retain_offscreen,
CvarCheckbox("Retain off-screen statics",
REXCVAR_GET(skate3_native_render_scene_retain_offscreen),
"Keep recently seen statics drawn while the game view-culls "
"them: the smoothed render camera trails the guest pose, so "
"without this world geometry visibly tears down right at the "
"screen edges during pans/traversal"));
REXCVAR_SET(skate3_native_render_scene_2d,
CvarCheckbox("2D / HUD replay", REXCVAR_GET(skate3_native_render_scene_2d),
"APT/Flash HUD + glyph text + SimpleDraw icons"));
REXCVAR_SET(skate3_native_render_scene_splines,
CvarCheckbox("Neon splines", REXCVAR_GET(skate3_native_render_scene_splines),
"Waypoint arrows / marker beams"));
}
void DrawPacingSection() {
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)."));
{
float w = float(REXCVAR_GET(skate3_native_render_scene_smooth_camera_filter_ms));
if (ImGui::SliderFloat("camera filter (ms, 0 = off)", &w, 0.0f, 100.0f, "%.0f")) {
REXCVAR_SET(skate3_native_render_scene_smooth_camera_filter_ms, double(w));
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Boxcar average over the camera pose signal. The game's camera "
"advances in 60 Hz-quantized lumps at high fps (the measured cause "
"of stick-pan stutter); 50 ms = three 60 Hz periods cancels it "
"exactly for ~25 ms extra camera latency. 0 reverts to raw "
"interpolation.");
}
}
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).");
}
}
}
void DrawDiagnosticsSection() {
{
int mode = REXCVAR_GET(skate3_native_render_scene_debug);
const char* kModes[] = {"0: normal", "1: clear only", "2: solid colors",
"3: first 20 items", "4: no depth"};
if (ImGui::Combo("debug mode", &mode, kModes, 5)) {
REXCVAR_SET(skate3_native_render_scene_debug, mode);
}
}
{
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);
}
}
ImGui::SeparatorText("Reflective glass isolation (env fam 5/6/13)");
{
int mode = REXCVAR_GET(skate3_native_render_scene_refl_mode);
const char* kReflModes[] = {"0: normal",
"1: cube reflection OFF",
"2: cube at absolute LOD (slider)",
"3: flat normal (no normal map)",
"4: visualize cube sample only",
"5: body only (no spec, no cube)",
"6: normal-map LOD bias (slider)",
"7: visualize lightmap sample",
"8: visualize lightmap UV (frac x16)",
"9: lightmap resolve status (blue = missing)"};
if (ImGui::Combo("refl mode", &mode, kReflModes, 10)) {
REXCVAR_SET(skate3_native_render_scene_refl_mode, mode);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Live isolation of the glass-facade reflection term. Flip F5 to "
"compare against the emulated frame at each setting:\n"
"1 removes the cube term; if the artifact survives, it is NOT "
"the reflection.\n2 + slider finds which mip level (if any) "
"matches the emulated look.\n3 tests the normal-map perturbation."
"\n4 shows exactly what the reflection vector samples.\n5 leaves "
"only diffuse x lightmap.");
}
float lod = float(REXCVAR_GET(skate3_native_render_scene_refl_lod));
if (ImGui::SliderFloat("refl LOD / extra bias", &lod, -4.0f, 12.0f, "%.1f")) {
REXCVAR_SET(skate3_native_render_scene_refl_lod, double(lod));
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Mode 2: the absolute cube mip level (0 = sharpest; ~9 = the "
"face average).\nOther modes: extra LOD bias added to the "
"automatic 640p-parity bias.");
}
REXCVAR_SET(skate3_native_render_scene_refl_bias_auto,
CvarCheckbox("auto normal tilt (derive from material)",
REXCVAR_GET(skate3_native_render_scene_refl_bias_auto),
"Compute the constant tilt from each material's own "
"detail texture (hardware-exact BC1 decode), the "
"principled source of the hand-tuned reference "
"values. Uncheck to drive the sliders below "
"instead."));
float bx = float(REXCVAR_GET(skate3_native_render_scene_refl_bias_x));
if (ImGui::SliderFloat("normal tilt X (horizontal)", &bx, -0.2f, 0.2f, "%.3f")) {
REXCVAR_SET(skate3_native_render_scene_refl_bias_x, double(bx));
}
float by = float(REXCVAR_GET(skate3_native_render_scene_refl_bias_y));
if (ImGui::SliderFloat("normal tilt Y (vertical)", &by, -0.2f, 0.2f, "%.3f")) {
REXCVAR_SET(skate3_native_render_scene_refl_bias_y, double(by));
}
if (ImGui::IsItemHovered() || ImGui::IsItemActive()) {
ImGui::SetTooltip(
"Constant normal tilt on the glass, applied in EVERY mode; "
"rotates all reflections. Drag until the reflected content sits "
"where the emulated frame puts it (F5 to compare). Defaults "
"(0.028, 0.012) = the exact detail-map constant. 0.01 here is "
"roughly 1 degree of reflection rotation.");
}
}
ImGui::SeparatorText("Judder isolation");
{
int mode = REXCVAR_GET(skate3_native_render_scene_synthetic_pan);
const char* kPanModes[] = {"0: off", "1: time-based (build clock)",
"2: fixed step per frame", "3: through smoother"};
if (ImGui::Combo("Synthetic pan (P)", &mode, kPanModes, 4)) {
REXCVAR_SET(skate3_native_render_scene_synthetic_pan, mode);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Judder isolation probe: replaces the camera with a host-generated "
"constant-rate 360-degree pan. AFTER ENGAGING, SWEEP THE REAL CAMERA "
"AROUND ONCE WITH THE STICK; the game only submits what its own "
"frustum sees; the sweep fills a static-world union so the full "
"circle is populated.\n1 smooth = camera path is fine; 1 judders = "
"frame pacing / present / display, not the camera.\n2 = constant "
"angle per FRAME (smooth only if displayed frames are evenly spaced "
", the complement of 1).\n3 = feeds synthetic ~200 Hz samples "
"through the camera smoother and logs reconstruction error "
"(synthetic-pan: lines).");
}
float rate = float(REXCVAR_GET(skate3_native_render_scene_synthetic_pan_rate));
if (ImGui::SliderFloat("pan rate (deg/s)", &rate, 5.0f, 720.0f, "%.0f")) {
REXCVAR_SET(skate3_native_render_scene_synthetic_pan_rate, double(rate));
}
float amp = float(REXCVAR_GET(skate3_native_render_scene_synthetic_pan_amp));
if (ImGui::SliderFloat("pan amplitude (0 = full spin)", &amp, 0.0f, 60.0f, "%.0f")) {
REXCVAR_SET(skate3_native_render_scene_synthetic_pan_amp, double(amp));
}
if (ImGui::Button("Record camera signal (8 s)")) {
skate3::native_scene::RecordCameraSignal(8.0);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Click, then IMMEDIATELY pan the camera with the right stick at a "
"steady rate for 8 seconds (synthetic pan should be OFF). Records "
"every distinct guest camera pose with 1 kHz-sampler timestamps + "
"the smoothed output, to logs\\cam_signal_<ts>.csv; offline "
"analysis shows whether the game's own camera signal is jerky at "
"the source.");
}
ImGui::SameLine();
if (ImGui::Button("Record bone signal (6 s)")) {
skate3::native_scene::RecordBoneSignal(6.0);
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Click, then skate at a steady speed for 6 seconds. Records every "
"raw entity pose (bone palettes with timestamps) + the rendered "
"interpolation output to logs\\bone_signal_<ts>.bin, so wheel-vs-"
"deck lag/orbit can be measured and candidate fixes replayed "
"offline against real data.");
}
}
}
void DrawCachesSection() {
REXCVAR_SET(
skate3_native_render_scene_tex_revalidate,
CvarCheckbox("Texture payload revalidation",
REXCVAR_GET(skate3_native_render_scene_tex_revalidate),
"Re-fingerprint cached texture payloads every 16 frames, re-decode "
"on change (added to heal late-composed lightmap pages)"));
REXCVAR_SET(skate3_native_render_scene_mesh_revalidate,
CvarCheckbox("Mesh payload revalidation",
REXCVAR_GET(skate3_native_render_scene_mesh_revalidate),
"Re-decode cached meshes when the guest payload fingerprint "
"changes (streaming arena reuse / CPU-animated buffers)"));
REXCVAR_SET(skate3_native_render_scene_tex_mips,
CvarCheckbox("Texture mip chains",
REXCVAR_GET(skate3_native_render_scene_tex_mips),
"Upload guest mip chains (off = mip 0 only). Flush the "
"texture cache after toggling."));
if (ImGui::Button("Flush texture cache")) {
skate3::native_scene::FlushTextureCache();
}
ImGui::SameLine();
if (ImGui::Button("Flush mesh cache")) {
skate3::native_scene::FlushMeshCache();
}
}
} // namespace
void MaxQualityAutoCycle(uint64_t frame_number) {
const int32_t period = REXCVAR_GET(skate3_native_render_scene_maxq_cycle);
if (period <= 0) {
return;
}
static uint64_t next_frame = 0;
if (next_frame == 0) {
next_frame = frame_number + uint64_t(period);
return;
}
if (frame_number >= next_frame) {
next_frame = frame_number + uint64_t(period);
SetMaxQuality(!s_max_quality.active);
REXLOG_INFO("native-scene: max-quality auto-cycle -> {}",
s_max_quality.active ? "ON" : "OFF");
}
}
void NativeDebugDialog::Show() {
visible_ = true;
SetDrawActive(true);
}
void NativeDebugDialog::Hide() {
if (!visible_) {
return;
}
visible_ = false;
SetDrawActive(false);
}
void NativeDebugDialog::Toggle() {
if (visible_) {
Hide();
} else {
Show();
}
}
void NativeDebugDialog::OnDraw(ImGuiIO& io) {
(void)io;
if (!visible_) {
return;
}
bool open = visible_;
ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Native Render Debug (F12)", &open, ImGuiWindowFlags_NoCollapse)) {
ImGui::End();
if (!open) {
Hide();
}
return;
}
// Renderer master switches stay visible above the sections: they gate
// everything below and are the first thing checked when bisecting.
if (!REXCVAR_GET(skate3_native_render)) {
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f),
"skate3_native_render hook layer is OFF (boot-time)");
}
{
const bool v = CvarCheckbox("Native scene renderer (F5)",
REXCVAR_GET(skate3_native_render_scene),
"Full native/emulated switch, same as F5");
if (v != REXCVAR_GET(skate3_native_render_scene)) {
skate3::native_scene::ToggleSceneEnabled();
}
}
REXCVAR_SET(native_render_suppress_emulated_draws,
CvarCheckbox("Suppress emulated draws",
REXCVAR_GET(native_render_suppress_emulated_draws),
"Skip emulated GPU work for framebuffer-sized passes while "
"native output is active (perf). Small-surface passes "
"(lightmap page composition) always run."));
if (ImGui::CollapsingHeader("Showcase & capture",
ImGuiTreeNodeFlags_DefaultOpen)) {
DrawShowcaseCaptureSection();
}
if (ImGui::CollapsingHeader("Image quality",
ImGuiTreeNodeFlags_DefaultOpen)) {
DrawImageQualitySection();
}
if (ImGui::CollapsingHeader("Lighting & post")) {
DrawLightingPostSection();
}
if (ImGui::CollapsingHeader("Shadows")) {
DrawShadowsSection();
}
if (ImGui::CollapsingHeader("World shading")) {
DrawWorldShadingSection();
}
if (ImGui::CollapsingHeader("Scene content & overlays")) {
DrawSceneContentSection();
}
if (ImGui::CollapsingHeader("Smoothness & pacing")) {
DrawPacingSection();
}
if (ImGui::CollapsingHeader("Diagnostics")) {
DrawDiagnosticsSection();
}
if (ImGui::CollapsingHeader("Caches")) {
DrawCachesSection();
}
ImGui::End();
if (s_showcase_setup_open) {
DrawShowcaseSetupWindow(&s_showcase_setup_open);
}
if (!open) {
Hide();
}
}
void RenderModeIndicator::OnDraw(ImGuiIO& io) {
// Force-show while the native scene renderer is switched off (F5, the
// settings Renderer row, or the boot-time hook-layer master) or has hit an
// unrecoverable failure and permanently yields to the emulated output:
// running emulated is a degraded state the player should be able to see
// even with the indicator cvar off. Natural per-frame yields while the
// scene renderer is enabled (menus/loading) do not trigger this.
const bool scene_off = !REXCVAR_GET(skate3_native_render) ||
!REXCVAR_GET(skate3_native_render_scene) ||
skate3::native_scene::SceneFailed();
if (!REXCVAR_GET(skate3_native_render_mode_indicator) && !scene_off) {
return;
}
// Pre-runtime (installer wizards) no guest frame exists yet - there is no
// renderer to indicate.
if (rex::ui::Presenter* presenter = imgui_drawer()->presenter()) {
const rex::ui::Presenter::GuestOutputPaintRect rect =
presenter->GetLastGuestOutputPaintRect();
if (rect.width <= 0 || rect.height <= 0) {
return;
}
}
const bool native = rex::graphics::IsNativeGuestOutputActive();
ImGui::SetNextWindowPos(ImVec2(io.DisplaySize.x - 10.0f, 10.0f), ImGuiCond_Always,
ImVec2(1.0f, 0.0f));
ImGui::SetNextWindowBgAlpha(0.35f);
ImGui::Begin("##render_mode_indicator", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs |
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav);
ImGui::TextColored(native ? ImVec4(0.35f, 1.0f, 0.45f, 1.0f)
: ImVec4(1.0f, 0.65f, 0.25f, 1.0f),
"%s", native ? "NATIVE" : "EMULATED");
ImGui::End();
}
} // namespace skate3