mirror of
https://github.com/sal063/AC6_recomp
synced 2026-08-21 23:00:53 -04:00
Fix cvar preset timing and settings-save leaks; tidy the cvar surface
Config presets now run after the toml loads: the ac6_fix_* switches and their scale gates finally honor user settings. The settings save persists only user-set values, with toml escaping - no leaked presets, and a saved Windows path can no longer void the whole config. Runtime-forced values (performance mode) can no longer overwrite the user's saved choices. Menu: wrapped tooltips, "(set by <cvar>)" on driven entries, a developer toggle; AC6-added cvars grouped under the AC6 node as AC6/Fixes and AC6/Enhancements, with true restart/live lifecycles.
This commit is contained in:
@@ -36,23 +36,13 @@
|
||||
#include <rex/ppc.h>
|
||||
#include <rex/system/kernel_state.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_cutscene_resync, true, "AC6",
|
||||
"Keep in-engine cutscene video locked to its audio. Audio "
|
||||
"is the master clock and is never altered; after a render "
|
||||
"hitch (or under sustained slow rendering) the cutscene "
|
||||
"timeline catches up by running extra sequencer ticks - a "
|
||||
"bounded frame-skip, like every film player's audio-master "
|
||||
"sync. No effect on normal full-speed playback (catch-up "
|
||||
"engages only past 2 ticks of drift; verified extras=0 in "
|
||||
"steady state, injected-hitch recovery in 3 frames, and "
|
||||
"sustained 14fps @ 5x draw scale staying locked).");
|
||||
REXCVAR_DEFINE_INT32(ac6_cutscene_resync_max_ticks, 3, "AC6",
|
||||
"Max extra cutscene sequencer ticks per rendered frame "
|
||||
"while catching up (ac6_cutscene_resync). Also sets the "
|
||||
"slowest sustained render rate that stays in sync: N "
|
||||
"extras holds sync down to 30/(1+N) fps (3 -> 7.5 fps). "
|
||||
"Low = gentle brief fast-forward after a stall; 0 = "
|
||||
"uncapped, one hard jump-cut.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_cutscene_resync, true, "AC6/Fixes",
|
||||
"Keep cutscene video locked to its audio after render hitches "
|
||||
"by letting the timeline catch up (a bounded frame-skip).");
|
||||
REXCVAR_DEFINE_INT32(ac6_cutscene_resync_max_ticks, 3, "AC6/Fixes",
|
||||
"Max catch-up cutscene sequencer ticks per rendered frame "
|
||||
"during resync. 0 = uncapped (one hard jump-cut).")
|
||||
.debug_only();
|
||||
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_82184460); // CAce6DemoManager::Exec ("DD")
|
||||
PPC_EXTERN_FUNC(__imp__rex_sub_821856F8); // CX360DemoManagerEM::Exec ("EM")
|
||||
|
||||
@@ -67,13 +67,9 @@
|
||||
#include <rex/ppc.h>
|
||||
|
||||
REXCVAR_DEFINE_BOOL(
|
||||
ac6_effect_mode_fix, false, "AC6",
|
||||
"Fix the one-frame effects/clouds flicker under host CPU pressure. The effect update "
|
||||
"and the effect draw are two async worker jobs sharing one vtable slot, told apart only "
|
||||
"by a plain-store mode word with no per-job copy; when a host stall lets the second "
|
||||
"submit's write land first, both jobs draw and the effect + cloud passes are emitted "
|
||||
"twice. This snapshots the mode at submit and dispatches from the snapshot. Off by "
|
||||
"default.");
|
||||
ac6_effect_mode_fix, false, "AC6/Fixes",
|
||||
"Fix the one-frame effects/clouds flicker under heavy CPU load by "
|
||||
"snapshotting the effect job mode at submit. Off by default.");
|
||||
|
||||
// Dispatch targets, called directly. NOT via PPC_CALL_INDIRECT_FUNC: that macro
|
||||
// degrades to __builtin_debugtrap() unless ppc_config.h was included first,
|
||||
|
||||
@@ -67,14 +67,17 @@
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_enabled, false, "AC6/KBM",
|
||||
"Enable keyboard+mouse input (default off; set true in ac6recomp.toml)");
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_log, false, "AC6/KBM",
|
||||
"Log KBM diagnostics to the game log (info level)");
|
||||
REXCVAR_DEFINE_STRING(ac6_kbm_config, "ac6_input.toml", "AC6/KBM",
|
||||
"Path to the keyboard+mouse binding config");
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_padless, true, "AC6/KBM",
|
||||
"Present a synthetic pad on slot 0 when no controller is connected");
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_enabled, false, "AC6/Enhancements",
|
||||
"Enable keyboard and mouse controls.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_log, false, "AC6/Enhancements",
|
||||
"Log keyboard and mouse input diagnostics.")
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_STRING(ac6_kbm_config, "ac6_input.toml", "AC6/Enhancements",
|
||||
"Path to the keyboard and mouse bindings file. Edits to the "
|
||||
"file are picked up live.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_kbm_padless, true, "AC6/Enhancements",
|
||||
"Provide a virtual controller when none is connected, so "
|
||||
"keyboard and mouse work on their own.");
|
||||
|
||||
// The SDK's own mnk virtual-pad driver (input/mnk) - forced off while our
|
||||
// KB+M is enabled so two keyboard mappers never fight over the same pad.
|
||||
|
||||
@@ -50,29 +50,12 @@
|
||||
#include "../render_hooks.h"
|
||||
#include "ac6_widescreen.h"
|
||||
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen, false, "AC6",
|
||||
"Arbitrary aspect ratio (hor+), in-mission only. The target aspect "
|
||||
"is derived from the actual window size (re-checked continuously; "
|
||||
"a live resize adapts). While the game's mode task is the mission "
|
||||
"(gameplay, in-engine cutscenes, pause), cameras are patched from "
|
||||
"16:9 to the window aspect, the presenter fills the window, and "
|
||||
"the 2D layer is pre-squeezed at draw time for a crisp 16:9 HUD "
|
||||
"(the game-placed target-marker shader stays full-width by design; "
|
||||
"its art is squared by narrowing the geometry). Everywhere else "
|
||||
"(menus, hangar, briefing, FMV, attract) and at 16:9-or-narrower "
|
||||
"windows, cameras stay 16:9 and the presentation letterboxes - "
|
||||
"vanilla. While enabled the feature drives the presenter's "
|
||||
"letterbox decision itself; it never writes the present_letterbox "
|
||||
"cvar, so nothing leaks into a saved config.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen_cinematics, true, "AC6",
|
||||
"With ac6_widescreen: render in-engine cinematics wide too, even "
|
||||
"outside the mission mode task (detected via the demo-manager "
|
||||
"cinematic signal, the same one the cutscene frame-rate clamp "
|
||||
"uses - so ac6_cutscene_clamp=false disables this detection). "
|
||||
"Cinematics are staged and framed for 16:9, so widening can "
|
||||
"expose things outside that frame (set edges, actors popping in "
|
||||
"at the frustum border). Off = cinematics letterbox at 16:9 "
|
||||
"as staged.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen, false, "AC6/Enhancements",
|
||||
"Ultrawide support (hor+), in missions only. The aspect follows "
|
||||
"the window size; menus and the front end stay vanilla 16:9.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_widescreen_cinematics, true, "AC6/Enhancements",
|
||||
"With ultrawide on, render in-engine cinematics wide too. They "
|
||||
"are staged for 16:9, so widening can expose set edges.");
|
||||
// Read (never written) only to report it in the activation log.
|
||||
REXCVAR_DECLARE(bool, present_letterbox);
|
||||
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_backend);
|
||||
|
||||
// Defined in main.cpp: the config preset suite (session defaults + fix
|
||||
// payload expansion) and its post-logging summary lines.
|
||||
void ApplyAc6ConfigPresets();
|
||||
void LogAc6ConfigPresetSummary();
|
||||
|
||||
class Ac6recompApp : public rex::ReXApp {
|
||||
public:
|
||||
using rex::ReXApp::ReXApp;
|
||||
@@ -40,9 +45,15 @@ class Ac6recompApp : public rex::ReXApp {
|
||||
// and rejects wrong/corrupt images with a clear message.
|
||||
uint32_t OnGetExpectedTitleId() const override { return 0x4E4D07D1; }
|
||||
|
||||
// Runs right after the toml is parsed and before anything consumes cvars:
|
||||
// the one place presets can both see the user's real values and still act
|
||||
// before logging/window/graphics read the results.
|
||||
void OnConfigLoaded() override { ApplyAc6ConfigPresets(); }
|
||||
|
||||
void OnPreSetup(rex::RuntimeConfig& config) override {
|
||||
REXLOG_INFO("Ac6recompApp::OnPreSetup");
|
||||
rex::ReXApp::OnPreSetup(config);
|
||||
LogAc6ConfigPresetSummary();
|
||||
|
||||
const std::string requested_backend = REXCVAR_GET(ac6_graphics_backend);
|
||||
#if REX_HAS_VULKAN
|
||||
|
||||
+130
-116
@@ -12,37 +12,20 @@ REXCVAR_DECLARE(bool, ac6_native_graphics_enabled);
|
||||
REXCVAR_DECLARE(bool, ac6_force_safe_draw_resolution_scale);
|
||||
REXCVAR_DECLARE(bool, ac6_force_safe_direct_host_resolve);
|
||||
REXCVAR_DECLARE(std::string, ac6_graphics_mode);
|
||||
REXCVAR_DECLARE(bool, direct_host_resolve);
|
||||
REXCVAR_DECLARE(int32_t, resolution_scale);
|
||||
REXCVAR_DECLARE(int32_t, draw_resolution_scale_x);
|
||||
REXCVAR_DECLARE(int32_t, draw_resolution_scale_y);
|
||||
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
|
||||
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
|
||||
REXCVAR_DECLARE(std::string, ac6_neutralize_deswizzle_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_snap_guest_texel_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_x_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, ac6_densify_y_fetch_hashes);
|
||||
REXCVAR_DECLARE(std::string, log_file);
|
||||
REXCVAR_DECLARE(bool, param_gen_integer_guest_position);
|
||||
REXCVAR_DECLARE(bool, param_gen_host_subpixel_restore);
|
||||
REXCVAR_DECLARE(std::string, log_level);
|
||||
REXCVAR_DECLARE(bool, ac6_d3d_trace);
|
||||
REXCVAR_DECLARE(bool, ac6_backend_debug_swap);
|
||||
REXCVAR_DECLARE(bool, ac6_backend_log_signatures);
|
||||
REXCVAR_DECLARE(bool, ac6_backend_signature_diagnostics);
|
||||
REXCVAR_DECLARE(bool, ac6_texture_swaps_dump_enabled);
|
||||
REXCVAR_DECLARE(bool, vsync);
|
||||
REXCVAR_DECLARE(bool, guest_vblank_sync_to_refresh);
|
||||
REXCVAR_DECLARE(bool, host_present_from_non_ui_thread);
|
||||
#if REX_HAS_D3D12
|
||||
REXCVAR_DECLARE(bool, d3d12_allow_variable_refresh_rate_and_tearing);
|
||||
REXCVAR_DECLARE(bool, d3d12_low_latency_swap_chain);
|
||||
REXCVAR_DECLARE(int32_t, d3d12_max_frame_latency);
|
||||
#endif
|
||||
REXCVAR_DECLARE(bool, vfetch_index_rounding_bias);
|
||||
REXCVAR_DECLARE(int32_t, video_mode_width);
|
||||
REXCVAR_DECLARE(int32_t, video_mode_height);
|
||||
REXCVAR_DECLARE(std::string, resolution);
|
||||
REXCVAR_DECLARE(int32_t, window_width);
|
||||
REXCVAR_DECLARE(int32_t, window_height);
|
||||
|
||||
#if REX_HAS_VULKAN
|
||||
#define AC6_DEFAULT_GRAPHICS_BACKEND "vulkan"
|
||||
@@ -61,25 +44,21 @@ REXCVAR_DEFINE_STRING(ac6_graphics_backend, AC6_DEFAULT_GRAPHICS_BACKEND,
|
||||
REXCVAR_DEFINE_BOOL(ac6_performance_mode, true, "AC6/Performance",
|
||||
"Disable all diagnostics, logging, and development overlays for maximum runtime performance");
|
||||
|
||||
// Correctness-fix switches. Each expands
|
||||
// to its detailed cvars + AC6-specific shader hashes in ApplyAc6FixDefaults()
|
||||
// unless the user overrode them.
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_scaling, true, "AC6",
|
||||
"Fix the upscaling mosaic on AC6's deferred EDRAM restore passes by "
|
||||
"flooring the PsParamGen pixel position (+ host sub-pixel restore). "
|
||||
"Auto-applies only at draw resolution scale > 1; inert at 1x. On by default.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_deswizzle, true, "AC6",
|
||||
"Fix the de-swizzle mosaic/streaks: identity-override AC6's manual EDRAM "
|
||||
"sub-tile de-swizzle, which is always a wrong texel permutation once the "
|
||||
"emulator detiles to linear. On by default; effective at all draw scales "
|
||||
"(the swizzle is present at 1x too).");
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_dof, true, "AC6",
|
||||
"Fix the in-engine cutscene depth-of-field striping/ghosting at draw "
|
||||
"resolution scale > 1: the 6-pass DoF chain is authored for the 640x360 "
|
||||
"grid, so its guest-texel kernels alias against the extra detail of "
|
||||
"scaled sources (combed CoC weights, ghost copies at the sparse gather "
|
||||
"taps). Snaps the CoC pre-blurs to the guest grid and densifies the "
|
||||
"gather taps along each pass's axis. On by default; inert at 1x.");
|
||||
// Correctness-fix switches. Each expands to its detailed cvars + AC6-specific
|
||||
// shader hashes in ApplyAc6FixDefaults() unless the user overrode them. The
|
||||
// payloads apply at shader translation, so a change needs a restart.
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_scaling, true, "AC6/Fixes",
|
||||
"Fix the upscaling mosaic on deferred rendering passes. Only "
|
||||
"active at resolution scale above 1x.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_deswizzle, true, "AC6/Fixes",
|
||||
"Fix mosaic and streak artifacts from the game's manual texture "
|
||||
"de-swizzle. Effective at all resolution scales.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_dof, true, "AC6/Fixes",
|
||||
"Fix cutscene depth-of-field striping and ghosting at resolution "
|
||||
"scale above 1x.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
|
||||
#include "generated/ac6recomp_config.h"
|
||||
#include "generated/ac6recomp_init.h"
|
||||
@@ -105,63 +84,69 @@ void ApplyAc6HybridStartupSafetyOverrides() {
|
||||
}
|
||||
|
||||
if (REXCVAR_GET(ac6_force_safe_draw_resolution_scale)) {
|
||||
REXCVAR_SET(resolution_scale, 1);
|
||||
REXCVAR_SET(draw_resolution_scale_x, 1);
|
||||
REXCVAR_SET(draw_resolution_scale_y, 1);
|
||||
rex::cvar::SetSessionDefault("resolution_scale", "1",
|
||||
"ac6_force_safe_draw_resolution_scale");
|
||||
rex::cvar::SetSessionDefault("draw_resolution_scale_x", "1",
|
||||
"ac6_force_safe_draw_resolution_scale");
|
||||
rex::cvar::SetSessionDefault("draw_resolution_scale_y", "1",
|
||||
"ac6_force_safe_draw_resolution_scale");
|
||||
}
|
||||
// (The >1x param-gen mosaic fix is applied by ApplyAc6FixDefaults via
|
||||
// ac6_fix_scaling.)
|
||||
|
||||
if (REXCVAR_GET(ac6_force_safe_direct_host_resolve)) {
|
||||
REXCVAR_SET(direct_host_resolve, false);
|
||||
rex::cvar::SetSessionDefault("direct_host_resolve", "false",
|
||||
"ac6_force_safe_direct_host_resolve");
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyAc6DefaultSettings() {
|
||||
if (!rex::cvar::HasNonDefaultValue("vsync")) {
|
||||
REXCVAR_SET(vsync, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("guest_vblank_sync_to_refresh")) {
|
||||
REXCVAR_SET(guest_vblank_sync_to_refresh, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("host_present_from_non_ui_thread")) {
|
||||
REXCVAR_SET(host_present_from_non_ui_thread, true);
|
||||
}
|
||||
rex::cvar::SetSessionDefault("vsync", "true");
|
||||
rex::cvar::SetSessionDefault("guest_vblank_sync_to_refresh", "true");
|
||||
rex::cvar::SetSessionDefault("host_present_from_non_ui_thread", "true");
|
||||
#if REX_HAS_D3D12
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_allow_variable_refresh_rate_and_tearing")) {
|
||||
REXCVAR_SET(d3d12_allow_variable_refresh_rate_and_tearing, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_low_latency_swap_chain")) {
|
||||
REXCVAR_SET(d3d12_low_latency_swap_chain, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("d3d12_max_frame_latency")) {
|
||||
REXCVAR_SET(d3d12_max_frame_latency, 1);
|
||||
}
|
||||
rex::cvar::SetSessionDefault("d3d12_allow_variable_refresh_rate_and_tearing", "true");
|
||||
rex::cvar::SetSessionDefault("d3d12_low_latency_swap_chain", "true");
|
||||
rex::cvar::SetSessionDefault("d3d12_max_frame_latency", "1");
|
||||
#endif
|
||||
if (!rex::cvar::HasNonDefaultValue("vfetch_index_rounding_bias")) {
|
||||
REXCVAR_SET(vfetch_index_rounding_bias, true);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("direct_host_resolve")) {
|
||||
REXCVAR_SET(direct_host_resolve, false);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("video_mode_width")) {
|
||||
REXCVAR_SET(video_mode_width, 1920);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("video_mode_height")) {
|
||||
REXCVAR_SET(video_mode_height, 1080);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("resolution")) {
|
||||
REXCVAR_SET(resolution, "1080p");
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("window_width")) {
|
||||
REXCVAR_SET(window_width, 1920);
|
||||
}
|
||||
if (!rex::cvar::HasNonDefaultValue("window_height")) {
|
||||
REXCVAR_SET(window_height, 1080);
|
||||
}
|
||||
rex::cvar::SetSessionDefault("vfetch_index_rounding_bias", "true");
|
||||
rex::cvar::SetSessionDefault("direct_host_resolve", "false");
|
||||
rex::cvar::SetSessionDefault("video_mode_width", "1920");
|
||||
rex::cvar::SetSessionDefault("video_mode_height", "1080");
|
||||
rex::cvar::SetSessionDefault("resolution", "1080p");
|
||||
rex::cvar::SetSessionDefault("window_width", "1920");
|
||||
rex::cvar::SetSessionDefault("window_height", "1080");
|
||||
}
|
||||
|
||||
void ApplyAc6PerformanceModeOverrides() {
|
||||
if (REXCVAR_GET(ac6_performance_mode)) {
|
||||
// Session-default RECORDING: gives the settings menu the "(set by
|
||||
// ac6_performance_mode)" annotation and the boot-time value. The
|
||||
// actual enforcement stays the pre-existing per-frame re-assert
|
||||
// (EnforceAc6PerformanceModeOverrides below) - these cvars predate
|
||||
// this rework and keep their upstream semantics: performance mode
|
||||
// forces them off while enabled.
|
||||
rex::cvar::SetSessionDefault("log_level", "error", "ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_d3d_trace", "false", "ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_render_capture", "false", "ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_backend_debug_swap", "false", "ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_backend_log_signatures", "false",
|
||||
"ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_backend_signature_diagnostics", "false",
|
||||
"ac6_performance_mode");
|
||||
rex::cvar::SetSessionDefault("ac6_texture_swaps_dump_enabled", "false",
|
||||
"ac6_performance_mode");
|
||||
} else {
|
||||
rex::cvar::SetSessionDefault("log_level", "debug");
|
||||
}
|
||||
}
|
||||
|
||||
// The pre-existing enforcement, unchanged from before this rework: while
|
||||
// performance mode is on, the diagnostics are re-forced off every frame from
|
||||
// the graphics overlay. These writes go through REXCVAR_SET (raw storage), so
|
||||
// they no longer leak into a settings-menu save - the save writes the user's
|
||||
// own values (FlagEntry::user_value).
|
||||
void EnforceAc6PerformanceModeOverrides() {
|
||||
if (!REXCVAR_GET(ac6_performance_mode)) {
|
||||
return;
|
||||
}
|
||||
@@ -174,45 +159,86 @@ void ApplyAc6PerformanceModeOverrides() {
|
||||
REXCVAR_SET(ac6_texture_swaps_dump_enabled, false);
|
||||
}
|
||||
|
||||
// Each fix switch -> its detailed cvars + AC6-specific shader hashes. The
|
||||
// game-specific constants live here in code, not in the user's toml; each
|
||||
// value is filled in only if the user did NOT set it explicitly, so any toml
|
||||
// override still wins (the dev/debug path).
|
||||
#define AC6_SET_IF_UNSET(cv, value) \
|
||||
do { if (!rex::cvar::HasNonDefaultValue(#cv)) REXCVAR_SET(cv, value); } while (0)
|
||||
|
||||
void ApplyAc6FixDefaults() {
|
||||
// Post-config, these read the USER's draw scale - with the old app-create
|
||||
// timing this gate always saw the default 1x, so the scaling fix payload
|
||||
// never engaged for anyone who set their scale in the toml.
|
||||
const bool scaled = REXCVAR_GET(draw_resolution_scale_x) > 1 ||
|
||||
REXCVAR_GET(draw_resolution_scale_y) > 1;
|
||||
|
||||
// param_gen floor + host sub-pixel restore only bites at >1x --
|
||||
// inert at 1x, so gate it on the draw scale.
|
||||
if (REXCVAR_GET(ac6_fix_scaling) && scaled) {
|
||||
AC6_SET_IF_UNSET(param_gen_integer_guest_position, true);
|
||||
AC6_SET_IF_UNSET(param_gen_host_subpixel_restore, true);
|
||||
rex::cvar::SetSessionDefault("param_gen_integer_guest_position", "true",
|
||||
"ac6_fix_scaling");
|
||||
rex::cvar::SetSessionDefault("param_gen_host_subpixel_restore", "true",
|
||||
"ac6_fix_scaling");
|
||||
}
|
||||
// de-swizzle identity override is a texture-layout mismatch present
|
||||
// at ALL draw scales, so it is NOT gated on scale.
|
||||
if (REXCVAR_GET(ac6_fix_deswizzle)) {
|
||||
AC6_SET_IF_UNSET(ac6_neutralize_deswizzle_hashes,
|
||||
"7d22894002d16018, 17e5e4ac3e713245:4");
|
||||
rex::cvar::SetSessionDefault("ac6_neutralize_deswizzle_hashes",
|
||||
"7d22894002d16018, 17e5e4ac3e713245:4",
|
||||
"ac6_fix_deswizzle");
|
||||
}
|
||||
// Cutscene DoF chain (CoC pre-blur pair snapped to the guest grid; the
|
||||
// two 13-tap gathers densified along their pass axes). The translator
|
||||
// only emits these at draw scale > 1, so no scale gate is needed here.
|
||||
if (REXCVAR_GET(ac6_fix_dof)) {
|
||||
AC6_SET_IF_UNSET(ac6_snap_guest_texel_hashes,
|
||||
"85d733927e3bba9c, d050dfa6f58567f6");
|
||||
AC6_SET_IF_UNSET(ac6_densify_x_fetch_hashes, "6328f9c40913c82c");
|
||||
AC6_SET_IF_UNSET(ac6_densify_y_fetch_hashes, "5bd20f9d0d911687");
|
||||
rex::cvar::SetSessionDefault("ac6_snap_guest_texel_hashes",
|
||||
"85d733927e3bba9c, d050dfa6f58567f6", "ac6_fix_dof");
|
||||
rex::cvar::SetSessionDefault("ac6_densify_x_fetch_hashes", "6328f9c40913c82c",
|
||||
"ac6_fix_dof");
|
||||
rex::cvar::SetSessionDefault("ac6_densify_y_fetch_hashes", "5bd20f9d0d911687",
|
||||
"ac6_fix_dof");
|
||||
}
|
||||
}
|
||||
#undef AC6_SET_IF_UNSET
|
||||
|
||||
} // namespace
|
||||
|
||||
void ApplyAc6PerformanceModeOverridesPublic() {
|
||||
// The whole preset suite. Called from Ac6recompApp::OnConfigLoaded - AFTER
|
||||
// rex::cvar::LoadConfig parsed the user's toml (so the master switches and
|
||||
// gates read the user's real values, fixing the "ac6_fix_* dead from the
|
||||
// toml" bug) and BEFORE anything consumes cvars (logging, window, graphics).
|
||||
// Everything goes through SetSessionDefault: a user-set value always wins,
|
||||
// and none of these values leak into the user's config on a settings save.
|
||||
// The game-specific fix constants stay here in code, not in the user's toml.
|
||||
void ApplyAc6ConfigPresets() {
|
||||
rex::cvar::SetSessionDefault("log_file", "ac6recomp.log");
|
||||
ApplyAc6DefaultSettings();
|
||||
ApplyAc6HybridStartupSafetyOverrides();
|
||||
ApplyAc6FixDefaults();
|
||||
ApplyAc6PerformanceModeOverrides();
|
||||
// Runtime enforcement of performance mode stays the pre-existing
|
||||
// per-frame re-assert from the graphics overlay (see
|
||||
// ApplyAc6PerformanceModeOverridesPublic).
|
||||
}
|
||||
|
||||
void ApplyAc6PerformanceModeOverridesPublic() {
|
||||
EnforceAc6PerformanceModeOverrides();
|
||||
}
|
||||
|
||||
// One greppable summary of what the presets decided, logged from OnPreSetup
|
||||
// (the presets themselves run before logging is initialized). Logged at
|
||||
// error level so it survives performance mode's log_level=error session
|
||||
// default - the same reason the widescreen activation line logs at error.
|
||||
// This is the support line: it shows whether a master switch genuinely
|
||||
// disengaged its payload.
|
||||
void LogAc6ConfigPresetSummary() {
|
||||
REXLOG_ERROR("AC6 config: graphics mode={} capture={}", REXCVAR_GET(ac6_graphics_mode),
|
||||
REXCVAR_GET(ac6_render_capture) ? "true" : "false");
|
||||
REXLOG_ERROR("AC6 fixes: scaling={} deswizzle={} dof={} perf_mode={} unlock_fps={}",
|
||||
REXCVAR_GET(ac6_fix_scaling), REXCVAR_GET(ac6_fix_deswizzle),
|
||||
REXCVAR_GET(ac6_fix_dof), REXCVAR_GET(ac6_performance_mode),
|
||||
REXCVAR_GET(ac6_unlock_fps));
|
||||
REXLOG_ERROR("AC6 fix payloads: neutralize='{}' snap='{}' densify_x='{}' densify_y='{}' "
|
||||
"param_gen={}/{}",
|
||||
REXCVAR_GET(ac6_neutralize_deswizzle_hashes),
|
||||
REXCVAR_GET(ac6_snap_guest_texel_hashes),
|
||||
REXCVAR_GET(ac6_densify_x_fetch_hashes),
|
||||
REXCVAR_GET(ac6_densify_y_fetch_hashes),
|
||||
REXCVAR_GET(param_gen_integer_guest_position),
|
||||
REXCVAR_GET(param_gen_host_subpixel_restore));
|
||||
}
|
||||
|
||||
void InitEarlyLog() {
|
||||
@@ -230,24 +256,12 @@ std::unique_ptr<rex::ui::WindowedApp> Ac6recompAppCreate(rex::ui::WindowedAppCon
|
||||
g_boot_log << "Ac6recompApp::Create called" << std::endl;
|
||||
g_boot_log.flush();
|
||||
}
|
||||
|
||||
// Force SDK logging to a file as well
|
||||
REXCVAR_SET(log_file, "ac6recomp.log");
|
||||
if (!rex::cvar::HasNonDefaultValue("log_level")) {
|
||||
REXCVAR_SET(log_level, "debug");
|
||||
}
|
||||
// Smooth 60fps unlock is on by default now (zero-config for players); the
|
||||
// toml can still set ac6_unlock_fps=false for stock locked behaviour.
|
||||
REXCVAR_SET(ac6_unlock_fps, true);
|
||||
ApplyAc6DefaultSettings();
|
||||
ApplyAc6HybridStartupSafetyOverrides();
|
||||
ApplyAc6FixDefaults();
|
||||
ApplyAc6PerformanceModeOverrides();
|
||||
|
||||
REXLOG_INFO("Ac6recompAppCreate: graphics mode={} capture={}",
|
||||
REXCVAR_GET(ac6_graphics_mode),
|
||||
REXCVAR_GET(ac6_render_capture) ? "true" : "false");
|
||||
|
||||
|
||||
// No cvar presets here: this runs BEFORE rex::cvar::LoadConfig parses
|
||||
// the toml, so anything applied here reads defaults, not user values.
|
||||
// The preset suite lives in ApplyAc6ConfigPresets, called from
|
||||
// Ac6recompApp::OnConfigLoaded.
|
||||
|
||||
return Ac6recompApp::Create(ctx);
|
||||
}
|
||||
|
||||
|
||||
+16
-35
@@ -28,41 +28,22 @@ REXCVAR_DEFINE_BOOL(ac6_unlock_fps, true, "AC6",
|
||||
REXCVAR_DEFINE_BOOL(ac6_cutscene_clamp, true, "AC6",
|
||||
"Suspend the 60fps unlock during in-engine cutscenes so they "
|
||||
"play at native ~30fps instead of double speed");
|
||||
REXCVAR_DEFINE_BOOL(ac6_dynamic_vblank, true, "AC6",
|
||||
"With the FPS unlock active, pace frame-locked content (menus, "
|
||||
"cutscenes, pause) at the native 60Hz guest vblank while gameplay "
|
||||
"free-runs at the configured rate. Gameplay is detected via the "
|
||||
"world-compositor draw heartbeat; cutscenes via the cinematic "
|
||||
"hooks.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_fps_target, 60.0, "AC6",
|
||||
"The rate the simulation + presentation run at under the FPS unlock. "
|
||||
">0 = that exact rate (clamped to 30..ac6_max_sim_fps). "
|
||||
"0 = AUTO: the largest rate <= ac6_max_sim_fps that evenly divides your "
|
||||
"monitor's refresh, so frames land on refresh boundaries instead of "
|
||||
"beating against them (240Hz->60, 144Hz->48, 120Hz->60, 60Hz->60). "
|
||||
"Pick a target that divides your refresh; auto does this for you. "
|
||||
"Mirrors PA's native PC engine, which has no separate cap and simply "
|
||||
"runs the (dt-correct) sim at the display rate.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_max_sim_fps, 60.0, "AC6",
|
||||
"Ceiling on the simulation/pacing rate. AC6's physics is only validated "
|
||||
"to 60fps; above it, fixed-step assumptions surface (untested regime). "
|
||||
"Both ac6_fps_target and the auto (refresh-matched) target are clamped to "
|
||||
"this. Raise only once the >60 regime has been validated.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_dt_snap, true, "AC6",
|
||||
"With the FPS unlock active, snap the per-frame simulation delta to the "
|
||||
"EXACT pacing target when the real frame time is within ~1ms of it, so "
|
||||
"the fixed-step integrator sees a constant step. Removes the residual "
|
||||
"per-frame delta jitter that a fixed-step sim turns into visible shake; "
|
||||
"genuine slowdowns fall through to the precision path.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_min_sim_fps, 20.0, "AC6",
|
||||
"Lowest framerate the simulation runs at true speed before the "
|
||||
"game's frame-delta clamp forces slow motion. The stock game floors "
|
||||
"at 30 (below 30fps it plays in slow motion); this lowers the floor "
|
||||
"(default 20) so a sub-30fps dip runs at correct speed - just "
|
||||
"choppier - instead of slowing down and rubber-banding on recovery. "
|
||||
"Set 30 for stock behavior. Active with the FPS unlock; drives the "
|
||||
"frame-delta clamp and the physics step cap together so dynamics "
|
||||
"stay consistent with the kinematics.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_dynamic_vblank, true, "AC6/Enhancements",
|
||||
"Pace menus and cutscenes at the native 60Hz vblank while "
|
||||
"gameplay runs at the target rate.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_fps_target, 60.0, "AC6/Enhancements",
|
||||
"Target framerate under the FPS unlock. 0 = auto: the highest "
|
||||
"rate up to ac6_max_sim_fps that evenly divides your monitor's "
|
||||
"refresh rate.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_max_sim_fps, 60.0, "AC6/Enhancements",
|
||||
"Ceiling on the simulation rate. The game's physics is only "
|
||||
"validated to 60fps; raise only for testing.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_dt_snap, true, "AC6/Enhancements",
|
||||
"Snap the simulation step to the pacing target when within ~1ms "
|
||||
"of it, removing residual micro-stutter and shake.");
|
||||
REXCVAR_DEFINE_DOUBLE(ac6_min_sim_fps, 20.0, "AC6/Enhancements",
|
||||
"Lowest framerate the game runs at true speed before slow "
|
||||
"motion kicks in. Stock behaviour is 30.");
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
|
||||
+45
@@ -146,6 +146,25 @@ struct FlagEntry {
|
||||
Constraints constraints;
|
||||
std::string default_value;
|
||||
bool is_debug_only = false;
|
||||
// Set when the value came from the user - config file, console, or settings
|
||||
// UI (SetFlagByName). Serialization persists exactly these entries: a saved
|
||||
// config contains what the user chose, nothing else. Code writes via
|
||||
// REXCVAR_SET and per-launch command-line/environment overrides do not mark
|
||||
// this and are never persisted.
|
||||
bool user_set = false;
|
||||
// The last value the user set (canonical getter form), captured by
|
||||
// SetFlagByName. Serialization writes THIS, not the live value: a feature
|
||||
// that forces the live value (e.g. performance mode forcing a diagnostic
|
||||
// off) must not corrupt what the user chose in their saved config.
|
||||
std::string user_value;
|
||||
// Session default (SetSessionDefault): the value the app wants this session
|
||||
// when the user has not chosen one. Applied to the storage only when the
|
||||
// user did not set the flag; never serialized. session_driver optionally
|
||||
// names the cvar that drove it (e.g. a feature master switch) so settings
|
||||
// UIs can show the relationship.
|
||||
bool has_session_default = false;
|
||||
std::string session_default;
|
||||
std::string session_driver;
|
||||
};
|
||||
|
||||
std::vector<FlagEntry>& GetRegistry();
|
||||
@@ -198,6 +217,32 @@ void ResetToDefault(std::string_view name);
|
||||
void ResetAllToDefaults();
|
||||
bool HasNonDefaultValue(std::string_view name);
|
||||
std::vector<std::string> ListModifiedFlags();
|
||||
|
||||
/**
|
||||
* Apply a project/session default: the value a preset or feature wants this
|
||||
* session when the user has not explicitly chosen one. Unlike REXCVAR_SET,
|
||||
* this never clobbers a user-set value (config file, console, settings UI)
|
||||
* and the value is never serialized into a saved config. `driver` optionally
|
||||
* records the cvar that drove the value (a feature master switch such as
|
||||
* ac6_fix_deswizzle) for display in settings UIs.
|
||||
* Returns true if the value was applied to the flag, false if it was only
|
||||
* recorded (a user-set value wins) or rejected (unknown flag, command flag,
|
||||
* failed constraints).
|
||||
*/
|
||||
bool SetSessionDefault(std::string_view name, std::string_view value,
|
||||
std::string_view driver = {});
|
||||
|
||||
/// Whether the flag was explicitly set by the user this session (config file,
|
||||
/// console, settings UI). Command-line and environment overrides do not
|
||||
/// count: they are per-launch and are not persisted by SaveConfig.
|
||||
bool IsUserSet(std::string_view name);
|
||||
|
||||
/**
|
||||
* Serialize user-set flags (see FlagEntry::user_set) as TOML lines. This is
|
||||
* what SaveConfig writes: exactly the settings the user chose - loaded from
|
||||
* their config file or changed in the console/settings UI - and none of the
|
||||
* values that presets or feature code applied.
|
||||
*/
|
||||
std::string SerializeToTOML();
|
||||
std::string SerializeToTOML(std::string_view category);
|
||||
|
||||
|
||||
@@ -83,6 +83,15 @@ class ReXApp : public ui::WindowedApp, public ui::WindowListener, public ui::Win
|
||||
|
||||
// --- Virtual hooks for customization ---
|
||||
|
||||
/// Called right after the config file (cvars) was loaded - or would have
|
||||
/// been, when none exists - and before ANYTHING consumes cvar values
|
||||
/// (logging setup, game source resolution, runtime/graphics setup, window
|
||||
/// creation all come later). This is the place for app-level config
|
||||
/// presets and session defaults (rex::cvar::SetSessionDefault): here they
|
||||
/// see the user's real config values, unlike the app-create path, which
|
||||
/// runs before the config file is parsed.
|
||||
virtual void OnConfigLoaded() {}
|
||||
|
||||
/// Called before Runtime::Setup(). Override to modify backend config.
|
||||
virtual void OnPreSetup(RuntimeConfig& config) {}
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ class SettingsDialog : public ImGuiDialog {
|
||||
|
||||
private:
|
||||
bool visible_ = false;
|
||||
// Show debug-only cvars (diagnostics, investigation switches). Off by
|
||||
// default so players see the curated surface; toggled in the bottom bar.
|
||||
bool show_developer_ = false;
|
||||
std::filesystem::path config_path_;
|
||||
char search_buf_[128] = {};
|
||||
std::string selected_category_;
|
||||
|
||||
+158
-28
@@ -9,6 +9,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
@@ -162,6 +163,60 @@ bool ValidateConstraints(const FlagEntry& entry, std::string_view value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Caller holds the registry mutex (recursive, so callbacks may re-enter).
|
||||
void InvokeChangeCallbacksLocked(std::string_view name, std::string_view value) {
|
||||
auto& callbacks = GetCallbackStorage();
|
||||
auto it = callbacks.find(std::string(name));
|
||||
if (it != callbacks.end()) {
|
||||
for (const auto& callback : it->second) {
|
||||
callback(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TOML basic-string escaping for serialized values. Without this a saved
|
||||
// Windows path ("C:\Games\ac6.iso") produces an invalid escape sequence,
|
||||
// the next LoadConfig hits a parse error, and the WHOLE config silently
|
||||
// reverts to defaults.
|
||||
std::string EscapeTomlBasicString(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '\\':
|
||||
out += "\\\\";
|
||||
break;
|
||||
case '"':
|
||||
out += "\\\"";
|
||||
break;
|
||||
case '\n':
|
||||
out += "\\n";
|
||||
break;
|
||||
case '\r':
|
||||
out += "\\r";
|
||||
break;
|
||||
case '\t':
|
||||
out += "\\t";
|
||||
break;
|
||||
case '\b':
|
||||
out += "\\b";
|
||||
break;
|
||||
case '\f':
|
||||
out += "\\f";
|
||||
break;
|
||||
default:
|
||||
if (static_cast<unsigned char>(c) < 0x20) {
|
||||
char buf[8];
|
||||
std::snprintf(buf, sizeof(buf), "\\u%04X", static_cast<unsigned char>(c));
|
||||
out += buf;
|
||||
} else {
|
||||
out += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
//=============================================================================
|
||||
@@ -231,7 +286,7 @@ bool SetFlagByName(std::string_view name, std::string_view value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& entry = GetRegistryStorage()[it->second];
|
||||
auto& entry = GetRegistryStorage()[it->second];
|
||||
|
||||
// Check lifecycle
|
||||
if (!g_lifecycle_override && entry.lifecycle == Lifecycle::kInitOnly && IsFinalized()) {
|
||||
@@ -246,25 +301,70 @@ bool SetFlagByName(std::string_view name, std::string_view value) {
|
||||
|
||||
bool success = entry.setter(value);
|
||||
|
||||
// Track pending restart flags
|
||||
if (success && entry.lifecycle == Lifecycle::kRequiresRestart) {
|
||||
MarkPendingRestart(name);
|
||||
}
|
||||
|
||||
// Invoke registered callbacks
|
||||
if (success) {
|
||||
auto& callbacks = GetCallbackStorage();
|
||||
auto it = callbacks.find(std::string(name));
|
||||
if (it != callbacks.end()) {
|
||||
for (const auto& callback : it->second) {
|
||||
callback(name, value);
|
||||
}
|
||||
// Every SetFlagByName caller is a user-intent path (config file load,
|
||||
// console, settings UI), so the flag is now user-owned: SaveConfig
|
||||
// persists it, session defaults no longer apply to it. The canonical
|
||||
// value is captured so later code writes to the live value (REXCVAR_SET
|
||||
// enforcement) cannot corrupt what gets saved.
|
||||
entry.user_set = true;
|
||||
entry.user_value = entry.getter();
|
||||
|
||||
// Track pending restart flags
|
||||
if (entry.lifecycle == Lifecycle::kRequiresRestart) {
|
||||
MarkPendingRestart(name);
|
||||
}
|
||||
|
||||
InvokeChangeCallbacksLocked(name, value);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool SetSessionDefault(std::string_view name, std::string_view value, std::string_view driver) {
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
auto it = GetRegistryIndex().find(std::string(name));
|
||||
if (it == GetRegistryIndex().end()) {
|
||||
REXLOG_WARN("SetSessionDefault: unknown flag '{}'", name);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& entry = GetRegistryStorage()[it->second];
|
||||
if (entry.type == FlagType::Command) {
|
||||
return false;
|
||||
}
|
||||
if (!ValidateConstraints(entry, value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
entry.has_session_default = true;
|
||||
entry.session_default = std::string(value);
|
||||
entry.session_driver = std::string(driver);
|
||||
|
||||
// A user-set value always wins: record the session default (for UI display)
|
||||
// but leave the user's value in place.
|
||||
if (entry.user_set) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = entry.setter(value);
|
||||
if (success) {
|
||||
// Deliberately no MarkPendingRestart: this is the value the app starts
|
||||
// from, not a change the user needs to restart for.
|
||||
InvokeChangeCallbacksLocked(name, value);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool IsUserSet(std::string_view name) {
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
auto it = GetRegistryIndex().find(std::string(name));
|
||||
if (it == GetRegistryIndex().end()) {
|
||||
return false;
|
||||
}
|
||||
return GetRegistryStorage()[it->second].user_set;
|
||||
}
|
||||
|
||||
std::string GetFlagByName(std::string_view name) {
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
auto it = GetRegistryIndex().find(std::string(name));
|
||||
@@ -387,8 +487,17 @@ void ResetToDefault(std::string_view name) {
|
||||
if (it == GetRegistryIndex().end()) {
|
||||
return;
|
||||
}
|
||||
const auto& entry = GetRegistryStorage()[it->second];
|
||||
entry.setter(entry.default_value);
|
||||
auto& entry = GetRegistryStorage()[it->second];
|
||||
// Reset means "back to stock, forget my choice": the effective default is
|
||||
// the session default when one is in force, and the flag stops being
|
||||
// user-owned so the next SaveConfig drops it.
|
||||
const std::string& target =
|
||||
entry.has_session_default ? entry.session_default : entry.default_value;
|
||||
if (entry.setter(target)) {
|
||||
entry.user_set = false;
|
||||
entry.user_value.clear();
|
||||
InvokeChangeCallbacksLocked(name, target);
|
||||
}
|
||||
}
|
||||
|
||||
void ResetAllToDefaults() {
|
||||
@@ -419,16 +528,31 @@ std::vector<std::string> ListModifiedFlags() {
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void AppendTomlLine(std::string& result, const FlagEntry& entry) {
|
||||
// user_value, not getter(): the live value may be feature-forced (e.g.
|
||||
// performance mode holding a diagnostic off); the save keeps the user's
|
||||
// own choice.
|
||||
if (entry.type == FlagType::String) {
|
||||
result += entry.name + " = \"" + EscapeTomlBasicString(entry.user_value) + "\"\n";
|
||||
} else {
|
||||
result += entry.name + " = " + entry.user_value + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string SerializeToTOML() {
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
std::string result;
|
||||
for (const auto& entry : GetRegistryStorage()) {
|
||||
if (entry.getter() != entry.default_value) {
|
||||
if (entry.type == FlagType::String) {
|
||||
result += entry.name + " = \"" + entry.getter() + "\"\n";
|
||||
} else {
|
||||
result += entry.name + " = " + entry.getter() + "\n";
|
||||
}
|
||||
// Persist exactly what the user chose. Values written by presets or
|
||||
// feature code (REXCVAR_SET / SetSessionDefault) are session state and
|
||||
// must not outlive the code that applied them by leaking into the
|
||||
// user's config.
|
||||
if (entry.user_set) {
|
||||
AppendTomlLine(result, entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -438,12 +562,8 @@ std::string SerializeToTOML(std::string_view category) {
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
std::string result;
|
||||
for (const auto& entry : GetRegistryStorage()) {
|
||||
if (entry.category == category && entry.getter() != entry.default_value) {
|
||||
if (entry.type == FlagType::String) {
|
||||
result += entry.name + " = \"" + entry.getter() + "\"\n";
|
||||
} else {
|
||||
result += entry.name + " = " + entry.getter() + "\n";
|
||||
}
|
||||
if (entry.category == category && entry.user_set) {
|
||||
AppendTomlLine(result, entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
@@ -568,7 +688,17 @@ ScopedLifecycleOverride::~ScopedLifecycleOverride() {
|
||||
}
|
||||
|
||||
void ResetAllForTesting() {
|
||||
ResetAllToDefaults();
|
||||
{
|
||||
std::lock_guard lock(GetRegistryMutex());
|
||||
for (auto& entry : GetRegistryStorage()) {
|
||||
entry.setter(entry.default_value);
|
||||
entry.user_set = false;
|
||||
entry.user_value.clear();
|
||||
entry.has_session_default = false;
|
||||
entry.session_default.clear();
|
||||
entry.session_driver.clear();
|
||||
}
|
||||
}
|
||||
ClearPendingRestartFlags();
|
||||
g_finalized = false;
|
||||
}
|
||||
|
||||
+53
-71
@@ -33,81 +33,63 @@ REXCVAR_DEFINE_BOOL(vfetch_index_rounding_bias, false, "GPU/Shader",
|
||||
"flooring to fix black triangles caused by RCP precision");
|
||||
REXCVAR_DEFINE_BOOL(draw_resolution_scaled_texture_offsets, true, "GPU/Shader",
|
||||
"Scale texture offsets with draw resolution");
|
||||
REXCVAR_DEFINE_BOOL(param_gen_integer_guest_position, false, "GPU/Shader",
|
||||
"At >1x draw resolution scale, floor the PsParamGen pixel "
|
||||
"position to the integer guest-pixel index instead of keeping "
|
||||
"the sub-guest-pixel fraction. Fixes the mosaic in games that "
|
||||
"feed the position into their own integer pixel-address math "
|
||||
"(e.g. AC6's deferred EDRAM restore/de-swizzle passes), whose "
|
||||
"frac()-based bit extraction otherwise sees a doubled period and "
|
||||
"scrambles the sample coordinate. Those passes then sample at "
|
||||
"guest resolution. Off by default; harmless for shaders that pass "
|
||||
"the position straight to tfetch only at 1x.");
|
||||
REXCVAR_DEFINE_BOOL(param_gen_host_subpixel_restore, false, "GPU/Shader",
|
||||
"Builds on param_gen_integer_guest_position (and implies it): for "
|
||||
"resolution-scaled, position-derived 2D samples in pixel shaders "
|
||||
"that use PsParamGen, re-adds the host sub-pixel offset to the "
|
||||
"sample coordinate so the de-swizzle restore passes sample at full "
|
||||
"host resolution instead of the guest-texel center. Turns the "
|
||||
"mosaic fix from clean-but-soft into true resolution-scaled detail. "
|
||||
"Off by default; may need per-shader scoping if it disturbs other "
|
||||
"passes that read scaled render targets via interpolated coords.");
|
||||
REXCVAR_DEFINE_STRING(ac6_neutralize_deswizzle_hashes, "", "GPU/Shader",
|
||||
"AC6: comma/space-separated tokens \"<hash>[:<slot>[+<slot>...]]\" "
|
||||
"naming guest pixel-shader ucode hashes (hex) whose param_gen 2D "
|
||||
"texture samples manually de-swizzle the raw EDRAM sub-tile order of "
|
||||
"their source. The emulator's texture cache detiles everything to "
|
||||
"linear, so that de-swizzle is always a wrong texel permutation here "
|
||||
"(the mosaic/streak class). For a matching fetch the sample "
|
||||
"coordinate is replaced unconditionally with the identity host-texel "
|
||||
"UV (SV_Position.xy / (guest_size * scale)) -- a 1:1 copy. A bare "
|
||||
"hash overrides ALL of that shader's fetches; \":4\" limits it to "
|
||||
"Xenos tfetch slot 4 (needed when only some fetches de-swizzle, e.g. "
|
||||
"the AC6 cloud compositor: scene fetch de-swizzles, mask/cloud "
|
||||
"fetches use plain UVs and must be left alone). Runtime, no rebuild.");
|
||||
REXCVAR_DEFINE_STRING(ac6_snap_guest_texel_hashes, "", "GPU/Shader",
|
||||
"AC6: same \"<hash>[:<slot>[+<slot>...]]\" token list as "
|
||||
"ac6_neutralize_deswizzle_hashes, naming guest pixel-shader ucode "
|
||||
"hashes whose texture taps encode fractional guest-texel bilinear "
|
||||
"positions (e.g. the cutscene depth-of-field gather pair). Sampling "
|
||||
"a resolution-scaled texture with such a kernel reinterprets every "
|
||||
"blend ratio against the finer host grid and alternates the kernel "
|
||||
"phase per output pixel, aliasing into striping at >1x. For matching "
|
||||
"fetches the normalized coordinate is snapped to the guest texel "
|
||||
"center (where host bilinear of a scaled texture returns exactly the "
|
||||
"guest texel's box average), restoring the guest sampling grid at any "
|
||||
"draw resolution scale. Applied only at >1x and only to "
|
||||
"resolution-scaled textures. Runtime, no rebuild.");
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_x_fetch_hashes, "", "GPU/Shader",
|
||||
"AC6: \"<hash>[:<slot>[+<slot>...]]\" list for HORIZONTAL-axis "
|
||||
"separable filter passes (e.g. the cutscene DoF H gather "
|
||||
"6328f9c40913c82c). Each allowlisted fetch becomes the average of "
|
||||
"2*draw_resolution_scale_x samples spread along X across one "
|
||||
"guest-texel half-gap on each side of the original tap - the union "
|
||||
"of all taps' cells covers the kernel span continuously, so ghost "
|
||||
"copies of arbitrarily thin features merge at any resolution scale. "
|
||||
"Convolving the authored kernel with the 2-texel cell box widens the "
|
||||
"blur by ~2% (visually shape-faithful). Per-axis scale aware; no-op "
|
||||
"when draw_resolution_scale_x is 1. Read at shader translation.");
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_y_fetch_hashes, "", "GPU/Shader",
|
||||
"AC6: as ac6_densify_x_fetch_hashes, for VERTICAL-axis separable "
|
||||
"filter passes (e.g. the cutscene DoF V gather 5bd20f9d0d911687): "
|
||||
"2*draw_resolution_scale_y samples along Y per fetch. No-op when "
|
||||
"draw_resolution_scale_y is 1.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_flare_drop_quad2, true, "GPU/Shader",
|
||||
"AC6: cull the sun lens-flare's spurious second billboard "
|
||||
"(vertices 4-7) to remove the faint rectangle in the sky");
|
||||
REXCVAR_DEFINE_INT32(ac6_flare_drop_index_min, 4, "GPU/Shader",
|
||||
"AC6: first vertex index of the flare draw to cull (the "
|
||||
"spurious billboard is vertices 4-7 of 8)");
|
||||
// The param_gen pair and the ac6_* hash lists below are consumed at shader
|
||||
// translation. Translated shaders persist in the shader storage, but storage
|
||||
// stores ucode and re-translates at load, so a change applies at the next
|
||||
// launch: kRequiresRestart.
|
||||
REXCVAR_DEFINE_BOOL(param_gen_integer_guest_position, false, "AC6/Fixes",
|
||||
"At above-1x draw resolution scale, floor the PsParamGen pixel "
|
||||
"position to the integer guest-pixel index. Fixes mosaic in "
|
||||
"passes doing their own integer pixel-address math.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_BOOL(param_gen_host_subpixel_restore, false, "AC6/Fixes",
|
||||
"With param_gen_integer_guest_position: re-add the host "
|
||||
"sub-pixel offset so restore passes keep full scaled detail.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_STRING(ac6_neutralize_deswizzle_hashes, "", "AC6/Fixes",
|
||||
"Pixel-shader token list \"<hash>[:<slot>[+<slot>...]]\": replace "
|
||||
"matching fetches' manual EDRAM de-swizzle with the identity "
|
||||
"host-texel UV. Normally driven by ac6_fix_deswizzle.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_STRING(ac6_snap_guest_texel_hashes, "", "AC6/Fixes",
|
||||
"Pixel-shader token list \"<hash>[:<slot>[+<slot>...]]\": snap "
|
||||
"matching fetches to guest texel centers at above-1x scale. "
|
||||
"Normally driven by ac6_fix_dof.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_x_fetch_hashes, "", "AC6/Fixes",
|
||||
"Token list: densify matching horizontal separable-filter "
|
||||
"fetches at above-1x scale so ghost copies merge. Normally "
|
||||
"driven by ac6_fix_dof.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_STRING(ac6_densify_y_fetch_hashes, "", "AC6/Fixes",
|
||||
"As ac6_densify_x_fetch_hashes, for vertical-axis passes. "
|
||||
"Normally driven by ac6_fix_dof.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_BOOL(ac6_flare_drop_quad2, true, "AC6/Fixes",
|
||||
"Fix the faint rectangle around the sun by culling the lens "
|
||||
"flare's spurious second billboard.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
REXCVAR_DEFINE_INT32(ac6_flare_drop_index_min, 4, "AC6/Fixes",
|
||||
"First vertex index of the flare draw to cull (the spurious "
|
||||
"billboard is vertices 4-7 of 8).")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart)
|
||||
.debug_only();
|
||||
REXCVAR_DEFINE_BOOL(gpu_debug_markers, false, "GPU",
|
||||
"Insert debug markers into GPU command streams for tools "
|
||||
"like PIX and RenderDoc. Automatically enabled when "
|
||||
"RenderDoc is detected.");
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_trails, true, "AC6",
|
||||
"Fix invisible missile/jet trails: drop stale cached vertex-buffer "
|
||||
"residency so the GPU copy of AC6's fixed-address trail history ring "
|
||||
"refreshes when the CPU writes it. On by default.");
|
||||
// Consumed once at graphics SetupContext (a one-shot write-watch callback
|
||||
// registration), so a change needs a restart to take effect.
|
||||
REXCVAR_DEFINE_BOOL(ac6_fix_trails, true, "AC6/Fixes",
|
||||
"Fix invisible missile and jet trails by refreshing the GPU copy "
|
||||
"of the trail history when the CPU rewrites it.")
|
||||
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
|
||||
|
||||
bool IsGpuDebugMarkersEnabled() {
|
||||
static bool cached = false;
|
||||
|
||||
+18
-47
@@ -19,53 +19,24 @@ REXCVAR_DEFINE_BOOL(audio_trace_render_driver_verbose, false, "Audio",
|
||||
"Trace render-driver activity");
|
||||
REXCVAR_DEFINE_BOOL(audio_deep_trace, false, "Audio",
|
||||
"Enable verbose runtime audio tracing");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_loop_guard, true, "Audio",
|
||||
"Require a real loop window (loop_start < loop_end, "
|
||||
"read_offset >= loop_end) before engaging XMA loop "
|
||||
"handling, as in current Xenia master (PR #1808, debugged "
|
||||
"on Ace Combat 6). Off: legacy behavior, where streamed "
|
||||
"voices carrying degenerate loop metadata (loop_count="
|
||||
"0xff, loop_start == loop_end == 0) re-fire the loop "
|
||||
"machinery on every input buffer swap, truncating/"
|
||||
"skipping subframes of the stream (confirmed firing on "
|
||||
"AC6 cutscene streams via the deep-trace counter).");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_header_straddle_fix, true, "Audio",
|
||||
"Decode XMA frames whose 15-bit frame header straddles a "
|
||||
"2 KiB packet boundary. The frame walk treated "
|
||||
"fewer-than-15 remaining bits as end-of-chain, but the "
|
||||
"straddled frame is real: the encoder's own seek ledger "
|
||||
"confirms it on every occurrence (its size field, read "
|
||||
"across the boundary, equals the next packet's "
|
||||
"first-frame offset minus the packet header). Without "
|
||||
"this, one whole 512-sample frame is silently dropped per "
|
||||
"occurrence, so the parallel streams of a multi-stream "
|
||||
"voice drift apart in 512-sample steps and the premix "
|
||||
"combs on the stereo fold - the long-reported doubled/"
|
||||
"robotic cutscene dialogue. Off restores the legacy "
|
||||
"walk for A/B.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_loop_end_guard, true, "Audio",
|
||||
"Apply the degenerate-loop guard (see "
|
||||
"audio_xma_loop_guard) to the loop-end frame test as "
|
||||
"well. That test is evaluated before the loop-status "
|
||||
"update, so the guard there never reaches it: with "
|
||||
"loop_count > 0 and loop_start >= loop_end (metadata "
|
||||
"AC6's streamed voices carry), the clamped loop_end "
|
||||
"equals a voice's starting read offset for the stream "
|
||||
"that begins at packet 0, arming the frame output limit "
|
||||
"and silently discarding 384 samples of that stream's "
|
||||
"first frame - permanently offsetting one context of a "
|
||||
"multi-stream voice from its siblings. Off restores the "
|
||||
"legacy behavior for A/B.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_preserve_timeline, true, "Audio",
|
||||
"When the XMA decoder backend rejects a damaged frame, emit that "
|
||||
"frame's worth of silence instead of dropping it. The input read "
|
||||
"offset advances past the frame either way, so dropping the output "
|
||||
"silently shortens the stream's timeline by 512 samples per damaged "
|
||||
"frame - which would permanently desynchronize multi-stream sources "
|
||||
"(5.1 premixes carried as parallel stereo streams, e.g. AC6's "
|
||||
"cutscene mixes). Robustness fix - no in-game trigger is known in "
|
||||
"AC6 (instrumented runs decode every frame). On by default; off "
|
||||
"restores the legacy drop behavior.");
|
||||
// The four audio_xma_* guards below are correctness fixes debugged on Ace
|
||||
// Combat 6 (details in the commits introducing each). All default on; off
|
||||
// restores the legacy behaviour for A/B comparison.
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_loop_guard, true, "AC6/Fixes",
|
||||
"Guard XMA looping against degenerate loop metadata that "
|
||||
"truncates streamed voices. Off = legacy behaviour for A/B.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_header_straddle_fix, true, "AC6/Fixes",
|
||||
"Decode XMA frames whose header straddles a packet boundary. "
|
||||
"Off = legacy walk, which drops a frame per occurrence and "
|
||||
"combs multi-stream cutscene dialogue.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_loop_end_guard, true, "AC6/Fixes",
|
||||
"Apply the degenerate-loop guard to the loop-end frame test "
|
||||
"too, keeping multi-stream voices aligned from their first "
|
||||
"frame. Off = legacy behaviour for A/B.");
|
||||
REXCVAR_DEFINE_BOOL(audio_xma_preserve_timeline, true, "AC6/Fixes",
|
||||
"Emit silence for XMA frames the decoder rejects instead of "
|
||||
"dropping them, preserving multi-stream timing. Off = legacy "
|
||||
"drop behaviour.");
|
||||
|
||||
namespace rex::audio {
|
||||
|
||||
|
||||
@@ -379,6 +379,10 @@ bool ReXApp::OnInitialize() {
|
||||
rex::cvar::LoadConfig(config_path);
|
||||
}
|
||||
|
||||
// App-level config presets / session defaults: cvars hold their final
|
||||
// user-config values and nothing has consumed them yet.
|
||||
OnConfigLoaded();
|
||||
|
||||
// Logging setup from CVARs
|
||||
std::string log_file_cvar = REXCVAR_GET(log_file);
|
||||
std::string log_level_str = REXCVAR_GET(log_level);
|
||||
|
||||
+52
-8
@@ -164,10 +164,21 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
|
||||
auto& registry = rex::cvar::GetRegistry();
|
||||
|
||||
// Collect sorted unique category paths.
|
||||
// Debug-only cvars (diagnostics, one-shot investigation switches) are
|
||||
// hidden unless the developer toggle is on; they stay settable from the
|
||||
// config file and the console.
|
||||
auto entry_visible = [&](const rex::cvar::FlagEntry& entry) -> bool {
|
||||
return show_developer_ || !entry.is_debug_only;
|
||||
};
|
||||
|
||||
// Collect sorted unique category paths (of visible entries only, so a
|
||||
// category that holds nothing but developer cvars does not show an empty
|
||||
// page).
|
||||
std::set<std::string> category_set;
|
||||
for (auto& entry : registry) {
|
||||
category_set.insert(entry.category);
|
||||
if (entry_visible(entry)) {
|
||||
category_set.insert(entry.category);
|
||||
}
|
||||
}
|
||||
|
||||
// Build tree: for each category path like "Input/Keybinds/Controller",
|
||||
@@ -296,6 +307,9 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
|
||||
ImGui::BeginChild("##cvars", ImVec2(0, -30.0f), false);
|
||||
for (auto& entry : registry) {
|
||||
if (!entry_visible(entry)) {
|
||||
continue;
|
||||
}
|
||||
// Filter by category (unless searching).
|
||||
if (!searching) {
|
||||
if (!category_matches(entry.category)) {
|
||||
@@ -386,7 +400,10 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Reset##v")) {
|
||||
rex::cvar::SetFlagByName(entry.name, entry.default_value);
|
||||
// ResetToDefault, not SetFlagByName: reset means "back to stock,
|
||||
// forget my choice" - the flag stops being user-owned and the next
|
||||
// save drops it from the config.
|
||||
rex::cvar::ResetToDefault(entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +436,7 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
} else {
|
||||
// Non-keybind CVARs: colored label on left, value widget on right
|
||||
ImGui::TextColored(LifecycleColor(entry.lifecycle), "%-20s", entry.name.c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
if (ImGui::IsItemHovered() && ImGui::BeginTooltip()) {
|
||||
const char* lifecycle_label = "";
|
||||
switch (entry.lifecycle) {
|
||||
case rex::cvar::Lifecycle::kHotReload:
|
||||
@@ -432,11 +449,24 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
lifecycle_label = "Read-only - set at initialization only";
|
||||
break;
|
||||
}
|
||||
// Wrap: without a wrap position a long description renders as one
|
||||
// screen-wide line.
|
||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 28.0f);
|
||||
if (!entry.description.empty()) {
|
||||
ImGui::SetTooltip("%s\n[%s]", entry.description.c_str(), lifecycle_label);
|
||||
} else {
|
||||
ImGui::SetTooltip("[%s]", lifecycle_label);
|
||||
ImGui::TextUnformatted(entry.description.c_str());
|
||||
}
|
||||
ImGui::TextDisabled("[%s]", lifecycle_label);
|
||||
if (entry.has_session_default && !entry.session_driver.empty()) {
|
||||
if (entry.user_set) {
|
||||
ImGui::TextDisabled("Default '%s' comes from %s; your setting overrides it.",
|
||||
entry.session_default.c_str(), entry.session_driver.c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("Set to '%s' by %s.", entry.session_default.c_str(),
|
||||
entry.session_driver.c_str());
|
||||
}
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
ImGui::SameLine(240.0f);
|
||||
|
||||
@@ -497,6 +527,18 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
rex::cvar::SetFlagByName(entry.name, buf);
|
||||
}
|
||||
}
|
||||
|
||||
// A cvar driven by another cvar (fix master switch, performance mode)
|
||||
// says so inline, so a user editing it can tell where the value came
|
||||
// from and what wins.
|
||||
if (entry.has_session_default && !entry.session_driver.empty()) {
|
||||
ImGui::SameLine();
|
||||
if (entry.user_set) {
|
||||
ImGui::TextDisabled("(overrides %s)", entry.session_driver.c_str());
|
||||
} else {
|
||||
ImGui::TextDisabled("(set by %s)", entry.session_driver.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (read_only)
|
||||
@@ -506,13 +548,15 @@ void SettingsDialog::OnDraw(ImGuiIO& /*io*/) {
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
// Bottom bar: Save button.
|
||||
// Bottom bar: Save button + developer toggle.
|
||||
ImGui::Separator();
|
||||
if (ImGui::Button("Save to config")) {
|
||||
rex::cvar::SaveConfig(config_path_);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(%s)", config_path_.filename().string().c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox("Show developer settings", &show_developer_);
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
+155
-12
@@ -276,6 +276,11 @@ TEST_CASE("cvar ListFlagsByLifecycle", "[cvar]") {
|
||||
}
|
||||
|
||||
TEST_CASE("cvar reset and diff utilities", "[cvar]") {
|
||||
// ResetToDefault honors a session default when one is in force; start from
|
||||
// a clean slate so a session default left by another test case (random
|
||||
// execution order) cannot change what "default" means here.
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
SECTION("HasNonDefaultValue detects changes") {
|
||||
REXCVAR_SET(test_int32_flag, 42); // Reset to default
|
||||
CHECK_FALSE(rex::cvar::HasNonDefaultValue("test_int32_flag"));
|
||||
@@ -316,20 +321,157 @@ TEST_CASE("cvar testing utilities", "[cvar]") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cvar TOML serialization", "[cvar]") {
|
||||
TEST_CASE("cvar TOML serialization persists user-set flags only", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
REXCVAR_SET(test_int32_flag, 999);
|
||||
REXCVAR_SET(test_string_flag, "custom");
|
||||
// User-intent path (config load / console / settings UI): persisted.
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "999"));
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_string_flag", "custom"));
|
||||
|
||||
// Code write (REXCVAR_SET): session state, never persisted.
|
||||
REXCVAR_SET(test_bool_flag, true);
|
||||
|
||||
auto toml = rex::cvar::SerializeToTOML();
|
||||
|
||||
// Should contain modified flags
|
||||
CHECK(toml.find("test_int32_flag = 999") != std::string::npos);
|
||||
CHECK(toml.find("test_string_flag = \"custom\"") != std::string::npos);
|
||||
|
||||
// Should not contain flags at default
|
||||
CHECK(toml.find("test_bool_flag") == std::string::npos);
|
||||
|
||||
SECTION("a user-set flag persists even at its default value") {
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "42")); // == default
|
||||
auto toml2 = rex::cvar::SerializeToTOML();
|
||||
CHECK(toml2.find("test_int32_flag = 42") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cvar user-set tracking", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
SECTION("IsUserSet reflects how the value was set") {
|
||||
CHECK_FALSE(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
|
||||
REXCVAR_SET(test_int32_flag, 7);
|
||||
CHECK_FALSE(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "8"));
|
||||
CHECK(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
}
|
||||
|
||||
SECTION("LoadConfig marks flags user-set; save round-trips exactly the file") {
|
||||
auto config_path = std::filesystem::temp_directory_path() / "test_user_set.toml";
|
||||
{
|
||||
std::ofstream file(config_path);
|
||||
file << "test_int32_flag = 500\n";
|
||||
file << "test_bool_flag = true\n";
|
||||
}
|
||||
rex::cvar::LoadConfig(config_path);
|
||||
std::filesystem::remove(config_path);
|
||||
|
||||
CHECK(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
CHECK(rex::cvar::IsUserSet("test_bool_flag"));
|
||||
|
||||
auto toml = rex::cvar::SerializeToTOML();
|
||||
CHECK(toml == "test_bool_flag = true\ntest_int32_flag = 500\n");
|
||||
}
|
||||
|
||||
SECTION("failed sets do not mark user-set") {
|
||||
CHECK_FALSE(rex::cvar::SetFlagByName("test_ranged_flag", "100"));
|
||||
CHECK_FALSE(rex::cvar::IsUserSet("test_ranged_flag"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cvar session defaults", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
SECTION("applies when the user has not set the flag") {
|
||||
REQUIRE(rex::cvar::SetSessionDefault("test_int32_flag", "77"));
|
||||
CHECK(REXCVAR_GET(test_int32_flag) == 77);
|
||||
CHECK_FALSE(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
// Never serialized.
|
||||
CHECK(rex::cvar::SerializeToTOML().find("test_int32_flag") == std::string::npos);
|
||||
}
|
||||
|
||||
SECTION("recorded but not applied over a user-set value") {
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "8"));
|
||||
CHECK_FALSE(rex::cvar::SetSessionDefault("test_int32_flag", "77"));
|
||||
CHECK(REXCVAR_GET(test_int32_flag) == 8);
|
||||
|
||||
auto* info = rex::cvar::GetFlagInfo("test_int32_flag");
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->has_session_default);
|
||||
CHECK(info->session_default == "77");
|
||||
}
|
||||
|
||||
SECTION("user value wins even when it equals the registered default") {
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "42")); // == default
|
||||
CHECK_FALSE(rex::cvar::SetSessionDefault("test_int32_flag", "77"));
|
||||
CHECK(REXCVAR_GET(test_int32_flag) == 42);
|
||||
}
|
||||
|
||||
SECTION("driver is recorded for UI display") {
|
||||
REQUIRE(rex::cvar::SetSessionDefault("test_string_flag", "driven", "test_bool_flag"));
|
||||
auto* info = rex::cvar::GetFlagInfo("test_string_flag");
|
||||
REQUIRE(info != nullptr);
|
||||
CHECK(info->session_driver == "test_bool_flag");
|
||||
}
|
||||
|
||||
SECTION("constraints still validate") {
|
||||
CHECK_FALSE(rex::cvar::SetSessionDefault("test_ranged_flag", "100"));
|
||||
CHECK(REXCVAR_GET(test_ranged_flag) == 5);
|
||||
}
|
||||
|
||||
SECTION("ResetToDefault returns to the session default and drops the save line") {
|
||||
REQUIRE(rex::cvar::SetSessionDefault("test_int32_flag", "77"));
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "8"));
|
||||
CHECK(rex::cvar::SerializeToTOML().find("test_int32_flag") != std::string::npos);
|
||||
|
||||
rex::cvar::ResetToDefault("test_int32_flag");
|
||||
CHECK(REXCVAR_GET(test_int32_flag) == 77);
|
||||
CHECK_FALSE(rex::cvar::IsUserSet("test_int32_flag"));
|
||||
CHECK(rex::cvar::SerializeToTOML().find("test_int32_flag") == std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cvar saves preserve the user's value over later code forcing", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
// The user chooses a value (toml / settings UI)...
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "8"));
|
||||
// ...then feature code forces the live value (enforcement, e.g.
|
||||
// performance mode re-asserting a diagnostic off). The live value
|
||||
// changes; the saved value must stay the user's choice.
|
||||
REXCVAR_SET(test_int32_flag, 0);
|
||||
CHECK(REXCVAR_GET(test_int32_flag) == 0);
|
||||
|
||||
auto toml = rex::cvar::SerializeToTOML();
|
||||
CHECK(toml.find("test_int32_flag = 8") != std::string::npos);
|
||||
CHECK(toml.find("test_int32_flag = 0") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("cvar TOML serialization escapes string values", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
// A Windows path with backslashes and quotes: unescaped, this voids the
|
||||
// whole config file with a parse error on the next load.
|
||||
const std::string nasty = "C:\\Games\\ac6 \"test\".iso";
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_string_flag", nasty));
|
||||
|
||||
auto toml = rex::cvar::SerializeToTOML();
|
||||
CHECK(toml.find("\\\\") != std::string::npos);
|
||||
|
||||
SECTION("serialized output round-trips through LoadConfig") {
|
||||
auto config_path = std::filesystem::temp_directory_path() / "test_escape.toml";
|
||||
{
|
||||
std::ofstream file(config_path);
|
||||
file << toml;
|
||||
}
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
CHECK(REXCVAR_GET(test_string_flag) == "default");
|
||||
|
||||
rex::cvar::LoadConfig(config_path);
|
||||
std::filesystem::remove(config_path);
|
||||
CHECK(REXCVAR_GET(test_string_flag) == nasty);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cvar metadata integration test", "[cvar][integration]") {
|
||||
@@ -466,9 +608,10 @@ TEST_CASE("cvar ResetAllToDefaults", "[cvar]") {
|
||||
TEST_CASE("cvar SerializeToTOML with category filter", "[cvar]") {
|
||||
rex::cvar::testing::ResetAllForTesting();
|
||||
|
||||
// Modify flags in different categories
|
||||
REXCVAR_SET(test_int32_flag, 123); // Category: Test
|
||||
REXCVAR_SET(test_category_flag, "changed"); // Category: TestCategory
|
||||
// Modify flags in different categories (via the user path: serialization
|
||||
// persists user-set flags only)
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "123")); // Category: Test
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_category_flag", "changed")); // Category: TestCategory
|
||||
|
||||
SECTION("Filter by category returns only that category") {
|
||||
auto test_toml = rex::cvar::SerializeToTOML("Test");
|
||||
@@ -491,9 +634,9 @@ TEST_CASE("cvar SaveConfig", "[cvar]") {
|
||||
// Clean up any existing file
|
||||
std::filesystem::remove(save_path);
|
||||
|
||||
SECTION("SaveConfig writes modified flags to file") {
|
||||
REXCVAR_SET(test_int32_flag, 777);
|
||||
REXCVAR_SET(test_string_flag, "saved_value");
|
||||
SECTION("SaveConfig writes user-set flags to file") {
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_int32_flag", "777"));
|
||||
REQUIRE(rex::cvar::SetFlagByName("test_string_flag", "saved_value"));
|
||||
|
||||
rex::cvar::SaveConfig(save_path);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user