mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-09-10 14:07:25 +00:00
[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:
@@ -0,0 +1,11 @@
|
||||
--- a/source/effect_lexer.cpp
|
||||
+++ b/source/effect_lexer.cpp
|
||||
@@ -1181,7 +1181,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
-#if 0
|
||||
+#if !defined(__cpp_lib_to_chars)
|
||||
exponent += decimal_location - mantissa_size;
|
||||
|
||||
const bool exponent_negative = exponent < 0;
|
||||
@@ -85,6 +85,8 @@ option(ENABLE_WERROR "Enable -Werror diagnostics" ON)
|
||||
# Lossless Scaling frame generation. Only Android.
|
||||
cmake_dependent_option(ENABLE_LSFG "Enable Lossless Scaling frame generation" ON "ANDROID" OFF)
|
||||
|
||||
option(ENABLE_RESHADE "Enable ReShade FX post-processing effects" ON)
|
||||
|
||||
# non-linux bundled qt are static
|
||||
if (YUZU_USE_BUNDLED_QT AND (APPLE OR NOT UNIX))
|
||||
set(YUZU_STATIC_BUILD ON)
|
||||
|
||||
@@ -238,6 +238,14 @@
|
||||
"repo": "stachenov/quazip",
|
||||
"version": "2e95c9001b"
|
||||
},
|
||||
"reshade": {
|
||||
"hash": "6fc9b39821ca6e2d91160f05846b3a42159fec759f5d6fba91d1a57801a1bdaf6047740578c136f0b1a60a9c29ad160d87dbfe0dbfbcdf6ea879a8a078377b8c",
|
||||
"patches": [
|
||||
"0001-from-chars-fallback.patch"
|
||||
],
|
||||
"repo": "crosire/reshade",
|
||||
"version": "v6.8.0"
|
||||
},
|
||||
"sdl3": {
|
||||
"hash": "df5a323af7ac366661a3c0e887969c72584d232f3cc211419d59b0487b620b6b2859d4549c9e8df002ee489290062e466fcfddf7edc0872a37b1f2845e81c0f3",
|
||||
"min_version": "3.2.10",
|
||||
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+3
@@ -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
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from bloomnoblur.fsh in PPSSPP.
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Radius <
|
||||
ui_type = "slider";
|
||||
ui_label = "Radius";
|
||||
ui_tooltip = "How far the glow spreads from bright areas.";
|
||||
ui_min = 0.0; ui_max = 4.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Amount <
|
||||
ui_type = "slider";
|
||||
ui_label = "Amount";
|
||||
ui_tooltip = "Strength of the glow added on top of the image.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 0.6;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float Weight(float3 color)
|
||||
{
|
||||
float gray = (color.r + color.g + color.b) / 3.0;
|
||||
float saturation = (abs(color.r - gray) + abs(color.g - gray) + abs(color.b - gray)) / 3.0;
|
||||
return gray * gray / max(saturation, 0.25);
|
||||
}
|
||||
|
||||
float4 PS_Bloom(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float3 color = tex2D(BackBuffer, uv).rgb;
|
||||
|
||||
float gray = (color.r + color.g + color.b) / 3.0;
|
||||
float saturation = (abs(color.r - gray) + abs(color.g - gray) + abs(color.b - gray)) / 3.0;
|
||||
float spread = 0.002 * gray / max(saturation, 0.25) * Radius;
|
||||
|
||||
float3 sum = float3(0.0, 0.0, 0.0);
|
||||
[unroll] for (int x = -3; x <= 3; x += 2)
|
||||
{
|
||||
[unroll] for (int y = -3; y <= 3; y += 2)
|
||||
{
|
||||
float3 tap = tex2D(BackBuffer, uv + float2(x, y) * spread).rgb;
|
||||
sum += tap * Weight(tap);
|
||||
}
|
||||
}
|
||||
sum /= 16.0;
|
||||
|
||||
return float4(saturate(color + sum * Amount), 1.0);
|
||||
}
|
||||
|
||||
technique Bloom <
|
||||
ui_label = "Bloom";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Bloom;
|
||||
}
|
||||
}
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Radius <
|
||||
ui_type = "slider";
|
||||
ui_label = "Radius";
|
||||
ui_tooltip = "How far the blur reaches, in pixels.";
|
||||
ui_min = 1.0; ui_max = 12.0; ui_step = 0.5;
|
||||
> = 4.0;
|
||||
|
||||
uniform float Strength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Strength";
|
||||
ui_tooltip = "Blend between the original image and the blurred one.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float FocusSize <
|
||||
ui_type = "slider";
|
||||
ui_label = "Sharp Centre";
|
||||
ui_tooltip = "Size of the region left in focus at the centre of the screen. At zero the whole image is blurred evenly.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.0;
|
||||
|
||||
uniform float FocusSoftness <
|
||||
ui_type = "slider";
|
||||
ui_label = "Focus Falloff";
|
||||
ui_tooltip = "How gradually the sharp centre gives way to the blur.";
|
||||
ui_min = 0.05; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.4;
|
||||
|
||||
static const int TAPS = 17;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Blur(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
float3 original = tex2D(BackBuffer, uv).rgb;
|
||||
|
||||
float2 centred = (uv - 0.5) * float2(BUFFER_WIDTH * BUFFER_RCP_HEIGHT, 1.0) * 2.0;
|
||||
float distance_from_centre = length(centred);
|
||||
float no_focus = 1.0 - step(0.001, FocusSize);
|
||||
float focus = max(smoothstep(FocusSize, FocusSize + FocusSoftness, distance_from_centre), no_focus);
|
||||
float amount = Strength * focus;
|
||||
|
||||
float angle = frac(sin(dot(pos.xy, float2(12.9898, 78.233))) * 43758.5453) * 6.2831853;
|
||||
float2 spoke = float2(cos(angle), sin(angle));
|
||||
|
||||
const float2 turn = float2(cos(2.39996323), sin(2.39996323));
|
||||
const float decay = exp(-2.0 / float(TAPS));
|
||||
float weight = exp(-1.0 / float(TAPS));
|
||||
|
||||
float3 sum = float3(0.0, 0.0, 0.0);
|
||||
float total = 0.0;
|
||||
[unroll] for (int i = 0; i < TAPS; ++i)
|
||||
{
|
||||
float reach = sqrt((float(i) + 0.5) / float(TAPS));
|
||||
sum += tex2D(BackBuffer, uv + spoke * reach * texel * Radius).rgb * weight;
|
||||
total += weight;
|
||||
weight *= decay;
|
||||
spoke = float2(spoke.x * turn.x - spoke.y * turn.y,
|
||||
spoke.x * turn.y + spoke.y * turn.x);
|
||||
}
|
||||
|
||||
float3 blurred = sum / total;
|
||||
return float4(lerp(original, blurred, amount), 1.0);
|
||||
}
|
||||
|
||||
technique Blur <
|
||||
ui_label = "Blur";
|
||||
ui_tooltip = "Gaussian blur with an optional sharp centre, for softening the picture or faking depth of field.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Blur;
|
||||
}
|
||||
}
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from crt.fsh in PPSSPP, by KillaMaaki.
|
||||
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Timer < source = "timer"; > = 0.0;
|
||||
|
||||
uniform float Density <
|
||||
ui_type = "slider";
|
||||
ui_label = "Line Density";
|
||||
ui_min = 60.0; ui_max = 720.0; ui_step = 10.0;
|
||||
> = 272.0;
|
||||
|
||||
uniform float RollSpeed <
|
||||
ui_type = "slider";
|
||||
ui_label = "Roll Speed";
|
||||
ui_tooltip = "Speed of the rolling bar. Zero disables it.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Bleed <
|
||||
ui_type = "slider";
|
||||
ui_label = "Colour Bleed";
|
||||
ui_tooltip = "Horizontal separation of the red and green channels.";
|
||||
ui_min = 0.0; ui_max = 4.0; ui_step = 0.1;
|
||||
> = 1.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_CRT(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float seconds = Timer * 0.001;
|
||||
float scan = floor((uv.y + seconds * RollSpeed * 0.5) * Density);
|
||||
float line_intensity = frac(scan * 0.5) * 2.0;
|
||||
|
||||
float2 shift = float2(line_intensity * 0.0005, 0.0);
|
||||
float2 bleed = float2(BUFFER_RCP_WIDTH * Bleed, 0.0);
|
||||
|
||||
float r = tex2D(BackBuffer, uv + bleed + shift).r;
|
||||
float g = tex2D(BackBuffer, uv - bleed + shift).g;
|
||||
float b = tex2D(BackBuffer, uv).b;
|
||||
|
||||
float3 color = float3(r, g * 0.99, b) * clamp(line_intensity, 0.85, 1.0);
|
||||
|
||||
if (RollSpeed > 0.0)
|
||||
{
|
||||
float rollbar = sin((uv.y + seconds * RollSpeed) * 4.0);
|
||||
color += rollbar * 0.02;
|
||||
}
|
||||
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
technique CRT <
|
||||
ui_label = "CRT";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_CRT;
|
||||
}
|
||||
}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-FileCopyrightText: guest(r)
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from cartoon.fsh in PPSSPP, Advanced Cartoon shader I by guest(r).
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float EdgeStrength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Edge Strength";
|
||||
ui_tooltip = "Darkness of the ink outline drawn around detected edges.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 0.5;
|
||||
|
||||
uniform float Levels <
|
||||
ui_type = "slider";
|
||||
ui_label = "Colour Levels";
|
||||
ui_tooltip = "How many bands the colours are quantised into.";
|
||||
ui_min = 2.0; ui_max = 16.0; ui_step = 1.0;
|
||||
> = 4.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Cartoon(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 c00 = tex2D(BackBuffer, uv + texel * float2(-1.0, -1.0)).rgb;
|
||||
float3 c10 = tex2D(BackBuffer, uv + texel * float2( 0.0, -1.0)).rgb;
|
||||
float3 c20 = tex2D(BackBuffer, uv + texel * float2( 1.0, -1.0)).rgb;
|
||||
float3 c01 = tex2D(BackBuffer, uv + texel * float2(-1.0, 0.0)).rgb;
|
||||
float3 c11 = tex2D(BackBuffer, uv).rgb;
|
||||
float3 c21 = tex2D(BackBuffer, uv + texel * float2( 1.0, 0.0)).rgb;
|
||||
float3 c02 = tex2D(BackBuffer, uv + texel * float2(-1.0, 1.0)).rgb;
|
||||
float3 c12 = tex2D(BackBuffer, uv + texel * float2( 0.0, 1.0)).rgb;
|
||||
float3 c22 = tex2D(BackBuffer, uv + texel * float2( 1.0, 1.0)).rgb;
|
||||
|
||||
const float3 dt = float3(1.0, 1.0, 1.0);
|
||||
|
||||
float d1 = dot(abs(c00 - c22), dt);
|
||||
float d2 = dot(abs(c20 - c02), dt);
|
||||
float hl = dot(abs(c01 - c21), dt);
|
||||
float vl = dot(abs(c10 - c12), dt);
|
||||
float edge = EdgeStrength * (d1 + d2 + hl + vl) / (dot(c11, dt) + 0.15);
|
||||
|
||||
float lc = Levels * length(c11);
|
||||
float f = frac(lc);
|
||||
f *= f;
|
||||
lc = (floor(lc) + f * f) / Levels + 0.05;
|
||||
|
||||
float3 unit = normalize(max(c11, 0.0001));
|
||||
float3 quant = Levels * unit;
|
||||
float3 frct = frac(quant);
|
||||
frct *= frct;
|
||||
quant = floor(quant) + 0.05 * dt + frct * frct;
|
||||
|
||||
float3 color = lc * (1.1 - edge * sqrt(edge)) * quant / Levels;
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
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, so try Cartoon Soft or Cel Shading if the blacks smear.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Cartoon;
|
||||
}
|
||||
}
|
||||
Vendored
+100
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-FileCopyrightText: guest(r)
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float EdgeStrength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Edge Strength";
|
||||
ui_tooltip = "Darkness of the ink outline drawn around detected edges.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 0.45;
|
||||
|
||||
uniform float ShadowGuard <
|
||||
ui_type = "slider";
|
||||
ui_label = "Shadow Guard";
|
||||
ui_tooltip = "Holds the outline back in dark areas. Raise it if shadows turn into black blobs.";
|
||||
ui_min = 0.1; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 0.8;
|
||||
|
||||
uniform float Levels <
|
||||
ui_type = "slider";
|
||||
ui_label = "Colour Levels";
|
||||
ui_tooltip = "How many bands the colours are quantised into.";
|
||||
ui_min = 2.0; ui_max = 16.0; ui_step = 1.0;
|
||||
> = 6.0;
|
||||
|
||||
uniform float Smoothing <
|
||||
ui_type = "slider";
|
||||
ui_label = "Banding";
|
||||
ui_tooltip = "How far the picture is pushed towards flat bands.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.75;
|
||||
|
||||
uniform float Saturation <
|
||||
ui_type = "slider";
|
||||
ui_label = "Saturation";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.15;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_CartoonSoft(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 c00 = tex2D(BackBuffer, uv + texel * float2(-1.0, -1.0)).rgb;
|
||||
float3 c10 = tex2D(BackBuffer, uv + texel * float2( 0.0, -1.0)).rgb;
|
||||
float3 c20 = tex2D(BackBuffer, uv + texel * float2( 1.0, -1.0)).rgb;
|
||||
float3 c01 = tex2D(BackBuffer, uv + texel * float2(-1.0, 0.0)).rgb;
|
||||
float3 c11 = tex2D(BackBuffer, uv).rgb;
|
||||
float3 c21 = tex2D(BackBuffer, uv + texel * float2( 1.0, 0.0)).rgb;
|
||||
float3 c02 = tex2D(BackBuffer, uv + texel * float2(-1.0, 1.0)).rgb;
|
||||
float3 c12 = tex2D(BackBuffer, uv + texel * float2( 0.0, 1.0)).rgb;
|
||||
float3 c22 = tex2D(BackBuffer, uv + texel * float2( 1.0, 1.0)).rgb;
|
||||
|
||||
const float3 dt = float3(1.0, 1.0, 1.0);
|
||||
const float3 luma_weights = float3(0.299, 0.587, 0.114);
|
||||
|
||||
float d1 = dot(abs(c00 - c22), dt);
|
||||
float d2 = dot(abs(c20 - c02), dt);
|
||||
float hl = dot(abs(c01 - c21), dt);
|
||||
float vl = dot(abs(c10 - c12), dt);
|
||||
|
||||
float luma = dot(c11, luma_weights);
|
||||
float response = (d1 + d2 + hl + vl) / (luma * 2.0 + ShadowGuard);
|
||||
float ink = 1.0 - saturate(response * EdgeStrength);
|
||||
|
||||
float scaled = luma * Levels;
|
||||
float step_position = frac(scaled);
|
||||
float eased = step_position * step_position * (3.0 - 2.0 * step_position);
|
||||
float banded = (floor(scaled) + eased) / Levels;
|
||||
float target = lerp(luma, banded, Smoothing);
|
||||
|
||||
float3 tinted = c11 * (target / max(luma, 0.001));
|
||||
float3 grey = dot(tinted, luma_weights);
|
||||
float3 color = lerp(grey, tinted, Saturation) * ink;
|
||||
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
technique CartoonSoft <
|
||||
ui_label = "Cartoon Soft";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_CartoonSoft;
|
||||
}
|
||||
}
|
||||
Vendored
+120
@@ -0,0 +1,120 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Bands <
|
||||
ui_type = "slider";
|
||||
ui_label = "Shading Bands";
|
||||
ui_tooltip = "How many flat steps the lighting is collapsed into. Three or four gives the classic look.";
|
||||
ui_min = 2.0; ui_max = 8.0; ui_step = 1.0;
|
||||
> = 4.0;
|
||||
|
||||
uniform float BandContrast <
|
||||
ui_type = "slider";
|
||||
ui_label = "Band Contrast";
|
||||
ui_tooltip = "Spreads the bands towards pure black and white. Lower it if the shadows crush.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.5;
|
||||
|
||||
uniform float BandEdge <
|
||||
ui_type = "slider";
|
||||
ui_label = "Band Edge";
|
||||
ui_tooltip = "Width of the transition between bands. Low values give hard cel steps.";
|
||||
ui_min = 0.02; ui_max = 1.0; ui_step = 0.02;
|
||||
> = 0.15;
|
||||
|
||||
uniform float Detail <
|
||||
ui_type = "slider";
|
||||
ui_label = "Texture Detail";
|
||||
ui_tooltip = "How much surface texture survives the flattening. At zero the bands are completely flat.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.6;
|
||||
|
||||
uniform float OutlineStrength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Outline Strength";
|
||||
ui_tooltip = "Opacity of the ink line drawn along detected edges.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.8;
|
||||
|
||||
uniform float OutlineThreshold <
|
||||
ui_type = "slider";
|
||||
ui_label = "Outline Threshold";
|
||||
ui_tooltip = "How strong an edge has to be before it is inked. Raise it to keep ink off textures and specular highlights.";
|
||||
ui_min = 0.01; ui_max = 0.4; ui_step = 0.01;
|
||||
> = 0.18;
|
||||
|
||||
uniform float Saturation <
|
||||
ui_type = "slider";
|
||||
ui_label = "Saturation";
|
||||
ui_tooltip = "Colour intensity of the flat regions.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.25;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_CelShading(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
const float3 luma_weights = float3(0.2126, 0.7152, 0.0722);
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 centre = tex2D(BackBuffer, uv).rgb;
|
||||
float luma = dot(centre, luma_weights);
|
||||
|
||||
float2 near = texel * 1.5;
|
||||
float2 far = texel * 3.5;
|
||||
|
||||
float a00 = dot(tex2D(BackBuffer, uv + near * float2(-1.0, -1.0)).rgb, luma_weights);
|
||||
float a20 = dot(tex2D(BackBuffer, uv + near * float2( 1.0, -1.0)).rgb, luma_weights);
|
||||
float a02 = dot(tex2D(BackBuffer, uv + near * float2(-1.0, 1.0)).rgb, luma_weights);
|
||||
float a22 = dot(tex2D(BackBuffer, uv + near * float2( 1.0, 1.0)).rgb, luma_weights);
|
||||
|
||||
float b00 = dot(tex2D(BackBuffer, uv + far * float2(-1.0, -1.0)).rgb, luma_weights);
|
||||
float b20 = dot(tex2D(BackBuffer, uv + far * float2( 1.0, -1.0)).rgb, luma_weights);
|
||||
float b02 = dot(tex2D(BackBuffer, uv + far * float2(-1.0, 1.0)).rgb, luma_weights);
|
||||
float b22 = dot(tex2D(BackBuffer, uv + far * float2( 1.0, 1.0)).rgb, luma_weights);
|
||||
|
||||
float gx = (a00 + a02) - (a20 + a22);
|
||||
float gy = (a00 + a20) - (a02 + a22);
|
||||
float gradient = sqrt(gx * gx + gy * gy);
|
||||
float ink = smoothstep(OutlineThreshold, OutlineThreshold + 0.04, gradient);
|
||||
|
||||
float lighting = (a00 + a20 + a02 + a22 + b00 + b20 + b02 + b22) * 0.125;
|
||||
float detail = luma - lighting;
|
||||
|
||||
float softness = max(BandEdge, 0.001);
|
||||
float coord = lighting * Bands - softness * 0.5;
|
||||
float index = floor(coord) + smoothstep(1.0 - softness, 1.0, frac(coord));
|
||||
|
||||
float centred = (index + 0.5) / Bands;
|
||||
float stretched = index / max(Bands - 1.0, 1.0);
|
||||
float banded = saturate(lerp(centred, stretched, BandContrast));
|
||||
|
||||
float target = saturate(banded + detail * Detail);
|
||||
|
||||
float gain = min(target / max(luma, 0.001), 4.0);
|
||||
float3 shaded = centre * gain;
|
||||
float3 grey = dot(shaded, luma_weights);
|
||||
float3 color = lerp(grey, shaded, Saturation);
|
||||
color *= 1.0 - ink * OutlineStrength;
|
||||
|
||||
return float4(saturate(color), 1.0);
|
||||
}
|
||||
|
||||
technique CelShading <
|
||||
ui_label = "Cel Shading";
|
||||
ui_tooltip = "Collapses the lighting into a few hard steps and inks the edges, keeping texture detail and hue intact.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_CelShading;
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Strength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Strength";
|
||||
ui_tooltip = "Channel separation in pixels, measured at the edge of the screen.";
|
||||
ui_min = 0.0; ui_max = 8.0; ui_step = 0.1;
|
||||
> = 1.5;
|
||||
|
||||
uniform float Falloff <
|
||||
ui_type = "slider";
|
||||
ui_label = "Falloff";
|
||||
ui_tooltip = "How fast the separation grows away from the centre. Higher keeps the middle clean.";
|
||||
ui_min = 1.0; ui_max = 4.0; ui_step = 0.1;
|
||||
> = 2.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_ChromaticAberration(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float2 direction = uv - float2(0.5, 0.5);
|
||||
float radius = length(direction);
|
||||
float2 unit = direction / max(radius, 0.0001);
|
||||
float2 offset = unit * Strength * pow(radius * 2.0, Falloff) * texel;
|
||||
|
||||
float red = tex2D(BackBuffer, uv + offset).r;
|
||||
float green = tex2D(BackBuffer, uv).g;
|
||||
float blue = tex2D(BackBuffer, uv - offset).b;
|
||||
|
||||
return float4(red, green, blue, 1.0);
|
||||
}
|
||||
|
||||
technique ChromaticAberration <
|
||||
ui_label = "Chromatic Aberration";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_ChromaticAberration;
|
||||
}
|
||||
}
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Based on colorcorrection.fsh in PPSSPP.
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Saturation <
|
||||
ui_type = "slider";
|
||||
ui_label = "Saturation";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Brightness <
|
||||
ui_type = "slider";
|
||||
ui_label = "Brightness";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Contrast <
|
||||
ui_type = "slider";
|
||||
ui_label = "Contrast";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Gamma <
|
||||
ui_type = "slider";
|
||||
ui_label = "Gamma";
|
||||
ui_min = 0.5; ui_max = 2.0; 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(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_ColorGrade(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float3 rgb = tex2D(BackBuffer, uv).rgb;
|
||||
|
||||
float luma = dot(rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
rgb = lerp(float3(luma, luma, luma), rgb, Saturation);
|
||||
rgb *= Brightness;
|
||||
rgb = (rgb - 0.5) * Contrast + 0.5;
|
||||
rgb = pow(max(rgb, 0.0), 1.0 / max(Gamma, 0.0001));
|
||||
|
||||
return float4(saturate(rgb), 1.0);
|
||||
}
|
||||
|
||||
technique ColorGrade <
|
||||
ui_label = "Colour Grade";
|
||||
ui_tooltip = "The four basic colour controls in one pass: saturation, brightness, contrast and gamma. Reach for this before anything more specialised.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_ColorGrade;
|
||||
}
|
||||
}
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2015 Niklas Haas
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Threshold <
|
||||
ui_type = "slider";
|
||||
ui_label = "Threshold";
|
||||
ui_tooltip = "How flat a neighbourhood must be before it gets smoothed. Raise it to catch wider bands, lower it to keep more detail.";
|
||||
ui_min = 0.002; ui_max = 0.05; ui_step = 0.001;
|
||||
> = 0.012;
|
||||
|
||||
uniform float Radius <
|
||||
ui_type = "slider";
|
||||
ui_label = "Radius";
|
||||
ui_tooltip = "How far the sampling reaches, in pixels. Wider gradients need a larger radius.";
|
||||
ui_min = 1.0; ui_max = 32.0; ui_step = 1.0;
|
||||
> = 8.0;
|
||||
|
||||
uniform float Grain <
|
||||
ui_type = "slider";
|
||||
ui_label = "Dither";
|
||||
ui_tooltip = "Noise added to break up whatever banding survives the smoothing.";
|
||||
ui_min = 0.0; ui_max = 0.02; ui_step = 0.001;
|
||||
> = 0.004;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
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_Deband(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 centre = tex2D(BackBuffer, uv).rgb;
|
||||
float base = Hash(pos.xy) * 6.2831853;
|
||||
|
||||
float3 total = float3(0.0, 0.0, 0.0);
|
||||
float3 deviation = float3(0.0, 0.0, 0.0);
|
||||
|
||||
[unroll] for (int ring = 1; ring <= 2; ++ring)
|
||||
{
|
||||
float angle = base + float(ring) * 2.3999632;
|
||||
float reach = Radius * float(ring) * 0.5;
|
||||
float2 along = float2(cos(angle), sin(angle)) * reach;
|
||||
float2 across = float2(-along.y, along.x);
|
||||
|
||||
float3 s0 = tex2D(BackBuffer, uv + along * texel).rgb;
|
||||
float3 s1 = tex2D(BackBuffer, uv - along * texel).rgb;
|
||||
float3 s2 = tex2D(BackBuffer, uv + across * texel).rgb;
|
||||
float3 s3 = tex2D(BackBuffer, uv - across * texel).rgb;
|
||||
|
||||
total += s0 + s1 + s2 + s3;
|
||||
deviation = max(deviation, max(max(abs(s0 - centre), abs(s1 - centre)),
|
||||
max(abs(s2 - centre), abs(s3 - centre))));
|
||||
}
|
||||
|
||||
float3 average = total * 0.125;
|
||||
float3 flatness = float3(1.0, 1.0, 1.0) -
|
||||
smoothstep(Threshold * 0.5, Threshold, deviation);
|
||||
float3 result = lerp(centre, average, flatness);
|
||||
|
||||
float dither = (Hash(pos.xy + float2(71.3, 41.7)) - 0.5) * Grain;
|
||||
|
||||
return float4(saturate(result + dither), 1.0);
|
||||
}
|
||||
|
||||
technique Deband <
|
||||
ui_label = "Deband";
|
||||
ui_tooltip = "Smooths the visible steps in gradients such as skies, then dithers whatever survives. Worth adding whenever large flat areas show rings.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Deband;
|
||||
}
|
||||
}
|
||||
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2019-2021 bloc97
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Strength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Strength";
|
||||
ui_min = 0.01; ui_max = 0.5; ui_step = 0.01;
|
||||
> = 0.1;
|
||||
|
||||
uniform float Radius <
|
||||
ui_type = "slider";
|
||||
ui_label = "Radius";
|
||||
ui_min = 0.3; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Curve <
|
||||
ui_type = "slider";
|
||||
ui_label = "Shadow Bias";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float3 IntensityWeight(float3 value, float3 sigma, float3 centre)
|
||||
{
|
||||
float3 scaled = (value - centre) / sigma;
|
||||
return exp(-0.5 * scaled * scaled);
|
||||
}
|
||||
|
||||
float SpatialWeight(float distance, float sigma)
|
||||
{
|
||||
float scaled = distance / sigma;
|
||||
return exp(-0.5 * scaled * scaled);
|
||||
}
|
||||
|
||||
float4 PS_Denoise(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 centre = tex2D(BackBuffer, uv).rgb;
|
||||
float3 intensity_sigma = max(pow(centre + 0.0001, Curve) * Strength, 0.0001);
|
||||
float spatial_sigma = max(Radius, 0.05);
|
||||
|
||||
float3 sum = float3(0.0, 0.0, 0.0);
|
||||
float3 total = float3(0.0, 0.0, 0.0);
|
||||
|
||||
[unroll] for (int y = -2; y <= 2; ++y)
|
||||
{
|
||||
[unroll] for (int x = -2; x <= 2; ++x)
|
||||
{
|
||||
float2 offset = float2(x, y);
|
||||
float3 tap = tex2D(BackBuffer, uv + offset * texel).rgb;
|
||||
float3 weight = IntensityWeight(tap, intensity_sigma, centre) *
|
||||
SpatialWeight(length(offset), spatial_sigma);
|
||||
sum += weight * tap;
|
||||
total += weight;
|
||||
}
|
||||
}
|
||||
|
||||
return float4(sum / total, 1.0);
|
||||
}
|
||||
|
||||
technique Denoise <
|
||||
ui_label = "Denoise";
|
||||
ui_tooltip = "Edge preserving blur that clears dithering and compression noise while leaving outlines sharp. Ported from Anime4K.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Denoise;
|
||||
}
|
||||
}
|
||||
Vendored
+65
@@ -0,0 +1,65 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+88
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Length <
|
||||
ui_type = "slider";
|
||||
ui_label = "Length";
|
||||
ui_tooltip = "How far the streaks reach, as a percentage of screen height.";
|
||||
ui_min = 0.0; ui_max = 20.0; ui_step = 0.5;
|
||||
> = 5.0;
|
||||
|
||||
uniform float Zoom <
|
||||
ui_type = "slider";
|
||||
ui_label = "Zoom";
|
||||
ui_tooltip = "Streaks running out from the centre of the screen, as if rushing forward. The centre stays sharp.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Pan <
|
||||
ui_type = "slider";
|
||||
ui_label = "Pan";
|
||||
ui_tooltip = "Streaks running in one fixed direction, as if the camera were sweeping across.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.0;
|
||||
|
||||
uniform float Angle <
|
||||
ui_type = "slider";
|
||||
ui_label = "Pan Angle";
|
||||
ui_tooltip = "Direction of the sweep, in degrees.";
|
||||
ui_min = 0.0; ui_max = 360.0; ui_step = 5.0;
|
||||
> = 0.0;
|
||||
|
||||
uniform float Spin <
|
||||
ui_type = "slider";
|
||||
ui_label = "Spin";
|
||||
ui_tooltip = "Streaks curling around the centre, as if the camera were rolling. Negative turns the other way.";
|
||||
ui_min = -1.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.0;
|
||||
|
||||
static const int TAPS = 24;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_MotionBlur(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float aspect = BUFFER_WIDTH * BUFFER_RCP_HEIGHT;
|
||||
float2 to_screen = float2(aspect, 1.0);
|
||||
|
||||
float2 centred = (uv - 0.5) * to_screen * 2.0;
|
||||
float2 outward = centred;
|
||||
float2 around = float2(-centred.y, centred.x);
|
||||
|
||||
float radians_angle = Angle * 0.01745329;
|
||||
float2 sweep = float2(cos(radians_angle), sin(radians_angle));
|
||||
|
||||
float2 velocity = sweep * Pan + outward * Zoom + around * Spin;
|
||||
velocity *= Length * 0.01;
|
||||
velocity /= to_screen;
|
||||
|
||||
float jitter = frac(sin(dot(pos.xy, float2(12.9898, 78.233))) * 43758.5453);
|
||||
|
||||
float3 sum = float3(0.0, 0.0, 0.0);
|
||||
[unroll] for (int i = 0; i < TAPS; ++i)
|
||||
{
|
||||
float t = (float(i) + jitter) / float(TAPS) - 0.5;
|
||||
sum += tex2D(BackBuffer, uv + velocity * t).rgb;
|
||||
}
|
||||
|
||||
return float4(sum / float(TAPS), 1.0);
|
||||
}
|
||||
|
||||
technique MotionBlur <
|
||||
ui_label = "Motion Blur";
|
||||
ui_tooltip = "Camera style motion blur: streaks the picture outwards, sideways or around, without touching still detail at the centre.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_MotionBlur;
|
||||
}
|
||||
}
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from naturalA.fsh in PPSSPP, by ShadX, modified by SimoneT.
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Luma <
|
||||
ui_type = "slider";
|
||||
ui_label = "Luma Curve";
|
||||
ui_tooltip = "Gamma applied to the luminance channel in YIQ space.";
|
||||
ui_min = 0.5; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 1.2;
|
||||
|
||||
uniform float Chroma <
|
||||
ui_type = "slider";
|
||||
ui_label = "Chroma Gain";
|
||||
ui_tooltip = "Boost applied to the two colour difference channels.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 1.2;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Natural(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
const float3x3 RGBtoYIQ = float3x3(0.299, 0.587, 0.114,
|
||||
0.596, -0.275, -0.321,
|
||||
0.212, -0.523, 0.311);
|
||||
|
||||
const float3x3 YIQtoRGB = float3x3(1.0, 0.95568806, 0.61985809,
|
||||
1.0, -0.27158180, -0.64687382,
|
||||
1.0, -1.10817733, 1.70506456);
|
||||
|
||||
float3 rgb = tex2D(BackBuffer, uv).rgb;
|
||||
float3 yiq = mul(RGBtoYIQ, rgb);
|
||||
|
||||
yiq.x = pow(max(yiq.x, 0.0), Luma);
|
||||
yiq.yz *= Chroma;
|
||||
|
||||
return float4(saturate(mul(YIQtoRGB, yiq)), 1.0);
|
||||
}
|
||||
|
||||
technique NaturalColors <
|
||||
ui_label = "Natural Colours";
|
||||
ui_tooltip = "Warms the picture and lifts saturation for a less washed out look. Ported from the PPSSPP shader.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Natural;
|
||||
}
|
||||
}
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Timer < source = "timer"; > = 0.0;
|
||||
|
||||
uniform float Horizon <
|
||||
ui_type = "slider";
|
||||
ui_label = "Reflection Line";
|
||||
ui_tooltip = "Height on screen where the reflective floor begins. Everything above it is left untouched.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.01;
|
||||
> = 0.55;
|
||||
|
||||
uniform float Amount <
|
||||
ui_type = "slider";
|
||||
ui_label = "Amount";
|
||||
ui_tooltip = "How strongly the reflection shows through the floor.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.35;
|
||||
|
||||
uniform float Falloff <
|
||||
ui_type = "slider";
|
||||
ui_label = "Falloff";
|
||||
ui_tooltip = "How quickly the reflection fades as the floor comes towards the viewer. At zero it stays even.";
|
||||
ui_min = 0.0; ui_max = 4.0; ui_step = 0.1;
|
||||
> = 1.2;
|
||||
|
||||
uniform float Perspective <
|
||||
ui_type = "slider";
|
||||
ui_label = "Perspective";
|
||||
ui_tooltip = "Stretches or squashes the mirrored image. One is a true mirror, higher values pull it towards the line.";
|
||||
ui_min = 0.2; ui_max = 2.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
uniform float Ripple <
|
||||
ui_type = "slider";
|
||||
ui_label = "Ripple";
|
||||
ui_tooltip = "Amplitude of the waves disturbing the reflection. At zero the mirror is perfectly still.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.0;
|
||||
|
||||
uniform float RippleSpeed <
|
||||
ui_type = "slider";
|
||||
ui_label = "Ripple Speed";
|
||||
ui_tooltip = "How fast the waves travel.";
|
||||
ui_min = 0.0; ui_max = 4.0; ui_step = 0.1;
|
||||
> = 1.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Reflections(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float3 color = tex2D(BackBuffer, uv).rgb;
|
||||
|
||||
float depth = uv.y - Horizon;
|
||||
float on_floor = step(0.0, depth);
|
||||
float span = max(1.0 - Horizon, 0.001);
|
||||
float distance_down = saturate(depth / span);
|
||||
|
||||
float seconds = Timer * 0.001;
|
||||
float wave = sin(uv.x * 38.0 + seconds * RippleSpeed * 2.0) *
|
||||
sin(uv.y * 21.0 - seconds * RippleSpeed * 1.3);
|
||||
float2 disturbance = float2(wave * 0.004, wave * 0.002) * Ripple * distance_down;
|
||||
|
||||
float2 mirrored = float2(uv.x, Horizon - depth * Perspective) + disturbance;
|
||||
float3 reflection = tex2D(BackBuffer, saturate(mirrored)).rgb;
|
||||
|
||||
float fade = pow(max(1.0 - distance_down, 0.0001), Falloff);
|
||||
float strength = Amount * fade * on_floor;
|
||||
|
||||
return float4(lerp(color, reflection, strength), 1.0);
|
||||
}
|
||||
|
||||
technique Reflections <
|
||||
ui_label = "Reflections";
|
||||
ui_tooltip = "Mirrors the picture into a reflective floor below a line you choose, with optional water ripples.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Reflections;
|
||||
}
|
||||
}
|
||||
Vendored
+66
@@ -0,0 +1,66 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from scanlines.fsh in PPSSPP.
|
||||
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Density <
|
||||
ui_type = "slider";
|
||||
ui_label = "Line Density";
|
||||
ui_tooltip = "Number of scanline pairs across the screen.";
|
||||
ui_min = 60.0; ui_max = 720.0; ui_step = 10.0;
|
||||
> = 340.0;
|
||||
|
||||
uniform float Intensity <
|
||||
ui_type = "slider";
|
||||
ui_label = "Intensity";
|
||||
ui_tooltip = "How dark the gaps between lines become.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 0.5;
|
||||
|
||||
uniform float Tint <
|
||||
ui_type = "slider";
|
||||
ui_label = "Phosphor Tint";
|
||||
ui_tooltip = "Strength of the green-warm phosphor cast.";
|
||||
ui_min = 0.0; ui_max = 1.0; ui_step = 0.05;
|
||||
> = 1.0;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Scanlines(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float line_pos = uv.y * Density * 0.5;
|
||||
float gate = cos((frac(line_pos) - 0.5) * 3.1415926 * Intensity) * 1.5;
|
||||
|
||||
float3 rgb = tex2D(BackBuffer, uv).rgb;
|
||||
float3 color = rgb * 0.5 + 0.5 * rgb * rgb * 1.2;
|
||||
|
||||
float3 phosphor = lerp(float3(1.0, 1.0, 1.0), float3(0.9, 1.0, 0.7), Tint);
|
||||
color *= phosphor;
|
||||
|
||||
float2 diff = uv - 0.5;
|
||||
color *= 1.1 - 0.6 * (dot(diff, diff) * 2.0);
|
||||
|
||||
return float4(saturate(color * saturate(gate)), 1.0);
|
||||
}
|
||||
|
||||
technique Scanlines <
|
||||
ui_label = "Scanlines";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Scanlines;
|
||||
}
|
||||
}
|
||||
Vendored
+43
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Amount <
|
||||
ui_type = "slider";
|
||||
ui_label = "Amount";
|
||||
ui_min = 0.0; ui_max = 3.0; ui_step = 0.01;
|
||||
> = 0.6;
|
||||
|
||||
void VS_PostProcess(in uint id : SV_VertexID, out float4 pos : SV_Position, out float2 uv : TEXCOORD)
|
||||
{
|
||||
uv = float2(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Sharpen(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 texel = float2(BUFFER_RCP_WIDTH, BUFFER_RCP_HEIGHT);
|
||||
|
||||
float3 centre = tex2D(BackBuffer, uv).rgb;
|
||||
float3 blur = tex2D(BackBuffer, uv + float2(-texel.x, 0.0)).rgb;
|
||||
blur += tex2D(BackBuffer, uv + float2(texel.x, 0.0)).rgb;
|
||||
blur += tex2D(BackBuffer, uv + float2(0.0, -texel.y)).rgb;
|
||||
blur += tex2D(BackBuffer, uv + float2(0.0, texel.y)).rgb;
|
||||
blur *= 0.25;
|
||||
|
||||
return float4(centre + (centre - blur) * Amount, 1.0);
|
||||
}
|
||||
|
||||
technique Sharpen <
|
||||
ui_label = "Sharpen";
|
||||
ui_tooltip = "Unsharp mask that brings back edge detail lost to scaling. Most useful after upscaling a low internal resolution.";
|
||||
>
|
||||
{
|
||||
pass
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Sharpen;
|
||||
}
|
||||
}
|
||||
Vendored
+75
@@ -0,0 +1,75 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2012 PPSSPP Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// Ported from vignette.fsh in PPSSPP, by Henrik Rydgard.
|
||||
|
||||
texture BackBufferTex : COLOR;
|
||||
sampler BackBuffer { Texture = BackBufferTex; };
|
||||
|
||||
uniform float Strength <
|
||||
ui_type = "slider";
|
||||
ui_label = "Strength";
|
||||
ui_tooltip = "How dark the corners become.";
|
||||
ui_min = 0.0; ui_max = 2.0; ui_step = 0.01;
|
||||
> = 0.6;
|
||||
|
||||
uniform float Aspect <
|
||||
ui_type = "slider";
|
||||
ui_label = "Aspect";
|
||||
ui_min = 0.5; ui_max = 2.0; 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(float(id & 2), float((id & 1) << 1));
|
||||
pos = float4(uv * float2(2.0, -2.0) + float2(-1.0, 1.0), 0.0, 1.0);
|
||||
}
|
||||
|
||||
float4 PS_Vignette(float4 pos : SV_Position, float2 uv : TEXCOORD) : SV_Target
|
||||
{
|
||||
float2 diff = uv - 0.5;
|
||||
diff.x *= Aspect;
|
||||
diff.y /= max(Aspect, 0.0001);
|
||||
|
||||
float falloff = 1.0 - min(1.0, Strength * dot(diff, diff) * 2.0);
|
||||
float3 rgb = tex2D(BackBuffer, uv).rgb;
|
||||
|
||||
return float4(rgb * falloff, 1.0);
|
||||
}
|
||||
|
||||
technique Vignette <
|
||||
ui_label = "Vignette";
|
||||
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
|
||||
{
|
||||
VertexShader = VS_PostProcess;
|
||||
PixelShader = PS_Vignette;
|
||||
}
|
||||
}
|
||||
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
// 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(float(id & 2), float((id & 1) << 1));
|
||||
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;
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -195,6 +195,50 @@ else()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# reshadefx
|
||||
if (ENABLE_RESHADE)
|
||||
AddJsonPackage(NAME reshade DOWNLOAD_ONLY)
|
||||
|
||||
set(RESHADEFX_SHIM_DIR ${CMAKE_CURRENT_BINARY_DIR}/reshadefx_shim)
|
||||
file(WRITE ${RESHADEFX_SHIM_DIR}/spirv.hpp "#include <spirv/unified1/spirv.hpp>\n")
|
||||
file(WRITE ${RESHADEFX_SHIM_DIR}/GLSL.std.450.h "#include <spirv/unified1/GLSL.std.450.h>\n")
|
||||
if (APPLE)
|
||||
file(WRITE ${RESHADEFX_SHIM_DIR}/malloc.h "#include <stdlib.h>\n#include <alloca.h>\n")
|
||||
endif()
|
||||
|
||||
add_library(reshadefx STATIC
|
||||
${reshade_SOURCE_DIR}/source/effect_codegen_spirv.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_expression.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_lexer.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_parser_exp.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_parser_stmt.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_preprocessor.cpp
|
||||
${reshade_SOURCE_DIR}/source/effect_symbol_table.cpp
|
||||
)
|
||||
|
||||
target_include_directories(reshadefx SYSTEM PUBLIC ${reshade_SOURCE_DIR}/source)
|
||||
target_include_directories(reshadefx PRIVATE ${RESHADEFX_SHIM_DIR})
|
||||
target_link_libraries(reshadefx PUBLIC SPIRV-Headers::SPIRV-Headers)
|
||||
|
||||
if (NOT MSVC)
|
||||
target_compile_options(reshadefx PRIVATE -w -fno-char8_t)
|
||||
else()
|
||||
target_compile_options(reshadefx PRIVATE /w /Zc:char8_t-)
|
||||
endif()
|
||||
|
||||
if (WIN32)
|
||||
if (NOT MSVC)
|
||||
target_compile_options(reshadefx PRIVATE -include share.h)
|
||||
else()
|
||||
target_compile_options(reshadefx PRIVATE /FIshare.h)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT TARGET reshadefx::reshadefx)
|
||||
add_library(reshadefx::reshadefx ALIAS reshadefx)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Catch2
|
||||
if (YUZU_TESTS OR DYNARMIC_TESTS)
|
||||
AddJsonPackage(catch2)
|
||||
|
||||
@@ -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
|
||||
@@ -18,11 +20,28 @@ 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
|
||||
|
||||
class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||
private val expandedShaders = mutableSetOf<Int>()
|
||||
|
||||
private fun forgetShaderSlot(index: Int) {
|
||||
val shifted = mutableSetOf<Int>()
|
||||
for (slot in expandedShaders) {
|
||||
if (slot < index) {
|
||||
shifted.add(slot)
|
||||
}
|
||||
if (slot > index) {
|
||||
shifted.add(slot - 1)
|
||||
}
|
||||
}
|
||||
expandedShaders.clear()
|
||||
expandedShaders.addAll(shifted)
|
||||
}
|
||||
|
||||
private fun saveSettings() {
|
||||
if (emulationFragment.shouldUseCustom) {
|
||||
NativeConfig.savePerGameConfig()
|
||||
@@ -232,6 +251,428 @@ class QuickSettings(val emulationFragment: EmulationFragment) {
|
||||
container.addView(itemView)
|
||||
}
|
||||
|
||||
fun addChoice(
|
||||
title: String,
|
||||
container: ViewGroup,
|
||||
choices: List<String>,
|
||||
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<ViewGroup>(R.id.setting_header)
|
||||
val titleView = itemView.findViewById<TextView>(R.id.setting_title)
|
||||
val valueView = itemView.findViewById<TextView>(R.id.setting_value)
|
||||
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.expand_icon)
|
||||
val radioGroup = itemView.findViewById<RadioGroup>(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,
|
||||
onCommitted: (Int) -> Unit,
|
||||
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<ViewGroup>(R.id.slider_container)
|
||||
val titleView = itemView.findViewById<TextView>(R.id.slider_title)
|
||||
val valueDisplay = itemView.findViewById<TextView>(R.id.slider_value_display)
|
||||
val slider = itemView.findViewById<com.google.android.material.slider.Slider>(
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
var pressedValue = slider.value
|
||||
|
||||
slider.setOnTouchListener { _, event ->
|
||||
val drawer = emulationFragment.view?.findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
drawer?.requestDisallowInterceptTouchEvent(true)
|
||||
pressedValue = slider.value
|
||||
}
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
||||
drawer?.requestDisallowInterceptTouchEvent(false)
|
||||
if (slider.value != pressedValue) {
|
||||
onCommitted(slider.value.toInt())
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
container.addView(itemView)
|
||||
}
|
||||
|
||||
fun addShaderCard(
|
||||
index: Int,
|
||||
title: String,
|
||||
summary: String,
|
||||
container: ViewGroup,
|
||||
onRemove: () -> Unit
|
||||
): ViewGroup {
|
||||
val inflater = LayoutInflater.from(emulationFragment.requireContext())
|
||||
val itemView = inflater.inflate(R.layout.item_quick_settings_shader, container, false)
|
||||
|
||||
val headerView = itemView.findViewById<ViewGroup>(R.id.shader_header)
|
||||
val titleView = itemView.findViewById<TextView>(R.id.shader_title)
|
||||
val summaryView = itemView.findViewById<TextView>(R.id.shader_summary)
|
||||
val removeView = itemView.findViewById<android.widget.ImageView>(R.id.shader_remove)
|
||||
val expandIcon = itemView.findViewById<android.widget.ImageView>(R.id.shader_expand)
|
||||
val bodyView = itemView.findViewById<ViewGroup>(R.id.shader_body)
|
||||
|
||||
titleView.text = title
|
||||
if (summary.isEmpty()) {
|
||||
summaryView.visibility = View.GONE
|
||||
} else {
|
||||
summaryView.text = summary
|
||||
}
|
||||
|
||||
var isExpanded = expandedShaders.contains(index)
|
||||
if (isExpanded) {
|
||||
bodyView.visibility = View.VISIBLE
|
||||
expandIcon.rotation = 180f
|
||||
}
|
||||
|
||||
headerView.setOnClickListener {
|
||||
isExpanded = !isExpanded
|
||||
if (isExpanded) {
|
||||
expandedShaders.add(index)
|
||||
bodyView.visibility = View.VISIBLE
|
||||
expandIcon.animate().rotation(180f).setDuration(200).start()
|
||||
} else {
|
||||
expandedShaders.remove(index)
|
||||
bodyView.visibility = View.GONE
|
||||
expandIcon.animate().rotation(0f).setDuration(200).start()
|
||||
}
|
||||
}
|
||||
|
||||
removeView.setOnClickListener {
|
||||
onRemove()
|
||||
}
|
||||
|
||||
container.addView(itemView)
|
||||
return bodyView
|
||||
}
|
||||
|
||||
fun addEffectPicker(
|
||||
container: ViewGroup,
|
||||
choices: List<String>,
|
||||
hasEffects: Boolean,
|
||||
onRemoveAll: () -> Unit,
|
||||
onPicked: (Int) -> Unit
|
||||
) {
|
||||
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>(
|
||||
R.id.add_button
|
||||
)
|
||||
val removeButton =
|
||||
itemView.findViewById<com.google.android.material.button.MaterialButton>(
|
||||
R.id.remove_all_button
|
||||
)
|
||||
val choiceGroup = itemView.findViewById<RadioGroup>(R.id.add_choices)
|
||||
|
||||
choices.forEachIndexed { index, name ->
|
||||
val radioButton = com.google.android.material.radiobutton.MaterialRadioButton(context)
|
||||
radioButton.text = name
|
||||
radioButton.id = View.generateViewId()
|
||||
radioButton.setPadding(16, 8, 16, 8)
|
||||
radioButton.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
onPicked(index)
|
||||
}
|
||||
}
|
||||
choiceGroup.addView(radioButton)
|
||||
}
|
||||
|
||||
var closedLabel = R.string.post_processing_add
|
||||
if (hasEffects) {
|
||||
closedLabel = R.string.post_processing_open_list
|
||||
removeButton.visibility = View.VISIBLE
|
||||
}
|
||||
button.setText(closedLabel)
|
||||
|
||||
val removeBackground = MaterialColors.getColor(
|
||||
removeButton,
|
||||
com.google.android.material.R.attr.colorErrorContainer
|
||||
)
|
||||
val removeForeground = MaterialColors.getColor(
|
||||
removeButton,
|
||||
com.google.android.material.R.attr.colorOnErrorContainer
|
||||
)
|
||||
removeButton.backgroundTintList = ColorStateList.valueOf(removeBackground)
|
||||
removeButton.setTextColor(removeForeground)
|
||||
removeButton.iconTint = ColorStateList.valueOf(removeForeground)
|
||||
removeButton.setOnClickListener {
|
||||
onRemoveAll()
|
||||
}
|
||||
|
||||
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.setText(R.string.post_processing_close_list)
|
||||
button.setIconResource(R.drawable.ic_clear)
|
||||
} else {
|
||||
choiceGroup.animate()
|
||||
.alpha(0.0f)
|
||||
.translationY(slide)
|
||||
.setDuration(160)
|
||||
.withEndAction {
|
||||
choiceGroup.visibility = View.GONE
|
||||
}
|
||||
.start()
|
||||
|
||||
button.setText(closedLabel)
|
||||
button.setIconResource(R.drawable.ic_add)
|
||||
}
|
||||
}
|
||||
|
||||
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 ->
|
||||
emulationFragment.editPostProcessing {
|
||||
NativePostProcessing.setEnabled(checked)
|
||||
}
|
||||
}
|
||||
|
||||
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 = ""
|
||||
val described = NativePostProcessing.presets().firstOrNull { it.name == preset }
|
||||
if (described != null) {
|
||||
summary = described.description
|
||||
}
|
||||
if (summary.isEmpty()) {
|
||||
summary =
|
||||
YuzuApplication.appContext.getString(R.string.post_processing_preset_locked)
|
||||
}
|
||||
if (NativePostProcessing.isPresetModified()) {
|
||||
summary = summary + "\n" +
|
||||
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>()
|
||||
|
||||
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()
|
||||
chain.forEachIndexed { index, entry ->
|
||||
val effect = usable.firstOrNull { it.file == entry.file }
|
||||
|
||||
var title = entry.file
|
||||
var summary = ""
|
||||
if (effect != null) {
|
||||
title = effect.label
|
||||
if (effect.techniques.size > 1) {
|
||||
title = effect.label + " \u00b7 " + entry.technique
|
||||
}
|
||||
summary = effect.description
|
||||
}
|
||||
|
||||
val body = addShaderCard(index, title, summary, container) {
|
||||
emulationFragment.editPostProcessing {
|
||||
NativePostProcessing.remove(index)
|
||||
}
|
||||
forgetShaderSlot(index)
|
||||
onStructureChanged()
|
||||
}
|
||||
|
||||
if (effect != null) {
|
||||
for (uniform in effect.uniforms) {
|
||||
addUniformSliders(body, index, uniform)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEffectPicker(
|
||||
container,
|
||||
labels,
|
||||
chain.isNotEmpty(),
|
||||
{
|
||||
emulationFragment.editPostProcessing {
|
||||
NativePostProcessing.clearChain()
|
||||
}
|
||||
expandedShaders.clear()
|
||||
onStructureChanged()
|
||||
}
|
||||
) { picked ->
|
||||
emulationFragment.editPostProcessing {
|
||||
NativePostProcessing.append(files[picked], techniques[picked])
|
||||
}
|
||||
onStructureChanged()
|
||||
}
|
||||
}
|
||||
|
||||
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 -> uniform.describe(position) },
|
||||
{ emulationFragment.persistPostProcessing() }
|
||||
) { position ->
|
||||
NativePostProcessing.setValue(
|
||||
index,
|
||||
uniform.name,
|
||||
component,
|
||||
uniform.min + position * uniform.step
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addDivider(container: ViewGroup) {
|
||||
val inflater = LayoutInflater.from(emulationFragment.requireContext())
|
||||
val dividerView = inflater.inflate(R.layout.item_quick_settings_divider, container, false)
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
class FxPresetNameSetting(private val onNamed: (String) -> Unit) : AbstractStringSetting {
|
||||
override val key: String
|
||||
get() = "fx_preset_name"
|
||||
|
||||
override val defaultValue: Any
|
||||
get() = ""
|
||||
|
||||
override val isRuntimeModifiable: Boolean
|
||||
get() = true
|
||||
|
||||
override val pairedSettingKey: String
|
||||
get() = ""
|
||||
|
||||
override val isSwitchable: Boolean
|
||||
get() = false
|
||||
|
||||
override val isSaveable: Boolean
|
||||
get() = true
|
||||
|
||||
override var global: Boolean
|
||||
get() = true
|
||||
set(_) {}
|
||||
|
||||
override fun getString(needsGlobal: Boolean): String = ""
|
||||
|
||||
override fun setString(value: String) = onNamed(value)
|
||||
|
||||
override fun getValueAsString(needsGlobal: Boolean): String = ""
|
||||
|
||||
override fun reset() {}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.model
|
||||
|
||||
import org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
abstract class FxUniformSetting(
|
||||
protected val index: Int,
|
||||
protected val uniform: NativePostProcessing.Uniform,
|
||||
protected val component: Int
|
||||
) : AbstractSetting {
|
||||
override val key: String
|
||||
get() = "fx_${index}_${uniform.name}_$component"
|
||||
|
||||
override val isRuntimeModifiable: Boolean
|
||||
get() = true
|
||||
|
||||
override val pairedSettingKey: String
|
||||
get() = ""
|
||||
|
||||
override val isSwitchable: Boolean
|
||||
get() = false
|
||||
|
||||
override val isSaveable: Boolean
|
||||
get() = true
|
||||
|
||||
override var global: Boolean
|
||||
get() = true
|
||||
set(_) {}
|
||||
|
||||
protected fun currentValue(): Float {
|
||||
if (NativePostProcessing.hasValue(index, uniform.name)) {
|
||||
return NativePostProcessing.getValue(index, uniform.name, component)
|
||||
}
|
||||
return uniform.defaultAt(component)
|
||||
}
|
||||
|
||||
protected fun commit(value: Float) {
|
||||
NativePostProcessing.setValue(index, uniform.name, component, value)
|
||||
NativePostProcessing.store()
|
||||
}
|
||||
|
||||
override fun reset() = commit(uniform.defaultAt(component))
|
||||
}
|
||||
|
||||
class FxUniformSliderSetting(
|
||||
index: Int,
|
||||
uniform: NativePostProcessing.Uniform,
|
||||
component: Int
|
||||
) : FxUniformSetting(index, uniform, component), AbstractIntSetting {
|
||||
override val defaultValue: Any
|
||||
get() = ((uniform.defaultAt(component) - uniform.min) / uniform.step).roundToInt()
|
||||
|
||||
override fun getInt(needsGlobal: Boolean): Int =
|
||||
((currentValue() - uniform.min) / uniform.step).roundToInt()
|
||||
|
||||
override fun setInt(value: Int) = commit(uniform.min + value * uniform.step)
|
||||
|
||||
override fun getValueAsString(needsGlobal: Boolean): String {
|
||||
if (uniform.kind == NativePostProcessing.KIND_FLOAT) {
|
||||
return String.format("%.3f", currentValue())
|
||||
}
|
||||
return currentValue().roundToInt().toString()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ object Settings {
|
||||
SECTION_SYSTEM(R.string.preferences_system),
|
||||
SECTION_RENDERER(R.string.preferences_graphics),
|
||||
SECTION_FRAME_GEN(R.string.frame_gen),
|
||||
SECTION_POST_PROCESSING(R.string.post_processing),
|
||||
SECTION_PERFORMANCE_STATS(R.string.stats_overlay_options),
|
||||
SECTION_INPUT_OVERLAY(R.string.input_overlay_options),
|
||||
SECTION_SOC_OVERLAY(R.string.soc_overlay_options),
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ enum class StringSetting(override val key: String) : AbstractStringSetting {
|
||||
DEVICE_NAME("device_name"),
|
||||
LOG_FILTER("log_filter"),
|
||||
PROGRAM_ARGS("program_args"),
|
||||
POST_SHADER_CHAIN("post_shader_chain"),
|
||||
|
||||
WEB_TOKEN("eden_token"),
|
||||
WEB_USERNAME("eden_username")
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// 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 FxButtonSetting(
|
||||
@StringRes titleId: Int,
|
||||
val onClick: () -> Unit
|
||||
) : SettingsItem(emptySetting, titleId, "", 0, "") {
|
||||
override val type = TYPE_FX_BUTTON
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.features.settings.model.view
|
||||
|
||||
class FxPresetSetting(
|
||||
titleString: String,
|
||||
descriptionString: String = "",
|
||||
val deletable: Boolean = false,
|
||||
val onApply: () -> Unit,
|
||||
val onDelete: () -> Unit = {}
|
||||
) : SettingsItem(emptySetting, 0, titleString, 0, descriptionString) {
|
||||
override val type = TYPE_FX_PRESET
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// 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 org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
|
||||
class FxShaderCardSetting(
|
||||
titleString: String,
|
||||
descriptionString: String,
|
||||
val index: Int,
|
||||
val expanded: Boolean,
|
||||
val uniforms: List<NativePostProcessing.Uniform>,
|
||||
val onToggle: () -> Unit,
|
||||
val onRemove: () -> Unit,
|
||||
val onReset: () -> Unit
|
||||
) : SettingsItem(emptySetting, 0, titleString, 0, descriptionString) {
|
||||
override val type = TYPE_FX_SHADER
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// 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 FxToolbarSetting(
|
||||
@StringRes val addLabelId: Int,
|
||||
val listOpen: Boolean,
|
||||
val presetLabel: String,
|
||||
val hasEffects: Boolean,
|
||||
val createPreset: StringInputSetting,
|
||||
val onAdd: () -> Unit,
|
||||
val onPresets: () -> Unit,
|
||||
val onRemoveAll: () -> Unit
|
||||
) : SettingsItem(emptySetting, 0, "", 0, "") {
|
||||
override val type = TYPE_FX_TOOLBAR
|
||||
}
|
||||
+4
@@ -144,6 +144,10 @@ abstract class SettingsItem(
|
||||
const val TYPE_LAUNCHABLE = 13
|
||||
const val TYPE_PATH = 14
|
||||
const val TYPE_GPU_UNSWIZZLE = 15
|
||||
const val TYPE_FX_TOOLBAR = 16
|
||||
const val TYPE_FX_PRESET = 17
|
||||
const val TYPE_FX_SHADER = 18
|
||||
const val TYPE_FX_BUTTON = 19
|
||||
|
||||
const val FASTMEM_COMBINED = "fastmem_combined"
|
||||
const val GPU_UNSWIZZLE_COMBINED = "gpu_unswizzle_combined"
|
||||
|
||||
+32
@@ -23,6 +23,10 @@ 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.ListItemSettingFxButtonBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingFxPresetBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingFxShaderBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingFxToolbarBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingInputBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingsHeaderBinding
|
||||
@@ -106,6 +110,34 @@ class SettingsAdapter(
|
||||
GpuUnswizzleViewHolder(ListItemSettingBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_FX_TOOLBAR -> {
|
||||
FxToolbarViewHolder(
|
||||
ListItemSettingFxToolbarBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_FX_PRESET -> {
|
||||
FxPresetViewHolder(
|
||||
ListItemSettingFxPresetBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_FX_SHADER -> {
|
||||
FxShaderCardViewHolder(
|
||||
ListItemSettingFxShaderBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
SettingsItem.TYPE_FX_BUTTON -> {
|
||||
FxButtonViewHolder(
|
||||
ListItemSettingFxButtonBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
|
||||
}
|
||||
|
||||
+214
@@ -18,6 +18,7 @@ import org.yuzu.yuzu_emu.features.input.model.NpadStyleIndex
|
||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.AbstractIntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.FxPresetNameSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
|
||||
@@ -30,6 +31,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.*
|
||||
import org.yuzu.yuzu_emu.utils.InputHandler
|
||||
import org.yuzu.yuzu_emu.utils.LosslessScalingHelper
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
|
||||
import org.yuzu.yuzu_emu.utils.FullscreenHelper
|
||||
import androidx.core.content.edit
|
||||
@@ -44,6 +46,14 @@ class SettingsFragmentPresenter(
|
||||
) {
|
||||
private var settingsList = ArrayList<SettingsItem>()
|
||||
|
||||
private val expandedShaderSlots = mutableSetOf<Int>()
|
||||
|
||||
private var shaderPickerOpen = false
|
||||
|
||||
private var presetPickerOpen = false
|
||||
|
||||
private var postProcessingSynced = false
|
||||
|
||||
private val context get() = YuzuApplication.appContext
|
||||
|
||||
// Extension for altering settings list based on each setting's properties
|
||||
@@ -163,6 +173,7 @@ class SettingsFragmentPresenter(
|
||||
MenuTag.SECTION_SYSTEM -> addSystemSettings(sl)
|
||||
MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl)
|
||||
MenuTag.SECTION_FRAME_GEN -> addFrameGenSettings(sl)
|
||||
MenuTag.SECTION_POST_PROCESSING -> addPostProcessingSettings(sl)
|
||||
MenuTag.SECTION_PERFORMANCE_STATS -> addPerformanceOverlaySettings(sl)
|
||||
MenuTag.SECTION_SOC_OVERLAY -> addSocOverlaySettings(sl)
|
||||
MenuTag.SECTION_INPUT_OVERLAY -> addInputOverlaySettings(sl)
|
||||
@@ -190,6 +201,209 @@ class SettingsFragmentPresenter(
|
||||
}
|
||||
}
|
||||
|
||||
private fun addPostProcessingSettings(sl: ArrayList<SettingsItem>) {
|
||||
if (!postProcessingSynced) {
|
||||
postProcessingSynced = true
|
||||
NativePostProcessing.reload()
|
||||
}
|
||||
|
||||
val usable = NativePostProcessing.catalog().filter { it.valid }
|
||||
|
||||
sl.apply {
|
||||
if (usable.isEmpty()) {
|
||||
add(
|
||||
RunnableSetting(
|
||||
titleId = R.string.post_processing_empty,
|
||||
descriptionString = NativePostProcessing.getShaderDirectory(),
|
||||
isRunnable = false
|
||||
) {}
|
||||
)
|
||||
return@apply
|
||||
}
|
||||
|
||||
val labels = mutableListOf<String>()
|
||||
val summaries = mutableListOf<String>()
|
||||
val files = mutableListOf<String>()
|
||||
val techniques = mutableListOf<String>()
|
||||
for (effect in usable) {
|
||||
for (technique in effect.techniques) {
|
||||
if (effect.techniques.size == 1) {
|
||||
labels.add(effect.label)
|
||||
} else {
|
||||
labels.add(effect.label + " \u00b7 " + technique)
|
||||
}
|
||||
summaries.add(effect.description)
|
||||
files.add(effect.file)
|
||||
techniques.add(technique)
|
||||
}
|
||||
}
|
||||
|
||||
val chain = NativePostProcessing.chain()
|
||||
val active = NativePostProcessing.getActivePreset()
|
||||
|
||||
var addLabel = R.string.post_processing_add
|
||||
if (chain.isNotEmpty()) {
|
||||
addLabel = R.string.post_processing_open_list
|
||||
}
|
||||
if (shaderPickerOpen) {
|
||||
addLabel = R.string.post_processing_close_list
|
||||
}
|
||||
|
||||
var presetLabel = context.getString(R.string.post_processing_presets)
|
||||
if (active.isNotEmpty()) {
|
||||
presetLabel = active
|
||||
}
|
||||
|
||||
val createPreset = StringInputSetting(
|
||||
setting = FxPresetNameSetting { name ->
|
||||
NativePostProcessing.savePreset(name, "")
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
titleId = R.string.post_processing_preset_new,
|
||||
descriptionId = R.string.post_processing_preset_new_description,
|
||||
validator = { it != null && it.isNotBlank() && !it.contains('=') },
|
||||
errorId = R.string.post_processing_preset_name_invalid
|
||||
)
|
||||
|
||||
add(
|
||||
FxToolbarSetting(
|
||||
addLabelId = addLabel,
|
||||
listOpen = shaderPickerOpen,
|
||||
presetLabel = presetLabel,
|
||||
hasEffects = chain.isNotEmpty(),
|
||||
createPreset = createPreset,
|
||||
onAdd = {
|
||||
shaderPickerOpen = !shaderPickerOpen
|
||||
presetPickerOpen = false
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
onPresets = {
|
||||
presetPickerOpen = !presetPickerOpen
|
||||
shaderPickerOpen = false
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
onRemoveAll = {
|
||||
NativePostProcessing.clearChain()
|
||||
NativePostProcessing.clearPreset()
|
||||
NativePostProcessing.store()
|
||||
expandedShaderSlots.clear()
|
||||
shaderPickerOpen = false
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (presetPickerOpen) {
|
||||
add(
|
||||
FxPresetSetting(
|
||||
titleString = context.getString(R.string.post_processing_preset_none),
|
||||
onApply = {
|
||||
NativePostProcessing.clearPreset()
|
||||
presetPickerOpen = false
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
}
|
||||
)
|
||||
)
|
||||
for (preset in NativePostProcessing.presets()) {
|
||||
add(
|
||||
FxPresetSetting(
|
||||
titleString = preset.name,
|
||||
descriptionString = preset.description,
|
||||
deletable = !preset.bundled,
|
||||
onApply = {
|
||||
NativePostProcessing.applyPreset(preset.name)
|
||||
NativePostProcessing.store()
|
||||
presetPickerOpen = false
|
||||
expandedShaderSlots.clear()
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
onDelete = {
|
||||
NativePostProcessing.deletePreset(preset.name)
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (active.isNotEmpty()) {
|
||||
add(
|
||||
FxButtonSetting(titleId = R.string.post_processing_preset_reset) {
|
||||
NativePostProcessing.applyPreset(active)
|
||||
NativePostProcessing.store()
|
||||
expandedShaderSlots.clear()
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
for (index in chain.indices) {
|
||||
val entry = chain[index]
|
||||
val effect = usable.firstOrNull { it.file == entry.file }
|
||||
|
||||
var header = entry.file
|
||||
var summary = ""
|
||||
var uniforms = emptyList<NativePostProcessing.Uniform>()
|
||||
if (effect != null) {
|
||||
header = effect.label
|
||||
if (effect.techniques.size > 1) {
|
||||
header = effect.label + " \u00b7 " + entry.technique
|
||||
}
|
||||
summary = effect.description
|
||||
uniforms = effect.uniforms
|
||||
}
|
||||
|
||||
val isOpen = expandedShaderSlots.contains(index)
|
||||
|
||||
add(
|
||||
FxShaderCardSetting(
|
||||
titleString = header,
|
||||
descriptionString = summary,
|
||||
index = index,
|
||||
expanded = isOpen,
|
||||
uniforms = uniforms,
|
||||
onToggle = {
|
||||
if (isOpen) {
|
||||
expandedShaderSlots.remove(index)
|
||||
} else {
|
||||
expandedShaderSlots.add(index)
|
||||
}
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
onRemove = {
|
||||
NativePostProcessing.remove(index)
|
||||
NativePostProcessing.store()
|
||||
expandedShaderSlots.clear()
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
},
|
||||
onReset = {
|
||||
NativePostProcessing.resetValues(index)
|
||||
NativePostProcessing.store()
|
||||
settingsViewModel.setReloadListAndNotifyDataset(true)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
|
||||
sl.apply {
|
||||
add(
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// 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.ListItemSettingFxButtonBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.FxButtonSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
|
||||
class FxButtonViewHolder(
|
||||
val binding: ListItemSettingFxButtonBinding,
|
||||
adapter: SettingsAdapter
|
||||
) : SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: FxButtonSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as FxButtonSetting
|
||||
|
||||
binding.fxButton.text = item.title
|
||||
binding.fxButton.setOnClickListener { setting.onClick.invoke() }
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean = true
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// 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.ListItemSettingFxPresetBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.FxPresetSetting
|
||||
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 FxPresetViewHolder(
|
||||
val binding: ListItemSettingFxPresetBinding,
|
||||
adapter: SettingsAdapter
|
||||
) : SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: FxPresetSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as FxPresetSetting
|
||||
|
||||
binding.presetName.text = item.title
|
||||
binding.presetDescription.text = item.description
|
||||
binding.presetDescription.setVisible(item.description.isNotEmpty())
|
||||
|
||||
binding.presetRow.setOnClickListener { setting.onApply.invoke() }
|
||||
|
||||
binding.presetDelete.setVisible(setting.deletable)
|
||||
binding.presetDelete.setOnClickListener { setting.onDelete.invoke() }
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean = true
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// 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.LayoutInflater
|
||||
import android.view.View
|
||||
import org.yuzu.yuzu_emu.databinding.ItemSettingFxActionsBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ItemSettingFxSliderBinding
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingFxShaderBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.FxUniformSliderSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.FxShaderCardSetting
|
||||
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.NativePostProcessing
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
|
||||
class FxShaderCardViewHolder(
|
||||
val binding: ListItemSettingFxShaderBinding,
|
||||
adapter: SettingsAdapter
|
||||
) : SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: FxShaderCardSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as FxShaderCardSetting
|
||||
|
||||
binding.shaderTitle.text = item.title
|
||||
binding.shaderSummary.text = item.description
|
||||
binding.shaderSummary.setVisible(item.description.isNotEmpty())
|
||||
|
||||
var rotation = 0f
|
||||
if (setting.expanded) {
|
||||
rotation = 180f
|
||||
}
|
||||
binding.shaderExpand.rotation = rotation
|
||||
|
||||
binding.shaderHeader.setOnClickListener { setting.onToggle.invoke() }
|
||||
binding.shaderRemove.setOnClickListener { setting.onRemove.invoke() }
|
||||
|
||||
binding.shaderBody.removeAllViews()
|
||||
binding.shaderBody.setVisible(setting.expanded)
|
||||
if (!setting.expanded) {
|
||||
return
|
||||
}
|
||||
|
||||
val inflater = LayoutInflater.from(binding.root.context)
|
||||
for (uniform in setting.uniforms) {
|
||||
if (uniform.uiType == NativePostProcessing.UI_HIDDEN) {
|
||||
continue
|
||||
}
|
||||
for (component in 0 until uniform.components) {
|
||||
addSlider(inflater, uniform, component)
|
||||
}
|
||||
}
|
||||
addActions(inflater)
|
||||
}
|
||||
|
||||
private fun addSlider(
|
||||
inflater: LayoutInflater,
|
||||
uniform: NativePostProcessing.Uniform,
|
||||
component: Int
|
||||
) {
|
||||
val row = ItemSettingFxSliderBinding.inflate(inflater, binding.shaderBody, false)
|
||||
val value = FxUniformSliderSetting(setting.index, uniform, component)
|
||||
|
||||
var title = uniform.label
|
||||
if (uniform.components > 1) {
|
||||
title = uniform.label + " [" + component + "]"
|
||||
}
|
||||
row.fxSliderTitle.text = title
|
||||
|
||||
val steps = uniform.steps.toFloat()
|
||||
row.fxSlider.valueFrom = 0f
|
||||
row.fxSlider.valueTo = steps
|
||||
row.fxSlider.stepSize = 1f
|
||||
row.fxSlider.value = value.getInt(false).toFloat().coerceIn(0f, steps)
|
||||
row.fxSliderValue.text = uniform.describe(row.fxSlider.value.toInt())
|
||||
|
||||
row.fxSlider.addOnChangeListener { _, position, fromUser ->
|
||||
if (fromUser) {
|
||||
value.setInt(position.toInt())
|
||||
row.fxSliderValue.text = uniform.describe(position.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
binding.shaderBody.addView(row.root)
|
||||
}
|
||||
|
||||
private fun addActions(inflater: LayoutInflater) {
|
||||
val row = ItemSettingFxActionsBinding.inflate(inflater, binding.shaderBody, false)
|
||||
|
||||
row.fxReset.setOnClickListener { setting.onReset.invoke() }
|
||||
|
||||
binding.shaderBody.addView(row.root)
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean = true
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// 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.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import org.yuzu.yuzu_emu.R
|
||||
import org.yuzu.yuzu_emu.databinding.ListItemSettingFxToolbarBinding
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.FxToolbarSetting
|
||||
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
|
||||
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
|
||||
|
||||
class FxToolbarViewHolder(
|
||||
val binding: ListItemSettingFxToolbarBinding,
|
||||
adapter: SettingsAdapter
|
||||
) : SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var setting: FxToolbarSetting
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
setting = item as FxToolbarSetting
|
||||
|
||||
binding.fxAdd.setText(setting.addLabelId)
|
||||
var addIcon = R.drawable.ic_add
|
||||
if (setting.listOpen) {
|
||||
addIcon = R.drawable.ic_clear
|
||||
}
|
||||
binding.fxAdd.setIconResource(addIcon)
|
||||
binding.fxAdd.setOnClickListener { setting.onAdd.invoke() }
|
||||
|
||||
binding.fxPresets.text = setting.presetLabel
|
||||
binding.fxPresets.setOnClickListener { setting.onPresets.invoke() }
|
||||
|
||||
binding.fxCreatePreset.isEnabled = setting.hasEffects
|
||||
binding.fxCreatePreset.setOnClickListener {
|
||||
adapter.onStringInputClick(setting.createPreset, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
val removeBackground = MaterialColors.getColor(
|
||||
binding.fxRemoveAll,
|
||||
com.google.android.material.R.attr.colorErrorContainer
|
||||
)
|
||||
val removeForeground = MaterialColors.getColor(
|
||||
binding.fxRemoveAll,
|
||||
com.google.android.material.R.attr.colorOnErrorContainer
|
||||
)
|
||||
binding.fxRemoveAll.backgroundTintList = ColorStateList.valueOf(removeBackground)
|
||||
binding.fxRemoveAll.iconTint = ColorStateList.valueOf(removeForeground)
|
||||
binding.fxRemoveAll.isEnabled = setting.hasEffects
|
||||
binding.fxRemoveAll.setOnClickListener { setting.onRemoveAll.invoke() }
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {}
|
||||
|
||||
override fun onLongClick(clicked: View): Boolean = true
|
||||
}
|
||||
@@ -94,6 +94,7 @@ import org.yuzu.yuzu_emu.utils.InputHandler
|
||||
import org.yuzu.yuzu_emu.utils.Log
|
||||
import org.yuzu.yuzu_emu.utils.NativeConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativeFreedrenoConfig
|
||||
import org.yuzu.yuzu_emu.utils.NativePostProcessing
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils
|
||||
import org.yuzu.yuzu_emu.utils.ViewUtils.setVisible
|
||||
import org.yuzu.yuzu_emu.utils.collect
|
||||
@@ -883,6 +884,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
if (shouldUseCustom) {
|
||||
SettingsFile.loadCustomConfig(game!!)
|
||||
}
|
||||
refreshPostProcessing()
|
||||
addQuickSettings()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,6 +1090,34 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private fun withPerGameConfig(create: Boolean, action: () -> Unit) {
|
||||
val target = game
|
||||
var owned = false
|
||||
if (target != null && !NativeConfig.isPerGameConfigLoaded()) {
|
||||
if (create || SettingsFile.getCustomSettingsFile(target).exists()) {
|
||||
SettingsFile.loadCustomConfig(target)
|
||||
owned = true
|
||||
}
|
||||
}
|
||||
action()
|
||||
if (owned) {
|
||||
NativeConfig.unloadPerGameConfig()
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshPostProcessing() = withPerGameConfig(false) {
|
||||
NativePostProcessing.reload()
|
||||
}
|
||||
|
||||
fun persistPostProcessing() = withPerGameConfig(true) {
|
||||
NativePostProcessing.persist()
|
||||
}
|
||||
|
||||
fun editPostProcessing(action: () -> Unit) = withPerGameConfig(true) {
|
||||
action()
|
||||
NativePostProcessing.persist()
|
||||
}
|
||||
|
||||
private fun addQuickSettings() {
|
||||
binding.quickSettingsSheet.apply {
|
||||
val container = binding.quickSettingsSheet.findViewById<ViewGroup>(R.id.quick_settings_container)
|
||||
@@ -1194,6 +1225,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||
R.array.rendererAntiAliasingNames,
|
||||
R.array.rendererAntiAliasingValues
|
||||
)
|
||||
|
||||
quickSettings.addPostProcessing(container) {
|
||||
addQuickSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -383,6 +383,20 @@ class GamePropertiesFragment : Fragment() {
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
SubmenuProperty(
|
||||
R.string.post_processing,
|
||||
R.string.post_processing_per_game_description,
|
||||
R.drawable.ic_post_processing,
|
||||
action = {
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
args.game,
|
||||
Settings.MenuTag.SECTION_POST_PROCESSING
|
||||
)
|
||||
binding.root.findNavController().navigate(action)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if (GpuDriverHelper.isAdrenoGpu()) {
|
||||
add(
|
||||
|
||||
@@ -171,6 +171,20 @@ class HomeSettingsFragment : Fragment() {
|
||||
)
|
||||
)
|
||||
}
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.post_processing,
|
||||
R.string.post_processing_description,
|
||||
R.drawable.ic_post_processing,
|
||||
{
|
||||
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
|
||||
null,
|
||||
Settings.MenuTag.SECTION_POST_PROCESSING
|
||||
)
|
||||
binding.root.findNavController().navigate(action)
|
||||
}
|
||||
)
|
||||
)
|
||||
add(
|
||||
HomeSetting(
|
||||
R.string.lossless_scaling,
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package org.yuzu.yuzu_emu.utils
|
||||
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object NativePostProcessing {
|
||||
const val KIND_BOOL = 0
|
||||
const val KIND_INT = 1
|
||||
const val KIND_FLOAT = 2
|
||||
|
||||
const val UI_HIDDEN = 0
|
||||
const val UI_SLIDER = 1
|
||||
const val UI_DRAG = 2
|
||||
const val UI_COMBO = 3
|
||||
const val UI_RADIO = 4
|
||||
const val UI_CHECKBOX = 5
|
||||
const val UI_COLOR = 6
|
||||
const val UI_INPUT_BOX = 7
|
||||
|
||||
external fun getCatalogJson(): String
|
||||
|
||||
external fun getChainJson(): String
|
||||
|
||||
external fun append(file: String, technique: String)
|
||||
|
||||
external fun replace(index: Int, file: String, technique: String)
|
||||
|
||||
external fun remove(index: Int)
|
||||
|
||||
external fun move(index: Int, delta: Int)
|
||||
|
||||
external fun resetValues(index: Int)
|
||||
|
||||
external fun getValue(index: Int, uniform: String, component: Int): Float
|
||||
|
||||
external fun hasValue(index: Int, uniform: String): Boolean
|
||||
|
||||
external fun setValue(index: Int, uniform: String, component: Int, value: Float)
|
||||
|
||||
external fun store()
|
||||
|
||||
external fun reload()
|
||||
|
||||
external fun clearChain()
|
||||
|
||||
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() {
|
||||
val perGame = NativeConfig.isPerGameConfigLoaded()
|
||||
store()
|
||||
if (perGame) {
|
||||
NativeConfig.savePerGameConfig()
|
||||
} else {
|
||||
NativeConfig.saveGlobalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
external fun getShaderDirectory(): String
|
||||
|
||||
data class Uniform(
|
||||
val name: String,
|
||||
val label: String,
|
||||
val tooltip: String,
|
||||
val category: String,
|
||||
val kind: Int,
|
||||
val uiType: Int,
|
||||
val components: Int,
|
||||
val min: Float,
|
||||
val max: Float,
|
||||
val step: Float,
|
||||
val items: List<String>,
|
||||
val defaults: List<Float>
|
||||
) {
|
||||
val steps: Int
|
||||
get() {
|
||||
val span = max - min
|
||||
if (step <= 0f) {
|
||||
return 1
|
||||
}
|
||||
val count = Math.round(span / step)
|
||||
if (count < 1) {
|
||||
return 1
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
fun describe(position: Int): String {
|
||||
val value = min + position * step
|
||||
if (kind == NativePostProcessing.KIND_FLOAT) {
|
||||
return String.format("%.3f", value)
|
||||
}
|
||||
return Math.round(value).toString()
|
||||
}
|
||||
|
||||
fun defaultAt(component: Int): Float {
|
||||
if (component < defaults.size) {
|
||||
return defaults[component]
|
||||
}
|
||||
return 0f
|
||||
}
|
||||
}
|
||||
|
||||
data class Effect(
|
||||
val file: String,
|
||||
val name: String,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val error: String,
|
||||
val techniques: List<String>,
|
||||
val uniforms: List<Uniform>
|
||||
) {
|
||||
val valid: Boolean
|
||||
get() = error.isEmpty() && techniques.isNotEmpty()
|
||||
}
|
||||
|
||||
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())
|
||||
for (i in 0 until array.length()) {
|
||||
val obj = array.getJSONObject(i)
|
||||
out.add(
|
||||
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()
|
||||
)
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fun chain(): List<ChainEntry> {
|
||||
val out = mutableListOf<ChainEntry>()
|
||||
val array = JSONArray(getChainJson())
|
||||
for (i in 0 until array.length()) {
|
||||
val obj = array.getJSONObject(i)
|
||||
out.add(ChainEntry(obj.optString("file"), obj.optString("technique")))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
fun findEffect(file: String): Effect? = catalog().firstOrNull { it.file == file }
|
||||
|
||||
private fun JSONArray?.toStringList(): List<String> {
|
||||
if (this == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val out = mutableListOf<String>()
|
||||
for (i in 0 until length()) {
|
||||
out.add(optString(i))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun JSONArray?.toFloatList(): List<Float> {
|
||||
if (this == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val out = mutableListOf<Float>()
|
||||
for (i in 0 until length()) {
|
||||
out.add(optDouble(i, 0.0).toFloat())
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun JSONArray?.toUniformList(): List<Uniform> {
|
||||
if (this == null) {
|
||||
return emptyList()
|
||||
}
|
||||
val out = mutableListOf<Uniform>()
|
||||
for (i in 0 until length()) {
|
||||
val obj: JSONObject = optJSONObject(i) ?: continue
|
||||
out.add(
|
||||
Uniform(
|
||||
name = obj.optString("name"),
|
||||
label = obj.optString("label"),
|
||||
tooltip = obj.optString("tooltip"),
|
||||
category = obj.optString("category"),
|
||||
kind = obj.optInt("kind", KIND_FLOAT),
|
||||
uiType = obj.optInt("uiType", UI_HIDDEN),
|
||||
components = obj.optInt("components", 1),
|
||||
min = obj.optDouble("min", 0.0).toFloat(),
|
||||
max = obj.optDouble("max", 1.0).toFloat(),
|
||||
step = obj.optDouble("step", 0.01).toFloat(),
|
||||
items = obj.optJSONArray("items").toStringList(),
|
||||
defaults = obj.optJSONArray("defaults").toFloatList()
|
||||
)
|
||||
)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ add_library(yuzu-android SHARED
|
||||
android_config.cpp
|
||||
android_config.h
|
||||
native_input.cpp
|
||||
native_post_processing.cpp
|
||||
)
|
||||
|
||||
set_property(TARGET yuzu-android PROPERTY IMPORTED_LOCATION ${FFmpeg_LIBRARY_DIR})
|
||||
|
||||
@@ -15,10 +15,22 @@
|
||||
#include "frontend_common/config.h"
|
||||
#include "frontend_common/settings_generator.h"
|
||||
#include "native.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "video_core/post_processing/fx_chain.h"
|
||||
#endif
|
||||
|
||||
std::unique_ptr<AndroidConfig> global_config;
|
||||
std::unique_ptr<AndroidConfig> per_game_config;
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
static void ResetFxChainToGlobal() {
|
||||
VideoCore::UseGlobalFxSettings();
|
||||
VideoCore::FxChain::Instance().LoadFromSettings();
|
||||
}
|
||||
#else
|
||||
static void ResetFxChainToGlobal() {}
|
||||
#endif
|
||||
|
||||
template <typename T>
|
||||
Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
|
||||
auto key = Common::Android::GetJString(env, jkey);
|
||||
@@ -39,6 +51,7 @@ extern "C" {
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config = std::make_unique<AndroidConfig>();
|
||||
FrontendCommon::GenerateSettings();
|
||||
ResetFxChainToGlobal();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
@@ -47,6 +60,7 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env,
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
global_config->AndroidConfig::ReloadAllValues();
|
||||
ResetFxChainToGlobal();
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <jni.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "common/android/android_common.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "android_config.h"
|
||||
#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"
|
||||
|
||||
extern std::unique_ptr<AndroidConfig> per_game_config;
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
bool EditingPerGame() {
|
||||
return per_game_config != nullptr;
|
||||
}
|
||||
|
||||
void BeginFxEdit() {
|
||||
if (!EditingPerGame()) {
|
||||
return;
|
||||
}
|
||||
VideoCore::UsePerGameFxSettings();
|
||||
}
|
||||
|
||||
nlohmann::json SerializeUniform(const VideoCore::FxUniformDesc& uniform) {
|
||||
nlohmann::json out;
|
||||
out["name"] = uniform.name;
|
||||
out["label"] = uniform.label;
|
||||
out["tooltip"] = uniform.tooltip;
|
||||
out["category"] = uniform.category;
|
||||
out["kind"] = static_cast<int>(uniform.kind);
|
||||
out["uiType"] = static_cast<int>(uniform.ui_type);
|
||||
out["components"] = uniform.components;
|
||||
out["min"] = uniform.ui_min;
|
||||
out["max"] = uniform.ui_max;
|
||||
out["step"] = uniform.ui_step;
|
||||
out["items"] = uniform.items;
|
||||
|
||||
nlohmann::json defaults = nlohmann::json::array();
|
||||
for (u32 i = 0; i < uniform.components; ++i) {
|
||||
defaults.push_back(uniform.default_value[i]);
|
||||
}
|
||||
out["defaults"] = defaults;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<f32, 4> DefaultValueOf(size_t index, const std::string& uniform) {
|
||||
const auto entries = VideoCore::FxChain::Instance().Entries();
|
||||
if (index >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
const VideoCore::FxEffectDesc* effect = VideoCore::FindFxEffect(entries[index].file);
|
||||
if (effect == nullptr) {
|
||||
return {};
|
||||
}
|
||||
const VideoCore::FxUniformDesc* desc = VideoCore::FindFxUniform(*effect, uniform);
|
||||
if (desc == nullptr) {
|
||||
return {};
|
||||
}
|
||||
return desc->default_value;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getCatalogJson(JNIEnv* env,
|
||||
jobject obj) {
|
||||
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;
|
||||
|
||||
nlohmann::json uniforms = nlohmann::json::array();
|
||||
for (const auto& uniform : effect.uniforms) {
|
||||
uniforms.push_back(SerializeUniform(uniform));
|
||||
}
|
||||
entry["uniforms"] = uniforms;
|
||||
|
||||
out.push_back(entry);
|
||||
}
|
||||
#endif
|
||||
|
||||
return Common::Android::ToJString(env, out.dump());
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getChainJson(JNIEnv* env, jobject obj) {
|
||||
nlohmann::json out = nlohmann::json::array();
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
for (const auto& entry : VideoCore::FxChain::Instance().Entries()) {
|
||||
nlohmann::json item;
|
||||
item["file"] = entry.file;
|
||||
item["technique"] = entry.technique;
|
||||
|
||||
nlohmann::json values = nlohmann::json::object();
|
||||
for (const auto& [name, value] : entry.values) {
|
||||
values[name] = {value[0], value[1], value[2], value[3]};
|
||||
}
|
||||
item["values"] = values;
|
||||
|
||||
out.push_back(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
return Common::Android::ToJString(env, out.dump());
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_append(JNIEnv* env, jobject obj,
|
||||
jstring jfile, jstring jtechnique) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().Append(Common::Android::GetJString(env, jfile),
|
||||
Common::Android::GetJString(env, jtechnique));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_replace(JNIEnv* env, jobject obj,
|
||||
jint index, jstring jfile,
|
||||
jstring jtechnique) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().Replace(static_cast<size_t>(index),
|
||||
Common::Android::GetJString(env, jfile),
|
||||
Common::Android::GetJString(env, jtechnique));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_remove(JNIEnv* env, jobject obj,
|
||||
jint index) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().Remove(static_cast<size_t>(index));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_move(JNIEnv* env, jobject obj, jint index,
|
||||
jint delta) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().Move(static_cast<size_t>(index), delta);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_resetValues(JNIEnv* env, jobject obj,
|
||||
jint index) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().ResetValues(static_cast<size_t>(index));
|
||||
#endif
|
||||
}
|
||||
|
||||
jfloat Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getValue(JNIEnv* env, jobject obj,
|
||||
jint index, jstring juniform,
|
||||
jint component) {
|
||||
#ifdef HAS_RESHADE
|
||||
if (component < 0 || component >= 4) {
|
||||
return 0.0f;
|
||||
}
|
||||
const auto value = VideoCore::FxChain::Instance().GetValue(
|
||||
static_cast<size_t>(index), Common::Android::GetJString(env, juniform));
|
||||
return value[static_cast<size_t>(component)];
|
||||
#else
|
||||
return 0.0f;
|
||||
#endif
|
||||
}
|
||||
|
||||
jboolean Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_hasValue(JNIEnv* env, jobject obj,
|
||||
jint index,
|
||||
jstring juniform) {
|
||||
#ifdef HAS_RESHADE
|
||||
return static_cast<jboolean>(VideoCore::FxChain::Instance().HasValue(
|
||||
static_cast<size_t>(index), Common::Android::GetJString(env, juniform)));
|
||||
#else
|
||||
return static_cast<jboolean>(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_setValue(JNIEnv* env, jobject obj,
|
||||
jint index, jstring juniform,
|
||||
jint component, jfloat value) {
|
||||
#ifdef HAS_RESHADE
|
||||
if (component < 0 || component >= 4) {
|
||||
return;
|
||||
}
|
||||
const std::string uniform = Common::Android::GetJString(env, juniform);
|
||||
auto& chain = VideoCore::FxChain::Instance();
|
||||
const auto slot = static_cast<size_t>(index);
|
||||
|
||||
auto current = chain.GetValue(slot, uniform);
|
||||
if (!chain.HasValue(slot, uniform)) {
|
||||
current = DefaultValueOf(slot, uniform);
|
||||
}
|
||||
|
||||
current[static_cast<size_t>(component)] = value;
|
||||
chain.SetValue(slot, uniform, current);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_store(JNIEnv* env, jobject obj) {
|
||||
#ifdef HAS_RESHADE
|
||||
BeginFxEdit();
|
||||
VideoCore::FxChain::Instance().StoreToSettings();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_reload(JNIEnv* env, jobject obj) {
|
||||
#ifdef HAS_RESHADE
|
||||
if (!EditingPerGame()) {
|
||||
VideoCore::UseGlobalFxSettings();
|
||||
}
|
||||
VideoCore::FxChain::Instance().LoadFromSettings();
|
||||
#endif
|
||||
}
|
||||
|
||||
void Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_clearChain(JNIEnv* env, jobject obj) {
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
jstring Java_org_yuzu_yuzu_1emu_utils_NativePostProcessing_getShaderDirectory(JNIEnv* env,
|
||||
jobject obj) {
|
||||
#ifdef HAS_RESHADE
|
||||
return Common::Android::ToJString(env, VideoCore::GetFxRootDirectory().string());
|
||||
#else
|
||||
return Common::Android::ToJString(env, "");
|
||||
#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
|
||||
BeginFxEdit();
|
||||
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
|
||||
BeginFxEdit();
|
||||
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
|
||||
BeginFxEdit();
|
||||
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
|
||||
BeginFxEdit();
|
||||
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
|
||||
BeginFxEdit();
|
||||
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,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M8.5,3 L19.4,3 Q21,3 21,4.6 L21,16.5 L19.4,16.5 L19.4,4.6 L8.5,4.6 Z M5,7 L15,7 Q17,7 17,9 L17,19 Q17,21 15,21 L5,21 Q3,21 3,19 L3,9 Q3,7 5,7 Z M5.2,8.6 L14.8,8.6 Q15.4,8.6 15.4,9.2 L15.4,18.8 Q15.4,19.4 14.8,19.4 L5.2,19.4 Q4.6,19.4 4.6,18.8 L4.6,9.2 Q4.6,8.6 5.2,8.6 Z M10,10.9 A3.1,3.1 0 0 1 10,17.1 Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="?attr/colorSurfaceVariant" />
|
||||
|
||||
<corners android:radius="16dp" />
|
||||
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?attr/colorOutline" />
|
||||
|
||||
</shape>
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/post_processing_add"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/remove_all_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/post_processing_remove_all"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_delete"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/add_choices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -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,82 @@
|
||||
<?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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shader_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<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/shader_title"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/shader_summary"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shader_remove"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:contentDescription="@string/post_processing_remove"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shader_expand"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:src="@drawable/ic_dropdown_arrow"
|
||||
android:contentDescription=""
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shader_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?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:orientation="horizontal"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_reset"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@string/post_processing_reset" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?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:orientation="vertical"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/fx_slider_title"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/fx_slider_value"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.slider.Slider
|
||||
android:id="@+id/fx_slider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stepSize="1"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="100" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?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:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_button"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/preset_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<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_name"
|
||||
style="@style/TextAppearance.Material3.TitleSmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/preset_description"
|
||||
style="@style/TextAppearance.Material3.BodySmall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/preset_delete"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/post_processing_preset_delete"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:visibility="gone"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,82 @@
|
||||
<?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:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shader_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="14dp"
|
||||
android:paddingBottom="14dp">
|
||||
|
||||
<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/shader_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/shader_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>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shader_remove"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/post_processing_remove"
|
||||
android:focusable="true"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_delete"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/shader_expand"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:contentDescription=""
|
||||
android:src="@drawable/ic_dropdown_arrow"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/shader_body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?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:baselineAligned="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_add"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/post_processing_add"
|
||||
app:icon="@drawable/ic_add"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_presets"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1.4"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:minHeight="48dp"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:text="@string/post_processing_presets"
|
||||
app:icon="@drawable/ic_dropdown_arrow"
|
||||
app:iconGravity="textEnd"
|
||||
app:iconPadding="6dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_create_preset"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/post_processing_preset_new"
|
||||
app:icon="@drawable/ic_add" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fx_remove_all"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/post_processing_remove_all"
|
||||
app:icon="@drawable/ic_delete" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -298,6 +298,25 @@
|
||||
<string name="gpu_driver_fetcher">GPU driver fetcher</string>
|
||||
<string name="gpu_driver_manager">GPU driver manager</string>
|
||||
<string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
|
||||
<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_add">Add effect</string>
|
||||
<string name="post_processing_remove">Remove</string>
|
||||
<string name="post_processing_open_list">Open list</string>
|
||||
<string name="post_processing_close_list">Close list</string>
|
||||
<string name="post_processing_remove_all">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">Values changed from the original preset.</string>
|
||||
<string name="post_processing_presets">Presets</string>
|
||||
<string name="post_processing_preset_new">New preset</string>
|
||||
<string name="post_processing_preset_new_description">Saves the effects you have loaded, with their current values, as a preset you can pick later.</string>
|
||||
<string name="post_processing_preset_name_invalid">Give the preset a name without an equals sign.</string>
|
||||
<string name="post_processing_preset_none">No preset</string>
|
||||
<string name="post_processing_preset_delete">Delete preset</string>
|
||||
<string name="post_processing_preset_reset">Reset preset values</string>
|
||||
<string name="post_processing_reset">Reset to default</string>
|
||||
<string name="post_processing_empty">No effects found. Place .fx files in this folder:</string>
|
||||
<string name="frame_gen">Frame generation</string>
|
||||
<string name="frame_gen_per_game_description">Configure frame generation for this game</string>
|
||||
<string name="frame_gen_description">Insert interpolated frames between rendered ones using Lossless Scaling. Forces FIFO presentation while enabled.</string>
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#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"
|
||||
#define SHADER_DIR "shader"
|
||||
|
||||
@@ -160,6 +160,8 @@ 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);
|
||||
GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR);
|
||||
|
||||
@@ -26,6 +26,8 @@ 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.
|
||||
SDMCDir, // Where the emulated SDMC is stored.
|
||||
|
||||
@@ -388,6 +388,30 @@ struct Values {
|
||||
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};
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
|
||||
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
|
||||
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
|
||||
"can produce a more stable picture in lower resolutions."));
|
||||
INSERT(Settings, post_shader_chain, QString(), QString());
|
||||
INSERT(Settings, fullscreen_mode, tr("Fullscreen Mode:"),
|
||||
tr("The method used to render the window in fullscreen.\nBorderless offers the best "
|
||||
"compatibility with the on-screen keyboard that some games request for "
|
||||
|
||||
@@ -302,6 +302,50 @@ if (ENABLE_LSFG)
|
||||
target_compile_definitions(video_core PUBLIC HAS_LSFG)
|
||||
endif()
|
||||
|
||||
if (ENABLE_RESHADE)
|
||||
set(BUNDLED_FX_DIR ${CMAKE_SOURCE_DIR}/dist/post_shaders)
|
||||
set(BUNDLED_FX_HEADER ${CMAKE_CURRENT_BINARY_DIR}/bundled_fx_effects.h)
|
||||
file(GLOB BUNDLED_FX_FILES ${BUNDLED_FX_DIR}/*.fx)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${BUNDLED_FX_HEADER}
|
||||
COMMAND ${CMAKE_COMMAND} -P
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/post_processing/GenerateBundledEffects.cmake
|
||||
${BUNDLED_FX_DIR} ${BUNDLED_FX_HEADER}
|
||||
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
|
||||
post_processing/fx_compile.cpp
|
||||
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)
|
||||
target_compile_definitions(video_core PUBLIC HAS_RESHADE)
|
||||
endif()
|
||||
|
||||
if (ENABLE_OPENGL)
|
||||
target_sources(video_core PRIVATE
|
||||
renderer_opengl/present/filters.cpp
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
set(EFFECT_DIR ${CMAKE_ARGV3})
|
||||
set(HEADER_FILE ${CMAKE_ARGV4})
|
||||
|
||||
file(GLOB EFFECT_FILES ${EFFECT_DIR}/*.fx)
|
||||
list(SORT EFFECT_FILES)
|
||||
|
||||
set(ENTRIES "")
|
||||
foreach(EFFECT_FILE IN LISTS EFFECT_FILES)
|
||||
get_filename_component(EFFECT_NAME ${EFFECT_FILE} NAME)
|
||||
file(READ ${EFFECT_FILE} EFFECT_BODY)
|
||||
|
||||
string(REGEX REPLACE ";" "{{SEMICOLON}}" EFFECT_BODY "${EFFECT_BODY}")
|
||||
string(REGEX REPLACE "\n" ";" EFFECT_BODY "${EFFECT_BODY}")
|
||||
|
||||
set(EFFECT_TEXT "")
|
||||
foreach(LINE IN LISTS EFFECT_BODY)
|
||||
string(CONCAT EFFECT_TEXT "${EFFECT_TEXT}" " R\"(${LINE}\n)\"\n")
|
||||
endforeach()
|
||||
string(REGEX REPLACE "{{SEMICOLON}}" ";" EFFECT_TEXT "${EFFECT_TEXT}")
|
||||
|
||||
string(CONCAT ENTRIES "${ENTRIES}"
|
||||
" {\n \"${EFFECT_NAME}\",\n${EFFECT_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 BundledFxEffect {
|
||||
std::string_view name;
|
||||
std::string_view source;
|
||||
};
|
||||
|
||||
constexpr BundledFxEffect BUNDLED_FX_EFFECTS[]{
|
||||
${ENTRIES}};
|
||||
|
||||
} // namespace VideoCore
|
||||
")
|
||||
@@ -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
|
||||
")
|
||||
@@ -0,0 +1,321 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "common/settings.h"
|
||||
#include "video_core/post_processing/fx_chain.h"
|
||||
#include "video_core/post_processing/fx_effect.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string_view> Split(std::string_view value, char separator) {
|
||||
std::vector<std::string_view> out;
|
||||
size_t start = 0;
|
||||
while (start <= value.size()) {
|
||||
size_t end = value.find(separator, start);
|
||||
if (end == std::string_view::npos) {
|
||||
end = value.size();
|
||||
}
|
||||
out.push_back(value.substr(start, end - start));
|
||||
start = end + 1;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool IsSerializableName(std::string_view value) {
|
||||
return value.find_first_of(";|,=") == std::string_view::npos;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
std::vector<FxChainEntry> ParseFxChain(std::string_view value) {
|
||||
std::vector<FxChainEntry> parsed;
|
||||
|
||||
for (const std::string_view record : Split(value, ';')) {
|
||||
if (record.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto fields = Split(record, '|');
|
||||
if (fields.size() < 2 || fields[0].empty() || fields[1].empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FxChainEntry entry;
|
||||
entry.file = std::string(fields[0]);
|
||||
entry.technique = std::string(fields[1]);
|
||||
|
||||
if (fields.size() >= 3) {
|
||||
for (const std::string_view assignment : Split(fields[2], ',')) {
|
||||
const size_t equals = assignment.find('=');
|
||||
if (equals == std::string_view::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string name(assignment.substr(0, equals));
|
||||
if (name.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::array<f32, 4> components{};
|
||||
size_t index = 0;
|
||||
for (const std::string_view piece : Split(assignment.substr(equals + 1), '/')) {
|
||||
if (index >= components.size()) {
|
||||
break;
|
||||
}
|
||||
const std::string text(piece);
|
||||
if (!text.empty()) {
|
||||
components[index] = std::strtof(text.c_str(), nullptr);
|
||||
}
|
||||
++index;
|
||||
}
|
||||
entry.values.emplace(name, components);
|
||||
}
|
||||
}
|
||||
|
||||
parsed.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
std::string SerializeFxChain(std::span<const FxChainEntry> entries) {
|
||||
std::string out;
|
||||
|
||||
for (const auto& entry : entries) {
|
||||
if (!IsSerializableName(entry.file) || !IsSerializableName(entry.technique)) {
|
||||
continue;
|
||||
}
|
||||
if (!out.empty()) {
|
||||
out += ';';
|
||||
}
|
||||
out += fmt::format("{}|{}|", entry.file, entry.technique);
|
||||
|
||||
bool first = true;
|
||||
for (const auto& [name, value] : entry.values) {
|
||||
if (!IsSerializableName(name)) {
|
||||
continue;
|
||||
}
|
||||
if (!first) {
|
||||
out += ',';
|
||||
}
|
||||
first = false;
|
||||
out += name;
|
||||
out += '=';
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (i > 0) {
|
||||
out += '/';
|
||||
}
|
||||
out += fmt::format("{}", value[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void UseGlobalFxSettings() {
|
||||
Settings::values.post_shader_chain.SetGlobal(true);
|
||||
Settings::values.post_shader_preset.SetGlobal(true);
|
||||
Settings::values.post_shader_enabled.SetGlobal(true);
|
||||
}
|
||||
|
||||
void UsePerGameFxSettings() {
|
||||
const std::string chain = Settings::values.post_shader_chain.GetValue();
|
||||
const std::string preset = Settings::values.post_shader_preset.GetValue();
|
||||
const bool enabled = Settings::values.post_shader_enabled.GetValue();
|
||||
|
||||
Settings::values.post_shader_chain.SetGlobal(false);
|
||||
Settings::values.post_shader_preset.SetGlobal(false);
|
||||
Settings::values.post_shader_enabled.SetGlobal(false);
|
||||
|
||||
Settings::values.post_shader_chain.SetValue(chain);
|
||||
Settings::values.post_shader_preset.SetValue(preset);
|
||||
Settings::values.post_shader_enabled.SetValue(enabled);
|
||||
}
|
||||
|
||||
FxChain& FxChain::Instance() {
|
||||
static FxChain instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
FxChainSnapshot FxChain::Snapshot() const {
|
||||
std::scoped_lock lock{mutex};
|
||||
return FxChainSnapshot{
|
||||
.entries = entries,
|
||||
.generation = generation.load(std::memory_order_relaxed),
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<FxChainEntry> FxChain::Entries() const {
|
||||
std::scoped_lock lock{mutex};
|
||||
return entries;
|
||||
}
|
||||
|
||||
size_t FxChain::Size() const {
|
||||
std::scoped_lock lock{mutex};
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
void FxChain::Append(std::string_view file, std::string_view technique) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
FxChainEntry entry;
|
||||
entry.file = std::string(file);
|
||||
entry.technique = std::string(technique);
|
||||
entries.push_back(std::move(entry));
|
||||
}
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void FxChain::Replace(size_t index, std::string_view file, std::string_view technique) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return;
|
||||
}
|
||||
if (entries[index].file == file && entries[index].technique == technique) {
|
||||
return;
|
||||
}
|
||||
FxChainEntry entry;
|
||||
entry.file = std::string(file);
|
||||
entry.technique = std::string(technique);
|
||||
entries[index] = std::move(entry);
|
||||
}
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void FxChain::Remove(size_t index) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return;
|
||||
}
|
||||
entries.erase(entries.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void FxChain::Move(size_t index, int delta) {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return;
|
||||
}
|
||||
const std::ptrdiff_t target = static_cast<std::ptrdiff_t>(index) + delta;
|
||||
if (target < 0 || target >= static_cast<std::ptrdiff_t>(entries.size())) {
|
||||
return;
|
||||
}
|
||||
std::swap(entries[index], entries[static_cast<size_t>(target)]);
|
||||
}
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void FxChain::Clear() {
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
entries.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()) {
|
||||
return;
|
||||
}
|
||||
entries[index].values[std::string(uniform)] = value;
|
||||
}
|
||||
|
||||
std::array<f32, 4> FxChain::GetValue(size_t index, std::string_view uniform) const {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
const auto it = entries[index].values.find(std::string(uniform));
|
||||
if (it == entries[index].values.end()) {
|
||||
return {};
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::map<std::string, std::array<f32, 4>> FxChain::EntryValues(size_t index) const {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return {};
|
||||
}
|
||||
return entries[index].values;
|
||||
}
|
||||
|
||||
bool FxChain::HasValue(size_t index, std::string_view uniform) const {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return false;
|
||||
}
|
||||
return entries[index].values.contains(std::string(uniform));
|
||||
}
|
||||
|
||||
void FxChain::ResetValues(size_t index) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (index >= entries.size()) {
|
||||
return;
|
||||
}
|
||||
entries[index].values.clear();
|
||||
}
|
||||
|
||||
void FxChain::LoadFromSettings() {
|
||||
auto parsed = ParseFxChain(Settings::values.post_shader_chain.GetValue());
|
||||
|
||||
std::scoped_lock lock{mutex};
|
||||
if (entries == parsed) {
|
||||
return;
|
||||
}
|
||||
entries = std::move(parsed);
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void FxChain::StoreToSettings() const {
|
||||
std::string serialized;
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
serialized = SerializeFxChain(entries);
|
||||
}
|
||||
Settings::values.post_shader_chain.SetValue(serialized);
|
||||
}
|
||||
|
||||
void FxChain::DropUnknownEntries() {
|
||||
bool changed = false;
|
||||
{
|
||||
std::scoped_lock lock{mutex};
|
||||
const auto removed = std::remove_if(entries.begin(), entries.end(), [](const FxChainEntry& entry) {
|
||||
const FxEffectDesc* effect = FindFxEffect(entry.file);
|
||||
if (effect == nullptr || !effect->Valid()) {
|
||||
return true;
|
||||
}
|
||||
return std::find(effect->techniques.begin(), effect->techniques.end(),
|
||||
entry.technique) == effect->techniques.end();
|
||||
});
|
||||
if (removed != entries.end()) {
|
||||
entries.erase(removed, entries.end());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
generation.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,86 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
struct FxChainEntry {
|
||||
std::string file;
|
||||
std::string technique;
|
||||
std::map<std::string, std::array<f32, 4>> values;
|
||||
|
||||
bool operator==(const FxChainEntry&) const = default;
|
||||
};
|
||||
|
||||
struct FxChainSnapshot {
|
||||
std::vector<FxChainEntry> entries;
|
||||
u64 generation{};
|
||||
};
|
||||
|
||||
std::vector<FxChainEntry> ParseFxChain(std::string_view value);
|
||||
|
||||
std::string SerializeFxChain(std::span<const FxChainEntry> entries);
|
||||
|
||||
void UseGlobalFxSettings();
|
||||
|
||||
void UsePerGameFxSettings();
|
||||
|
||||
class FxChain {
|
||||
public:
|
||||
static FxChain& Instance();
|
||||
|
||||
FxChainSnapshot Snapshot() const;
|
||||
|
||||
std::vector<FxChainEntry> Entries() const;
|
||||
|
||||
size_t Size() const;
|
||||
|
||||
void Append(std::string_view file, std::string_view technique);
|
||||
|
||||
void Replace(size_t index, std::string_view file, std::string_view technique);
|
||||
|
||||
void Remove(size_t index);
|
||||
|
||||
void Move(size_t index, int delta);
|
||||
|
||||
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;
|
||||
|
||||
std::map<std::string, std::array<f32, 4>> EntryValues(size_t index) const;
|
||||
|
||||
bool HasValue(size_t index, std::string_view uniform) const;
|
||||
|
||||
void ResetValues(size_t index);
|
||||
|
||||
void LoadFromSettings();
|
||||
|
||||
void StoreToSettings() const;
|
||||
|
||||
void DropUnknownEntries();
|
||||
|
||||
private:
|
||||
FxChain() = default;
|
||||
|
||||
mutable std::mutex mutex;
|
||||
std::vector<FxChainEntry> entries;
|
||||
std::atomic<u64> generation{1};
|
||||
};
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,105 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
|
||||
#include "effect_codegen.hpp"
|
||||
#include "effect_parser.hpp"
|
||||
#include "effect_preprocessor.hpp"
|
||||
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "video_core/post_processing/fx_compile.h"
|
||||
#include "video_core/post_processing/fx_effect.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
|
||||
u32 color_bit_depth) {
|
||||
FxCompileResult result;
|
||||
|
||||
if (!Common::FS::Exists(path)) {
|
||||
result.error = "Effect file not found: " + Common::FS::PathToUTF8String(path);
|
||||
return result;
|
||||
}
|
||||
|
||||
reshadefx::preprocessor preprocessor;
|
||||
preprocessor.add_macro_definition("__RESHADE__", "60800");
|
||||
preprocessor.add_macro_definition("__RESHADE_PERFORMANCE_MODE__", "1");
|
||||
preprocessor.add_macro_definition("__RENDERER__", "0x20000");
|
||||
preprocessor.add_macro_definition("__VENDOR__", "0");
|
||||
preprocessor.add_macro_definition("__DEVICE__", "0");
|
||||
preprocessor.add_macro_definition("__APPLICATION__", "0");
|
||||
preprocessor.add_macro_definition("BUFFER_WIDTH", std::to_string(width));
|
||||
preprocessor.add_macro_definition("BUFFER_HEIGHT", std::to_string(height));
|
||||
preprocessor.add_macro_definition("BUFFER_RCP_WIDTH", "(1.0 / BUFFER_WIDTH)");
|
||||
preprocessor.add_macro_definition("BUFFER_RCP_HEIGHT", "(1.0 / BUFFER_HEIGHT)");
|
||||
preprocessor.add_macro_definition("BUFFER_COLOR_DEPTH", std::to_string(color_bit_depth));
|
||||
preprocessor.add_macro_definition("BUFFER_COLOR_BIT_DEPTH", std::to_string(color_bit_depth));
|
||||
|
||||
for (const auto& include : GetFxIncludePaths(path)) {
|
||||
preprocessor.add_include_path(include);
|
||||
}
|
||||
|
||||
if (!preprocessor.append_file(path)) {
|
||||
result.error = preprocessor.errors();
|
||||
if (result.error.empty()) {
|
||||
result.error = "Failed to preprocess " + Common::FS::PathToUTF8String(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::unique_ptr<reshadefx::codegen> backend(
|
||||
reshadefx::create_codegen_spirv(true, false, false, false, true));
|
||||
|
||||
reshadefx::parser parser;
|
||||
if (!parser.parse(preprocessor.output(), backend.get())) {
|
||||
result.error = parser.errors();
|
||||
if (result.error.empty()) {
|
||||
result.error = "Failed to parse " + Common::FS::PathToUTF8String(path);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
result.module = backend->module();
|
||||
|
||||
std::set<std::string> wanted;
|
||||
for (const auto& technique : result.module.techniques) {
|
||||
for (const auto& pass : technique.passes) {
|
||||
if (!pass.vs_entry_point.empty()) {
|
||||
wanted.insert(pass.vs_entry_point);
|
||||
}
|
||||
if (!pass.ps_entry_point.empty()) {
|
||||
wanted.insert(pass.ps_entry_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& name : wanted) {
|
||||
std::string binary;
|
||||
std::string assembly;
|
||||
std::string errors;
|
||||
if (!backend->assemble_code_for_entry_point(name, binary, assembly, errors)) {
|
||||
result.error = "Failed to assemble entry point '" + name + "': " + errors;
|
||||
return result;
|
||||
}
|
||||
if (binary.size() % sizeof(u32) != 0) {
|
||||
result.error = "Entry point '" + name + "' produced a malformed SPIR-V module";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<u32> words(binary.size() / sizeof(u32));
|
||||
std::memcpy(words.data(), binary.data(), binary.size());
|
||||
result.entry_points.emplace(name, std::move(words));
|
||||
}
|
||||
|
||||
if (result.entry_points.empty()) {
|
||||
result.error = "Effect declares no usable entry points";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "effect_module.hpp"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
struct FxCompileResult {
|
||||
reshadefx::effect_module module;
|
||||
std::map<std::string, std::vector<u32>> entry_points;
|
||||
std::string error;
|
||||
|
||||
bool Succeeded() const {
|
||||
return error.empty() && !entry_points.empty();
|
||||
}
|
||||
};
|
||||
|
||||
FxCompileResult CompileFxEffect(const std::filesystem::path& path, u32 width, u32 height,
|
||||
u32 color_bit_depth);
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,307 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "bundled_fx_effects.h"
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "common/fs/path_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "video_core/post_processing/fx_compile.h"
|
||||
#include "video_core/post_processing/fx_effect.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr u32 CATALOG_PROBE_WIDTH = 1280;
|
||||
constexpr u32 CATALOG_PROBE_HEIGHT = 720;
|
||||
constexpr u32 CATALOG_PROBE_DEPTH = 8;
|
||||
|
||||
std::vector<FxEffectDesc> catalog;
|
||||
bool catalog_scanned = false;
|
||||
|
||||
const reshadefx::annotation* FindAnnotation(const std::vector<reshadefx::annotation>& annotations,
|
||||
std::string_view name) {
|
||||
const auto it = std::find_if(annotations.begin(), annotations.end(),
|
||||
[&](const reshadefx::annotation& a) { return a.name == name; });
|
||||
if (it == annotations.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*it;
|
||||
}
|
||||
|
||||
std::string AnnotationString(const std::vector<reshadefx::annotation>& annotations,
|
||||
std::string_view name) {
|
||||
const reshadefx::annotation* a = FindAnnotation(annotations, name);
|
||||
if (a == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
return a->value.string_data;
|
||||
}
|
||||
|
||||
bool AnnotationFloat(const std::vector<reshadefx::annotation>& annotations, std::string_view name,
|
||||
f32& out) {
|
||||
const reshadefx::annotation* a = FindAnnotation(annotations, name);
|
||||
if (a == nullptr) {
|
||||
return false;
|
||||
}
|
||||
if (a->type.is_floating_point()) {
|
||||
out = a->value.as_float[0];
|
||||
return true;
|
||||
}
|
||||
if (a->type.is_integral()) {
|
||||
out = static_cast<f32>(a->value.as_int[0]);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FxUiType ParseUiType(std::string_view value) {
|
||||
if (value == "slider") {
|
||||
return FxUiType::Slider;
|
||||
}
|
||||
if (value == "drag") {
|
||||
return FxUiType::Drag;
|
||||
}
|
||||
if (value == "combo") {
|
||||
return FxUiType::Combo;
|
||||
}
|
||||
if (value == "radio") {
|
||||
return FxUiType::Radio;
|
||||
}
|
||||
if (value == "check" || value == "checkbox") {
|
||||
return FxUiType::CheckBox;
|
||||
}
|
||||
if (value == "color") {
|
||||
return FxUiType::Color;
|
||||
}
|
||||
if (value == "input") {
|
||||
return FxUiType::InputBox;
|
||||
}
|
||||
return FxUiType::Hidden;
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitItems(const std::string& items) {
|
||||
std::vector<std::string> out;
|
||||
std::string current;
|
||||
for (const char c : items) {
|
||||
if (c == '\0') {
|
||||
out.push_back(current);
|
||||
current.clear();
|
||||
continue;
|
||||
}
|
||||
current += c;
|
||||
}
|
||||
if (!current.empty()) {
|
||||
out.push_back(current);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
FxUniformDesc DescribeUniform(const reshadefx::uniform& info) {
|
||||
FxUniformDesc desc;
|
||||
desc.name = info.name;
|
||||
desc.components = std::min<u32>(info.type.components(), 4);
|
||||
|
||||
if (info.type.is_boolean()) {
|
||||
desc.kind = FxUniformKind::Boolean;
|
||||
} else if (info.type.is_integral()) {
|
||||
desc.kind = FxUniformKind::Integer;
|
||||
} else {
|
||||
desc.kind = FxUniformKind::Floating;
|
||||
}
|
||||
|
||||
desc.label = AnnotationString(info.annotations, "ui_label");
|
||||
if (desc.label.empty()) {
|
||||
desc.label = info.name;
|
||||
}
|
||||
desc.tooltip = AnnotationString(info.annotations, "ui_tooltip");
|
||||
desc.category = AnnotationString(info.annotations, "ui_category");
|
||||
desc.ui_type = ParseUiType(AnnotationString(info.annotations, "ui_type"));
|
||||
desc.items = SplitItems(AnnotationString(info.annotations, "ui_items"));
|
||||
|
||||
if (desc.kind == FxUniformKind::Boolean) {
|
||||
desc.ui_min = 0.0f;
|
||||
desc.ui_max = 1.0f;
|
||||
desc.ui_step = 1.0f;
|
||||
} else if (desc.kind == FxUniformKind::Integer) {
|
||||
desc.ui_min = 0.0f;
|
||||
desc.ui_max = 100.0f;
|
||||
desc.ui_step = 1.0f;
|
||||
}
|
||||
|
||||
void(AnnotationFloat(info.annotations, "ui_min", desc.ui_min));
|
||||
void(AnnotationFloat(info.annotations, "ui_max", desc.ui_max));
|
||||
void(AnnotationFloat(info.annotations, "ui_step", desc.ui_step));
|
||||
|
||||
if (desc.ui_step <= 0.0f) {
|
||||
desc.ui_step = 0.01f;
|
||||
if (desc.kind != FxUniformKind::Floating) {
|
||||
desc.ui_step = 1.0f;
|
||||
}
|
||||
}
|
||||
if (desc.ui_max < desc.ui_min) {
|
||||
std::swap(desc.ui_min, desc.ui_max);
|
||||
}
|
||||
|
||||
if (info.has_initializer_value) {
|
||||
for (u32 i = 0; i < desc.components; ++i) {
|
||||
if (desc.kind == FxUniformKind::Floating) {
|
||||
desc.default_value[i] = info.initializer_value.as_float[i];
|
||||
} else {
|
||||
desc.default_value[i] = static_cast<f32>(info.initializer_value.as_int[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
FxEffectDesc DescribeEffect(const std::filesystem::path& path, const std::filesystem::path& root) {
|
||||
FxEffectDesc desc;
|
||||
desc.file = Common::FS::PathToUTF8String(std::filesystem::relative(path, root));
|
||||
desc.name = Common::FS::PathToUTF8String(path.stem());
|
||||
|
||||
const auto compiled =
|
||||
CompileFxEffect(path, CATALOG_PROBE_WIDTH, CATALOG_PROBE_HEIGHT, CATALOG_PROBE_DEPTH);
|
||||
if (!compiled.Succeeded()) {
|
||||
desc.error = compiled.error;
|
||||
return desc;
|
||||
}
|
||||
|
||||
for (const auto& technique : compiled.module.techniques) {
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
desc.uniforms.push_back(std::move(uniform_desc));
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
std::filesystem::path GetFxRootDirectory() {
|
||||
return Common::FS::GetEdenPath(Common::FS::EdenPath::PostShaderDir);
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path) {
|
||||
const auto root = GetFxRootDirectory();
|
||||
std::vector<std::filesystem::path> paths;
|
||||
paths.push_back(effect_path.parent_path());
|
||||
paths.push_back(root);
|
||||
paths.push_back(root / "Shaders");
|
||||
|
||||
const auto last = std::unique(paths.begin(), paths.end());
|
||||
paths.erase(last, paths.end());
|
||||
return paths;
|
||||
}
|
||||
|
||||
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
|
||||
std::string_view source) {
|
||||
const auto root = GetFxRootDirectory();
|
||||
const std::filesystem::path name{source};
|
||||
|
||||
const std::array candidates{
|
||||
effect_path.parent_path() / name,
|
||||
root / "Textures" / name,
|
||||
root / name,
|
||||
};
|
||||
|
||||
for (const auto& candidate : candidates) {
|
||||
if (Common::FS::Exists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return std::filesystem::path();
|
||||
}
|
||||
|
||||
void ReloadFxCatalog() {
|
||||
catalog.clear();
|
||||
catalog_scanned = true;
|
||||
|
||||
const auto root = GetFxRootDirectory();
|
||||
if (!Common::FS::Exists(root) && !Common::FS::CreateDirs(root)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& bundled : BUNDLED_FX_EFFECTS) {
|
||||
const auto path = root / bundled.name;
|
||||
if (Common::FS::Exists(path)) {
|
||||
continue;
|
||||
}
|
||||
void(Common::FS::WriteStringToFile(path, Common::FS::FileType::TextFile, bundled.source));
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> effect_files;
|
||||
Common::FS::IterateDirEntriesRecursively(
|
||||
root,
|
||||
[&](const std::filesystem::directory_entry& entry) {
|
||||
if (entry.path().extension() == ".fx") {
|
||||
effect_files.push_back(entry.path());
|
||||
}
|
||||
return true;
|
||||
},
|
||||
Common::FS::DirEntryFilter::File);
|
||||
|
||||
std::sort(effect_files.begin(), effect_files.end());
|
||||
|
||||
for (const auto& file : effect_files) {
|
||||
FxEffectDesc desc = DescribeEffect(file, root);
|
||||
if (!desc.error.empty()) {
|
||||
LOG_WARNING(Render, "Post-processing effect '{}' failed to compile:\n{}", desc.file,
|
||||
desc.error);
|
||||
}
|
||||
catalog.push_back(std::move(desc));
|
||||
}
|
||||
|
||||
const size_t usable = std::count_if(catalog.begin(), catalog.end(),
|
||||
[](const FxEffectDesc& d) { return d.Valid(); });
|
||||
LOG_INFO(Render, "Loaded {} post-processing effects ({} usable)", catalog.size(), usable);
|
||||
}
|
||||
|
||||
const std::vector<FxEffectDesc>& GetFxCatalog() {
|
||||
if (!catalog_scanned) {
|
||||
ReloadFxCatalog();
|
||||
}
|
||||
return catalog;
|
||||
}
|
||||
|
||||
const FxEffectDesc* FindFxEffect(std::string_view file) {
|
||||
const auto& effects = GetFxCatalog();
|
||||
const auto it = std::find_if(effects.begin(), effects.end(),
|
||||
[&](const FxEffectDesc& d) { return d.file == file; });
|
||||
if (it == effects.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*it;
|
||||
}
|
||||
|
||||
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name) {
|
||||
const auto it = std::find_if(effect.uniforms.begin(), effect.uniforms.end(),
|
||||
[&](const FxUniformDesc& u) { return u.name == name; });
|
||||
if (it == effect.uniforms.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*it;
|
||||
}
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace VideoCore {
|
||||
|
||||
enum class FxUniformKind {
|
||||
Boolean,
|
||||
Integer,
|
||||
Floating,
|
||||
};
|
||||
|
||||
enum class FxUiType {
|
||||
Hidden,
|
||||
Slider,
|
||||
Drag,
|
||||
Combo,
|
||||
Radio,
|
||||
CheckBox,
|
||||
Color,
|
||||
InputBox,
|
||||
};
|
||||
|
||||
struct FxUniformDesc {
|
||||
std::string name;
|
||||
std::string label;
|
||||
std::string tooltip;
|
||||
std::string category;
|
||||
FxUniformKind kind{FxUniformKind::Floating};
|
||||
u32 components{1};
|
||||
FxUiType ui_type{FxUiType::Hidden};
|
||||
f32 ui_min{0.0f};
|
||||
f32 ui_max{1.0f};
|
||||
f32 ui_step{0.01f};
|
||||
std::vector<std::string> items;
|
||||
std::array<f32, 4> default_value{};
|
||||
};
|
||||
|
||||
struct FxEffectDesc {
|
||||
std::string file;
|
||||
std::string name;
|
||||
std::string label;
|
||||
std::string description;
|
||||
std::vector<std::string> techniques;
|
||||
std::vector<FxUniformDesc> uniforms;
|
||||
std::string error;
|
||||
|
||||
bool Valid() const {
|
||||
return error.empty() && !techniques.empty();
|
||||
}
|
||||
};
|
||||
|
||||
std::filesystem::path GetFxRootDirectory();
|
||||
|
||||
std::vector<std::filesystem::path> GetFxIncludePaths(const std::filesystem::path& effect_path);
|
||||
|
||||
std::filesystem::path ResolveFxTexturePath(const std::filesystem::path& effect_path,
|
||||
std::string_view source);
|
||||
|
||||
void ReloadFxCatalog();
|
||||
|
||||
const std::vector<FxEffectDesc>& GetFxCatalog();
|
||||
|
||||
const FxEffectDesc* FindFxEffect(std::string_view file);
|
||||
|
||||
const FxUniformDesc* FindFxUniform(const FxEffectDesc& effect, std::string_view name);
|
||||
|
||||
} // namespace VideoCore
|
||||
@@ -0,0 +1,261 @@
|
||||
// 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()) !=
|
||||
SerializeFxChain(ParseFxChain(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
|
||||
@@ -18,6 +18,10 @@
|
||||
#include "video_core/renderer_vulkan/present/sgsr.h"
|
||||
#include "video_core/renderer_vulkan/present/fxaa.h"
|
||||
#include "video_core/renderer_vulkan/present/layer.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "video_core/post_processing/fx_chain.h"
|
||||
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||
#endif
|
||||
#include "video_core/renderer_vulkan/present/present_push_constants.h"
|
||||
#include "video_core/renderer_vulkan/present/smaa.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
@@ -93,6 +97,9 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
|
||||
|
||||
RefreshResources(device, framebuffer);
|
||||
SetAntiAliasPass(device);
|
||||
#ifdef HAS_RESHADE
|
||||
SetPostProcessPass(device);
|
||||
#endif
|
||||
|
||||
// Finish any pending renderpass
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
@@ -115,6 +122,12 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c
|
||||
smaa->Draw(device, scheduler, image_index, &source_image, &source_image_view);
|
||||
}
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
if (post_process.has_value()) {
|
||||
post_process->Draw(device, scheduler, image_index, &source_image, &source_image_view);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height);
|
||||
const VkExtent2D render_extent{
|
||||
.width = scaled_width,
|
||||
@@ -214,6 +227,43 @@ void Layer::SetAntiAliasPass(const Device& device) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
void Layer::SetPostProcessPass(const Device& device) {
|
||||
const VkExtent2D render_area{
|
||||
.width = Settings::values.resolution_info.ScaleUp(raw_width),
|
||||
.height = Settings::values.resolution_info.ScaleUp(raw_height),
|
||||
};
|
||||
|
||||
const u64 generation = VideoCore::FxChain::Instance().Snapshot().generation;
|
||||
const bool enabled = Settings::values.post_shader_enabled.GetValue();
|
||||
|
||||
if (post_process_generation == generation && post_process_enabled == enabled &&
|
||||
post_process_extent.width == render_area.width &&
|
||||
post_process_extent.height == render_area.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const u64 tick : resource_ticks) {
|
||||
scheduler.Wait(tick);
|
||||
}
|
||||
|
||||
post_process_generation = generation;
|
||||
post_process_enabled = enabled;
|
||||
post_process_extent = render_area;
|
||||
post_process.reset();
|
||||
|
||||
if (!enabled || VideoCore::FxChain::Instance().Size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
post_process.emplace(device, memory_allocator, scheduler, image_count, render_area);
|
||||
|
||||
if (post_process->Empty()) {
|
||||
post_process.reset();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void Layer::ReleaseRawImages() {
|
||||
for (const u64 tick : resource_ticks) {
|
||||
scheduler.Wait(tick);
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
#include "video_core/renderer_vulkan/present/fsr.h"
|
||||
#include "video_core/renderer_vulkan/present/sgsr.h"
|
||||
#include "video_core/renderer_vulkan/present/fxaa.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||
#endif
|
||||
#include "video_core/renderer_vulkan/present/smaa.h"
|
||||
|
||||
namespace Layout {
|
||||
@@ -66,6 +69,9 @@ private:
|
||||
|
||||
void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer);
|
||||
void SetAntiAliasPass(const Device& device);
|
||||
#ifdef HAS_RESHADE
|
||||
void SetPostProcessPass(const Device& device);
|
||||
#endif
|
||||
void ReleaseRawImages();
|
||||
|
||||
u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const;
|
||||
@@ -95,6 +101,12 @@ private:
|
||||
Settings::AntiAliasing anti_alias_setting{};
|
||||
std::variant<std::monostate, FXAA, SMAA> anti_alias{};
|
||||
std::variant<std::monostate, SGSR, FSR> sr_filter{};
|
||||
#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{};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,899 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/fs_util.h"
|
||||
#include "common/logging.h"
|
||||
#include "video_core/post_processing/fx_chain.h"
|
||||
#include "video_core/post_processing/fx_compile.h"
|
||||
#include "video_core/post_processing/fx_effect.h"
|
||||
#include "video_core/renderer_vulkan/present/post_process.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
#include "video_core/renderer_vulkan/vk_scheduler.h"
|
||||
#include "video_core/vulkan_common/vulkan_device.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr VkFormat BACKBUFFER_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
constexpr size_t NO_TEXTURE = ~size_t{0};
|
||||
|
||||
VkFormat ToVkFormat(reshadefx::texture_format format) {
|
||||
switch (format) {
|
||||
case reshadefx::texture_format::r8:
|
||||
return VK_FORMAT_R8_UNORM;
|
||||
case reshadefx::texture_format::r16f:
|
||||
return VK_FORMAT_R16_SFLOAT;
|
||||
case reshadefx::texture_format::r32f:
|
||||
return VK_FORMAT_R32_SFLOAT;
|
||||
case reshadefx::texture_format::rg8:
|
||||
return VK_FORMAT_R8G8_UNORM;
|
||||
case reshadefx::texture_format::rg16:
|
||||
return VK_FORMAT_R16G16_UNORM;
|
||||
case reshadefx::texture_format::rg16f:
|
||||
return VK_FORMAT_R16G16_SFLOAT;
|
||||
case reshadefx::texture_format::rg32f:
|
||||
return VK_FORMAT_R32G32_SFLOAT;
|
||||
case reshadefx::texture_format::rgba8:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
case reshadefx::texture_format::rgba16:
|
||||
return VK_FORMAT_R16G16B16A16_UNORM;
|
||||
case reshadefx::texture_format::rgba16f:
|
||||
return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
case reshadefx::texture_format::rgba32f:
|
||||
return VK_FORMAT_R32G32B32A32_SFLOAT;
|
||||
case reshadefx::texture_format::rgb10a2:
|
||||
return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
|
||||
default:
|
||||
return VK_FORMAT_R8G8B8A8_UNORM;
|
||||
}
|
||||
}
|
||||
|
||||
VkSamplerAddressMode ToAddressMode(reshadefx::texture_address_mode mode) {
|
||||
switch (mode) {
|
||||
case reshadefx::texture_address_mode::wrap:
|
||||
return VK_SAMPLER_ADDRESS_MODE_REPEAT;
|
||||
case reshadefx::texture_address_mode::mirror:
|
||||
return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
|
||||
case reshadefx::texture_address_mode::border:
|
||||
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER;
|
||||
case reshadefx::texture_address_mode::clamp:
|
||||
default:
|
||||
return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE;
|
||||
}
|
||||
}
|
||||
|
||||
VkBlendFactor ToBlendFactor(reshadefx::blend_factor func) {
|
||||
switch (func) {
|
||||
case reshadefx::blend_factor::zero:
|
||||
return VK_BLEND_FACTOR_ZERO;
|
||||
case reshadefx::blend_factor::source_color:
|
||||
return VK_BLEND_FACTOR_SRC_COLOR;
|
||||
case reshadefx::blend_factor::source_alpha:
|
||||
return VK_BLEND_FACTOR_SRC_ALPHA;
|
||||
case reshadefx::blend_factor::one_minus_source_color:
|
||||
return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR;
|
||||
case reshadefx::blend_factor::one_minus_source_alpha:
|
||||
return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
case reshadefx::blend_factor::dest_color:
|
||||
return VK_BLEND_FACTOR_DST_COLOR;
|
||||
case reshadefx::blend_factor::dest_alpha:
|
||||
return VK_BLEND_FACTOR_DST_ALPHA;
|
||||
case reshadefx::blend_factor::one_minus_dest_color:
|
||||
return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR;
|
||||
case reshadefx::blend_factor::one_minus_dest_alpha:
|
||||
return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA;
|
||||
case reshadefx::blend_factor::one:
|
||||
default:
|
||||
return VK_BLEND_FACTOR_ONE;
|
||||
}
|
||||
}
|
||||
|
||||
VkBlendOp ToBlendOp(reshadefx::blend_op op) {
|
||||
switch (op) {
|
||||
case reshadefx::blend_op::subtract:
|
||||
return VK_BLEND_OP_SUBTRACT;
|
||||
case reshadefx::blend_op::reverse_subtract:
|
||||
return VK_BLEND_OP_REVERSE_SUBTRACT;
|
||||
case reshadefx::blend_op::min:
|
||||
return VK_BLEND_OP_MIN;
|
||||
case reshadefx::blend_op::max:
|
||||
return VK_BLEND_OP_MAX;
|
||||
case reshadefx::blend_op::add:
|
||||
default:
|
||||
return VK_BLEND_OP_ADD;
|
||||
}
|
||||
}
|
||||
|
||||
VkPrimitiveTopology ToTopology(reshadefx::primitive_topology topology) {
|
||||
switch (topology) {
|
||||
case reshadefx::primitive_topology::point_list:
|
||||
return VK_PRIMITIVE_TOPOLOGY_POINT_LIST;
|
||||
case reshadefx::primitive_topology::line_list:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_LIST;
|
||||
case reshadefx::primitive_topology::line_strip:
|
||||
return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP;
|
||||
case reshadefx::primitive_topology::triangle_strip:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP;
|
||||
case reshadefx::primitive_topology::triangle_list:
|
||||
default:
|
||||
return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
vk::RenderPass CreateFxRenderPass(const Device& device, VkFormat format, bool clear) {
|
||||
VkAttachmentLoadOp load_op = VK_ATTACHMENT_LOAD_OP_LOAD;
|
||||
VkImageLayout initial_layout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
if (clear) {
|
||||
load_op = VK_ATTACHMENT_LOAD_OP_CLEAR;
|
||||
initial_layout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
}
|
||||
|
||||
const VkAttachmentDescription attachment{
|
||||
.flags = 0,
|
||||
.format = format,
|
||||
.samples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.loadOp = load_op,
|
||||
.storeOp = VK_ATTACHMENT_STORE_OP_STORE,
|
||||
.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE,
|
||||
.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE,
|
||||
.initialLayout = initial_layout,
|
||||
.finalLayout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
};
|
||||
|
||||
const VkAttachmentReference reference{
|
||||
.attachment = 0,
|
||||
.layout = VK_IMAGE_LAYOUT_GENERAL,
|
||||
};
|
||||
|
||||
const VkSubpassDescription subpass{
|
||||
.flags = 0,
|
||||
.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS,
|
||||
.inputAttachmentCount = 0,
|
||||
.pInputAttachments = nullptr,
|
||||
.colorAttachmentCount = 1,
|
||||
.pColorAttachments = &reference,
|
||||
.pResolveAttachments = nullptr,
|
||||
.pDepthStencilAttachment = nullptr,
|
||||
.preserveAttachmentCount = 0,
|
||||
.pPreserveAttachments = nullptr,
|
||||
};
|
||||
|
||||
return device.GetLogical().CreateRenderPass(VkRenderPassCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.attachmentCount = 1,
|
||||
.pAttachments = &attachment,
|
||||
.subpassCount = 1,
|
||||
.pSubpasses = &subpass,
|
||||
.dependencyCount = 0,
|
||||
.pDependencies = nullptr,
|
||||
});
|
||||
}
|
||||
|
||||
vk::Pipeline CreateFxPipeline(const Device& device, vk::RenderPass& renderpass,
|
||||
vk::PipelineLayout& layout, VkShaderModule vertex_shader,
|
||||
VkShaderModule fragment_shader,
|
||||
const reshadefx::pass& pass) {
|
||||
const std::array<VkPipelineShaderStageCreateInfo, 2> stages{{
|
||||
{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.stage = VK_SHADER_STAGE_VERTEX_BIT,
|
||||
.module = vertex_shader,
|
||||
.pName = pass.vs_entry_point.c_str(),
|
||||
.pSpecializationInfo = nullptr,
|
||||
},
|
||||
{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
|
||||
.module = fragment_shader,
|
||||
.pName = pass.ps_entry_point.c_str(),
|
||||
.pSpecializationInfo = nullptr,
|
||||
},
|
||||
}};
|
||||
|
||||
constexpr VkPipelineVertexInputStateCreateInfo vertex_input{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.vertexBindingDescriptionCount = 0,
|
||||
.pVertexBindingDescriptions = nullptr,
|
||||
.vertexAttributeDescriptionCount = 0,
|
||||
.pVertexAttributeDescriptions = nullptr,
|
||||
};
|
||||
|
||||
const VkPipelineInputAssemblyStateCreateInfo input_assembly{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.topology = ToTopology(pass.topology),
|
||||
.primitiveRestartEnable = VK_FALSE,
|
||||
};
|
||||
|
||||
constexpr VkPipelineViewportStateCreateInfo viewport_state{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.viewportCount = 1,
|
||||
.pViewports = nullptr,
|
||||
.scissorCount = 1,
|
||||
.pScissors = nullptr,
|
||||
};
|
||||
|
||||
constexpr VkPipelineRasterizationStateCreateInfo rasterization{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.depthClampEnable = VK_FALSE,
|
||||
.rasterizerDiscardEnable = VK_FALSE,
|
||||
.polygonMode = VK_POLYGON_MODE_FILL,
|
||||
.cullMode = VK_CULL_MODE_NONE,
|
||||
.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE,
|
||||
.depthBiasEnable = VK_FALSE,
|
||||
.depthBiasConstantFactor = 0.0f,
|
||||
.depthBiasClamp = 0.0f,
|
||||
.depthBiasSlopeFactor = 0.0f,
|
||||
.lineWidth = 1.0f,
|
||||
};
|
||||
|
||||
constexpr VkPipelineMultisampleStateCreateInfo multisampling{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT,
|
||||
.sampleShadingEnable = VK_FALSE,
|
||||
.minSampleShading = 0.0f,
|
||||
.pSampleMask = nullptr,
|
||||
.alphaToCoverageEnable = VK_FALSE,
|
||||
.alphaToOneEnable = VK_FALSE,
|
||||
};
|
||||
|
||||
VkBool32 blend_enable = VK_FALSE;
|
||||
if (pass.blend_enable[0]) {
|
||||
blend_enable = VK_TRUE;
|
||||
}
|
||||
|
||||
const VkPipelineColorBlendAttachmentState blending{
|
||||
.blendEnable = blend_enable,
|
||||
.srcColorBlendFactor = ToBlendFactor(pass.source_color_blend_factor[0]),
|
||||
.dstColorBlendFactor = ToBlendFactor(pass.dest_color_blend_factor[0]),
|
||||
.colorBlendOp = ToBlendOp(pass.color_blend_op[0]),
|
||||
.srcAlphaBlendFactor = ToBlendFactor(pass.source_alpha_blend_factor[0]),
|
||||
.dstAlphaBlendFactor = ToBlendFactor(pass.dest_alpha_blend_factor[0]),
|
||||
.alphaBlendOp = ToBlendOp(pass.alpha_blend_op[0]),
|
||||
.colorWriteMask = static_cast<VkColorComponentFlags>(pass.render_target_write_mask[0] & 0xF),
|
||||
};
|
||||
|
||||
const VkPipelineColorBlendStateCreateInfo color_blend{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.logicOpEnable = VK_FALSE,
|
||||
.logicOp = VK_LOGIC_OP_COPY,
|
||||
.attachmentCount = 1,
|
||||
.pAttachments = &blending,
|
||||
.blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
|
||||
};
|
||||
|
||||
constexpr std::array dynamic_states{
|
||||
VK_DYNAMIC_STATE_VIEWPORT,
|
||||
VK_DYNAMIC_STATE_SCISSOR,
|
||||
};
|
||||
|
||||
const VkPipelineDynamicStateCreateInfo dynamic_state{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.dynamicStateCount = static_cast<u32>(dynamic_states.size()),
|
||||
.pDynamicStates = dynamic_states.data(),
|
||||
};
|
||||
|
||||
return device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.stageCount = static_cast<u32>(stages.size()),
|
||||
.pStages = stages.data(),
|
||||
.pVertexInputState = &vertex_input,
|
||||
.pInputAssemblyState = &input_assembly,
|
||||
.pTessellationState = nullptr,
|
||||
.pViewportState = &viewport_state,
|
||||
.pRasterizationState = &rasterization,
|
||||
.pMultisampleState = &multisampling,
|
||||
.pDepthStencilState = nullptr,
|
||||
.pColorBlendState = &color_blend,
|
||||
.pDynamicState = &dynamic_state,
|
||||
.layout = *layout,
|
||||
.renderPass = *renderpass,
|
||||
.subpass = 0,
|
||||
.basePipelineHandle = nullptr,
|
||||
.basePipelineIndex = 0,
|
||||
});
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
PostProcessChain::PostProcessChain(const Device& device, MemoryAllocator& allocator,
|
||||
Scheduler& scheduler, size_t image_count, VkExtent2D extent)
|
||||
: m_extent(extent)
|
||||
, m_image_count(u32(image_count))
|
||||
{
|
||||
m_start = std::chrono::steady_clock::now();
|
||||
m_previous = m_start;
|
||||
|
||||
CreatePingPongImages(device, allocator);
|
||||
|
||||
m_fallback_sampler = CreateWrappedSampler(device);
|
||||
m_fallback_image = CreateWrappedImage(allocator, VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM);
|
||||
m_fallback_view = CreateWrappedImageView(device, m_fallback_image, VK_FORMAT_R8G8B8A8_UNORM);
|
||||
|
||||
if (!BuildEffects(device, allocator, scheduler)) {
|
||||
m_effects.clear();
|
||||
}
|
||||
}
|
||||
|
||||
PostProcessChain::~PostProcessChain() = default;
|
||||
|
||||
bool PostProcessChain::Empty() const {
|
||||
return m_effects.empty();
|
||||
}
|
||||
|
||||
void PostProcessChain::CreatePingPongImages(const Device& device, MemoryAllocator& allocator) {
|
||||
m_frames.resize(m_image_count);
|
||||
for (auto& frame : m_frames) {
|
||||
for (size_t i = 0; i < frame.images.size(); ++i) {
|
||||
frame.images[i] = CreateWrappedImage(allocator, m_extent, BACKBUFFER_FORMAT);
|
||||
frame.views[i] = CreateWrappedImageView(device, frame.images[i], BACKBUFFER_FORMAT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PostProcessChain::BuildEffects(const Device& device, MemoryAllocator& allocator,
|
||||
Scheduler& scheduler) {
|
||||
const auto snapshot = VideoCore::FxChain::Instance().Snapshot();
|
||||
if (snapshot.entries.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto root = VideoCore::GetFxRootDirectory();
|
||||
|
||||
for (size_t entry_index = 0; entry_index < snapshot.entries.size(); ++entry_index) {
|
||||
const auto& entry = snapshot.entries[entry_index];
|
||||
const auto path = root / entry.file;
|
||||
|
||||
const auto compiled = VideoCore::CompileFxEffect(path, m_extent.width, m_extent.height, 8);
|
||||
if (!compiled.Succeeded()) {
|
||||
LOG_ERROR(Render_Vulkan, "Post-processing effect '{}' failed to compile:\n{}",
|
||||
entry.file, compiled.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& module = compiled.module;
|
||||
|
||||
const auto technique = std::find_if(
|
||||
module.techniques.begin(), module.techniques.end(),
|
||||
[&](const reshadefx::technique& t) { return t.name == entry.technique; });
|
||||
if (technique == module.techniques.end()) {
|
||||
LOG_ERROR(Render_Vulkan, "Effect '{}' has no technique '{}'", entry.file,
|
||||
entry.technique);
|
||||
continue;
|
||||
}
|
||||
|
||||
Effect effect;
|
||||
effect.entry_index = entry_index;
|
||||
effect.file = entry.file;
|
||||
effect.uniform_size = module.total_uniform_size;
|
||||
for (const auto& [name, words] : compiled.entry_points) {
|
||||
effect.shaders.emplace(name, CreateWrappedShaderModule(device, words));
|
||||
}
|
||||
|
||||
for (const auto& texture : module.textures) {
|
||||
Texture out;
|
||||
out.name = texture.unique_name;
|
||||
out.extent = VkExtent2D{texture.width, texture.height};
|
||||
out.format = ToVkFormat(texture.format);
|
||||
|
||||
if (texture.semantic == "COLOR") {
|
||||
out.is_backbuffer = true;
|
||||
effect.textures.push_back(std::move(out));
|
||||
continue;
|
||||
}
|
||||
if (texture.semantic == "DEPTH") {
|
||||
effect.textures.push_back(std::move(out));
|
||||
continue;
|
||||
}
|
||||
|
||||
out.image = CreateWrappedImage(allocator, out.extent, out.format);
|
||||
out.view = CreateWrappedImageView(device, out.image, out.format);
|
||||
effect.textures.push_back(std::move(out));
|
||||
}
|
||||
|
||||
for (const auto& sampler : module.samplers) {
|
||||
Sampler out;
|
||||
out.texture_index = NO_TEXTURE;
|
||||
for (size_t i = 0; i < effect.textures.size(); ++i) {
|
||||
if (effect.textures[i].name == sampler.texture_name) {
|
||||
out.texture_index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
VkFilter mag_filter = VK_FILTER_LINEAR;
|
||||
VkFilter min_filter = VK_FILTER_LINEAR;
|
||||
VkSamplerMipmapMode mip_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
|
||||
const u32 filter = static_cast<u32>(sampler.filter);
|
||||
if ((filter & 0x10) == 0) {
|
||||
min_filter = VK_FILTER_NEAREST;
|
||||
}
|
||||
if ((filter & 0x04) == 0) {
|
||||
mag_filter = VK_FILTER_NEAREST;
|
||||
}
|
||||
if ((filter & 0x01) == 0) {
|
||||
mip_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
}
|
||||
|
||||
out.sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.magFilter = mag_filter,
|
||||
.minFilter = min_filter,
|
||||
.mipmapMode = mip_mode,
|
||||
.addressModeU = ToAddressMode(sampler.address_u),
|
||||
.addressModeV = ToAddressMode(sampler.address_v),
|
||||
.addressModeW = ToAddressMode(sampler.address_w),
|
||||
.mipLodBias = sampler.lod_bias,
|
||||
.anisotropyEnable = VK_FALSE,
|
||||
.maxAnisotropy = 1.0f,
|
||||
.compareEnable = VK_FALSE,
|
||||
.compareOp = VK_COMPARE_OP_NEVER,
|
||||
.minLod = sampler.min_lod,
|
||||
.maxLod = sampler.max_lod,
|
||||
.borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
|
||||
.unnormalizedCoordinates = VK_FALSE,
|
||||
});
|
||||
|
||||
effect.samplers.push_back(std::move(out));
|
||||
}
|
||||
|
||||
for (const auto& uniform : module.uniforms) {
|
||||
UniformWrite write;
|
||||
write.name = uniform.name;
|
||||
write.offset = uniform.offset;
|
||||
write.components = std::min<u32>(uniform.type.components(), 4);
|
||||
write.kind = UniformKind::Floating;
|
||||
if (uniform.type.is_boolean()) {
|
||||
write.kind = UniformKind::Boolean;
|
||||
} else if (uniform.type.is_integral()) {
|
||||
write.kind = UniformKind::Integer;
|
||||
}
|
||||
|
||||
for (const auto& annotation : uniform.annotations) {
|
||||
if (annotation.name != "source") {
|
||||
continue;
|
||||
}
|
||||
const std::string& source = annotation.value.string_data;
|
||||
if (source == "frametime") {
|
||||
write.source = UniformSource::FrameTime;
|
||||
} else if (source == "framecount") {
|
||||
write.source = UniformSource::FrameCount;
|
||||
} else if (source == "timer") {
|
||||
write.source = UniformSource::Timer;
|
||||
} else if (source == "random") {
|
||||
write.source = UniformSource::Random;
|
||||
} else if (source == "pingpong") {
|
||||
write.source = UniformSource::PingPong;
|
||||
}
|
||||
}
|
||||
|
||||
if (uniform.has_initializer_value) {
|
||||
for (u32 i = 0; i < write.components; ++i) {
|
||||
if (write.kind == UniformKind::Floating) {
|
||||
write.fallback[i] = uniform.initializer_value.as_float[i];
|
||||
} else {
|
||||
write.fallback[i] = static_cast<f32>(uniform.initializer_value.as_int[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write.args = {0.0f, 1.0f, 1.0f, 0.0f};
|
||||
for (const auto& annotation : uniform.annotations) {
|
||||
if (annotation.name == "min" && annotation.type.is_floating_point()) {
|
||||
write.args[0] = annotation.value.as_float[0];
|
||||
}
|
||||
if (annotation.name == "max" && annotation.type.is_floating_point()) {
|
||||
write.args[1] = annotation.value.as_float[0];
|
||||
}
|
||||
if (annotation.name == "step" && annotation.type.is_floating_point()) {
|
||||
write.args[2] = annotation.value.as_float[0];
|
||||
}
|
||||
}
|
||||
write.state = write.args[0];
|
||||
|
||||
effect.uniforms.push_back(std::move(write));
|
||||
}
|
||||
|
||||
effect.uniform_layout = CreateWrappedDescriptorSetLayout(
|
||||
device, std::array{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER},
|
||||
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||
|
||||
for (const auto& pass : technique->passes) {
|
||||
Pass out;
|
||||
out.num_vertices = pass.num_vertices;
|
||||
out.clear = pass.clear_render_targets != 0;
|
||||
out.target_texture = NO_TEXTURE;
|
||||
out.extent = m_extent;
|
||||
|
||||
const std::string& target = pass.render_target_names[0];
|
||||
if (target.empty()) {
|
||||
out.writes_backbuffer = true;
|
||||
} else {
|
||||
for (size_t i = 0; i < effect.textures.size(); ++i) {
|
||||
if (effect.textures[i].name == target) {
|
||||
out.target_texture = i;
|
||||
out.extent = effect.textures[i].extent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (out.target_texture == NO_TEXTURE) {
|
||||
LOG_WARNING(Render_Vulkan, "Effect '{}' pass targets unknown texture '{}'",
|
||||
entry.file, target);
|
||||
out.writes_backbuffer = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (pass.viewport_width != 0 && pass.viewport_height != 0) {
|
||||
out.extent = VkExtent2D{pass.viewport_width, pass.viewport_height};
|
||||
}
|
||||
|
||||
VkFormat target_format = BACKBUFFER_FORMAT;
|
||||
if (!out.writes_backbuffer) {
|
||||
target_format = effect.textures[out.target_texture].format;
|
||||
}
|
||||
|
||||
u32 binding_count = 0;
|
||||
for (const auto& binding : pass.sampler_bindings) {
|
||||
SamplerBinding entry_binding;
|
||||
entry_binding.binding = binding.entry_point_binding;
|
||||
entry_binding.sampler_index = binding.index;
|
||||
out.sampler_bindings.push_back(entry_binding);
|
||||
binding_count = std::max<u32>(binding_count, binding.entry_point_binding + 1);
|
||||
}
|
||||
|
||||
const std::vector<VkDescriptorType> sampler_types(
|
||||
std::max<size_t>(binding_count, 1), VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
|
||||
out.sampler_layout = CreateWrappedDescriptorSetLayout(
|
||||
device, sampler_types, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT);
|
||||
|
||||
const std::array set_layouts{*effect.uniform_layout, *out.sampler_layout};
|
||||
out.pipeline_layout =
|
||||
device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{
|
||||
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
|
||||
.pNext = nullptr,
|
||||
.flags = 0,
|
||||
.setLayoutCount = static_cast<u32>(set_layouts.size()),
|
||||
.pSetLayouts = set_layouts.data(),
|
||||
.pushConstantRangeCount = 0,
|
||||
.pPushConstantRanges = nullptr,
|
||||
});
|
||||
|
||||
const auto vertex_shader = effect.shaders.find(pass.vs_entry_point);
|
||||
const auto fragment_shader = effect.shaders.find(pass.ps_entry_point);
|
||||
if (vertex_shader == effect.shaders.end() ||
|
||||
fragment_shader == effect.shaders.end()) {
|
||||
LOG_WARNING(Render_Vulkan, "Effect '{}' pass references a missing entry point",
|
||||
entry.file);
|
||||
continue;
|
||||
}
|
||||
|
||||
out.renderpass = CreateFxRenderPass(device, target_format, out.clear);
|
||||
out.pipeline = CreateFxPipeline(device, out.renderpass, out.pipeline_layout,
|
||||
*vertex_shader->second, *fragment_shader->second, pass);
|
||||
|
||||
if (out.writes_backbuffer) {
|
||||
out.backbuffer_slot = static_cast<u32>(effect.backbuffer_pass_count % 2);
|
||||
++effect.backbuffer_pass_count;
|
||||
for (u32 image = 0; image < m_image_count; ++image) {
|
||||
for (size_t slot = 0; slot < 2; ++slot) {
|
||||
out.framebuffers.push_back(CreateWrappedFramebuffer(
|
||||
device, out.renderpass, m_frames[image].views[slot], out.extent));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
out.framebuffers.push_back(
|
||||
CreateWrappedFramebuffer(device, out.renderpass,
|
||||
effect.textures[out.target_texture].view, out.extent));
|
||||
}
|
||||
|
||||
effect.passes.push_back(std::move(out));
|
||||
}
|
||||
|
||||
if (effect.passes.empty()) {
|
||||
LOG_WARNING(Render_Vulkan, "Effect '{}' technique '{}' has no passes", entry.file,
|
||||
entry.technique);
|
||||
continue;
|
||||
}
|
||||
|
||||
const u32 buffer_size = std::max<u32>(effect.uniform_size, 4);
|
||||
for (u32 i = 0; i < m_image_count; ++i) {
|
||||
effect.uniform_buffers.push_back(
|
||||
CreateWrappedBuffer(allocator, buffer_size, MemoryUsage::Upload));
|
||||
}
|
||||
|
||||
size_t sampler_descriptor_count = 0;
|
||||
size_t sampler_set_count = 0;
|
||||
for (const auto& pass : effect.passes) {
|
||||
sampler_descriptor_count +=
|
||||
m_image_count * std::max<size_t>(pass.sampler_bindings.size(), 1);
|
||||
sampler_set_count += m_image_count;
|
||||
}
|
||||
|
||||
effect.descriptor_pool = CreateWrappedDescriptorPool(
|
||||
device, m_image_count + sampler_descriptor_count, m_image_count + sampler_set_count,
|
||||
{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER});
|
||||
|
||||
const std::vector<VkDescriptorSetLayout> uniform_layouts(m_image_count,
|
||||
*effect.uniform_layout);
|
||||
effect.uniform_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, uniform_layouts);
|
||||
|
||||
for (auto& pass : effect.passes) {
|
||||
const std::vector<VkDescriptorSetLayout> layouts(m_image_count, *pass.sampler_layout);
|
||||
pass.sampler_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, layouts);
|
||||
}
|
||||
|
||||
m_effects.push_back(std::move(effect));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PostProcessChain::PrepareImages(const Device& device, Scheduler& scheduler) {
|
||||
if (m_images_ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
scheduler.Record([this](vk::CommandBuffer cmdbuf) {
|
||||
ClearColorImage(cmdbuf, *m_fallback_image);
|
||||
for (auto& frame : m_frames) {
|
||||
for (auto& image : frame.images) {
|
||||
ClearColorImage(cmdbuf, *image);
|
||||
}
|
||||
}
|
||||
for (auto& effect : m_effects) {
|
||||
for (auto& texture : effect.textures) {
|
||||
if (texture.image) {
|
||||
ClearColorImage(cmdbuf, *texture.image);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
scheduler.Finish();
|
||||
|
||||
m_images_ready = true;
|
||||
}
|
||||
|
||||
void PostProcessChain::UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds) {
|
||||
if (effect.uniform_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
static thread_local std::mt19937 rng{std::random_device{}()};
|
||||
|
||||
std::vector<u8> staging(effect.uniform_size, 0);
|
||||
const f32 elapsed =
|
||||
std::chrono::duration<f32>(std::chrono::steady_clock::now() - m_start).count();
|
||||
const auto overrides = VideoCore::FxChain::Instance().EntryValues(effect.entry_index);
|
||||
|
||||
for (auto& uniform : effect.uniforms) {
|
||||
std::array<f32, 4> value = uniform.fallback;
|
||||
|
||||
const auto override = overrides.find(uniform.name);
|
||||
if (override != overrides.end()) {
|
||||
value = override->second;
|
||||
}
|
||||
|
||||
switch (uniform.source) {
|
||||
case UniformSource::FrameTime:
|
||||
value[0] = delta_seconds * 1000.0f;
|
||||
break;
|
||||
case UniformSource::FrameCount:
|
||||
value[0] = static_cast<f32>(m_frame_count);
|
||||
break;
|
||||
case UniformSource::Timer:
|
||||
value[0] = elapsed * 1000.0f;
|
||||
break;
|
||||
case UniformSource::Random: {
|
||||
const int low = static_cast<int>(uniform.args[0]);
|
||||
int high = static_cast<int>(uniform.args[1]);
|
||||
if (high <= low) {
|
||||
high = low + 1;
|
||||
}
|
||||
std::uniform_int_distribution<int> dist(low, high);
|
||||
value[0] = static_cast<f32>(dist(rng));
|
||||
break;
|
||||
}
|
||||
case UniformSource::PingPong: {
|
||||
const f32 min_value = uniform.args[0];
|
||||
f32 max_value = uniform.args[1];
|
||||
if (max_value <= min_value) {
|
||||
max_value = min_value + 1.0f;
|
||||
}
|
||||
f32 step = uniform.args[2];
|
||||
if (step == 0.0f) {
|
||||
step = 1.0f;
|
||||
}
|
||||
uniform.state += uniform.direction * step * delta_seconds;
|
||||
if (uniform.state >= max_value) {
|
||||
uniform.state = max_value;
|
||||
uniform.direction = -1.0f;
|
||||
}
|
||||
if (uniform.state <= min_value) {
|
||||
uniform.state = min_value;
|
||||
uniform.direction = 1.0f;
|
||||
}
|
||||
value[0] = uniform.state;
|
||||
value[1] = uniform.direction;
|
||||
break;
|
||||
}
|
||||
case UniformSource::Value:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < uniform.components; ++i) {
|
||||
const size_t offset = uniform.offset + i * sizeof(u32);
|
||||
if (offset + sizeof(u32) > staging.size()) {
|
||||
break;
|
||||
}
|
||||
if (uniform.kind == UniformKind::Floating) {
|
||||
const f32 element = value[i];
|
||||
std::memcpy(staging.data() + offset, &element, sizeof(f32));
|
||||
} else {
|
||||
const s32 element = static_cast<s32>(value[i]);
|
||||
std::memcpy(staging.data() + offset, &element, sizeof(s32));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::span<u8> mapped = effect.uniform_buffers[image_index].Mapped();
|
||||
if (mapped.size() >= staging.size()) {
|
||||
std::memcpy(mapped.data(), staging.data(), staging.size());
|
||||
effect.uniform_buffers[image_index].Flush();
|
||||
}
|
||||
}
|
||||
|
||||
void PostProcessChain::UpdateDescriptors(const Device& device, Effect& effect, Pass& pass,
|
||||
size_t image_index, VkImageView backbuffer_view) {
|
||||
std::vector<VkDescriptorImageInfo> image_infos;
|
||||
std::vector<VkWriteDescriptorSet> writes;
|
||||
image_infos.reserve(pass.sampler_bindings.size() + 1);
|
||||
|
||||
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
|
||||
|
||||
for (const auto& binding : pass.sampler_bindings) {
|
||||
VkImageView view = *m_fallback_view;
|
||||
VkSampler handle = *m_fallback_sampler;
|
||||
|
||||
if (binding.sampler_index < effect.samplers.size()) {
|
||||
const Sampler& sampler = effect.samplers[binding.sampler_index];
|
||||
if (sampler.sampler) {
|
||||
handle = *sampler.sampler;
|
||||
}
|
||||
if (sampler.texture_index != NO_TEXTURE) {
|
||||
const Texture& texture = effect.textures[sampler.texture_index];
|
||||
if (texture.is_backbuffer) {
|
||||
view = backbuffer_view;
|
||||
} else if (texture.view) {
|
||||
view = *texture.view;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
writes.push_back(
|
||||
CreateWriteDescriptorSet(image_infos, handle, view, sampler_set, binding.binding));
|
||||
}
|
||||
|
||||
const VkDescriptorBufferInfo buffer_info{
|
||||
.buffer = *effect.uniform_buffers[image_index],
|
||||
.offset = 0,
|
||||
.range = VK_WHOLE_SIZE,
|
||||
};
|
||||
|
||||
writes.push_back(VkWriteDescriptorSet{
|
||||
.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
|
||||
.pNext = nullptr,
|
||||
.dstSet = effect.uniform_sets[image_index],
|
||||
.dstBinding = 0,
|
||||
.dstArrayElement = 0,
|
||||
.descriptorCount = 1,
|
||||
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
|
||||
.pImageInfo = nullptr,
|
||||
.pBufferInfo = &buffer_info,
|
||||
.pTexelBufferView = nullptr,
|
||||
});
|
||||
|
||||
device.GetLogical().UpdateDescriptorSets(writes, {});
|
||||
}
|
||||
|
||||
void PostProcessChain::Draw(const Device& device, Scheduler& scheduler, size_t image_index,
|
||||
VkImage* inout_image, VkImageView* inout_image_view) {
|
||||
if (m_effects.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PrepareImages(device, scheduler);
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const f32 delta_seconds = std::chrono::duration<f32>(now - m_previous).count();
|
||||
m_previous = now;
|
||||
++m_frame_count;
|
||||
|
||||
FrameImages& frame = m_frames[image_index];
|
||||
VkImage current_image = *inout_image;
|
||||
VkImageView current_view = *inout_image_view;
|
||||
u32 slot = 0;
|
||||
|
||||
for (auto& effect : m_effects) {
|
||||
UpdateUniforms(effect, image_index, delta_seconds);
|
||||
|
||||
for (size_t pass_index = 0; pass_index < effect.passes.size(); ++pass_index) {
|
||||
Pass& pass = effect.passes[pass_index];
|
||||
|
||||
UpdateDescriptors(device, effect, pass, image_index, current_view);
|
||||
|
||||
VkFramebuffer framebuffer{};
|
||||
VkImage target_image{};
|
||||
if (pass.writes_backbuffer) {
|
||||
const u32 target_slot = (slot + 1) % 2;
|
||||
framebuffer = *pass.framebuffers[image_index * 2 + target_slot];
|
||||
target_image = *frame.images[target_slot];
|
||||
} else {
|
||||
framebuffer = *pass.framebuffers[0];
|
||||
target_image = *effect.textures[pass.target_texture].image;
|
||||
}
|
||||
|
||||
const VkImage source_image = current_image;
|
||||
const VkRenderPass renderpass = *pass.renderpass;
|
||||
const VkPipeline pipeline = *pass.pipeline;
|
||||
const VkPipelineLayout layout = *pass.pipeline_layout;
|
||||
const VkDescriptorSet uniform_set = effect.uniform_sets[image_index];
|
||||
const VkDescriptorSet sampler_set = pass.sampler_sets[image_index];
|
||||
const VkExtent2D extent = pass.extent;
|
||||
const u32 vertices = pass.num_vertices;
|
||||
|
||||
scheduler.RequestOutsideRenderPassOperationContext();
|
||||
scheduler.Record([=](vk::CommandBuffer cmdbuf) {
|
||||
TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||
BeginRenderPass(cmdbuf, renderpass, framebuffer, extent);
|
||||
cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
|
||||
cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0,
|
||||
std::array{uniform_set, sampler_set}, {});
|
||||
cmdbuf.Draw(vertices, 1, 0, 0);
|
||||
cmdbuf.EndRenderPass();
|
||||
TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL);
|
||||
});
|
||||
|
||||
if (pass.writes_backbuffer) {
|
||||
slot = (slot + 1) % 2;
|
||||
current_image = *frame.images[slot];
|
||||
current_view = *frame.views[slot];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*inout_image = current_image;
|
||||
*inout_image_view = current_view;
|
||||
}
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "common/common_types.h"
|
||||
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
|
||||
#include "video_core/vulkan_common/vulkan_wrapper.h"
|
||||
|
||||
namespace Vulkan {
|
||||
|
||||
class Device;
|
||||
class Scheduler;
|
||||
|
||||
class PostProcessChain {
|
||||
public:
|
||||
explicit PostProcessChain(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler,
|
||||
size_t image_count, VkExtent2D extent);
|
||||
~PostProcessChain();
|
||||
|
||||
void Draw(const Device& device, Scheduler& scheduler, size_t image_index, VkImage* inout_image,
|
||||
VkImageView* inout_image_view);
|
||||
|
||||
bool Empty() const;
|
||||
|
||||
private:
|
||||
enum class UniformKind : u32 {
|
||||
Boolean,
|
||||
Integer,
|
||||
Floating,
|
||||
};
|
||||
|
||||
enum class UniformSource : u32 {
|
||||
Value,
|
||||
FrameTime,
|
||||
FrameCount,
|
||||
Timer,
|
||||
Random,
|
||||
PingPong,
|
||||
};
|
||||
|
||||
struct UniformWrite {
|
||||
std::string name;
|
||||
u32 offset{};
|
||||
u32 components{};
|
||||
UniformKind kind{UniformKind::Floating};
|
||||
UniformSource source{UniformSource::Value};
|
||||
std::array<f32, 4> fallback{};
|
||||
std::array<f32, 4> args{};
|
||||
f32 state{};
|
||||
f32 direction{1.0f};
|
||||
};
|
||||
|
||||
struct Texture {
|
||||
std::string name;
|
||||
vk::Image image{};
|
||||
vk::ImageView view{};
|
||||
VkExtent2D extent{};
|
||||
VkFormat format{};
|
||||
bool is_backbuffer{};
|
||||
};
|
||||
|
||||
struct Sampler {
|
||||
vk::Sampler sampler{};
|
||||
size_t texture_index{};
|
||||
};
|
||||
|
||||
struct SamplerBinding {
|
||||
u32 binding{};
|
||||
size_t sampler_index{};
|
||||
};
|
||||
|
||||
struct Pass {
|
||||
vk::RenderPass renderpass{};
|
||||
vk::Pipeline pipeline{};
|
||||
vk::DescriptorSetLayout sampler_layout{};
|
||||
vk::PipelineLayout pipeline_layout{};
|
||||
vk::DescriptorSets sampler_sets{};
|
||||
std::vector<SamplerBinding> sampler_bindings{};
|
||||
std::vector<vk::Framebuffer> framebuffers{};
|
||||
size_t target_texture{};
|
||||
VkExtent2D extent{};
|
||||
u32 num_vertices{3};
|
||||
bool clear{};
|
||||
bool writes_backbuffer{};
|
||||
u32 backbuffer_slot{};
|
||||
};
|
||||
|
||||
struct Effect {
|
||||
size_t entry_index{};
|
||||
std::string file{};
|
||||
std::map<std::string, vk::ShaderModule> shaders{};
|
||||
std::vector<Texture> textures{};
|
||||
std::vector<Sampler> samplers{};
|
||||
std::vector<Pass> passes{};
|
||||
std::vector<UniformWrite> uniforms{};
|
||||
u32 uniform_size{};
|
||||
std::vector<vk::Buffer> uniform_buffers{};
|
||||
vk::DescriptorSetLayout uniform_layout{};
|
||||
vk::DescriptorPool descriptor_pool{};
|
||||
vk::DescriptorSets uniform_sets{};
|
||||
size_t backbuffer_pass_count{};
|
||||
u32 backbuffer_slots{1};
|
||||
};
|
||||
|
||||
struct FrameImages {
|
||||
std::array<vk::Image, 2> images{};
|
||||
std::array<vk::ImageView, 2> views{};
|
||||
};
|
||||
|
||||
bool BuildEffects(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler);
|
||||
void CreatePingPongImages(const Device& device, MemoryAllocator& allocator);
|
||||
void PrepareImages(const Device& device, Scheduler& scheduler);
|
||||
void UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds);
|
||||
void UpdateDescriptors(const Device& device, Effect& effect, Pass& pass, size_t image_index,
|
||||
VkImageView backbuffer_view);
|
||||
|
||||
const VkExtent2D m_extent;
|
||||
const u32 m_image_count;
|
||||
|
||||
std::vector<Effect> m_effects{};
|
||||
std::vector<FrameImages> m_frames{};
|
||||
|
||||
vk::Sampler m_fallback_sampler{};
|
||||
vk::Image m_fallback_image{};
|
||||
vk::ImageView m_fallback_view{};
|
||||
|
||||
std::chrono::steady_clock::time_point m_start{};
|
||||
std::chrono::steady_clock::time_point m_previous{};
|
||||
u64 m_frame_count{};
|
||||
bool m_images_ready{};
|
||||
};
|
||||
|
||||
} // namespace Vulkan
|
||||
@@ -24,6 +24,9 @@
|
||||
#include "video_core/gpu.h"
|
||||
#include "video_core/present.h"
|
||||
#include "video_core/renderer_vulkan/present/util.h"
|
||||
#ifdef HAS_RESHADE
|
||||
#include "video_core/post_processing/fx_chain.h"
|
||||
#endif
|
||||
#include "video_core/renderer_vulkan/renderer_vulkan.h"
|
||||
#include "video_core/renderer_vulkan/vk_blit_screen.h"
|
||||
#include "video_core/renderer_vulkan/vk_rasterizer.h"
|
||||
@@ -183,6 +186,10 @@ try
|
||||
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
|
||||
}
|
||||
|
||||
#ifdef HAS_RESHADE
|
||||
VideoCore::FxChain::Instance().LoadFromSettings();
|
||||
#endif
|
||||
|
||||
Report();
|
||||
} catch (const vk::Exception& exception) {
|
||||
LOG_ERROR(Render_Vulkan, "Vulkan initialization failed with error: {}", exception.what());
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
@@ -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 &Performance Overlay</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Post_Processing_Shaders">
|
||||
<property name="text">
|
||||
<string>Post-Processing &Shaders...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="action_Carousel_View">
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user