Refine quick settings fx shader + add presets + polish UI bugs :'v

This commit is contained in:
CamilleLaVey
2026-09-08 22:03:50 -04:00
parent 5b51a960d5
commit ffc431848c
44 changed files with 1076 additions and 117 deletions
+3
View File
@@ -0,0 +1,3 @@
name=Anime
description=Cel shaded look: the picture is cleaned first, then the lighting is collapsed into flat bands with inked edges, finished with a small glow.
chain=Denoise.fx|Denoise|Strength=0.18,Radius=1.2;CelShading.fx|CelShading|Bands=5,BandContrast=0.35,BandEdge=0.2,Detail=0.7,OutlineStrength=0.7,OutlineThreshold=0.2,Saturation=1.3;Bloom.fx|Bloom|Radius=1.2,Amount=0.25
+3
View File
@@ -0,0 +1,3 @@
name=Cinematic
description=Film look: soft glow on the highlights, a filmic contrast curve, cool shadows against warm highlights, fine grain and a gentle vignette.
chain=Bloom.fx|Bloom|Radius=1.6,Amount=0.35;FilmicCurve.fx|FilmicCurve|Toe=1.35,Shoulder=1.3,Amount=0.85;SplitToning.fx|SplitToning|ShadowHue=210,ShadowStrength=0.25,HighlightHue=38,HighlightStrength=0.3;FilmGrain.fx|FilmGrain|Intensity=0.022;Vignette.fx|Vignette|Strength=0.5
+3
View File
@@ -0,0 +1,3 @@
name=Clean
description=Cleans up a low internal resolution: removes dithering and compression noise, smooths banded skies and brings edge detail back. The safe starting point if you are not sure what you want.
chain=Denoise.fx|Denoise|Strength=0.14,Curve=1.2;Deband.fx|Deband|;Sharpen.fx|Sharpen|Amount=0.75
+3
View File
@@ -0,0 +1,3 @@
name=Dreamy
description=Soft focus: the centre of the screen stays sharp while everything around it blurs, with a wide glow over the highlights. Good for slower games and cutscenes.
chain=Blur.fx|Blur|Radius=6,Strength=0.85,FocusSize=0.28,FocusSoftness=0.5;Bloom.fx|Bloom|Radius=2.0,Amount=0.55
+3
View File
@@ -0,0 +1,3 @@
name=Retro
description=Old television look: colour fringing at the edges of the screen, curved scanlines with a phosphor mask, and darkened corners. The scanline roll is switched off so it does not crawl.
chain=ChromaticAberration.fx|ChromaticAberration|Strength=1.0;CRT.fx|CRT|RollSpeed=0,Bleed=1.2;Vignette.fx|Vignette|Strength=0.7
+3
View File
@@ -0,0 +1,3 @@
name=Vivid
description=Punchier colour without stylising the image: warmer and more saturated, blacks and whites pushed to the ends of the range, and a light sharpen.
chain=NaturalColors.fx|NaturalColors|Luma=1.15,Chroma=1.35;Levels.fx|Levels|InputBlack=0.02,InputWhite=0.98;Sharpen.fx|Sharpen|Amount=0.5
+1 -1
View File
@@ -59,7 +59,7 @@ float4 PS_Bloom(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Bloom <
ui_label = "Bloom";
ui_tooltip = "Blooms bright areas into a soft glow.";
ui_tooltip = "Bleeds light out of the brightest areas into a soft halo, the way a camera does when pointed at something too bright.";
>
{
pass
+1 -1
View File
@@ -63,7 +63,7 @@ float4 PS_CRT(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique CRT <
ui_label = "CRT";
ui_tooltip = "Curved scanlines and the phosphor mask of a CRT television.";
ui_tooltip = "Curved scanlines and the phosphor mask of a CRT television, for games that were drawn with that kind of screen in mind.";
>
{
pass
+1 -1
View File
@@ -68,7 +68,7 @@ float4 PS_Cartoon(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Cartoon <
ui_label = "Cartoon";
ui_tooltip = "Ink outlines and flat colour bands. Faithful port of the PPSSPP shader; the outline gets heavy in dark scenes.";
ui_tooltip = "Ink outlines and flat colour bands. Faithful port of the PPSSPP shader; the outline gets heavy in dark scenes, so try Cartoon Soft or Cel Shading if the blacks smear.";
>
{
pass
+1 -1
View File
@@ -89,7 +89,7 @@ float4 PS_CartoonSoft(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Targe
technique CartoonSoft <
ui_label = "Cartoon Soft";
ui_tooltip = "Ink outlines and flat colour bands, with the outline held back in the shadows.";
ui_tooltip = "Ink outlines and flat colour bands, with the outline held back in the shadows so dark scenes do not turn into black blobs.";
>
{
pass
+1 -1
View File
@@ -42,7 +42,7 @@ float4 PS_ChromaticAberration(float4 pos : SV_Position, float2 uv : TEXCOORD) :
technique ChromaticAberration <
ui_label = "Chromatic Aberration";
ui_tooltip = "Splits the colour channels apart towards the edges of the screen, like a cheap lens.";
ui_tooltip = "Splits the colour channels apart towards the edges of the screen, the way a cheap lens fails to focus every colour on the same spot.";
>
{
pass
+1 -1
View File
@@ -53,7 +53,7 @@ float4 PS_ColorGrade(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique ColorGrade <
ui_label = "Colour Grade";
ui_tooltip = "Global saturation, brightness, contrast and gamma.";
ui_tooltip = "The four basic colour controls in one pass: saturation, brightness, contrast and gamma. Reach for this before anything more specialised.";
>
{
pass
+1 -1
View File
@@ -80,7 +80,7 @@ float4 PS_Deband(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Deband <
ui_label = "Deband";
ui_tooltip = "Smooths the visible steps in gradients such as skies, then dithers whatever survives.";
ui_tooltip = "Smooths the visible steps in gradients such as skies, then dithers whatever survives. Worth adding whenever large flat areas show rings.";
>
{
pass
+1 -1
View File
@@ -72,7 +72,7 @@ float4 PS_Denoise(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Denoise <
ui_label = "Denoise";
ui_tooltip = "Edge preserving blur that clears dithering and compression noise. Ported from Anime4K.";
ui_tooltip = "Edge preserving blur that clears dithering and compression noise while leaving outlines sharp. Ported from Anime4K.";
>
{
pass
+1 -1
View File
@@ -49,7 +49,7 @@ float4 PS_Natural(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique NaturalColors <
ui_label = "Natural Colours";
ui_tooltip = "Warmer and more saturated look. Ported from the PPSSPP shader.";
ui_tooltip = "Warms the picture and lifts saturation for a less washed out look. Ported from the PPSSPP shader.";
>
{
pass
+1 -1
View File
@@ -55,7 +55,7 @@ float4 PS_Scanlines(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Scanlines <
ui_label = "Scanlines";
ui_tooltip = "Horizontal scanlines of a CRT display.";
ui_tooltip = "Darkens alternating rows of pixels to imitate the horizontal scanlines of a CRT. Cheaper than the full CRT effect when you only want the lines.";
>
{
pass
+1 -1
View File
@@ -32,7 +32,7 @@ float4 PS_Sharpen(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Sharpen <
ui_label = "Sharpen";
ui_tooltip = "Unsharp mask that brings back edge detail lost to scaling.";
ui_tooltip = "Unsharp mask that brings back edge detail lost to scaling. Most useful after upscaling a low internal resolution.";
>
{
pass
+1 -1
View File
@@ -41,7 +41,7 @@ float4 PS_Vignette(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
technique Vignette <
ui_label = "Vignette";
ui_tooltip = "Darkens the corners of the screen.";
ui_tooltip = "Darkens the corners of the screen to pull the eye towards the middle, the way a camera lens falls off at its edges.";
>
{
pass
@@ -3,6 +3,8 @@
package org.yuzu.yuzu_emu.dialogs
import android.content.res.ColorStateList
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
@@ -389,7 +391,8 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
choices: List<String>,
onPicked: (Int) -> Unit
) {
val inflater = LayoutInflater.from(emulationFragment.requireContext())
val context = emulationFragment.requireContext()
val inflater = LayoutInflater.from(context)
val itemView = inflater.inflate(R.layout.item_quick_settings_add, container, false)
val button = itemView.findViewById<com.google.android.material.button.MaterialButton>(
@@ -398,9 +401,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
val choiceGroup = itemView.findViewById<RadioGroup>(R.id.add_choices)
choices.forEachIndexed { index, name ->
val radioButton = com.google.android.material.radiobutton.MaterialRadioButton(
emulationFragment.requireContext()
)
val radioButton = com.google.android.material.radiobutton.MaterialRadioButton(context)
radioButton.text = name
radioButton.id = View.generateViewId()
radioButton.setPadding(16, 8, 16, 8)
@@ -412,25 +413,113 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
choiceGroup.addView(radioButton)
}
val closedBackground = MaterialColors.getColor(
button,
com.google.android.material.R.attr.colorSecondaryContainer
)
val closedForeground = MaterialColors.getColor(
button,
com.google.android.material.R.attr.colorOnSecondaryContainer
)
val openBackground = MaterialColors.getColor(
button,
com.google.android.material.R.attr.colorErrorContainer
)
val openForeground = MaterialColors.getColor(
button,
com.google.android.material.R.attr.colorOnErrorContainer
)
val slide = context.resources.displayMetrics.density * 24.0f
var isOpen = false
button.setOnClickListener {
isOpen = !isOpen
if (isOpen) {
choiceGroup.alpha = 0.0f
choiceGroup.translationY = slide
choiceGroup.visibility = View.VISIBLE
choiceGroup.animate()
.alpha(1.0f)
.translationY(0.0f)
.setDuration(220)
.withEndAction {
choiceGroup.requestRectangleOnScreen(
Rect(0, 0, choiceGroup.width, choiceGroup.height),
false
)
}
.start()
button.text = context.getString(R.string.post_processing_hide_effects)
button.setIconResource(R.drawable.ic_clear)
button.backgroundTintList = ColorStateList.valueOf(openBackground)
button.setTextColor(openForeground)
button.iconTint = ColorStateList.valueOf(openForeground)
} else {
choiceGroup.visibility = View.GONE
choiceGroup.animate()
.alpha(0.0f)
.translationY(slide)
.setDuration(160)
.withEndAction {
choiceGroup.visibility = View.GONE
}
.start()
button.text = context.getString(R.string.post_processing_add)
button.setIconResource(R.drawable.ic_add)
button.backgroundTintList = ColorStateList.valueOf(closedBackground)
button.setTextColor(closedForeground)
button.iconTint = ColorStateList.valueOf(closedForeground)
}
}
container.addView(itemView)
}
fun addPresetBand(container: ViewGroup, name: String, summary: String) {
val inflater = LayoutInflater.from(emulationFragment.requireContext())
val itemView = inflater.inflate(R.layout.item_quick_settings_preset, container, false)
val titleView = itemView.findViewById<TextView>(R.id.preset_title)
val summaryView = itemView.findViewById<TextView>(R.id.preset_summary)
val switchView = itemView.findViewById<MaterialSwitch>(R.id.preset_switch)
titleView.text = name
if (summary.isEmpty()) {
summaryView.visibility = View.GONE
} else {
summaryView.text = summary
}
switchView.isChecked = NativePostProcessing.isEnabled()
switchView.setOnCheckedChangeListener { _, checked ->
NativePostProcessing.setEnabled(checked)
saveSettings()
}
container.addView(itemView)
}
fun addPostProcessing(container: ViewGroup, onStructureChanged: () -> Unit) {
val usable = NativePostProcessing.catalog().filter { it.valid }
if (usable.isEmpty()) {
return
}
val preset = NativePostProcessing.getActivePreset()
if (preset.isNotEmpty()) {
addDivider(container)
var summary =
YuzuApplication.appContext.getString(R.string.post_processing_preset_locked)
if (NativePostProcessing.isPresetModified()) {
summary =
YuzuApplication.appContext.getString(R.string.post_processing_preset_modified)
}
addPresetBand(container, preset, summary)
return
}
val labels = mutableListOf<String>()
val files = mutableListOf<String>()
val techniques = mutableListOf<String>()
@@ -465,7 +554,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
val body = addShaderCard(title, summary, container) {
NativePostProcessing.remove(index)
NativePostProcessing.persist()
NativePostProcessing.persistFor(emulationFragment.shouldUseCustom)
onStructureChanged()
}
@@ -478,7 +567,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
addEffectPicker(container, labels) { picked ->
NativePostProcessing.append(files[picked], techniques[picked])
NativePostProcessing.persist()
NativePostProcessing.persistFor(emulationFragment.shouldUseCustom)
onStructureChanged()
}
}
@@ -519,7 +608,7 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
component,
uniform.min + position * uniform.step
)
NativePostProcessing.persist()
NativePostProcessing.persistFor(emulationFragment.shouldUseCustom)
}
}
}
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.model.view
import androidx.annotation.StringRes
class CardSetting(
@StringRes titleId: Int = 0,
titleString: String = "",
@StringRes descriptionId: Int = 0,
descriptionString: String = "",
val expanded: Boolean,
val runnable: () -> Unit
) : SettingsItem(emptySetting, titleId, titleString, descriptionId, descriptionString) {
override val type = TYPE_CARD
}
@@ -144,6 +144,7 @@ abstract class SettingsItem(
const val TYPE_LAUNCHABLE = 13
const val TYPE_PATH = 14
const val TYPE_GPU_UNSWIZZLE = 15
const val TYPE_CARD = 16
const val FASTMEM_COMBINED = "fastmem_combined"
const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined"
@@ -23,6 +23,7 @@ import com.google.android.material.timepicker.TimeFormat
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.SettingsNavigationDirections
import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingCardBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
@@ -106,6 +107,10 @@ class SettingsAdapter(
GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_CARD -> {
CardViewHolder(ListItemSettingCardBinding.inflate(inflater), this)
}
else -> {
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
}
@@ -50,6 +50,10 @@ class SettingsFragmentPresenter(
private val expandedShaderSlots = mutableSetOf<Int>()
private var shaderPickerOpen = false
private var presetPickerOpen = false
private val context get() = YuzuApplication.appContext
// Extension for altering settings list based on each setting's properties
@@ -212,7 +216,70 @@ class SettingsFragmentPresenter(
return@apply
}
val active = NativePostProcessing.getActivePreset()
var presetSummary = context.getString(R.string.post_processing_preset_none)
if (active.isNotEmpty()) {
presetSummary = active
if (NativePostProcessing.isPresetModified()) {
presetSummary = context.getString(R.string.post_processing_preset_modified)
}
}
add(
CardSetting(
titleId = R.string.post_processing_preset,
descriptionString = presetSummary,
expanded = presetPickerOpen
) {
presetPickerOpen = !presetPickerOpen
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
if (presetPickerOpen) {
add(
RunnableSetting(
titleId = R.string.post_processing_preset_none,
isRunnable = true
) {
NativePostProcessing.clearPreset()
presetPickerOpen = false
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
for (preset in NativePostProcessing.presets()) {
add(
RunnableSetting(
titleString = preset.name,
descriptionString = preset.description,
isRunnable = true
) {
NativePostProcessing.applyPreset(preset.name)
NativePostProcessing.store()
presetPickerOpen = false
expandedShaderSlots.clear()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
}
}
if (active.isNotEmpty()) {
add(
RunnableSetting(
titleId = R.string.post_processing_preset_reset,
isRunnable = true
) {
NativePostProcessing.applyPreset(active)
NativePostProcessing.store()
expandedShaderSlots.clear()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
}
val labels = mutableListOf<String>()
val summaries = mutableListOf<String>()
val files = mutableListOf<String>()
val techniques = mutableListOf<String>()
for (effect in usable) {
@@ -222,6 +289,7 @@ class SettingsFragmentPresenter(
} else {
labels.add(effect.label + " \u00b7 " + technique)
}
summaries.add(effect.description)
files.add(effect.file)
techniques.add(technique)
}
@@ -243,17 +311,12 @@ class SettingsFragmentPresenter(
}
val isOpen = expandedShaderSlots.contains(index)
var chevron = R.drawable.ic_arrow_forward
if (isOpen) {
chevron = R.drawable.ic_dropdown_arrow
}
add(
RunnableSetting(
CardSetting(
titleString = header,
descriptionString = summary,
isRunnable = true,
iconId = chevron
expanded = isOpen
) {
if (isOpen) {
expandedShaderSlots.remove(index)
@@ -268,15 +331,6 @@ class SettingsFragmentPresenter(
continue
}
add(
IntSingleChoiceSetting(
buildSlotSelector(index, entry, files, techniques),
titleId = R.string.post_processing_effect,
choices = labels.toTypedArray(),
values = labels.indices.toList().toTypedArray()
)
)
if (effect != null) {
for (uniform in effect.uniforms) {
addUniform(this, index, uniform)
@@ -290,7 +344,7 @@ class SettingsFragmentPresenter(
isRunnable = true
) {
NativePostProcessing.move(index, -1)
NativePostProcessing.persist()
NativePostProcessing.store()
expandedShaderSlots.clear()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
@@ -303,7 +357,7 @@ class SettingsFragmentPresenter(
isRunnable = true
) {
NativePostProcessing.move(index, 1)
NativePostProcessing.persist()
NativePostProcessing.store()
expandedShaderSlots.clear()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
@@ -315,7 +369,7 @@ class SettingsFragmentPresenter(
isRunnable = true
) {
NativePostProcessing.resetValues(index)
NativePostProcessing.persist()
NativePostProcessing.store()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
@@ -325,7 +379,7 @@ class SettingsFragmentPresenter(
isRunnable = true
) {
NativePostProcessing.remove(index)
NativePostProcessing.persist()
NativePostProcessing.store()
expandedShaderSlots.clear()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
@@ -333,78 +387,32 @@ class SettingsFragmentPresenter(
}
add(
IntSingleChoiceSetting(
buildAddSelector(files, techniques),
CardSetting(
titleId = R.string.post_processing_add,
choices = labels.toTypedArray(),
values = labels.indices.toList().toTypedArray()
)
expanded = shaderPickerOpen
) {
shaderPickerOpen = !shaderPickerOpen
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
}
}
private fun buildAddSelector(
files: List<String>,
techniques: List<String>
): AbstractIntSetting = object : AbstractIntSetting {
override val key = "fx_add"
override fun getInt(needsGlobal: Boolean): Int = -1
override fun setInt(value: Int) {
if (value < 0 || value >= files.size) {
return
}
NativePostProcessing.append(files[value], techniques[value])
NativePostProcessing.persist()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = -1
override fun getValueAsString(needsGlobal: Boolean): String = ""
override fun reset() {}
override val isRuntimeModifiable = true
override val pairedSettingKey = ""
override val isSwitchable = false
override val isSaveable = true
override var global: Boolean
get() = true
set(_) {}
}
private fun buildSlotSelector(
index: Int,
entry: NativePostProcessing.ChainEntry,
files: List<String>,
techniques: List<String>
): AbstractIntSetting = object : AbstractIntSetting {
override val key = "fx_slot_$index"
override fun getInt(needsGlobal: Boolean): Int {
for (i in files.indices) {
if (files[i] == entry.file && techniques[i] == entry.technique) {
return i
if (shaderPickerOpen) {
for (choice in labels.indices) {
add(
RunnableSetting(
titleString = labels[choice],
descriptionString = summaries[choice],
isRunnable = true
) {
NativePostProcessing.append(files[choice], techniques[choice])
NativePostProcessing.store()
shaderPickerOpen = false
settingsViewModel.setReloadListAndNotifyDataset(true)
}
)
}
}
return -1
}
override fun setInt(value: Int) {
NativePostProcessing.replace(index, files[value], techniques[value])
NativePostProcessing.persist()
settingsViewModel.setReloadListAndNotifyDataset(true)
}
override val defaultValue = 0
override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
override fun reset() {}
override val isRuntimeModifiable = true
override val pairedSettingKey = ""
override val isSwitchable = false
override val isSaveable = true
override var global: Boolean
get() = true
set(_) {}
}
private fun addUniform(
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
package org.yuzu.yuzu_emu.features.settings.ui.viewholder
import android.view.View
import org.yuzu.yuzu_emu.databinding.ListItemSettingCardBinding
import org.yuzu.yuzu_emu.features.settings.model.view.CardSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
class CardViewHolder(val binding: ListItemSettingCardBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: CardSetting
override fun bind(item: SettingsItem) {
setting = item as CardSetting
binding.textCardName.text = item.title
binding.textCardDescription.text = item.description
binding.textCardDescription.setVisible(item.description.isNotEmpty())
var rotation = 0f
if (setting.expanded) {
rotation = 180f
}
binding.cardExpand.rotation = rotation
}
override fun onClick(clicked: View) {
val target = binding.cardExpand
var rotation = 180f
if (setting.expanded) {
rotation = 0f
}
target.animate().rotation(rotation).setDuration(200).start()
setting.runnable.invoke()
}
override fun onLongClick(clicked: View): Boolean = true
}
@@ -42,11 +42,40 @@ object NativePostProcessing {
external fun store()
external fun getPresetsJson(): String
external fun getActivePreset(): String
external fun isPresetModified(): Boolean
external fun applyPreset(name: String): Boolean
external fun savePreset(name: String, description: String): Boolean
external fun deletePreset(name: String): Boolean
external fun clearPreset()
external fun isEnabled(): Boolean
external fun setEnabled(enabled: Boolean)
external fun getPresetDirectory(): String
fun persist() {
store()
NativeConfig.saveGlobalConfig()
}
fun persistFor(perGame: Boolean) {
store()
if (perGame) {
NativeConfig.savePerGameConfig()
} else {
NativeConfig.saveGlobalConfig()
}
}
external fun getShaderDirectory(): String
@@ -100,6 +129,28 @@ object NativePostProcessing {
data class ChainEntry(val file: String, val technique: String)
data class Preset(
val name: String,
val description: String,
val bundled: Boolean
)
fun presets(): List<Preset> {
val out = mutableListOf<Preset>()
val array = JSONArray(getPresetsJson())
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
out.add(
Preset(
name = obj.optString("name"),
description = obj.optString("description"),
bundled = obj.optBoolean("bundled")
)
)
}
return out
}
fun catalog(): List<Effect> {
val out = mutableListOf<Effect>()
val array = JSONArray(getCatalogJson())
@@ -10,7 +10,9 @@
#include "common/android/android_common.h"
#ifdef HAS_RESHADE
#include "video_core/post_processing/fx_chain.h"
#include "common/settings.h"
#include "video_core/post_processing/fx_effect.h"
#include "video_core/post_processing/fx_preset.h"
#endif
namespace {
@@ -212,4 +214,101 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JN
#endif
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getPresetsJson(JNIEnv* env,
jobject obj) {
nlohmann::json out = nlohmann::json::array();
#ifdef HAS_RESHADE
VideoCore::ReloadFxPresetCatalog();
for (const auto& preset : VideoCore::GetFxPresetCatalog()) {
nlohmann::json entry;
entry["name"] = preset.name;
entry["description"] = preset.description;
entry["bundled"] = preset.bundled;
out.push_back(entry);
}
#endif
return Common::Android::ToJString(env, out.dump());
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getActivePreset(JNIEnv* env,
jobject obj) {
#ifdef HAS_RESHADE
return Common::Android::ToJString(env, VideoCore::GetActiveFxPreset());
#else
return Common::Android::ToJString(env, "");
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isPresetModified(JNIEnv* env,
jobject obj) {
#ifdef HAS_RESHADE
return static_cast<jboolean>(VideoCore::IsActiveFxPresetModified());
#else
return static_cast<jboolean>(false);
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_applyPreset(JNIEnv* env, jobject obj,
jstring jname) {
#ifdef HAS_RESHADE
return static_cast<jboolean>(
VideoCore::ApplyFxPreset(Common::Android::GetJString(env, jname)));
#else
return static_cast<jboolean>(false);
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_savePreset(JNIEnv* env, jobject obj,
jstring jname,
jstring jdescription) {
#ifdef HAS_RESHADE
return static_cast<jboolean>(
VideoCore::SaveFxPreset(Common::Android::GetJString(env, jname),
Common::Android::GetJString(env, jdescription)));
#else
return static_cast<jboolean>(false);
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_deletePreset(JNIEnv* env, jobject obj,
jstring jname) {
#ifdef HAS_RESHADE
return static_cast<jboolean>(
VideoCore::DeleteFxPreset(Common::Android::GetJString(env, jname)));
#else
return static_cast<jboolean>(false);
#endif
}
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_clearPreset(JNIEnv* env, jobject obj) {
#ifdef HAS_RESHADE
VideoCore::SetActiveFxPreset(std::string_view());
#endif
}
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_isEnabled(JNIEnv* env, jobject obj) {
#ifdef HAS_RESHADE
return static_cast<jboolean>(Settings::values.post_shader_enabled.GetValue());
#else
return static_cast<jboolean>(false);
#endif
}
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_setEnabled(JNIEnv* env, jobject obj,
jboolean enabled) {
#ifdef HAS_RESHADE
Settings::values.post_shader_enabled.SetValue(enabled != JNI_FALSE);
#endif
}
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getPresetDirectory(JNIEnv* env,
jobject obj) {
#ifdef HAS_RESHADE
return Common::Android::ToJString(env, VideoCore::GetFxPresetDirectory().string());
#else
return Common::Android::ToJString(env, "");
#endif
}
} // extern "C"
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:background="@drawable/shader_card_background"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/preset_title"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/preset_summary"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/preset_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:minHeight="48dp" />
</LinearLayout>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
android:background="@drawable/shader_card_background"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingEnd="16dp"
android:paddingTop="16dp"
android:paddingBottom="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_card_name"
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text_card_description"
style="@style/TextAppearance.Material3.BodySmall"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
<ImageView
android:id="@+id/card_expand"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:contentDescription=""
android:src="@drawable/ic_dropdown_arrow"
app:tint="?attr/colorPrimary" />
</LinearLayout>
@@ -301,9 +301,14 @@
<string name="post_processing">Post-Processing Effects</string>
<string name="post_processing_description">ReShade FX effects applied after rendering</string>
<string name="post_processing_per_game_description">Configure the effect chain for this game</string>
<string name="post_processing_effect">Effect</string>
<string name="post_processing_add">Add effect</string>
<string name="post_processing_remove">Remove</string>
<string name="post_processing_hide_effects">Remove effects</string>
<string name="post_processing_preset_locked">Preset active. Edit it in Post-Processing Effects before starting a game.</string>
<string name="post_processing_preset_modified">Preset active, with your own changes.</string>
<string name="post_processing_preset">Preset</string>
<string name="post_processing_preset_none">No preset</string>
<string name="post_processing_preset_reset">Reset preset values</string>
<string name="post_processing_move_up">Move up</string>
<string name="post_processing_move_down">Move down</string>
<string name="post_processing_reset">Reset to defaults</string>
+1
View File
@@ -23,6 +23,7 @@
#define LOSSLESS_DIR "lossless"
#define NAND_DIR "nand"
#define PLAY_TIME_DIR "play_time"
#define POST_PRESET_DIR "post_presets"
#define POST_SHADER_DIR "post_shaders"
#define SCREENSHOTS_DIR "screenshots"
#define SDMC_DIR "sdmc"
+1
View File
@@ -160,6 +160,7 @@ public:
GenerateEdenPath(EdenPath::LosslessDir, eden_path / LOSSLESS_DIR);
GenerateEdenPath(EdenPath::NANDDir, eden_path / NAND_DIR);
GenerateEdenPath(EdenPath::PlayTimeDir, eden_path / PLAY_TIME_DIR);
GenerateEdenPath(EdenPath::PostPresetDir, eden_path / POST_PRESET_DIR);
GenerateEdenPath(EdenPath::PostShaderDir, eden_path / POST_SHADER_DIR);
GenerateEdenPath(EdenPath::SaveDir, eden_path / NAND_DIR);
GenerateEdenPath(EdenPath::ScreenshotsDir, eden_path / SCREENSHOTS_DIR);
+1
View File
@@ -26,6 +26,7 @@ enum class EdenPath {
LosslessDir, // Where the user-supplied Lossless Scaling library is stored.
NANDDir, // Where the emulated NAND is stored.
PlayTimeDir, // Where play time data is stored.
PostPresetDir,
PostShaderDir, // Where user post-processing shaders are stored.
SaveDir, // Where save data is stored.
ScreenshotsDir, // Where yuzu screenshots are stored.
+23 -7
View File
@@ -388,13 +388,29 @@ struct Values {
true,
true};
Setting<std::string> post_shader_chain{linkage,
std::string(),
"post_shader_chain",
Category::Renderer,
Specialization::Default,
true,
true};
SwitchableSetting<std::string> post_shader_chain{linkage,
std::string(),
"post_shader_chain",
Category::Renderer,
Specialization::Default,
true,
true};
SwitchableSetting<std::string> post_shader_preset{linkage,
std::string(),
"post_shader_preset",
Category::Renderer,
Specialization::Default,
true,
true};
SwitchableSetting<bool> post_shader_enabled{linkage,
true,
"post_shader_enabled",
Category::Renderer,
Specialization::Default,
true,
true};
SwitchableSetting<bool> frame_gen{linkage, false, "frame_gen", Category::Renderer,
Specialization::Default, true, false};
+15
View File
@@ -312,6 +312,18 @@ if (ENABLE_RESHADE)
DEPENDS ${BUNDLED_FX_FILES}
)
set(BUNDLED_PRESET_DIR ${CMAKE_SOURCE_DIR}/dist/post_presets)
set(BUNDLED_PRESET_HEADER ${CMAKE_CURRENT_BINARY_DIR}/bundled_fx_presets.h)
file(GLOB BUNDLED_PRESET_FILES ${BUNDLED_PRESET_DIR}/*.fxp)
add_custom_command(
OUTPUT ${BUNDLED_PRESET_HEADER}
COMMAND ${CMAKE_COMMAND} -P
${CMAKE_CURRENT_SOURCE_DIR}/post_processing/GenerateBundledPresets.cmake
${BUNDLED_PRESET_DIR} ${BUNDLED_PRESET_HEADER}
DEPENDS ${BUNDLED_PRESET_FILES}
)
target_sources(video_core PRIVATE
post_processing/fx_chain.cpp
post_processing/fx_chain.h
@@ -319,9 +331,12 @@ if (ENABLE_RESHADE)
post_processing/fx_compile.h
post_processing/fx_effect.cpp
post_processing/fx_effect.h
post_processing/fx_preset.cpp
post_processing/fx_preset.h
renderer_vulkan/present/post_process.cpp
renderer_vulkan/present/post_process.h
${BUNDLED_FX_HEADER}
${BUNDLED_PRESET_HEADER}
)
target_include_directories(video_core PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(video_core PRIVATE reshadefx::reshadefx)
@@ -0,0 +1,50 @@
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
set(PRESET_DIR ${CMAKE_ARGV3})
set(HEADER_FILE ${CMAKE_ARGV4})
file(GLOB PRESET_FILES ${PRESET_DIR}/*.fxp)
list(SORT PRESET_FILES)
set(ENTRIES "")
foreach(PRESET_FILE IN LISTS PRESET_FILES)
get_filename_component(PRESET_NAME ${PRESET_FILE} NAME)
file(READ ${PRESET_FILE} PRESET_BODY)
string(REGEX REPLACE ";" "{{SEMICOLON}}" PRESET_BODY "${PRESET_BODY}")
string(REGEX REPLACE "\n" ";" PRESET_BODY "${PRESET_BODY}")
set(PRESET_TEXT "")
foreach(LINE IN LISTS PRESET_BODY)
string(CONCAT PRESET_TEXT "${PRESET_TEXT}" " R\"(${LINE}\n)\"\n")
endforeach()
string(REGEX REPLACE "{{SEMICOLON}}" ";" PRESET_TEXT "${PRESET_TEXT}")
string(CONCAT ENTRIES "${ENTRIES}"
" {\n \"${PRESET_NAME}\",\n${PRESET_TEXT} },\n")
endforeach()
get_filename_component(OUTPUT_DIR ${HEADER_FILE} DIRECTORY)
make_directory(${OUTPUT_DIR})
file(WRITE ${HEADER_FILE}
"// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <string_view>
namespace VideoCore {
struct BundledFxPreset {
std::string_view name;
std::string_view source;
};
constexpr BundledFxPreset BUNDLED_FX_PRESETS[]{
${ENTRIES}};
} // namespace VideoCore
")
@@ -204,6 +204,14 @@ void FxChain::Clear() {
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::SetEntries(std::vector<FxChainEntry> next) {
{
std::scoped_lock lock{mutex};
entries = std::move(next);
}
generation.fetch_add(1, std::memory_order_relaxed);
}
void FxChain::SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value) {
std::scoped_lock lock{mutex};
if (index >= entries.size()) {
@@ -51,6 +51,8 @@ public:
void Clear();
void SetEntries(std::vector<FxChainEntry> next);
void SetValue(size_t index, std::string_view uniform, const std::array<f32, 4>& value);
std::array<f32, 4> GetValue(size_t index, std::string_view uniform) const;
@@ -0,0 +1,260 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#include <algorithm>
#include "bundled_fx_presets.h"
#include "common/fs/file.h"
#include "common/fs/fs.h"
#include "common/fs/path_util.h"
#include "common/settings.h"
#include "video_core/post_processing/fx_chain.h"
#include "video_core/post_processing/fx_preset.h"
namespace VideoCore {
namespace {
std::vector<FxPresetDesc> preset_catalog;
bool preset_catalog_scanned = false;
std::string Trim(std::string_view value) {
size_t begin = 0;
while (begin < value.size() && (value[begin] == ' ' || value[begin] == '\t')) {
++begin;
}
size_t end = value.size();
while (end > begin) {
const char c = value[end - 1];
if (c != ' ' && c != '\t' && c != '\r' && c != '\n') {
break;
}
--end;
}
return std::string(value.substr(begin, end - begin));
}
FxPresetDesc ParsePreset(std::string_view text) {
FxPresetDesc desc;
size_t start = 0;
while (start <= text.size()) {
size_t end = text.find('\n', start);
if (end == std::string_view::npos) {
end = text.size();
}
const std::string_view line = text.substr(start, end - start);
start = end + 1;
const size_t equals = line.find('=');
if (equals == std::string_view::npos) {
continue;
}
const std::string key = Trim(line.substr(0, equals));
const std::string value = Trim(line.substr(equals + 1));
if (key == "name") {
desc.name = value;
} else if (key == "description") {
desc.description = value;
} else if (key == "chain") {
desc.chain = value;
}
}
return desc;
}
std::string PresetFileName(std::string_view name) {
std::string out;
for (const char c : name) {
if (c == ' ') {
out += '_';
continue;
}
const bool keep = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_';
if (keep) {
out += c;
}
}
if (out.empty()) {
out = "preset";
}
return out + ".fxp";
}
std::string ComposePreset(const FxPresetDesc& desc) {
std::string out;
out += "name=";
out += desc.name;
out += '\n';
out += "description=";
out += desc.description;
out += '\n';
out += "chain=";
out += desc.chain;
out += '\n';
return out;
}
} // Anonymous namespace
std::filesystem::path GetFxPresetDirectory() {
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostPresetDir);
}
void ReloadFxPresetCatalog() {
preset_catalog.clear();
preset_catalog_scanned = true;
const auto root = GetFxPresetDirectory();
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
return;
}
std::vector<std::string> bundled_names;
for (const auto& bundled : BUNDLED_FX_PRESETS) {
const auto path = root / bundled.name;
bundled_names.emplace_back(bundled.name);
if (Common::FS::Exists(path)) {
continue;
}
void(Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, bundled.source));
}
std::vector<std::filesystem::path> preset_files;
Common::FS::IterateDirEntriesRecursively(
root,
[&](const std::filesystem::directory_entry& entry) {
if (entry.path().extension() == ".fxp") {
preset_files.push_back(entry.path());
}
return true;
},
Common::FS::DirEntryFilter::File);
std::sort(preset_files.begin(), preset_files.end());
for (const auto& file : preset_files) {
const std::string text =
Common::FS::ReadStringFromFile(file, Common::FS::FileType::TextFile);
FxPresetDesc desc = ParsePreset(text);
if (desc.name.empty() || desc.chain.empty()) {
continue;
}
desc.file = Common::FS::PathToUTF8String(file.filename());
desc.bundled = std::find(bundled_names.begin(), bundled_names.end(), desc.file) !=
bundled_names.end();
preset_catalog.push_back(std::move(desc));
}
}
const std::vector<FxPresetDesc>& GetFxPresetCatalog() {
if (!preset_catalog_scanned) {
ReloadFxPresetCatalog();
}
return preset_catalog;
}
const FxPresetDesc* FindFxPreset(std::string_view name) {
const auto& presets = GetFxPresetCatalog();
const auto it = std::find_if(presets.begin(), presets.end(),
[&](const FxPresetDesc& p) { return p.name == name; });
if (it == presets.end()) {
return nullptr;
}
return &*it;
}
bool ApplyFxPreset(std::string_view name) {
const FxPresetDesc* preset = FindFxPreset(name);
if (preset == nullptr) {
return false;
}
FxChain::Instance().SetEntries(ParseFxChain(preset->chain));
FxChain::Instance().DropUnknownEntries();
FxChain::Instance().StoreToSettings();
SetActiveFxPreset(preset->name);
return true;
}
bool SaveFxPreset(std::string_view name, std::string_view description) {
if (!IsFxPresetName(name)) {
return false;
}
const auto root = GetFxPresetDirectory();
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
return false;
}
FxPresetDesc desc;
desc.name = std::string(name);
desc.description = std::string(description);
desc.chain = SerializeFxChain(FxChain::Instance().Entries());
const std::string body = ComposePreset(desc);
const std::string file = PresetFileName(name);
if (Common::FS::WriteStringToFile(root / file, Common::FS::FileType::TextFile, body) !=
body.size()) {
return false;
}
ReloadFxPresetCatalog();
SetActiveFxPreset(name);
return true;
}
bool DeleteFxPreset(std::string_view name) {
const FxPresetDesc* preset = FindFxPreset(name);
if (preset == nullptr || preset->bundled) {
return false;
}
const auto path = GetFxPresetDirectory() / preset->file;
if (!Common::FS::RemoveFile(path)) {
return false;
}
if (GetActiveFxPreset() == name) {
SetActiveFxPreset(std::string_view());
}
ReloadFxPresetCatalog();
return true;
}
bool IsFxPresetName(std::string_view name) {
if (name.empty()) {
return false;
}
return name.find_first_of("\r\n=") == std::string_view::npos;
}
std::string GetActiveFxPreset() {
return Settings::values.post_shader_preset.GetValue();
}
void SetActiveFxPreset(std::string_view name) {
Settings::values.post_shader_preset.SetValue(std::string(name));
}
bool IsActiveFxPresetModified() {
const std::string active = GetActiveFxPreset();
if (active.empty()) {
return false;
}
const FxPresetDesc* preset = FindFxPreset(active);
if (preset == nullptr) {
return true;
}
return SerializeFxChain(FxChain::Instance().Entries()) != preset->chain;
}
} // namespace VideoCore
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
namespace VideoCore {
struct FxPresetDesc {
std::string file;
std::string name;
std::string description;
std::string chain;
bool bundled{};
};
std::filesystem::path GetFxPresetDirectory();
void ReloadFxPresetCatalog();
const std::vector<FxPresetDesc>& GetFxPresetCatalog();
const FxPresetDesc* FindFxPreset(std::string_view name);
bool ApplyFxPreset(std::string_view name);
bool SaveFxPreset(std::string_view name, std::string_view description);
bool DeleteFxPreset(std::string_view name);
bool IsFxPresetName(std::string_view name);
std::string GetActiveFxPreset();
void SetActiveFxPreset(std::string_view name);
bool IsActiveFxPresetModified();
} // namespace VideoCore
@@ -235,8 +235,10 @@ void Layer::SetPostProcessPass(const Device& device) {
};
const u64 generation = VideoCore::FxChain::Instance().Snapshot().generation;
const bool enabled = Settings::values.post_shader_enabled.GetValue();
if (post_process_generation == generation && post_process_extent.width == render_area.width &&
if (post_process_generation == generation && post_process_enabled == enabled &&
post_process_extent.width == render_area.width &&
post_process_extent.height == render_area.height) {
return;
}
@@ -246,10 +248,11 @@ void Layer::SetPostProcessPass(const Device& device) {
}
post_process_generation = generation;
post_process_enabled = enabled;
post_process_extent = render_area;
post_process.reset();
if (VideoCore::FxChain::Instance().Size() == 0) {
if (!enabled || VideoCore::FxChain::Instance().Size() == 0) {
return;
}
@@ -104,6 +104,7 @@ private:
#ifdef HAS_RESHADE
std::optional<PostProcessChain> post_process{};
u64 post_process_generation{};
bool post_process_enabled{};
VkExtent2D post_process_extent{};
#endif
std::vector<u64> resource_ticks{};
@@ -187,7 +187,7 @@ try
}
#ifdef HAS_RESHADE
VideoCore::FxChain::Instance().EnsureLoadedFromSettings();
VideoCore::FxChain::Instance().LoadFromSettings();
#endif
Report();
@@ -11,6 +11,7 @@
#include <QDialogButtonBox>
#include <QFrame>
#include <QGridLayout>
#include <QInputDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
@@ -20,8 +21,10 @@
#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 {
@@ -74,6 +77,75 @@ ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(pare
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);
@@ -112,8 +184,12 @@ ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(pare
root->addWidget(buttons);
VideoCore::ReloadFxCatalog();
VideoCore::ReloadFxPresetCatalog();
VideoCore::FxChain::Instance().LoadFromSettings();
VideoCore::FxChain::Instance().DropUnknownEntries();
PopulatePresetCombo();
RebuildRows();
RefreshPresetStatus();
}
ConfigurePostProcessing::~ConfigurePostProcessing() = default;
@@ -121,6 +197,47 @@ 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,
@@ -5,7 +5,9 @@
#include <QDialog>
class QCheckBox;
class QComboBox;
class QLabel;
class QVBoxLayout;
class QWidget;
@@ -29,7 +31,12 @@ private:
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{};
};