diff options
Diffstat (limited to 'src/yuzu/configuration/configure_post_processing.cpp')
| -rw-r--r-- | src/yuzu/configuration/configure_post_processing.cpp | 511 |
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])); + } +} |
