From f663f041ed3f6ad1eadbefd15e8f77e2dd32c294 Mon Sep 17 00:00:00 2001 From: "Herman S." <429230+has207@users.noreply.github.com> Date: Thu, 11 Dec 2025 17:05:11 +0900 Subject: [PATCH] [UI] Fix post-processing dialog. Should now have correct sharpening options and work right with gamepad input. Also fixing the per-game UI override as that had an extra entry for some reason. --- src/xenia/app/emulator_window.cc | 7 + src/xenia/app/emulator_window.h | 33 +- src/xenia/config.cc | 85 +++++ src/xenia/config.h | 12 + src/xenia/ui/config_helpers.h | 3 +- src/xenia/ui/gamepad_dialog_qt.h | 3 +- src/xenia/ui/performance_tuning_dialog_qt.cc | 67 +--- src/xenia/ui/performance_tuning_dialog_qt.h | 2 - src/xenia/ui/postprocessing_dialog_qt.cc | 325 ++++++++++++++----- src/xenia/ui/postprocessing_dialog_qt.h | 12 + 10 files changed, 394 insertions(+), 155 deletions(-) diff --git a/src/xenia/app/emulator_window.cc b/src/xenia/app/emulator_window.cc index 97b2f7afd..350e5c68e 100644 --- a/src/xenia/app/emulator_window.cc +++ b/src/xenia/app/emulator_window.cc @@ -1005,6 +1005,13 @@ void EmulatorWindow::UpdateDitherCvar(bool value) { } } +void EmulatorWindow::UpdateFsrMaxUpsamplingPassesCvar(uint32_t value) { + auto cvars_config = GetGuestOutputPaintConfigForCvars(); + if (cvars_config.GetFsrMaxUpsamplingPasses() != value) { + OVERRIDE_uint32(postprocess_ffx_fsr_max_upsampling_passes, value); + } +} + void EmulatorWindow::OnKeyDown(ui::KeyEvent& e) { if (!emulator_initialized_) { return; diff --git a/src/xenia/app/emulator_window.h b/src/xenia/app/emulator_window.h index c60d027de..8819b006c 100644 --- a/src/xenia/app/emulator_window.h +++ b/src/xenia/app/emulator_window.h @@ -131,6 +131,7 @@ class EmulatorWindow { void UpdateScalingAndSharpeningCvar( ui::Presenter::GuestOutputPaintConfig::Effect effect); void UpdateFsrSharpnessCvar(float value); + void UpdateFsrMaxUpsamplingPassesCvar(uint32_t value); void UpdateCasSharpnessCvar(float value); void UpdateDitherCvar(bool value); @@ -170,6 +171,22 @@ class EmulatorWindow { } }; + // For comparisons, use GetSwapPostEffectForCvarValue instead as the default + // fallback may be used for multiple values. + static const char* GetCvarValueForSwapPostEffect( + gpu::CommandProcessor::SwapPostEffect effect); + static gpu::CommandProcessor::SwapPostEffect GetSwapPostEffectForCvarValue( + const std::string& cvar_value); + // For comparisons, use GetGuestOutputPaintEffectForCvarValue instead as the + // default fallback may be used for multiple values. + static const char* GetCvarValueForGuestOutputPaintEffect( + ui::Presenter::GuestOutputPaintConfig::Effect effect); + static ui::Presenter::GuestOutputPaintConfig::Effect + GetGuestOutputPaintEffectForCvarValue(const std::string& cvar_value); + static ui::Presenter::GuestOutputPaintConfig + GetGuestOutputPaintConfigForCvars(); + void ApplyDisplayConfigForCvars(); + private: class EmulatorWindowListener final : public ui::WindowListener, public ui::WindowInputListener { @@ -198,22 +215,6 @@ class EmulatorWindow { bool Initialize(); - // For comparisons, use GetSwapPostEffectForCvarValue instead as the default - // fallback may be used for multiple values. - static const char* GetCvarValueForSwapPostEffect( - gpu::CommandProcessor::SwapPostEffect effect); - static gpu::CommandProcessor::SwapPostEffect GetSwapPostEffectForCvarValue( - const std::string& cvar_value); - // For comparisons, use GetGuestOutputPaintEffectForCvarValue instead as the - // default fallback may be used for multiple values. - static const char* GetCvarValueForGuestOutputPaintEffect( - ui::Presenter::GuestOutputPaintConfig::Effect effect); - static ui::Presenter::GuestOutputPaintConfig::Effect - GetGuestOutputPaintEffectForCvarValue(const std::string& cvar_value); - static ui::Presenter::GuestOutputPaintConfig - GetGuestOutputPaintConfigForCvars(); - void ApplyDisplayConfigForCvars(); - void OnKeyDown(ui::KeyEvent& e); void OnMouseDown(const ui::MouseEvent& e); void OnMouseDoubleClick(const ui::MouseEvent& e); diff --git a/src/xenia/config.cc b/src/xenia/config.cc index b2e5a5cac..5f521d6b1 100644 --- a/src/xenia/config.cc +++ b/src/xenia/config.cc @@ -17,6 +17,7 @@ #include "xenia/base/string.h" #include "xenia/base/string_buffer.h" #include "xenia/base/system.h" +#include "xenia/emulator.h" #include "xenia/ui/config_helpers.h" toml::parse_result ParseFile(const std::filesystem::path& filename) { @@ -271,6 +272,90 @@ void SaveGameConfig(uint32_t title_id, const toml::table& config_table) { } } +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, const std::string& value) { + if (!emulator || !emulator->is_title_open()) { + return; + } + + uint32_t title_id = emulator->title_id(); + toml::table config_table = LoadGameConfig(title_id); + + if (!config_table.contains(section)) { + config_table.insert(section, toml::table{}); + } + + auto* section_table = config_table[section].as_table(); + if (section_table) { + section_table->insert_or_assign(cvar_name, value); + } + + SaveGameConfig(title_id, config_table); +} + +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, bool value) { + if (!emulator || !emulator->is_title_open()) { + return; + } + + uint32_t title_id = emulator->title_id(); + toml::table config_table = LoadGameConfig(title_id); + + if (!config_table.contains(section)) { + config_table.insert(section, toml::table{}); + } + + auto* section_table = config_table[section].as_table(); + if (section_table) { + section_table->insert_or_assign(cvar_name, value); + } + + SaveGameConfig(title_id, config_table); +} + +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, uint32_t value) { + if (!emulator || !emulator->is_title_open()) { + return; + } + + uint32_t title_id = emulator->title_id(); + toml::table config_table = LoadGameConfig(title_id); + + if (!config_table.contains(section)) { + config_table.insert(section, toml::table{}); + } + + auto* section_table = config_table[section].as_table(); + if (section_table) { + section_table->insert_or_assign(cvar_name, value); + } + + SaveGameConfig(title_id, config_table); +} + +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, double value) { + if (!emulator || !emulator->is_title_open()) { + return; + } + + uint32_t title_id = emulator->title_id(); + toml::table config_table = LoadGameConfig(title_id); + + if (!config_table.contains(section)) { + config_table.insert(section, toml::table{}); + } + + auto* section_table = config_table[section].as_table(); + if (section_table) { + section_table->insert_or_assign(cvar_name, value); + } + + SaveGameConfig(title_id, config_table); +} + toml::table LoadGameConfig(uint32_t title_id) { const auto game_config_path = config_folder / "config" / diff --git a/src/xenia/config.h b/src/xenia/config.h index 872b61c61..1e85904e6 100644 --- a/src/xenia/config.h +++ b/src/xenia/config.h @@ -14,6 +14,10 @@ #include #include "third_party/tomlplusplus/toml.hpp" +namespace xe { +class Emulator; +} // namespace xe + toml::parse_result ParseFile(const std::filesystem::path& filename); namespace config { @@ -25,6 +29,14 @@ std::vector LoadGameConfigAsArgs(const std::string_view title_id); toml::table LoadGameConfig(uint32_t title_id); void SaveConfig(); void SaveGameConfig(uint32_t title_id, const toml::table& config_table); +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, const std::string& value); +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, bool value); +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, uint32_t value); +void SaveGameConfigSetting(xe::Emulator* emulator, const char* section, + const char* cvar_name, double value); void ReloadConfig(); void SetConfigSavedCallback(std::function callback); } // namespace config diff --git a/src/xenia/ui/config_helpers.h b/src/xenia/ui/config_helpers.h index 01f29b976..b4f00970e 100644 --- a/src/xenia/ui/config_helpers.h +++ b/src/xenia/ui/config_helpers.h @@ -82,8 +82,7 @@ GetKnownEnumOptions() { {"readback_resolve", {"fast", "some", "full", "none"}}, {"render_target_path", {"performance", "accuracy"}}, {"postprocess_antialiasing", {"off", "fxaa", "fxaa_extreme"}}, - {"postprocess_scaling_and_sharpening", - {"bilinear", "cas", "fsr", "nearest"}}, + {"postprocess_scaling_and_sharpening", {"none", "cas", "fsr"}}, {"spirv_version_override", {"auto", "1.0", "1.3", "1.4", "1.5", "1.6"}}, {"xma_decoder", {"old", "new", "master", "fake"}}, {"user_language", diff --git a/src/xenia/ui/gamepad_dialog_qt.h b/src/xenia/ui/gamepad_dialog_qt.h index d48321a9d..647152354 100644 --- a/src/xenia/ui/gamepad_dialog_qt.h +++ b/src/xenia/ui/gamepad_dialog_qt.h @@ -48,6 +48,8 @@ class GamepadDialog : public QDialog { // Override to customize which widgets are focusable virtual bool IsWidgetGamepadFocusable(QWidget* widget) const; + void UpdateFocusableWidgets(); + bool eventFilter(QObject* obj, QEvent* event) override; void showEvent(QShowEvent* event) override; @@ -55,7 +57,6 @@ class GamepadDialog : public QDialog { void PollGamepad(); private: - void UpdateFocusableWidgets(); void NavigateFocusVertical(int direction); void NavigateFocusHorizontal(int direction); void AcceptFocusedButton(); diff --git a/src/xenia/ui/performance_tuning_dialog_qt.cc b/src/xenia/ui/performance_tuning_dialog_qt.cc index 8ec0f638a..336a5cbe8 100644 --- a/src/xenia/ui/performance_tuning_dialog_qt.cc +++ b/src/xenia/ui/performance_tuning_dialog_qt.cc @@ -322,58 +322,6 @@ void PerformanceTuningDialogQt::LoadCurrentSettings() { clear_memory_checkbox_->blockSignals(false); } -void PerformanceTuningDialogQt::SaveToGameConfig(const char* cvar_name, - const std::string& value) { - auto emulator = emulator_window_->emulator(); - if (!emulator || !emulator->is_title_open()) { - return; - } - - uint32_t title_id = emulator->title_id(); - if (title_id == 0) { - return; - } - - toml::table config_table = config::LoadGameConfig(title_id); - - if (!config_table.contains("GPU")) { - config_table.insert("GPU", toml::table{}); - } - - auto* gpu_table = config_table["GPU"].as_table(); - if (gpu_table) { - gpu_table->insert_or_assign(cvar_name, value); - } - - config::SaveGameConfig(title_id, config_table); -} - -void PerformanceTuningDialogQt::SaveToGameConfig(const char* cvar_name, - bool value) { - auto emulator = emulator_window_->emulator(); - if (!emulator || !emulator->is_title_open()) { - return; - } - - uint32_t title_id = emulator->title_id(); - if (title_id == 0) { - return; - } - - toml::table config_table = config::LoadGameConfig(title_id); - - if (!config_table.contains("GPU")) { - config_table.insert("GPU", toml::table{}); - } - - auto* gpu_table = config_table["GPU"].as_table(); - if (gpu_table) { - gpu_table->insert_or_assign(cvar_name, value); - } - - config::SaveGameConfig(title_id, config_table); -} - void PerformanceTuningDialogQt::ShowNotification(const QString& title, const QString& description) { auto* notification = @@ -384,14 +332,16 @@ void PerformanceTuningDialogQt::ShowNotification(const QString& title, void PerformanceTuningDialogQt::OnVsyncChanged(int state) { bool enabled = (state == Qt::Checked); SetVsync(enabled); - SaveToGameConfig("vsync", enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", "vsync", + enabled); ShowNotification("VSync", enabled ? "Enabled" : "Disabled"); } void PerformanceTuningDialogQt::OnOcclusionQueryChanged(int state) { bool enabled = (state == Qt::Checked); SetOcclusionQueryEnable(enabled); - SaveToGameConfig("occlusion_query_enable", enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "occlusion_query_enable", enabled); ShowNotification("Occlusion Queries", enabled ? "Enabled" : "Disabled"); } @@ -447,8 +397,10 @@ void PerformanceTuningDialogQt::OnReadbackMemexportChanged(int value) { gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexport, memexport_enabled); gpu::SaveGPUSetting(gpu::GPUSetting::ReadbackMemexportFast, memexport_fast); - SaveToGameConfig("readback_memexport", memexport_enabled); - SaveToGameConfig("readback_memexport_fast", memexport_fast); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "readback_memexport", memexport_enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "readback_memexport_fast", memexport_fast); const char* mode_names[] = {"None", "Fast", "Full"}; ShowNotification("Readback Memexport", mode_names[value]); @@ -457,7 +409,8 @@ void PerformanceTuningDialogQt::OnReadbackMemexportChanged(int value) { void PerformanceTuningDialogQt::OnClearMemoryPageStateChanged(int state) { bool enabled = (state == Qt::Checked); gpu::SaveGPUSetting(gpu::GPUSetting::ClearMemoryPageState, enabled); - SaveToGameConfig("clear_memory_page_state", enabled); + config::SaveGameConfigSetting(emulator_window_->emulator(), "GPU", + "clear_memory_page_state", enabled); ShowNotification("Clear Memory Page State", enabled ? "Enabled" : "Disabled"); } diff --git a/src/xenia/ui/performance_tuning_dialog_qt.h b/src/xenia/ui/performance_tuning_dialog_qt.h index 12765b288..ac09dbcb6 100644 --- a/src/xenia/ui/performance_tuning_dialog_qt.h +++ b/src/xenia/ui/performance_tuning_dialog_qt.h @@ -49,8 +49,6 @@ class PerformanceTuningDialogQt : public ui::GamepadDialog { private: void SetupUI(); void LoadCurrentSettings(); - void SaveToGameConfig(const char* cvar_name, const std::string& value); - void SaveToGameConfig(const char* cvar_name, bool value); void ShowNotification(const QString& title, const QString& description); EmulatorWindow* emulator_window_; diff --git a/src/xenia/ui/postprocessing_dialog_qt.cc b/src/xenia/ui/postprocessing_dialog_qt.cc index 7b0130647..920462294 100644 --- a/src/xenia/ui/postprocessing_dialog_qt.cc +++ b/src/xenia/ui/postprocessing_dialog_qt.cc @@ -20,12 +20,14 @@ #include "third_party/fmt/include/fmt/format.h" #include "xenia/app/emulator_window.h" #include "xenia/base/cvar.h" +#include "xenia/config.h" #include "xenia/gpu/graphics_system.h" #include "xenia/ui/qt_util.h" DECLARE_bool(postprocess_dither); DECLARE_double(postprocess_ffx_cas_additional_sharpness); DECLARE_double(postprocess_ffx_fsr_sharpness_reduction); +DECLARE_uint32(postprocess_ffx_fsr_max_upsampling_passes); DECLARE_string(postprocess_antialiasing); DECLARE_string(postprocess_scaling_and_sharpening); @@ -210,32 +212,162 @@ void PostProcessingDialogQt::SetupUI() { resampling_layout->setSpacing(8); resampling_button_group_ = new QButtonGroup(this); + + // None / Bilinear option (no controls) effect_bilinear_radio_ = new QRadioButton("None / Bilinear", resampling_group_); - effect_cas_radio_ = new QRadioButton( - "AMD FidelityFX Contrast Adaptive Sharpening (CAS)", resampling_group_); - effect_fsr_radio_ = new QRadioButton( - "AMD FidelityFX Super Resolution 1.0 (FSR)", resampling_group_); - resampling_button_group_->addButton( effect_bilinear_radio_, static_cast( ui::Presenter::GuestOutputPaintConfig::Effect::kBilinear)); + resampling_layout->addWidget(effect_bilinear_radio_); + + // CAS option with controls as children + auto* cas_container = new QWidget(resampling_group_); + auto* cas_container_layout = new QVBoxLayout(cas_container); + cas_container_layout->setContentsMargins(0, 0, 0, 0); + cas_container_layout->setSpacing(4); + + effect_cas_radio_ = new QRadioButton( + "AMD FidelityFX Contrast Adaptive Sharpening (CAS)", cas_container); resampling_button_group_->addButton( effect_cas_radio_, static_cast(ui::Presenter::GuestOutputPaintConfig::Effect::kCas)); + cas_container_layout->addWidget(effect_cas_radio_); + + // CAS controls as children of cas_container + cas_sharpness_widget_ = new QWidget(cas_container); + auto* cas_layout = new QVBoxLayout(cas_sharpness_widget_); + cas_layout->setContentsMargins(20, 4, 0, 0); + + cas_sharpness_label_ = new QLabel( + "CAS additional sharpness (higher is sharper):", cas_sharpness_widget_); + cas_layout->addWidget(cas_sharpness_label_); + + auto* cas_slider_layout = new QHBoxLayout(); + cas_sharpness_slider_ = new QSlider(Qt::Horizontal, cas_sharpness_widget_); + cas_sharpness_slider_->setRange(0, 100); // Will be mapped to 0.0-1.0 + cas_sharpness_slider_->setFocusPolicy(Qt::StrongFocus); + cas_sharpness_value_label_ = new QLabel("0%", cas_sharpness_widget_); + cas_sharpness_value_label_->setMinimumWidth(50); + cas_reset_button_ = new QPushButton("Reset", cas_sharpness_widget_); + cas_reset_button_->setMaximumWidth(50); + cas_reset_button_->setFocusPolicy(Qt::StrongFocus); + + cas_slider_layout->addWidget(cas_sharpness_slider_); + cas_slider_layout->addWidget(cas_sharpness_value_label_); + cas_slider_layout->addWidget(cas_reset_button_); + cas_layout->addLayout(cas_slider_layout); + + cas_container_layout->addWidget(cas_sharpness_widget_); + resampling_layout->addWidget(cas_container); + + // FSR option with controls as children + auto* fsr_container = new QWidget(resampling_group_); + auto* fsr_container_layout = new QVBoxLayout(fsr_container); + fsr_container_layout->setContentsMargins(0, 0, 0, 0); + fsr_container_layout->setSpacing(4); + + effect_fsr_radio_ = new QRadioButton( + "AMD FidelityFX Super Resolution 1.0 (FSR)", fsr_container); resampling_button_group_->addButton( effect_fsr_radio_, static_cast(ui::Presenter::GuestOutputPaintConfig::Effect::kFsr)); + fsr_container_layout->addWidget(effect_fsr_radio_); - resampling_layout->addWidget(effect_bilinear_radio_); - resampling_layout->addWidget(effect_cas_radio_); - resampling_layout->addWidget(effect_fsr_radio_); + // FSR sharpness controls as children of fsr_container + fsr_sharpness_widget_ = new QWidget(fsr_container); + auto* fsr_layout = new QVBoxLayout(fsr_sharpness_widget_); + fsr_layout->setContentsMargins(20, 4, 0, 0); + + fsr_sharpness_label_ = + new QLabel("FSR sharpness reduction when upscaling (lower is sharper):", + fsr_sharpness_widget_); + fsr_layout->addWidget(fsr_sharpness_label_); + + auto* fsr_slider_layout = new QHBoxLayout(); + fsr_sharpness_slider_ = new QSlider(Qt::Horizontal, fsr_sharpness_widget_); + fsr_sharpness_slider_->setRange(0, 200); // Will be mapped to 0.0-2.0 + fsr_sharpness_slider_->setFocusPolicy(Qt::StrongFocus); + fsr_sharpness_value_label_ = new QLabel("0%", fsr_sharpness_widget_); + fsr_sharpness_value_label_->setMinimumWidth(50); + fsr_reset_button_ = new QPushButton("Reset", fsr_sharpness_widget_); + fsr_reset_button_->setMaximumWidth(50); + fsr_reset_button_->setFocusPolicy(Qt::StrongFocus); + + fsr_slider_layout->addWidget(fsr_sharpness_slider_); + fsr_slider_layout->addWidget(fsr_sharpness_value_label_); + fsr_slider_layout->addWidget(fsr_reset_button_); + fsr_layout->addLayout(fsr_slider_layout); + + fsr_container_layout->addWidget(fsr_sharpness_widget_); + + // FSR max upsampling controls as children of fsr_container + fsr_max_upsampling_widget_ = new QWidget(fsr_container); + auto* fsr_upsampling_layout = new QVBoxLayout(fsr_max_upsampling_widget_); + fsr_upsampling_layout->setContentsMargins(20, 4, 0, 0); + + fsr_max_upsampling_label_ = new QLabel( + "FSR max upsampling passes (lower is faster, higher is better quality):", + fsr_max_upsampling_widget_); + fsr_upsampling_layout->addWidget(fsr_max_upsampling_label_); + + fsr_max_upsampling_button_group_ = new QButtonGroup(this); + fsr_max_upsampling_radio_1_ = + new QRadioButton("1", fsr_max_upsampling_widget_); + fsr_max_upsampling_radio_1_->setFocusPolicy(Qt::StrongFocus); + fsr_max_upsampling_radio_2_ = + new QRadioButton("2", fsr_max_upsampling_widget_); + fsr_max_upsampling_radio_2_->setFocusPolicy(Qt::StrongFocus); + fsr_max_upsampling_radio_3_ = + new QRadioButton("3", fsr_max_upsampling_widget_); + fsr_max_upsampling_radio_3_->setFocusPolicy(Qt::StrongFocus); + fsr_max_upsampling_radio_4_ = + new QRadioButton("4", fsr_max_upsampling_widget_); + fsr_max_upsampling_radio_4_->setFocusPolicy(Qt::StrongFocus); + + fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_1_, 1); + fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_2_, 2); + fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_3_, 3); + fsr_max_upsampling_button_group_->addButton(fsr_max_upsampling_radio_4_, 4); + + auto* fsr_upsampling_radio_layout = new QHBoxLayout(); + fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_1_); + fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_2_); + fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_3_); + fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_radio_4_); + fsr_max_upsampling_reset_button_ = + new QPushButton("Reset", fsr_max_upsampling_widget_); + fsr_max_upsampling_reset_button_->setMaximumWidth(50); + fsr_max_upsampling_reset_button_->setFocusPolicy(Qt::StrongFocus); + fsr_upsampling_radio_layout->addWidget(fsr_max_upsampling_reset_button_); + + fsr_upsampling_layout->addLayout(fsr_upsampling_radio_layout); + fsr_container_layout->addWidget(fsr_max_upsampling_widget_); + + resampling_layout->addWidget(fsr_container); connect(resampling_button_group_, QOverload::of(&QButtonGroup::idClicked), this, &PostProcessingDialogQt::OnResamplingEffectChanged); + // Connect all the signal handlers + connect(fsr_sharpness_slider_, &QSlider::valueChanged, this, + &PostProcessingDialogQt::OnFsrSharpnessChanged); + connect(fsr_reset_button_, &QPushButton::clicked, this, + &PostProcessingDialogQt::OnResetFsrSharpness); + + connect(fsr_max_upsampling_button_group_, + QOverload::of(&QButtonGroup::idClicked), this, + &PostProcessingDialogQt::OnFsrMaxUpsamplingPassesChanged); + connect(fsr_max_upsampling_reset_button_, &QPushButton::clicked, this, + &PostProcessingDialogQt::OnResetFsrMaxUpsamplingPasses); + + connect(cas_sharpness_slider_, &QSlider::valueChanged, this, + &PostProcessingDialogQt::OnCasSharpnessChanged); + connect(cas_reset_button_, &QPushButton::clicked, this, + &PostProcessingDialogQt::OnResetCasSharpness); + // Effect description effect_description_label_ = new QLabel(resampling_group_); effect_description_label_->setWordWrap(true); @@ -251,63 +383,6 @@ void PostProcessingDialogQt::SetupUI() { "QLabel { margin-top: 10px; color: #aad4ff; font-weight: bold; }"); resampling_layout->addWidget(fxaa_recommendation_label_); - // FSR sharpness controls - fsr_sharpness_widget_ = new QWidget(resampling_group_); - auto* fsr_layout = new QVBoxLayout(fsr_sharpness_widget_); - fsr_layout->setContentsMargins(0, 10, 0, 0); - - fsr_sharpness_label_ = - new QLabel("FSR sharpness reduction when upscaling (lower is sharper):", - fsr_sharpness_widget_); - fsr_layout->addWidget(fsr_sharpness_label_); - - auto* fsr_slider_layout = new QHBoxLayout(); - fsr_sharpness_slider_ = new QSlider(Qt::Horizontal, fsr_sharpness_widget_); - fsr_sharpness_slider_->setRange(0, 200); // Will be mapped to 0.0-2.0 - fsr_sharpness_value_label_ = new QLabel("0%", fsr_sharpness_widget_); - fsr_sharpness_value_label_->setMinimumWidth(50); - fsr_reset_button_ = new QPushButton("Reset", fsr_sharpness_widget_); - - fsr_slider_layout->addWidget(fsr_sharpness_slider_); - fsr_slider_layout->addWidget(fsr_sharpness_value_label_); - fsr_slider_layout->addWidget(fsr_reset_button_); - fsr_layout->addLayout(fsr_slider_layout); - - connect(fsr_sharpness_slider_, &QSlider::valueChanged, this, - &PostProcessingDialogQt::OnFsrSharpnessChanged); - connect(fsr_reset_button_, &QPushButton::clicked, this, - &PostProcessingDialogQt::OnResetFsrSharpness); - - resampling_layout->addWidget(fsr_sharpness_widget_); - - // CAS sharpness controls - cas_sharpness_widget_ = new QWidget(resampling_group_); - auto* cas_layout = new QVBoxLayout(cas_sharpness_widget_); - cas_layout->setContentsMargins(0, 10, 0, 0); - - cas_sharpness_label_ = new QLabel( - "CAS additional sharpness (higher is sharper):", cas_sharpness_widget_); - cas_layout->addWidget(cas_sharpness_label_); - - auto* cas_slider_layout = new QHBoxLayout(); - cas_sharpness_slider_ = new QSlider(Qt::Horizontal, cas_sharpness_widget_); - cas_sharpness_slider_->setRange(0, 100); // Will be mapped to 0.0-1.0 - cas_sharpness_value_label_ = new QLabel("0%", cas_sharpness_widget_); - cas_sharpness_value_label_->setMinimumWidth(50); - cas_reset_button_ = new QPushButton("Reset", cas_sharpness_widget_); - - cas_slider_layout->addWidget(cas_sharpness_slider_); - cas_slider_layout->addWidget(cas_sharpness_value_label_); - cas_slider_layout->addWidget(cas_reset_button_); - cas_layout->addLayout(cas_slider_layout); - - connect(cas_sharpness_slider_, &QSlider::valueChanged, this, - &PostProcessingDialogQt::OnCasSharpnessChanged); - connect(cas_reset_button_, &QPushButton::clicked, this, - &PostProcessingDialogQt::OnResetCasSharpness); - - resampling_layout->addWidget(cas_sharpness_widget_); - content_layout->addWidget(resampling_group_); // Dithering group @@ -339,6 +414,14 @@ void PostProcessingDialogQt::LoadCurrentSettings() { return; } + // Block signals while loading to prevent triggering save handlers + aa_button_group_->blockSignals(true); + resampling_button_group_->blockSignals(true); + fsr_sharpness_slider_->blockSignals(true); + fsr_max_upsampling_button_group_->blockSignals(true); + cas_sharpness_slider_->blockSignals(true); + dither_checkbox_->blockSignals(true); + // Load anti-aliasing settings auto command_processor = graphics_system->command_processor(); if (command_processor) { @@ -357,18 +440,41 @@ void PostProcessingDialogQt::LoadCurrentSettings() { // Set FSR sharpness (convert from 0.0-2.0 to 0-200) float fsr_sharpness = config.GetFsrSharpnessReduction(); - // Apply power 2.0 scaling as done in ImGui version - fsr_sharpness = sqrt(2.f * fsr_sharpness); - fsr_sharpness_slider_->setValue(static_cast(fsr_sharpness * 100)); + // Apply power 2.0 scaling as done in ImGui version for slider position + float slider_position = sqrt(2.f * fsr_sharpness); + fsr_sharpness_slider_->setValue(static_cast(slider_position * 100)); + // Update label to show actual FSR sharpness percentage + fsr_sharpness_value_label_->setText(SafeQString( + fmt::format("{} %", static_cast(fsr_sharpness * 100)))); + + // Set FSR max upsampling passes + uint32_t fsr_max_upsampling = config.GetFsrMaxUpsamplingPasses(); + // Clamp to valid range 1-4 + fsr_max_upsampling = + std::max(uint32_t(1), std::min(uint32_t(4), fsr_max_upsampling)); + fsr_max_upsampling_button_group_ + ->button(static_cast(fsr_max_upsampling)) + ->setChecked(true); // Set CAS sharpness (convert from 0.0-1.0 to 0-100) float cas_sharpness = config.GetCasAdditionalSharpness(); cas_sharpness_slider_->setValue(static_cast(cas_sharpness * 100)); + // Update label to match slider value + cas_sharpness_value_label_->setText(SafeQString( + fmt::format("{} %", static_cast(cas_sharpness * 100)))); // Set dithering dither_checkbox_->setChecked(config.GetDither()); } + // Re-enable signals + aa_button_group_->blockSignals(false); + resampling_button_group_->blockSignals(false); + fsr_sharpness_slider_->blockSignals(false); + fsr_max_upsampling_button_group_->blockSignals(false); + cas_sharpness_slider_->blockSignals(false); + dither_checkbox_->blockSignals(false); + UpdateEffectDescription(); UpdateSharpnessControls(); } @@ -394,6 +500,12 @@ void PostProcessingDialogQt::OnAntiAliasingChanged(int index) { // Update cvar emulator_window_->UpdateAntiAliasingCvar(new_effect); + + // Save to game config + const char* effect_str = + EmulatorWindow::GetCvarValueForSwapPostEffect(new_effect); + config::SaveGameConfigSetting(emulator, "Display", "postprocess_antialiasing", + std::string(effect_str)); } void PostProcessingDialogQt::OnResamplingEffectChanged(int index) { @@ -421,6 +533,13 @@ void PostProcessingDialogQt::OnResamplingEffectChanged(int index) { // Update cvar emulator_window_->UpdateScalingAndSharpeningCvar(new_effect); + // Save to game config + const char* effect_str = + EmulatorWindow::GetCvarValueForGuestOutputPaintEffect(new_effect); + config::SaveGameConfigSetting(emulator, "Display", + "postprocess_scaling_and_sharpening", + std::string(effect_str)); + UpdateEffectDescription(); UpdateSharpnessControls(); } @@ -456,6 +575,11 @@ void PostProcessingDialogQt::OnFsrSharpnessChanged(int value) { // Update cvar emulator_window_->UpdateFsrSharpnessCvar(fsr_sharpness); + + // Save to game config + config::SaveGameConfigSetting(emulator, "Display", + "postprocess_ffx_fsr_sharpness_reduction", + fsr_sharpness); } void PostProcessingDialogQt::OnCasSharpnessChanged(int value) { @@ -487,6 +611,11 @@ void PostProcessingDialogQt::OnCasSharpnessChanged(int value) { // Update cvar emulator_window_->UpdateCasSharpnessCvar(cas_sharpness); + + // Save to game config + config::SaveGameConfigSetting(emulator, "Display", + "postprocess_ffx_cas_additional_sharpness", + cas_sharpness); } void PostProcessingDialogQt::OnDitherChanged(int state) { @@ -513,6 +642,10 @@ void PostProcessingDialogQt::OnDitherChanged(int state) { // Update cvar emulator_window_->UpdateDitherCvar(dither); + + // Save to game config + config::SaveGameConfigSetting(emulator, "Display", "postprocess_dither", + dither); } void PostProcessingDialogQt::OnResetFsrSharpness() { @@ -530,6 +663,45 @@ void PostProcessingDialogQt::OnResetCasSharpness() { cas_sharpness_slider_->setValue(static_cast(default_value * 100)); } +void PostProcessingDialogQt::OnFsrMaxUpsamplingPassesChanged(int value) { + auto emulator = emulator_window_->emulator(); + if (!emulator) { + return; + } + + auto graphics_system = emulator->graphics_system(); + if (!graphics_system) { + return; + } + + auto presenter = graphics_system->presenter(); + if (!presenter) { + return; + } + + // Convert slider value (1-8) to FSR max upsampling passes + uint32_t max_upsampling_passes = static_cast(value); + + auto config = presenter->GetGuestOutputPaintConfigFromUIThread(); + config.SetFsrMaxUpsamplingPasses(max_upsampling_passes); + presenter->SetGuestOutputPaintConfigFromUIThread(config); + + // Update cvar + emulator_window_->UpdateFsrMaxUpsamplingPassesCvar(max_upsampling_passes); + + // Save to game config + config::SaveGameConfigSetting(emulator, "Display", + "postprocess_ffx_fsr_max_upsampling_passes", + max_upsampling_passes); +} + +void PostProcessingDialogQt::OnResetFsrMaxUpsamplingPasses() { + uint32_t default_value = + ui::Presenter::GuestOutputPaintConfig::kFsrMaxUpscalingPassesMax; + fsr_max_upsampling_button_group_->button(static_cast(default_value)) + ->setChecked(true); +} + void PostProcessingDialogQt::UpdateEffectDescription() { auto emulator = emulator_window_->emulator(); if (!emulator) { @@ -607,23 +779,22 @@ void PostProcessingDialogQt::UpdateSharpnessControls() { // Show/hide FXAA recommendation fxaa_recommendation_label_->setVisible(is_cas || is_fsr); - // Show/hide FSR sharpness controls + // With the new widget hierarchy, we can simply show/hide the control widgets fsr_sharpness_widget_->setVisible(is_fsr); + fsr_max_upsampling_widget_->setVisible(is_fsr); + cas_sharpness_widget_->setVisible(is_cas); - // Show/hide CAS sharpness controls - cas_sharpness_widget_->setVisible(is_cas || is_fsr); - - // Update CAS label text based on effect - if (is_fsr) { - cas_sharpness_label_->setText( - "CAS additional sharpness when not upscaling (higher is sharper):"); - } else { + // CAS label text (only shown for pure CAS, not FSR) + if (is_cas) { cas_sharpness_label_->setText( "CAS additional sharpness (higher is sharper):"); } // Resize dialog to fit content after showing/hiding widgets adjustSize(); + + // Update the list of focusable widgets since visibility has changed + UpdateFocusableWidgets(); } } // namespace app diff --git a/src/xenia/ui/postprocessing_dialog_qt.h b/src/xenia/ui/postprocessing_dialog_qt.h index 516fe3e45..564bb71df 100644 --- a/src/xenia/ui/postprocessing_dialog_qt.h +++ b/src/xenia/ui/postprocessing_dialog_qt.h @@ -41,9 +41,11 @@ class PostProcessingDialogQt : public ui::GamepadDialog { void OnResamplingEffectChanged(int index); void OnFsrSharpnessChanged(int value); void OnCasSharpnessChanged(int value); + void OnFsrMaxUpsamplingPassesChanged(int value); void OnDitherChanged(int state); void OnResetFsrSharpness(); void OnResetCasSharpness(); + void OnResetFsrMaxUpsamplingPasses(); protected: void mousePressEvent(QMouseEvent* event) override; @@ -89,6 +91,16 @@ class PostProcessingDialogQt : public ui::GamepadDialog { QLabel* cas_sharpness_value_label_; QPushButton* cas_reset_button_; + // FSR max upsampling passes widgets + QWidget* fsr_max_upsampling_widget_; + QLabel* fsr_max_upsampling_label_; + QButtonGroup* fsr_max_upsampling_button_group_; + QRadioButton* fsr_max_upsampling_radio_1_; + QRadioButton* fsr_max_upsampling_radio_2_; + QRadioButton* fsr_max_upsampling_radio_3_; + QRadioButton* fsr_max_upsampling_radio_4_; + QPushButton* fsr_max_upsampling_reset_button_; + // Dithering widgets QGroupBox* dither_group_; QCheckBox* dither_checkbox_;