[vulkan, android] Add feature of post-processing shaders on Android (#4348)

Very self-explanatory; implementation of the feature for post-processing shaders for Android (at least for now); will allow users to enhance the graphic quality of video games based on the use of multiple shaders adjustable, presets and more, inspired on the PPSSPP implementation, this feature adds 22 customizable shaders (1 pass) that lives on the overlay of the screen, which means that aside of the capability to chain multiple shaders on the screen, this doesn't have depths (pixel/ depth z-buffer) so it ensures the performance with it's use. Few shaders were a faithful port from PPSSPP with their respective attribution on the shader headers for their respective owners; and there are adaptations from public references/ cinematographic (Anime4K) and the rest are my own addition.

_Special Credits:_

1.- PPSSPP Team for their contribution on the public references for shaders: Henrik Rydgard, ShadX, SimoneT, KillaMaaki and guest(r).
2.- Niklas Haas.
3.- bloc97.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4348
Reviewed-by: lizzie <lizzie@eden-emu.dev>
Reviewed-by: Maufeat <sahyno1996@gmail.com>
This commit is contained in:
CamilleLaVey
2026-09-10 15:14:02 +02:00
committed by crueter
parent 5f142c7926
commit ed566919f4
94 changed files with 7079 additions and 0 deletions
+6
View File
@@ -250,6 +250,12 @@ if (YUZU_CRASH_DUMPS)
target_compile_definitions(yuzu PRIVATE YUZU_CRASH_DUMPS)
endif()
if (ENABLE_RESHADE)
target_sources(yuzu PRIVATE
configuration/configure_post_processing.cpp
configuration/configure_post_processing.h)
endif()
if (CXX_CLANG)
target_compile_definitions(yuzu PRIVATE
$<$<VERSION_LESS:$<CXX_COMPILER_VERSION>,15>:CANNOT_EXPLICITLY_INSTANTIATE>)
@@ -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]));
}
}
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QDialog>
class QCheckBox;
class QComboBox;
class QLabel;
class QVBoxLayout;
class QWidget;
namespace VideoCore {
struct FxChainEntry;
struct FxEffectDesc;
struct FxUniformDesc;
}
class ConfigurePostProcessing : public QDialog {
Q_OBJECT
public:
explicit ConfigurePostProcessing(QWidget* parent = nullptr);
~ConfigurePostProcessing() override;
private:
void RebuildRows();
QWidget* BuildSlot(int index, const VideoCore::FxChainEntry& entry);
void BuildUniformWidget(QWidget* parent, QVBoxLayout* layout, int index,
const VideoCore::FxUniformDesc& uniform);
void PopulateEffectCombo(QComboBox* combo, const VideoCore::FxChainEntry& entry) const;
void ApplyStructuralChange();
void PopulatePresetCombo();
void RefreshPresetStatus();
QVBoxLayout* slots_layout{};
QWidget* slots_container{};
QComboBox* preset_combo{};
QLabel* preset_status{};
QCheckBox* enabled_box{};
};
+6
View File
@@ -148,6 +148,7 @@
<addaction name="action_Show_Filter_Bar"/>
<addaction name="action_Show_Status_Bar"/>
<addaction name="action_Show_Performance_Overlay"/>
<addaction name="action_Post_Processing_Shaders"/>
<addaction name="separator"/>
<addaction name="menu_Reset_Window_Size"/>
<addaction name="menu_View_Debugging"/>
@@ -604,6 +605,11 @@
<string>Show &amp;Performance Overlay</string>
</property>
</action>
<action name="action_Post_Processing_Shaders">
<property name="text">
<string>Post-Processing &amp;Shaders...</string>
</property>
</action>
<action name="action_Carousel_View">
<property name="checkable">
<bool>true</bool>
+24
View File
@@ -13,6 +13,9 @@
#include "common/settings_enums.h"
#include "frontend_common/settings_generator.h"
#include "render/performance_overlay.h"
#ifdef HAS_RESHADE
#include "configuration/configure_post_processing.h"
#endif
#include "updater/update_dialog.h"
#include "common/fs/ryujinx_compat.h"
@@ -1517,6 +1520,11 @@ void MainWindow::ConnectMenuEvents() {
connect_menu(ui->action_Show_Filter_Bar, &MainWindow::OnToggleFilterBar);
connect_menu(ui->action_Show_Status_Bar, &MainWindow::OnToggleStatusBar);
connect_menu(ui->action_Show_Performance_Overlay, &MainWindow::OnTogglePerfOverlay);
#ifdef HAS_RESHADE
connect_menu(ui->action_Post_Processing_Shaders, &MainWindow::OnPostProcessingShaders);
#else
ui->action_Post_Processing_Shaders->setVisible(false);
#endif
connect_menu(ui->action_Reset_Window_Size_720, &MainWindow::ResetWindowSize720);
connect_menu(ui->action_Reset_Window_Size_900, &MainWindow::ResetWindowSize900);
@@ -3893,6 +3901,22 @@ void MainWindow::OnTogglePerfOverlay() {
perf_overlay->setVisible(ui->action_Show_Performance_Overlay->isChecked());
}
#ifdef HAS_RESHADE
void MainWindow::OnPostProcessingShaders() {
if (post_processing_dialog == nullptr) {
post_processing_dialog = new ConfigurePostProcessing(this);
connect(post_processing_dialog, &QDialog::finished, post_processing_dialog, [this]() {
post_processing_dialog->deleteLater();
post_processing_dialog = nullptr;
});
}
post_processing_dialog->show();
post_processing_dialog->raise();
post_processing_dialog->activateWindow();
}
#endif
void MainWindow::OnGameListRefresh() {
// Resets metadata cache and reloads
QtCommon::Game::ResetMetadata(false);
+9
View File
@@ -56,6 +56,9 @@ class QSlider;
class QHBoxLayout;
class WaitTreeWidget;
class PerformanceOverlay;
#ifdef HAS_RESHADE
class ConfigurePostProcessing;
#endif
enum class GameListOpenTarget;
enum class DumpRomFSTarget;
class GameListPlaceholder;
@@ -391,6 +394,9 @@ private slots:
void OnToggleFilterBar();
void OnToggleStatusBar();
void OnTogglePerfOverlay();
#ifdef HAS_RESHADE
void OnPostProcessingShaders();
#endif
void OnGameListRefresh();
void InitializeHotkeys();
void ToggleFullscreen();
@@ -495,6 +501,9 @@ private:
QTimer shutdown_timer;
OverlayDialog* shutdown_dialog{};
PerformanceOverlay* perf_overlay = nullptr;
#ifdef HAS_RESHADE
ConfigurePostProcessing* post_processing_dialog = nullptr;
#endif
GameListPlaceholder* game_list_placeholder = nullptr;