From deb406bdb0e4b7cdc631ee9611e8091a5cb6a46b Mon Sep 17 00:00:00 2001 From: Dipshet <264011288+Dipshet@users.noreply.github.com> Date: Thu, 6 Aug 2026 15:46:15 +0200 Subject: [PATCH] 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 )" 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. --- src/ac6_backend_fixes/ac6_cutscene_resync.cpp | 24 +- src/ac6_backend_fixes/ac6_effect_mode_fix.cpp | 10 +- src/ac6_backend_fixes/ac6_kbm_input.cpp | 19 +- src/ac6_backend_fixes/ac6_widescreen.cpp | 29 +-- src/ac6recomp_app.h | 11 + src/main.cpp | 246 +++++++++--------- src/render_hooks.cpp | 51 ++-- thirdparty/rexglue-sdk/include/rex/cvar.h | 45 ++++ thirdparty/rexglue-sdk/include/rex/rex_app.h | 9 + .../include/rex/ui/overlay/settings_overlay.h | 3 + thirdparty/rexglue-sdk/src/core/cvar.cpp | 186 +++++++++++-- thirdparty/rexglue-sdk/src/graphics/flags.cpp | 124 ++++----- .../src/native/audio/audio_system.cpp | 65 ++--- .../rexglue-sdk/src/native/ui/rex_app.cpp | 4 + .../src/ui/overlay/settings_overlay.cpp | 60 ++++- .../rexglue-sdk/tests/unit/core/cvar_test.cpp | 167 +++++++++++- 16 files changed, 681 insertions(+), 372 deletions(-) diff --git a/src/ac6_backend_fixes/ac6_cutscene_resync.cpp b/src/ac6_backend_fixes/ac6_cutscene_resync.cpp index f05607a9..17af77f0 100644 --- a/src/ac6_backend_fixes/ac6_cutscene_resync.cpp +++ b/src/ac6_backend_fixes/ac6_cutscene_resync.cpp @@ -36,23 +36,13 @@ #include #include -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") diff --git a/src/ac6_backend_fixes/ac6_effect_mode_fix.cpp b/src/ac6_backend_fixes/ac6_effect_mode_fix.cpp index 78280ba9..632cce0b 100644 --- a/src/ac6_backend_fixes/ac6_effect_mode_fix.cpp +++ b/src/ac6_backend_fixes/ac6_effect_mode_fix.cpp @@ -67,13 +67,9 @@ #include 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, diff --git a/src/ac6_backend_fixes/ac6_kbm_input.cpp b/src/ac6_backend_fixes/ac6_kbm_input.cpp index 7840c4de..13c34690 100644 --- a/src/ac6_backend_fixes/ac6_kbm_input.cpp +++ b/src/ac6_backend_fixes/ac6_kbm_input.cpp @@ -67,14 +67,17 @@ #include #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. diff --git a/src/ac6_backend_fixes/ac6_widescreen.cpp b/src/ac6_backend_fixes/ac6_widescreen.cpp index 75fe404f..b4605148 100644 --- a/src/ac6_backend_fixes/ac6_widescreen.cpp +++ b/src/ac6_backend_fixes/ac6_widescreen.cpp @@ -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); diff --git a/src/ac6recomp_app.h b/src/ac6recomp_app.h index 8eb265bb..fe732081 100644 --- a/src/ac6recomp_app.h +++ b/src/ac6recomp_app.h @@ -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 diff --git a/src/main.cpp b/src/main.cpp index dcb6bff9..71afad9a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 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); } diff --git a/src/render_hooks.cpp b/src/render_hooks.cpp index 8ceb238d..a29f439e 100644 --- a/src/render_hooks.cpp +++ b/src/render_hooks.cpp @@ -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; diff --git a/thirdparty/rexglue-sdk/include/rex/cvar.h b/thirdparty/rexglue-sdk/include/rex/cvar.h index c6abbfb1..20f32e14 100644 --- a/thirdparty/rexglue-sdk/include/rex/cvar.h +++ b/thirdparty/rexglue-sdk/include/rex/cvar.h @@ -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& GetRegistry(); @@ -198,6 +217,32 @@ void ResetToDefault(std::string_view name); void ResetAllToDefaults(); bool HasNonDefaultValue(std::string_view name); std::vector 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); diff --git a/thirdparty/rexglue-sdk/include/rex/rex_app.h b/thirdparty/rexglue-sdk/include/rex/rex_app.h index e1f393cf..05f3d76c 100644 --- a/thirdparty/rexglue-sdk/include/rex/rex_app.h +++ b/thirdparty/rexglue-sdk/include/rex/rex_app.h @@ -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) {} diff --git a/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h b/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h index 922ae9d2..16e9b47a 100644 --- a/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h +++ b/thirdparty/rexglue-sdk/include/rex/ui/overlay/settings_overlay.h @@ -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_; diff --git a/thirdparty/rexglue-sdk/src/core/cvar.cpp b/thirdparty/rexglue-sdk/src/core/cvar.cpp index fa694879..fa65a0ad 100644 --- a/thirdparty/rexglue-sdk/src/core/cvar.cpp +++ b/thirdparty/rexglue-sdk/src/core/cvar.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -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(c) < 0x20) { + char buf[8]; + std::snprintf(buf, sizeof(buf), "\\u%04X", static_cast(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 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; } diff --git a/thirdparty/rexglue-sdk/src/graphics/flags.cpp b/thirdparty/rexglue-sdk/src/graphics/flags.cpp index 7d259ecd..879c6b37 100644 --- a/thirdparty/rexglue-sdk/src/graphics/flags.cpp +++ b/thirdparty/rexglue-sdk/src/graphics/flags.cpp @@ -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 \"[:[+...]]\" " - "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 \"[:[+...]]\" 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: \"[:[+...]]\" 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 \"[:[+...]]\": 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 \"[:[+...]]\": 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; diff --git a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp index 3288bd1c..88158ebe 100644 --- a/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp +++ b/thirdparty/rexglue-sdk/src/native/audio/audio_system.cpp @@ -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 { diff --git a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp index d30e9aa4..34fba7aa 100644 --- a/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp +++ b/thirdparty/rexglue-sdk/src/native/ui/rex_app.cpp @@ -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); diff --git a/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp b/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp index 6cd9fea9..14600670 100644 --- a/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp +++ b/thirdparty/rexglue-sdk/src/ui/overlay/settings_overlay.cpp @@ -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 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(); } diff --git a/thirdparty/rexglue-sdk/tests/unit/core/cvar_test.cpp b/thirdparty/rexglue-sdk/tests/unit/core/cvar_test.cpp index 07265661..6d907ce0 100644 --- a/thirdparty/rexglue-sdk/tests/unit/core/cvar_test.cpp +++ b/thirdparty/rexglue-sdk/tests/unit/core/cvar_test.cpp @@ -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);