// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 CurrentValue(int index, const VideoCore::FxUniformDesc& uniform) { auto& chain = VideoCore::FxChain::Instance(); if (chain.HasValue(static_cast(index), uniform.name)) { return chain.GetValue(static_cast(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(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(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 next{}; if (checked) { next[0] = 1.0f; } VideoCore::FxChain::Instance().SetValue(static_cast(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(i)); } if (combo->count() == 0) { combo->addItem(tr("Enabled"), 1); combo->addItem(tr("Disabled"), 0); } const int current = static_cast(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 next{}; next[0] = static_cast(selected); VideoCore::FxChain::Instance().SetValue(static_cast(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(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(steps) * desc.ui_step; VideoCore::FxChain::Instance().SetValue(static_cast(index), name, next); VideoCore::FxChain::Instance().StoreToSettings(); value_label->setText(FormatValue(desc, next[component])); }); grid->addWidget(name_label, static_cast(component), 0); grid->addWidget(slider, static_cast(component), 1); grid->addWidget(value_label, static_cast(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(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(index), -1); ApplyStructuralChange(); }); header->addWidget(up_button); auto* down_button = new QToolButton(group); down_button->setText(QStringLiteral("▼")); down_button->setEnabled(static_cast(index) + 1 < VideoCore::FxChain::Instance().Size()); connect(down_button, &QToolButton::clicked, this, [this, index]() { VideoCore::FxChain::Instance().Move(static_cast(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(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(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(i), entries[i])); } }