From 7eda109134081cb43056fdd3e7a2f266c2d1b91f Mon Sep 17 00:00:00 2001 From: CamilleLaVey Date: Mon, 7 Sep 2026 17:58:46 -0400 Subject: [PATCH] Adjustment on the design of UI + new shaders --- dist/post_shaders/Bloom.fx | 5 +- dist/post_shaders/CRT.fx | 5 +- dist/post_shaders/Cartoon.fx | 5 +- dist/post_shaders/CartoonSoft.fx | 5 +- dist/post_shaders/ChromaticAberration.fx | 5 +- dist/post_shaders/ColorGrade.fx | 5 +- dist/post_shaders/Deband.fx | 5 +- dist/post_shaders/Denoise.fx | 5 +- dist/post_shaders/FakeReflections.fx | 5 +- dist/post_shaders/FilmGrain.fx | 73 ++++++ dist/post_shaders/FilmicCurve.fx | 68 +++++ dist/post_shaders/LensDistortion.fx | 61 +++++ dist/post_shaders/Levels.fx | 74 ++++++ dist/post_shaders/NaturalColors.fx | 5 +- dist/post_shaders/Scanlines.fx | 5 +- dist/post_shaders/Sharpen.fx | 5 +- dist/post_shaders/SplitToning.fx | 83 ++++++ dist/post_shaders/Vignette.fx | 5 +- dist/post_shaders/WhiteBalance.fx | 84 ++++++ .../yuzu/yuzu_emu/dialogs/QuickSettings.kt | 245 ++++++++++++++++++ .../settings/ui/SettingsFragmentPresenter.kt | 20 +- .../yuzu_emu/fragments/EmulationFragment.kt | 4 + .../yuzu_emu/utils/NativePostProcessing.kt | 5 +- .../src/main/jni/native_post_processing.cpp | 11 +- .../app/src/main/res/values/strings.xml | 3 +- src/video_core/post_processing/fx_chain.cpp | 5 +- src/video_core/post_processing/fx_effect.cpp | 9 + src/video_core/post_processing/fx_effect.h | 2 + .../configure_post_processing.cpp | 25 +- 29 files changed, 774 insertions(+), 63 deletions(-) create mode 100644 dist/post_shaders/FilmGrain.fx create mode 100644 dist/post_shaders/FilmicCurve.fx create mode 100644 dist/post_shaders/LensDistortion.fx create mode 100644 dist/post_shaders/Levels.fx create mode 100644 dist/post_shaders/SplitToning.fx create mode 100644 dist/post_shaders/WhiteBalance.fx diff --git a/dist/post_shaders/Bloom.fx b/dist/post_shaders/Bloom.fx index ad607c69a9..396582cc98 100644 --- a/dist/post_shaders/Bloom.fx +++ b/dist/post_shaders/Bloom.fx @@ -65,7 +65,10 @@ float4 PS_Bloom(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(color + sum * Amount), 1.0); } -technique Bloom +technique Bloom < + ui_label = "Bloom"; + ui_tooltip = "Blooms bright areas into a soft glow."; +> { pass { diff --git a/dist/post_shaders/CRT.fx b/dist/post_shaders/CRT.fx index 528c6f9a3d..ec4b9840c5 100644 --- a/dist/post_shaders/CRT.fx +++ b/dist/post_shaders/CRT.fx @@ -69,7 +69,10 @@ float4 PS_CRT(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(color), 1.0); } -technique CRT +technique CRT < + ui_label = "CRT"; + ui_tooltip = "Curved scanlines and the phosphor mask of a CRT television."; +> { pass { diff --git a/dist/post_shaders/Cartoon.fx b/dist/post_shaders/Cartoon.fx index 2198673250..f0beba8dcc 100644 --- a/dist/post_shaders/Cartoon.fx +++ b/dist/post_shaders/Cartoon.fx @@ -74,7 +74,10 @@ float4 PS_Cartoon(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(color), 1.0); } -technique Cartoon +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."; +> { pass { diff --git a/dist/post_shaders/CartoonSoft.fx b/dist/post_shaders/CartoonSoft.fx index 3fd7ef03f3..7c1a2b481c 100644 --- a/dist/post_shaders/CartoonSoft.fx +++ b/dist/post_shaders/CartoonSoft.fx @@ -95,7 +95,10 @@ float4 PS_CartoonSoft(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Targe return float4(saturate(color), 1.0); } -technique CartoonSoft +technique CartoonSoft < + ui_label = "Cartoon Soft"; + ui_tooltip = "Ink outlines and flat colour bands, with the outline held back in the shadows."; +> { pass { diff --git a/dist/post_shaders/ChromaticAberration.fx b/dist/post_shaders/ChromaticAberration.fx index b5065a83c5..a83cc117ed 100644 --- a/dist/post_shaders/ChromaticAberration.fx +++ b/dist/post_shaders/ChromaticAberration.fx @@ -48,7 +48,10 @@ float4 PS_ChromaticAberration(float4 pos : SV_Position, float2 uv : TEXCOORD) : return float4(red, green, blue, 1.0); } -technique ChromaticAberration +technique ChromaticAberration < + ui_label = "Chromatic Aberration"; + ui_tooltip = "Splits the colour channels apart towards the edges of the screen, like a cheap lens."; +> { pass { diff --git a/dist/post_shaders/ColorGrade.fx b/dist/post_shaders/ColorGrade.fx index bf5c48f9e1..fe506fa0ca 100644 --- a/dist/post_shaders/ColorGrade.fx +++ b/dist/post_shaders/ColorGrade.fx @@ -59,7 +59,10 @@ float4 PS_ColorGrade(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(rgb), 1.0); } -technique ColorGrade +technique ColorGrade < + ui_label = "Colour Grade"; + ui_tooltip = "Global saturation, brightness, contrast and gamma."; +> { pass { diff --git a/dist/post_shaders/Deband.fx b/dist/post_shaders/Deband.fx index 25b3b315d4..a3eb04e85f 100644 --- a/dist/post_shaders/Deband.fx +++ b/dist/post_shaders/Deband.fx @@ -86,7 +86,10 @@ float4 PS_Deband(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(result + dither), 1.0); } -technique Deband +technique Deband < + ui_label = "Deband"; + ui_tooltip = "Smooths the visible steps in gradients such as skies, then dithers whatever survives."; +> { pass { diff --git a/dist/post_shaders/Denoise.fx b/dist/post_shaders/Denoise.fx index fd8046204e..ef1fcb1d5e 100644 --- a/dist/post_shaders/Denoise.fx +++ b/dist/post_shaders/Denoise.fx @@ -78,7 +78,10 @@ float4 PS_Denoise(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(sum / total, 1.0); } -technique Denoise +technique Denoise < + ui_label = "Denoise"; + ui_tooltip = "Edge preserving blur that clears dithering and compression noise. Ported from Anime4K."; +> { pass { diff --git a/dist/post_shaders/FakeReflections.fx b/dist/post_shaders/FakeReflections.fx index 3dbcf6875b..3ba34896fb 100644 --- a/dist/post_shaders/FakeReflections.fx +++ b/dist/post_shaders/FakeReflections.fx @@ -61,7 +61,10 @@ float4 PS_FakeReflections(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_T return float4(saturate(color + reflection), 1.0); } -technique FakeReflections +technique FakeReflections < + ui_label = "Fake Reflections"; + ui_tooltip = "Adds a wet looking reflection across the lower half of the screen."; +> { pass { diff --git a/dist/post_shaders/FilmGrain.fx b/dist/post_shaders/FilmGrain.fx new file mode 100644 index 0000000000..918c48e553 --- /dev/null +++ b/dist/post_shaders/FilmGrain.fx @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float Intensity < + ui_type = "slider"; + ui_label = "Intensity"; + ui_min = 0.0; ui_max = 0.2; ui_step = 0.005; +> = 0.03; + +uniform float Size < + ui_type = "slider"; + ui_label = "Size"; + ui_tooltip = "Grain cell size in pixels."; + ui_min = 1.0; ui_max = 4.0; ui_step = 1.0; +> = 1.0; + +uniform float Colored < + ui_type = "slider"; + ui_label = "Colour"; + ui_tooltip = "Zero gives monochrome grain, one gives independent noise per channel."; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.0; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float Hash(float2 p) +{ + float3 scattered = frac(float3(p.x, p.y, p.x) * 0.1031); + scattered += dot(scattered, scattered.yzx + 33.33); + return frac((scattered.x + scattered.y) * scattered.z); +} + +float4 PS_FilmGrain(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 rgb = tex2D(BackBuffer, uv).rgb; + + float2 cell = floor(pos.xy / max(Size, 1.0)); + float mono = Hash(cell) - 0.5; + float3 chroma = float3(Hash(cell + 11.7), Hash(cell + 23.1), Hash(cell + 37.5)) - 0.5; + float3 noise = lerp(float3(mono, mono, mono), chroma, Colored); + + float luma = dot(rgb, float3(0.2126, 0.7152, 0.0722)); + float response = 1.0 - abs(luma * 2.0 - 1.0); + + return float4(saturate(rgb + noise * Intensity * response), 1.0); +} + +technique FilmGrain < + ui_label = "Film Grain"; + ui_tooltip = "Adds photographic grain, strongest in the midtones and fading out in blacks and whites."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_FilmGrain; + } +} diff --git a/dist/post_shaders/FilmicCurve.fx b/dist/post_shaders/FilmicCurve.fx new file mode 100644 index 0000000000..9e5be6cd46 --- /dev/null +++ b/dist/post_shaders/FilmicCurve.fx @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float Exposure < + ui_type = "slider"; + ui_label = "Exposure"; + ui_min = 0.5; ui_max = 2.0; ui_step = 0.01; +> = 1.0; + +uniform float Toe < + ui_type = "slider"; + ui_label = "Toe"; + ui_tooltip = "Above 1.0 deepens the shadows, below 1.0 lifts them."; + ui_min = 0.5; ui_max = 2.0; ui_step = 0.01; +> = 1.2; + +uniform float Shoulder < + ui_type = "slider"; + ui_label = "Shoulder"; + ui_tooltip = "Above 1.0 opens up the highlights, below 1.0 compresses them."; + ui_min = 0.5; ui_max = 2.0; ui_step = 0.01; +> = 1.2; + +uniform float Amount < + ui_type = "slider"; + ui_label = "Amount"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.7; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float4 PS_FilmicCurve(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 rgb = tex2D(BackBuffer, uv).rgb; + + float3 curved = saturate(rgb * Exposure); + curved = pow(max(curved, 0.0), Toe); + curved = 1.0 - pow(max(1.0 - curved, 0.0), Shoulder); + + return float4(saturate(lerp(rgb, curved, Amount)), 1.0); +} + +technique FilmicCurve < + ui_label = "Filmic Curve"; + ui_tooltip = "Filmic contrast curve. Deepens the shadows and opens the highlights without clipping either end."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_FilmicCurve; + } +} diff --git a/dist/post_shaders/LensDistortion.fx b/dist/post_shaders/LensDistortion.fx new file mode 100644 index 0000000000..ea1aea7acd --- /dev/null +++ b/dist/post_shaders/LensDistortion.fx @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float Distortion < + ui_type = "slider"; + ui_label = "Distortion"; + ui_tooltip = "Positive bulges the picture outwards, negative pinches it inwards."; + ui_min = -0.5; ui_max = 0.5; ui_step = 0.01; +> = 0.1; + +uniform float Zoom < + ui_type = "slider"; + ui_label = "Zoom"; + ui_tooltip = "Scales the picture to hide the edges the warp pulls in."; + ui_min = 0.5; ui_max = 1.5; ui_step = 0.01; +> = 1.0; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float4 PS_LensDistortion(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float aspect = BUFFER_WIDTH * BUFFER_RCP_HEIGHT; + float2 half_size = float2(aspect, 1.0); + float2 unit = half_size / length(half_size); + + float2 centred = (uv - 0.5) * 2.0 * unit; + float r2 = dot(centred, centred); + centred *= 1.0 + Distortion * r2; + centred /= max(Zoom, 0.001); + + float2 source = centred / (2.0 * unit) + 0.5; + + return float4(tex2D(BackBuffer, source).rgb, 1.0); +} + +technique LensDistortion < + ui_label = "Lens Distortion"; + ui_tooltip = "Barrel or pincushion warp, like looking through a wide angle lens."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_LensDistortion; + } +} diff --git a/dist/post_shaders/Levels.fx b/dist/post_shaders/Levels.fx new file mode 100644 index 0000000000..3bf41a62c9 --- /dev/null +++ b/dist/post_shaders/Levels.fx @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float InputBlack < + ui_type = "slider"; + ui_label = "Input Black"; + ui_tooltip = "Input level mapped to black. Raise it to deepen washed out shadows."; + ui_min = 0.0; ui_max = 0.5; ui_step = 0.005; +> = 0.0; + +uniform float InputWhite < + ui_type = "slider"; + ui_label = "Input White"; + ui_min = 0.5; ui_max = 1.0; ui_step = 0.005; +> = 1.0; + +uniform float Gamma < + ui_type = "slider"; + ui_label = "Gamma"; + ui_min = 0.2; ui_max = 3.0; ui_step = 0.01; +> = 1.0; + +uniform float OutputBlack < + ui_type = "slider"; + ui_label = "Output Black"; + ui_tooltip = "Lifts crushed shadows so detail stops collapsing into one flat black."; + ui_min = 0.0; ui_max = 0.5; ui_step = 0.005; +> = 0.0; + +uniform float OutputWhite < + ui_type = "slider"; + ui_label = "Output White"; + ui_min = 0.5; ui_max = 1.0; ui_step = 0.005; +> = 1.0; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float4 PS_Levels(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 rgb = tex2D(BackBuffer, uv).rgb; + + rgb = saturate((rgb - InputBlack) / max(InputWhite - InputBlack, 0.001)); + rgb = pow(max(rgb, 0.0), 1.0 / max(Gamma, 0.001)); + rgb = lerp(OutputBlack, OutputWhite, rgb); + + return float4(saturate(rgb), 1.0); +} + +technique Levels < + ui_label = "Levels"; + ui_tooltip = "Black point, white point, gamma and output range. Use it to fix crushed or washed out shadows."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_Levels; + } +} diff --git a/dist/post_shaders/NaturalColors.fx b/dist/post_shaders/NaturalColors.fx index 7dda32776f..a4a32dcdd6 100644 --- a/dist/post_shaders/NaturalColors.fx +++ b/dist/post_shaders/NaturalColors.fx @@ -55,7 +55,10 @@ float4 PS_Natural(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(mul(YIQtoRGB, yiq)), 1.0); } -technique NaturalColors +technique NaturalColors < + ui_label = "Natural Colours"; + ui_tooltip = "Warmer and more saturated look. Ported from the PPSSPP shader."; +> { pass { diff --git a/dist/post_shaders/Scanlines.fx b/dist/post_shaders/Scanlines.fx index f091efc372..7b2fea31fe 100644 --- a/dist/post_shaders/Scanlines.fx +++ b/dist/post_shaders/Scanlines.fx @@ -61,7 +61,10 @@ float4 PS_Scanlines(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(saturate(color * saturate(gate)), 1.0); } -technique Scanlines +technique Scanlines < + ui_label = "Scanlines"; + ui_tooltip = "Horizontal scanlines of a CRT display."; +> { pass { diff --git a/dist/post_shaders/Sharpen.fx b/dist/post_shaders/Sharpen.fx index a0e6f56052..7764c9af62 100644 --- a/dist/post_shaders/Sharpen.fx +++ b/dist/post_shaders/Sharpen.fx @@ -38,7 +38,10 @@ float4 PS_Sharpen(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(centre + (centre - blur) * Amount, 1.0); } -technique Sharpen +technique Sharpen < + ui_label = "Sharpen"; + ui_tooltip = "Unsharp mask that brings back edge detail lost to scaling."; +> { pass { diff --git a/dist/post_shaders/SplitToning.fx b/dist/post_shaders/SplitToning.fx new file mode 100644 index 0000000000..69bedc7912 --- /dev/null +++ b/dist/post_shaders/SplitToning.fx @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float ShadowHue < + ui_type = "slider"; + ui_label = "Shadow Hue"; + ui_min = 0.0; ui_max = 360.0; ui_step = 1.0; +> = 210.0; + +uniform float ShadowStrength < + ui_type = "slider"; + ui_label = "Shadow Strength"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.0; + +uniform float HighlightHue < + ui_type = "slider"; + ui_label = "Highlight Hue"; + ui_min = 0.0; ui_max = 360.0; ui_step = 1.0; +> = 45.0; + +uniform float HighlightStrength < + ui_type = "slider"; + ui_label = "Highlight Strength"; + ui_min = 0.0; ui_max = 1.0; ui_step = 0.01; +> = 0.0; + +uniform float Balance < + ui_type = "slider"; + ui_label = "Balance"; + ui_tooltip = "Moves the split between what counts as shadow and what counts as highlight."; + ui_min = -0.5; ui_max = 0.5; ui_step = 0.01; +> = 0.0; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float3 HueToRGB(float hue) +{ + float h = frac(hue / 360.0) * 6.0; + return saturate(float3(abs(h - 3.0) - 1.0, 2.0 - abs(h - 2.0), 2.0 - abs(h - 4.0))); +} + +float4 PS_SplitToning(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 rgb = tex2D(BackBuffer, uv).rgb; + + float luma = saturate(dot(rgb, float3(0.2126, 0.7152, 0.0722)) + Balance); + + float3 shadow_tint = HueToRGB(ShadowHue) - 0.5; + float3 highlight_tint = HueToRGB(HighlightHue) - 0.5; + + rgb += shadow_tint * ShadowStrength * 0.25 * (1.0 - luma); + rgb += highlight_tint * HighlightStrength * 0.25 * luma; + + return float4(saturate(rgb), 1.0); +} + +technique SplitToning < + ui_label = "Split Toning"; + ui_tooltip = "Tints the shadows and the highlights towards two different hues."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_SplitToning; + } +} diff --git a/dist/post_shaders/Vignette.fx b/dist/post_shaders/Vignette.fx index f7c194c84e..1c127c64a9 100644 --- a/dist/post_shaders/Vignette.fx +++ b/dist/post_shaders/Vignette.fx @@ -47,7 +47,10 @@ float4 PS_Vignette(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target return float4(rgb * falloff, 1.0); } -technique Vignette +technique Vignette < + ui_label = "Vignette"; + ui_tooltip = "Darkens the corners of the screen."; +> { pass { diff --git a/dist/post_shaders/WhiteBalance.fx b/dist/post_shaders/WhiteBalance.fx new file mode 100644 index 0000000000..33ff15ea51 --- /dev/null +++ b/dist/post_shaders/WhiteBalance.fx @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +texture BackBufferTex : COLOR; +sampler BackBuffer { Texture = BackBufferTex; }; + +uniform float Temperature < + ui_type = "slider"; + ui_label = "Temperature"; + ui_tooltip = "Negative cools the picture towards blue, positive warms it towards orange."; + ui_min = -100.0; ui_max = 100.0; ui_step = 1.0; +> = 0.0; + +uniform float Tint < + ui_type = "slider"; + ui_label = "Tint"; + ui_tooltip = "Negative shifts towards green, positive towards magenta."; + ui_min = -100.0; ui_max = 100.0; ui_step = 1.0; +> = 0.0; + +void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD) +{ + uv = float2(0.0, 0.0); + if (id == 2) + { + uv.x = 2.0; + } + if (id == 1) + { + uv.y = 2.0; + } + pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0); +} + +float3 WhitePointLMS(float t1, float t2) +{ + float shift = 0.05; + if (t1 < 0.0) + { + shift = 0.10; + } + float x = 0.31271 - t1 * shift; + float y = 2.87 * x - 3.0 * x * x - 0.27509507 + t2 * 0.05; + + float big_y = 1.0; + float big_x = big_y * x / y; + float big_z = big_y * (1.0 - x - y) / y; + + return float3( 0.7328 * big_x + 0.4296 * big_y - 0.1624 * big_z, + -0.7036 * big_x + 1.6975 * big_y + 0.0061 * big_z, + 0.0030 * big_x + 0.0136 * big_y + 0.9834 * big_z); +} + +float4 PS_WhiteBalance(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target +{ + float3 rgb = pow(max(tex2D(BackBuffer, uv).rgb, 0.0), 2.2); + + float3 balance = float3(0.949237, 1.03542, 1.08728) / + WhitePointLMS(Temperature / 65.0, Tint / 65.0); + + const float3x3 rgb_to_lms = float3x3(0.390405, 0.549941, 0.008926, + 0.070841, 0.963172, 0.001358, + 0.023108, 0.128021, 0.936245); + const float3x3 lms_to_rgb = float3x3( 2.858470, -1.628790, -0.024891, + -0.210182, 1.158200, 0.000324, + -0.041812, -0.118169, 1.068670); + + float3 lms = mul(rgb_to_lms, rgb) * balance; + rgb = mul(lms_to_rgb, lms); + + return float4(saturate(pow(max(rgb, 0.0), 1.0 / 2.2)), 1.0); +} + +technique WhiteBalance < + ui_label = "White Balance"; + ui_tooltip = "Corrects a picture that looks too cool, too warm or tinted."; +> +{ + pass + { + VertexShader = VS_PostProcess; + PixelShader = PS_WhiteBalance; + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt index 7ba7772bd7..0d523a4e18 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/dialogs/QuickSettings.kt @@ -18,6 +18,7 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.fragments.EmulationFragment import org.yuzu.yuzu_emu.utils.NativeConfig +import org.yuzu.yuzu_emu.utils.NativePostProcessing import org.yuzu.yuzu_emu.features.settings.model.AbstractSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractShortSetting import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting @@ -232,6 +233,250 @@ class QuickSettings(val emulationFragment: EmulationFragment) { container.addView(itemView) } + fun addChoice( + title: String, + container: ViewGroup, + choices: List, + selectedIndex: Int, + onSelected: (Int) -> Unit + ) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) + val headerView = itemView.findViewById(R.id.setting_header) + val titleView = itemView.findViewById(R.id.setting_title) + val valueView = itemView.findViewById(R.id.setting_value) + val expandIcon = itemView.findViewById(R.id.expand_icon) + val radioGroup = itemView.findViewById(R.id.radio_group) + + titleView.text = title + + var current = "" + if (selectedIndex in choices.indices) { + current = choices[selectedIndex] + } + valueView.text = current + headerView.visibility = View.VISIBLE + + var isExpanded = false + choices.forEachIndexed { index, name -> + val radioButton = com.google.android.material.radiobutton.MaterialRadioButton( + emulationFragment.requireContext() + ) + radioButton.text = name + radioButton.id = View.generateViewId() + radioButton.isChecked = index == selectedIndex + radioButton.setPadding(16, 8, 16, 8) + + radioButton.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + valueView.text = name + onSelected(index) + } + } + radioGroup.addView(radioButton) + } + + headerView.setOnClickListener { + isExpanded = !isExpanded + if (isExpanded) { + radioGroup.visibility = View.VISIBLE + expandIcon.animate().rotation(180f).setDuration(200).start() + } else { + radioGroup.visibility = View.GONE + expandIcon.animate().rotation(0f).setDuration(200).start() + } + } + + container.addView(itemView) + } + + fun addStepSlider( + title: String, + container: ViewGroup, + steps: Int, + selectedStep: Int, + describe: (Int) -> String, + onChanged: (Int) -> Unit + ) { + val inflater = LayoutInflater.from(emulationFragment.requireContext()) + val itemView = inflater.inflate(R.layout.item_quick_settings_menu, container, false) + + val sliderContainer = itemView.findViewById(R.id.slider_container) + val titleView = itemView.findViewById(R.id.slider_title) + val valueDisplay = itemView.findViewById(R.id.slider_value_display) + val slider = itemView.findViewById( + R.id.setting_slider + ) + + titleView.text = title + sliderContainer.visibility = View.VISIBLE + + slider.valueFrom = 0f + slider.valueTo = steps.toFloat() + slider.stepSize = 1f + slider.value = selectedStep.toFloat().coerceIn(0f, steps.toFloat()) + valueDisplay.text = describe(slider.value.toInt()) + + slider.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val step = value.toInt() + onChanged(step) + valueDisplay.text = describe(step) + } + } + + slider.setOnTouchListener { _, event -> + val drawer = emulationFragment.view?.findViewById(R.id.drawer_layout) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + drawer?.requestDisallowInterceptTouchEvent(true) + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + drawer?.requestDisallowInterceptTouchEvent(false) + } + } + false + } + + container.addView(itemView) + } + + fun addPostProcessing(container: ViewGroup, onStructureChanged: () -> Unit) { + val usable = NativePostProcessing.catalog().filter { it.valid } + if (usable.isEmpty()) { + return + } + + val labels = mutableListOf( + YuzuApplication.appContext.getString(R.string.post_processing_none) + ) + val files = mutableListOf("") + val techniques = mutableListOf("") + + for (effect in usable) { + for (technique in effect.techniques) { + var label = effect.label + if (effect.techniques.size > 1) { + label = effect.label + " \u00b7 " + technique + } + labels.add(label) + files.add(effect.file) + techniques.add(technique) + } + } + + addDivider(container) + + val chain = NativePostProcessing.chain() + for (index in 0..chain.size) { + var selected = 0 + var effect: NativePostProcessing.Effect? = null + + if (index < chain.size) { + val entry = chain[index] + effect = usable.firstOrNull { it.file == entry.file } + for (i in files.indices) { + if (files[i] == entry.file && techniques[i] == entry.technique) { + selected = i + } + } + } + + var title = YuzuApplication.appContext.getString(R.string.post_processing_add) + if (index < chain.size) { + title = YuzuApplication.appContext.getString(R.string.post_processing_effect) + } + + addChoice(title, container, labels, selected) { picked -> + applyEffectPick(index, picked, files, techniques, chain.size) + onStructureChanged() + } + + if (effect != null) { + for (uniform in effect.uniforms) { + addUniformSliders(container, index, uniform) + } + } + } + } + + private fun applyEffectPick( + index: Int, + picked: Int, + files: List, + techniques: List, + chainSize: Int + ) { + if (index >= chainSize) { + if (picked > 0) { + NativePostProcessing.append(files[picked], techniques[picked]) + NativePostProcessing.persist() + } + return + } + + if (picked == 0) { + NativePostProcessing.remove(index) + NativePostProcessing.persist() + return + } + + NativePostProcessing.replace(index, files[picked], techniques[picked]) + NativePostProcessing.persist() + } + + private fun addUniformSliders( + container: ViewGroup, + index: Int, + uniform: NativePostProcessing.Uniform + ) { + if (uniform.uiType == NativePostProcessing.UI_HIDDEN) { + return + } + + for (component in 0 until uniform.components) { + var title = uniform.label + if (uniform.components > 1) { + title = uniform.label + " [" + component + "]" + } + + var value = uniform.defaultAt(component) + if (NativePostProcessing.hasValue(index, uniform.name)) { + value = NativePostProcessing.getValue(index, uniform.name, component) + } + + val steps = uniform.steps + val step = Math.round((value - uniform.min) / uniform.step) + + addStepSlider( + title, + container, + steps, + step, + { position -> describeUniform(uniform, position) } + ) { position -> + NativePostProcessing.setValue( + index, + uniform.name, + component, + uniform.min + position * uniform.step + ) + NativePostProcessing.persist() + } + } + } + + private fun describeUniform( + uniform: NativePostProcessing.Uniform, + position: Int + ): String { + val value = uniform.min + position * uniform.step + if (uniform.kind == NativePostProcessing.KIND_FLOAT) { + return String.format("%.3f", value) + } + return Math.round(value).toString() + } + fun addDivider(container: ViewGroup) { val inflater = LayoutInflater.from(emulationFragment.requireContext()) val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 66e0c49cda..44ac01301b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -199,17 +199,6 @@ class SettingsFragmentPresenter( val usable = NativePostProcessing.catalog().filter { it.valid } sl.apply { - add( - RunnableSetting( - titleId = R.string.post_processing_reload, - descriptionId = R.string.post_processing_reload_description, - isRunnable = true - ) { - NativePostProcessing.reload() - settingsViewModel.setReloadListAndNotifyDataset(true) - } - ) - if (usable.isEmpty()) { add( RunnableSetting( @@ -227,9 +216,9 @@ class SettingsFragmentPresenter( for (effect in usable) { for (technique in effect.techniques) { if (effect.techniques.size == 1) { - labels.add(effect.name) + labels.add(effect.label) } else { - labels.add(effect.name + " \u00b7 " + technique) + labels.add(effect.label + " \u00b7 " + technique) } files.add(effect.file) techniques.add(technique) @@ -242,8 +231,10 @@ class SettingsFragmentPresenter( val effect = usable.firstOrNull { it.file == entry.file } var header = entry.file + var summary = "" if (effect != null) { - header = effect.name + header = effect.label + summary = effect.description } add(HeaderSetting(titleString = header)) @@ -251,6 +242,7 @@ class SettingsFragmentPresenter( IntSingleChoiceSetting( buildSlotSelector(index, entry, files, techniques), titleId = R.string.post_processing_effect, + descriptionString = summary, choices = labels.toTypedArray(), values = labels.indices.toList().toTypedArray() ) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 016ac57356..1955981761 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1194,6 +1194,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.array.rendererAntiAliasingNames, R.array.rendererAntiAliasingValues ) + + quickSettings.addPostProcessing(container) { + addQuickSettings() + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt index c08a88133b..757a3ee125 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativePostProcessing.kt @@ -47,7 +47,6 @@ object NativePostProcessing { NativeConfig.saveGlobalConfig() } - external fun reload() external fun getShaderDirectory(): String @@ -89,6 +88,8 @@ object NativePostProcessing { data class Effect( val file: String, val name: String, + val label: String, + val description: String, val error: String, val techniques: List, val uniforms: List @@ -108,6 +109,8 @@ object NativePostProcessing { Effect( file = obj.optString("file"), name = obj.optString("name"), + label = obj.optString("label"), + description = obj.optString("description"), error = obj.optString("error"), techniques = obj.optJSONArray("techniques").toStringList(), uniforms = obj.optJSONArray("uniforms").toUniformList() diff --git a/src/android/app/src/main/jni/native_post_processing.cpp b/src/android/app/src/main/jni/native_post_processing.cpp index beeaa4d82a..10f312a955 100644 --- a/src/android/app/src/main/jni/native_post_processing.cpp +++ b/src/android/app/src/main/jni/native_post_processing.cpp @@ -65,10 +65,14 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getCatalogJson(JNIEnv nlohmann::json out = nlohmann::json::array(); #ifdef HAS_RESHADE + VideoCore::FxChain::Instance().DropUnknownEntries(); + for (const auto& effect : VideoCore::GetFxCatalog()) { nlohmann::json entry; entry["file"] = effect.file; entry["name"] = effect.name; + entry["label"] = effect.label; + entry["description"] = effect.description; entry["error"] = effect.error; entry["techniques"] = effect.techniques; @@ -199,13 +203,6 @@ void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_store(JNIEnv* env, jobje #endif } -void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_reload(JNIEnv* env, jobject obj) { -#ifdef HAS_RESHADE - VideoCore::ReloadFxCatalog(); - VideoCore::FxChain::Instance().DropUnknownEntries(); -#endif -} - jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JNIEnv* env, jobject obj) { #ifdef HAS_RESHADE diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 91bddacf60..65dbf2e7e6 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -302,13 +302,12 @@ ReShade FX effects applied after rendering Configure the effect chain for this game Effect + None Add effect Remove Move up Move down Reset to defaults - Reload from disk - Rescan the shader folder for .fx files No effects found. Place .fx files in this folder: Frame generation Configure frame generation for this game diff --git a/src/video_core/post_processing/fx_chain.cpp b/src/video_core/post_processing/fx_chain.cpp index 80cfe378a5..25f07f9cd6 100644 --- a/src/video_core/post_processing/fx_chain.cpp +++ b/src/video_core/post_processing/fx_chain.cpp @@ -94,10 +94,7 @@ std::string SerializeFxChain(std::span entries) { if (!out.empty()) { out += ';'; } - out += entry.file; - out += '|'; - out += entry.technique; - out += '|'; + out += fmt::format("{}|{}|", entry.file, entry.technique); bool first = true; for (const auto& [name, value] : entry.values) { diff --git a/src/video_core/post_processing/fx_effect.cpp b/src/video_core/post_processing/fx_effect.cpp index bfc61378cd..7bb36b0467 100644 --- a/src/video_core/post_processing/fx_effect.cpp +++ b/src/video_core/post_processing/fx_effect.cpp @@ -176,6 +176,15 @@ FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesy desc.techniques.push_back(technique.name); } + if (!compiled.module.techniques.empty()) { + const auto& annotations = compiled.module.techniques.front().annotations; + desc.label = AnnotationString(annotations, "ui_label"); + desc.description = AnnotationString(annotations, "ui_tooltip"); + } + if (desc.label.empty()) { + desc.label = desc.name; + } + for (const auto& uniform : compiled.module.uniforms) { FxUniformDesc uniform_desc = DescribeUniform(uniform); if (uniform_desc.ui_type == FxUiType::Hidden) { diff --git a/src/video_core/post_processing/fx_effect.h b/src/video_core/post_processing/fx_effect.h index b229e9c703..bedf68809a 100644 --- a/src/video_core/post_processing/fx_effect.h +++ b/src/video_core/post_processing/fx_effect.h @@ -48,6 +48,8 @@ struct FxUniformDesc { struct FxEffectDesc { std::string file; std::string name; + std::string label; + std::string description; std::vector techniques; std::vector uniforms; std::string error; diff --git a/src/yuzu/configuration/configure_post_processing.cpp b/src/yuzu/configuration/configure_post_processing.cpp index 9f2dee3231..79b43ccec5 100644 --- a/src/yuzu/configuration/configure_post_processing.cpp +++ b/src/yuzu/configuration/configure_post_processing.cpp @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -17,11 +16,8 @@ #include #include #include -#include #include -#include "common/fs/fs.h" -#include "common/fs/fs_util.h" #include "video_core/post_processing/fx_chain.h" #include "video_core/post_processing/fx_effect.h" #include "yuzu/configuration/configure_post_processing.h" @@ -53,7 +49,7 @@ QString FormatValue(const VideoCore::FxUniformDesc& uniform, float value) { } QString SlotLabel(const VideoCore::FxEffectDesc& effect, const std::string& technique) { - const QString name = QString::fromStdString(effect.name); + const QString name = QString::fromStdString(effect.label); if (effect.techniques.size() == 1) { return name; } @@ -99,23 +95,6 @@ ConfigurePostProcessing::ConfigurePostProcessing(QWidget* parent) : QDialog(pare }); actions->addWidget(add_button); - auto* reload_button = new QPushButton(tr("Reload From Disk"), this); - connect(reload_button, &QPushButton::clicked, this, [this]() { - VideoCore::ReloadFxCatalog(); - VideoCore::FxChain::Instance().DropUnknownEntries(); - ApplyStructuralChange(); - }); - actions->addWidget(reload_button); - - auto* open_button = new QPushButton(tr("Open Folder"), this); - connect(open_button, &QPushButton::clicked, this, []() { - const auto path = VideoCore::GetFxRootDirectory(); - void(Common::FS::CreateDirs(path)); - QDesktopServices::openUrl( - QUrl::fromLocalFile(QString::fromStdString(Common::FS::PathToUTF8String(path)))); - }); - actions->addWidget(open_button); - actions->addStretch(); root->addLayout(actions); @@ -147,6 +126,8 @@ void ConfigurePostProcessing::PopulateEffectCombo(QComboBox* combo, 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; }