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:
Dipshet
2026-08-06 15:46:15 +02:00
parent 9b81d7b7e6
commit deb406bdb0
16 changed files with 681 additions and 372 deletions
+130 -116
View File
@@ -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);
}