aboutsummaryrefslogtreecommitdiff
path: root/src/yuzu/configuration/configure_post_processing.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/yuzu/configuration/configure_post_processing.cpp')
-rw-r--r--src/yuzu/configuration/configure_post_processing.cpp511
1 files changed, 511 insertions, 0 deletions
diff --git a/src/yuzu/configuration/configure_post_processing.cpp b/src/yuzu/configuration/configure_post_processing.cpp
new file mode 100644
index 0000000000..6fe46da6c5
--- /dev/null
+++ b/src/yuzu/configuration/configure_post_processing.cpp
@@ -0,0 +1,511 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <cmath>
+#include <string>
+
+#include <QAction>
+#include <QCheckBox>
+#include <QComboBox>
+#include <QDialogButtonBox>
+#include <QFrame>
+#include <QGridLayout>
+#include <QInputDialog>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QMenu>
+#include <QPushButton>
+#include <QScrollArea>
+#include <QSlider>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+#include "common/settings.h"
+#include "video_core/post_processing/fx_chain.h"
+#include "video_core/post_processing/fx_effect.h"
+#include "video_core/post_processing/fx_preset.h"
+#include "yuzu/configuration/configure_post_processing.h"
+
+namespace {
+
+std::array<float, 4> CurrentValue(int index, const VideoCore::FxUniformDesc& uniform) {
+ auto& chain = VideoCore::FxChain::Instance();
+ if (chain.HasValue(static_cast<size_t>(index), uniform.name)) {
+ return chain.GetValue(static_cast<size_t>(index), uniform.name);
+ }
+ return uniform.default_value;
+}
+
+int SliderSteps(const VideoCore::FxUniformDesc& uniform) {
+ const float span = uniform.ui_max - uniform.ui_min;
+ const int steps = static_cast<int>(std::lround(span / uniform.ui_step));
+ if (steps < 1) {
+ return 1;
+ }
+ return steps;
+}
+
+QString FormatValue(const VideoCore::FxUniformDesc& uniform, float value) {
+ if (uniform.kind == VideoCore::FxUniformKind::Floating) {
+ return QString::number(value, 'f', 3);
+ }
+ return QString::number(static_cast<int>(std::lround(value)));
+}
+
+QString SlotLabel(const VideoCore::FxEffectDesc& effect, const std::string& technique) {
+ const QString name = QString::fromStdString(effect.label);
+ if (effect.techniques.size() == 1) {
+ return name;
+ }
+ return name + QStringLiteral(" · ") + QString::fromStdString(technique);
+}
+
+} // Anonymous namespace
+
+ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(parent) {
+ setWindowTitle(tr("Post-Processing Effects"));
+ setMinimumWidth(560);
+ setMinimumHeight(460);
+
+ auto* root = new QVBoxLayout(this);
+
+ auto* description = new QLabel(
+ tr("ReShade FX effects are loaded from the post_shaders folder in the Eden data "
+ "directory. Changes apply immediately while a game is running."),
+ this);
+ description->setWordWrap(true);
+ root->addWidget(description);
+
+ enabled_box = new QCheckBox(tr("Enable post-processing"), this);
+ enabled_box->setChecked(Settings::values.post_shader_enabled.GetValue());
+ connect(enabled_box, &QCheckBox::toggled, this, [](bool checked) {
+ Settings::values.post_shader_enabled.SetValue(checked);
+ });
+ root->addWidget(enabled_box);
+
+ auto* preset_row = new QHBoxLayout();
+ preset_row->addWidget(new QLabel(tr("Preset:"), this));
+
+ preset_combo = new QComboBox(this);
+ preset_row->addWidget(preset_combo, 1);
+
+ auto* apply_button = new QPushButton(tr("Apply"), this);
+ connect(apply_button, &QPushButton::clicked, this, [this]() {
+ const QString name = preset_combo->currentData().toString();
+ if (name.isEmpty()) {
+ return;
+ }
+ VideoCore::ApplyFxPreset(name.toStdString());
+ ApplyStructuralChange();
+ });
+ preset_row->addWidget(apply_button);
+
+ auto* reset_button = new QPushButton(tr("Reset Values"), this);
+ reset_button->setToolTip(tr("Return every effect to the values the preset ships with."));
+ connect(reset_button, &QPushButton::clicked, this, [this]() {
+ const std::string active = VideoCore::GetActiveFxPreset();
+ if (active.empty()) {
+ return;
+ }
+ VideoCore::ApplyFxPreset(active);
+ ApplyStructuralChange();
+ });
+ preset_row->addWidget(reset_button);
+
+ auto* save_button = new QPushButton(tr("Save As..."), this);
+ connect(save_button, &QPushButton::clicked, this, [this]() {
+ bool accepted = false;
+ const QString name =
+ QInputDialog::getText(this, tr("Save Preset"), tr("Preset name:"), QLineEdit::Normal,
+ QString(), &accepted);
+ if (!accepted || name.trimmed().isEmpty()) {
+ return;
+ }
+ VideoCore::SaveFxPreset(name.trimmed().toStdString(), std::string());
+ PopulatePresetCombo();
+ RefreshPresetStatus();
+ });
+ preset_row->addWidget(save_button);
+
+ auto* delete_button = new QPushButton(tr("Delete"), this);
+ connect(delete_button, &QPushButton::clicked, this, [this]() {
+ const QString name = preset_combo->currentData().toString();
+ if (name.isEmpty()) {
+ return;
+ }
+ VideoCore::DeleteFxPreset(name.toStdString());
+ PopulatePresetCombo();
+ RefreshPresetStatus();
+ });
+ preset_row->addWidget(delete_button);
+
+ root->addLayout(preset_row);
+
+ preset_status = new QLabel(this);
+ preset_status->setWordWrap(true);
+ root->addWidget(preset_status);
+
+ auto* scroll = new QScrollArea(this);
+ scroll->setWidgetResizable(true);
+ slots_container = new QWidget(scroll);
+ slots_layout = new QVBoxLayout(slots_container);
+ slots_layout->setAlignment(Qt::AlignTop);
+ scroll->setWidget(slots_container);
+ root->addWidget(scroll, 1);
+
+ auto* actions = new QHBoxLayout();
+
+ auto* add_button = new QPushButton(tr("Add Effect"), this);
+ auto* add_menu = new QMenu(add_button);
+ for (const auto& effect : VideoCore::GetFxCatalog()) {
+ if (!effect.Valid()) {
+ continue;
+ }
+ for (const auto& technique : effect.techniques) {
+ QAction* action = add_menu->addAction(SlotLabel(effect, technique));
+ action->setToolTip(QString::fromStdString(effect.description));
+ const std::string file = effect.file;
+ const std::string name = technique;
+ connect(action, &QAction::triggered, this, [this, file, name]() {
+ VideoCore::FxChain::Instance().Append(file, name);
+ ApplyStructuralChange();
+ });
+ }
+ }
+ add_button->setMenu(add_menu);
+ actions->addWidget(add_button);
+
+ actions->addStretch();
+ root->addLayout(actions);
+
+ auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::close);
+ root->addWidget(buttons);
+
+ VideoCore::ReloadFxCatalog();
+ VideoCore::ReloadFxPresetCatalog();
+ VideoCore::FxChain::Instance().LoadFromSettings();
+ VideoCore::FxChain::Instance().DropUnknownEntries();
+ PopulatePresetCombo();
+ RebuildRows();
+ RefreshPresetStatus();
+}
+
+ConfigurePostProcessing::~ConfigurePostProcessing() = default;
+
+void ConfigurePostProcessing::ApplyStructuralChange() {
+ VideoCore::FxChain::Instance().StoreToSettings();
+ RebuildRows();
+ RefreshPresetStatus();
+}
+
+void ConfigurePostProcessing::PopulatePresetCombo() {
+ const QString previous = preset_combo->currentData().toString();
+ preset_combo->clear();
+
+ for (const auto& preset : VideoCore::GetFxPresetCatalog()) {
+ const QString name = QString::fromStdString(preset.name);
+ preset_combo->addItem(name, name);
+ preset_combo->setItemData(preset_combo->count() - 1,
+ QString::fromStdString(preset.description), Qt::ToolTipRole);
+ }
+
+ const int restored = preset_combo->findData(previous);
+ if (restored >= 0) {
+ preset_combo->setCurrentIndex(restored);
+ return;
+ }
+
+ const int active = preset_combo->findData(QString::fromStdString(VideoCore::GetActiveFxPreset()));
+ if (active >= 0) {
+ preset_combo->setCurrentIndex(active);
+ }
+}
+
+void ConfigurePostProcessing::RefreshPresetStatus() {
+ const std::string active = VideoCore::GetActiveFxPreset();
+ if (active.empty()) {
+ preset_status->setText(tr("Custom chain. Pick a preset above and press Apply to replace it."));
+ return;
+ }
+
+ if (VideoCore::IsActiveFxPresetModified()) {
+ preset_status->setText(
+ tr("Active preset: %1 (modified). Press Reset Values to go back to how it ships.")
+ .arg(QString::fromStdString(active)));
+ return;
+ }
+
+ preset_status->setText(tr("Active preset: %1").arg(QString::fromStdString(active)));
+}
+
+void ConfigurePostProcessing::PopulateEffectCombo(QComboBox* combo,
+ const VideoCore::FxChainEntry& entry) const {
+ combo->clear();
+ int selected = -1;
+
+ for (const auto& effect : VideoCore::GetFxCatalog()) {
+ if (!effect.Valid()) {
+ continue;
+ }
+ for (const auto& technique : effect.techniques) {
+ const QString key = QString::fromStdString(effect.file + "|" + technique);
+ combo->addItem(SlotLabel(effect, technique), key);
+ combo->setItemData(combo->count() - 1, QString::fromStdString(effect.description),
+ Qt::ToolTipRole);
+ if (effect.file == entry.file && technique == entry.technique) {
+ selected = combo->count() - 1;
+ }
+ }
+ }
+
+ if (selected >= 0) {
+ combo->setCurrentIndex(selected);
+ }
+}
+
+void ConfigurePostProcessing::BuildUniformWidget(QWidget* parent, QVBoxLayout* layout, int index,
+ const VideoCore::FxUniformDesc& uniform) {
+ const auto value = CurrentValue(index, uniform);
+ const QString label = QString::fromStdString(uniform.label);
+
+ if (uniform.ui_type == VideoCore::FxUiType::CheckBox) {
+ auto* box = new QCheckBox(label, parent);
+ box->setChecked(value[0] != 0.0f);
+ if (!uniform.tooltip.empty()) {
+ box->setToolTip(QString::fromStdString(uniform.tooltip));
+ }
+ const std::string name = uniform.name;
+ connect(box, &QCheckBox::toggled, this, [index, name](bool checked) {
+ std::array<float, 4> next{};
+ if (checked) {
+ next[0] = 1.0f;
+ }
+ VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
+ VideoCore::FxChain::Instance().StoreToSettings();
+ });
+ layout->addWidget(box);
+ return;
+ }
+
+ if (uniform.ui_type == VideoCore::FxUiType::Combo ||
+ uniform.ui_type == VideoCore::FxUiType::Radio) {
+ auto* row = new QHBoxLayout();
+ row->addWidget(new QLabel(label, parent));
+ auto* combo = new QComboBox(parent);
+ for (size_t i = 0; i < uniform.items.size(); ++i) {
+ combo->addItem(QString::fromStdString(uniform.items[i]), static_cast<int>(i));
+ }
+ if (combo->count() == 0) {
+ combo->addItem(tr("Enabled"), 1);
+ combo->addItem(tr("Disabled"), 0);
+ }
+ const int current = static_cast<int>(std::lround(value[0]));
+ if (current >= 0 && current < combo->count()) {
+ combo->setCurrentIndex(current);
+ }
+ if (!uniform.tooltip.empty()) {
+ combo->setToolTip(QString::fromStdString(uniform.tooltip));
+ }
+ const std::string name = uniform.name;
+ connect(combo, &QComboBox::currentIndexChanged, this, [index, name](int selected) {
+ std::array<float, 4> next{};
+ next[0] = static_cast<float>(selected);
+ VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
+ VideoCore::FxChain::Instance().StoreToSettings();
+ });
+ row->addWidget(combo, 1);
+ layout->addLayout(row);
+ return;
+ }
+
+ auto* grid = new QGridLayout();
+ for (unsigned component = 0; component < uniform.components; ++component) {
+ QString component_label = label;
+ if (uniform.components > 1) {
+ component_label = label + QStringLiteral(" [%1]").arg(component);
+ }
+
+ auto* name_label = new QLabel(component_label, parent);
+ auto* value_label = new QLabel(parent);
+ value_label->setMinimumWidth(64);
+ value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
+ value_label->setText(FormatValue(uniform, value[component]));
+
+ auto* slider = new QSlider(Qt::Horizontal, parent);
+ slider->setMinimum(0);
+ slider->setMaximum(SliderSteps(uniform));
+ slider->setValue(
+ static_cast<int>(std::lround((value[component] - uniform.ui_min) / uniform.ui_step)));
+ if (!uniform.tooltip.empty()) {
+ slider->setToolTip(QString::fromStdString(uniform.tooltip));
+ }
+
+ const std::string name = uniform.name;
+ const auto desc = uniform;
+ connect(slider, &QSlider::valueChanged, this,
+ [index, name, desc, component, value_label](int steps) {
+ auto next = CurrentValue(index, desc);
+ next[component] = desc.ui_min + static_cast<float>(steps) * desc.ui_step;
+ VideoCore::FxChain::Instance().SetValue(static_cast<size_t>(index), name, next);
+ VideoCore::FxChain::Instance().StoreToSettings();
+ value_label->setText(FormatValue(desc, next[component]));
+ });
+
+ grid->addWidget(name_label, static_cast<int>(component), 0);
+ grid->addWidget(slider, static_cast<int>(component), 1);
+ grid->addWidget(value_label, static_cast<int>(component), 2);
+ }
+ layout->addLayout(grid);
+}
+
+QWidget* ConfigurePostProcessing::BuildSlot(int index, const VideoCore::FxChainEntry& entry) {
+ auto* group = new QFrame(slots_container);
+ group->setObjectName(QStringLiteral("fxSlot"));
+ group->setStyleSheet(QStringLiteral(
+ "QFrame#fxSlot { background-color: palette(alternate-base);"
+ " border: 1px solid palette(mid); border-radius: 6px; }"));
+ auto* layout = new QVBoxLayout(group);
+
+ auto* header = new QHBoxLayout();
+
+ auto* toggle = new QToolButton(group);
+ toggle->setArrowType(Qt::RightArrow);
+ toggle->setAutoRaise(true);
+ toggle->setCheckable(true);
+ header->addWidget(toggle);
+
+ auto* combo = new QComboBox(group);
+ PopulateEffectCombo(combo, entry);
+ connect(combo, &QComboBox::currentIndexChanged, this, [this, index, combo](int) {
+ const QString key = combo->currentData().toString();
+ const qsizetype separator = key.indexOf(QLatin1Char('|'));
+ if (separator < 0) {
+ return;
+ }
+ VideoCore::FxChain::Instance().Replace(static_cast<size_t>(index),
+ key.left(separator).toStdString(),
+ key.mid(separator + 1).toStdString());
+ ApplyStructuralChange();
+ });
+ header->addWidget(combo, 1);
+
+ auto* up_button = new QToolButton(group);
+ up_button->setText(QStringLiteral("▲"));
+ up_button->setEnabled(index > 0);
+ connect(up_button, &QToolButton::clicked, this, [this, index]() {
+ VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), -1);
+ ApplyStructuralChange();
+ });
+ header->addWidget(up_button);
+
+ auto* down_button = new QToolButton(group);
+ down_button->setText(QStringLiteral("▼"));
+ down_button->setEnabled(static_cast<size_t>(index) + 1 < VideoCore::FxChain::Instance().Size());
+ connect(down_button, &QToolButton::clicked, this, [this, index]() {
+ VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), 1);
+ ApplyStructuralChange();
+ });
+ header->addWidget(down_button);
+
+ auto* reset_button = new QToolButton(group);
+ reset_button->setText(QStringLiteral("⟲"));
+ reset_button->setToolTip(tr("Reset to defaults"));
+ connect(reset_button, &QToolButton::clicked, this, [this, index]() {
+ VideoCore::FxChain::Instance().ResetValues(static_cast<size_t>(index));
+ ApplyStructuralChange();
+ });
+ header->addWidget(reset_button);
+
+ auto* remove_button = new QToolButton(group);
+ remove_button->setText(QStringLiteral("✕"));
+ connect(remove_button, &QToolButton::clicked, this, [this, index]() {
+ VideoCore::FxChain::Instance().Remove(static_cast<size_t>(index));
+ ApplyStructuralChange();
+ });
+ header->addWidget(remove_button);
+
+ layout->addLayout(header);
+
+ auto* body = new QWidget(group);
+ auto* body_layout = new QVBoxLayout(body);
+ body_layout->setContentsMargins(0, 0, 0, 0);
+ body->setVisible(false);
+ layout->addWidget(body);
+
+ connect(toggle, &QToolButton::toggled, this, [toggle, body](bool open) {
+ body->setVisible(open);
+ if (open) {
+ toggle->setArrowType(Qt::DownArrow);
+ } else {
+ toggle->setArrowType(Qt::RightArrow);
+ }
+ });
+
+ const VideoCore::FxEffectDesc* effect = VideoCore::FindFxEffect(entry.file);
+ if (effect == nullptr) {
+ auto* missing =
+ new QLabel(tr("Effect '%1' was not found.").arg(QString::fromStdString(entry.file)),
+ group);
+ missing->setWordWrap(true);
+ body_layout->addWidget(missing);
+ return group;
+ }
+
+ if (!effect->error.empty()) {
+ auto* failed = new QLabel(
+ tr("Effect failed to compile:\n%1").arg(QString::fromStdString(effect->error)), group);
+ failed->setWordWrap(true);
+ body_layout->addWidget(failed);
+ return group;
+ }
+
+ std::string current_category;
+ for (const auto& uniform : effect->uniforms) {
+ if (uniform.category != current_category) {
+ current_category = uniform.category;
+ if (!current_category.empty()) {
+ auto* category = new QLabel(QString::fromStdString(current_category), group);
+ category->setStyleSheet(QStringLiteral("font-weight: bold;"));
+ body_layout->addWidget(category);
+ }
+ }
+ BuildUniformWidget(body, body_layout, index, uniform);
+ }
+
+ return group;
+}
+
+void ConfigurePostProcessing::RebuildRows() {
+ QLayoutItem* item = nullptr;
+ while ((item = slots_layout->takeAt(0)) != nullptr) {
+ if (item->widget() != nullptr) {
+ item->widget()->deleteLater();
+ }
+ delete item;
+ }
+
+ bool has_usable = false;
+ for (const auto& effect : VideoCore::GetFxCatalog()) {
+ if (effect.Valid()) {
+ has_usable = true;
+ break;
+ }
+ }
+
+ if (!has_usable) {
+ auto* empty = new QLabel(
+ tr("No usable ReShade FX effects were found. Place .fx files in the post_shaders "
+ "folder."),
+ slots_container);
+ empty->setWordWrap(true);
+ slots_layout->addWidget(empty);
+ return;
+ }
+
+ const auto entries = VideoCore::FxChain::Instance().Entries();
+ for (size_t i = 0; i < entries.size(); ++i) {
+ slots_layout->addWidget(BuildSlot(static_cast<int>(i), entries[i]));
+ }
+}