From f39e89ece79436f5058bb58d50a1a4dcd6823f4e Mon Sep 17 00:00:00 2001
From: ZenoArrows <129334871+ZenoArrows@users.noreply.github.com>
Date: Tue, 17 Sep 2024 20:30:50 +0200
Subject: [PATCH] Add area sampling scaler to allow for super-sampled
anti-aliasing. (#7304)
* Add area sampling scaler to allow for super-sampled anti-aliasing.
* Area scaling filter doesn't have a scaling level.
* Add further clarification to the tooltip on how to achieve supersampling.
* ShaderHelper: Merge the two CompileProgram functions.
* Convert tabs to spaces in area scaling shaders
* Fixup Vulkan and OpenGL project files.
* AreaScaling: Replace texture() by texelFetch() and use integer vectors.
No functional difference, but it cleans up the code a bit.
* AreaScaling: Delete unused sharpening level member.
Also rename _scale to _sharpeningLevel for clarity and consistency.
* AreaScaling: Delete unused scaleX/scaleY uniforms.
* AreaScaling: Force the alpha to 1 when storing the pixel.
* AreaScaling: Remove left-over sharpening buffer.
---
src/Ryujinx.Graphics.GAL/UpscaleType.cs | 1 +
.../Effects/AreaScalingFilter.cs | 106 +++++++++++++++
.../Effects/FsrScalingFilter.cs | 6 +-
.../Effects/ShaderHelper.cs | 23 ++--
.../Effects/Shaders/area_scaling.glsl | 119 +++++++++++++++++
.../Effects/Shaders/fsr_scaling.glsl | 2 +-
.../Ryujinx.Graphics.OpenGL.csproj | 1 +
src/Ryujinx.Graphics.OpenGL/Window.cs | 10 ++
.../Effects/AreaScalingFilter.cs | 101 +++++++++++++++
.../Effects/Shaders/AreaScaling.glsl | 122 ++++++++++++++++++
.../Effects/Shaders/AreaScaling.spv | Bin 0 -> 12428 bytes
.../Ryujinx.Graphics.Vulkan.csproj | 1 +
src/Ryujinx.Graphics.Vulkan/Window.cs | 7 +
src/Ryujinx/Assets/Locales/en_US.json | 3 +-
.../Views/Settings/SettingsGraphicsView.axaml | 5 +-
15 files changed, 489 insertions(+), 18 deletions(-)
create mode 100644 src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs
create mode 100644 src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl
create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs
create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl
create mode 100644 src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv
diff --git a/src/Ryujinx.Graphics.GAL/UpscaleType.cs b/src/Ryujinx.Graphics.GAL/UpscaleType.cs
index ca24199c4..e2482faef 100644
--- a/src/Ryujinx.Graphics.GAL/UpscaleType.cs
+++ b/src/Ryujinx.Graphics.GAL/UpscaleType.cs
@@ -5,5 +5,6 @@ namespace Ryujinx.Graphics.GAL
Bilinear,
Nearest,
Fsr,
+ Area,
}
}
diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs b/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs
new file mode 100644
index 000000000..9b19f2f26
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/Effects/AreaScalingFilter.cs
@@ -0,0 +1,106 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.OpenGL.Image;
+using System;
+using static Ryujinx.Graphics.OpenGL.Effects.ShaderHelper;
+
+namespace Ryujinx.Graphics.OpenGL.Effects
+{
+ internal class AreaScalingFilter : IScalingFilter
+ {
+ private readonly OpenGLRenderer _renderer;
+ private int _inputUniform;
+ private int _outputUniform;
+ private int _srcX0Uniform;
+ private int _srcX1Uniform;
+ private int _srcY0Uniform;
+ private int _scalingShaderProgram;
+ private int _srcY1Uniform;
+ private int _dstX0Uniform;
+ private int _dstX1Uniform;
+ private int _dstY0Uniform;
+ private int _dstY1Uniform;
+
+ public float Level { get; set; }
+
+ public AreaScalingFilter(OpenGLRenderer renderer)
+ {
+ Initialize();
+
+ _renderer = renderer;
+ }
+
+ public void Dispose()
+ {
+ if (_scalingShaderProgram != 0)
+ {
+ GL.DeleteProgram(_scalingShaderProgram);
+ }
+ }
+
+ private void Initialize()
+ {
+ var scalingShader = EmbeddedResources.ReadAllText("Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl");
+
+ _scalingShaderProgram = CompileProgram(scalingShader, ShaderType.ComputeShader);
+
+ _inputUniform = GL.GetUniformLocation(_scalingShaderProgram, "Source");
+ _outputUniform = GL.GetUniformLocation(_scalingShaderProgram, "imgOutput");
+
+ _srcX0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcX0");
+ _srcX1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcX1");
+ _srcY0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcY0");
+ _srcY1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "srcY1");
+ _dstX0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstX0");
+ _dstX1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstX1");
+ _dstY0Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstY0");
+ _dstY1Uniform = GL.GetUniformLocation(_scalingShaderProgram, "dstY1");
+ }
+
+ public void Run(
+ TextureView view,
+ TextureView destinationTexture,
+ int width,
+ int height,
+ Extents2D source,
+ Extents2D destination)
+ {
+ int previousProgram = GL.GetInteger(GetPName.CurrentProgram);
+ int previousUnit = GL.GetInteger(GetPName.ActiveTexture);
+ GL.ActiveTexture(TextureUnit.Texture0);
+ int previousTextureBinding = GL.GetInteger(GetPName.TextureBinding2D);
+
+ GL.BindImageTexture(0, destinationTexture.Handle, 0, false, 0, TextureAccess.ReadWrite, SizedInternalFormat.Rgba8);
+
+ int threadGroupWorkRegionDim = 16;
+ int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+ int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+
+ // Scaling pass
+ GL.UseProgram(_scalingShaderProgram);
+ view.Bind(0);
+ GL.Uniform1(_inputUniform, 0);
+ GL.Uniform1(_outputUniform, 0);
+ GL.Uniform1(_srcX0Uniform, (float)source.X1);
+ GL.Uniform1(_srcX1Uniform, (float)source.X2);
+ GL.Uniform1(_srcY0Uniform, (float)source.Y1);
+ GL.Uniform1(_srcY1Uniform, (float)source.Y2);
+ GL.Uniform1(_dstX0Uniform, (float)destination.X1);
+ GL.Uniform1(_dstX1Uniform, (float)destination.X2);
+ GL.Uniform1(_dstY0Uniform, (float)destination.Y1);
+ GL.Uniform1(_dstY1Uniform, (float)destination.Y2);
+ GL.DispatchCompute(dispatchX, dispatchY, 1);
+
+ GL.UseProgram(previousProgram);
+ GL.MemoryBarrier(MemoryBarrierFlags.ShaderImageAccessBarrierBit);
+
+ (_renderer.Pipeline as Pipeline).RestoreImages1And2();
+
+ GL.ActiveTexture(TextureUnit.Texture0);
+ GL.BindTexture(TextureTarget.Texture2D, previousTextureBinding);
+
+ GL.ActiveTexture((TextureUnit)previousUnit);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs b/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs
index 1a130bebb..0522e28e0 100644
--- a/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Effects/FsrScalingFilter.cs
@@ -18,7 +18,7 @@ namespace Ryujinx.Graphics.OpenGL.Effects
private int _srcY0Uniform;
private int _scalingShaderProgram;
private int _sharpeningShaderProgram;
- private float _scale = 1;
+ private float _sharpeningLevel = 1;
private int _srcY1Uniform;
private int _dstX0Uniform;
private int _dstX1Uniform;
@@ -30,10 +30,10 @@ namespace Ryujinx.Graphics.OpenGL.Effects
public float Level
{
- get => _scale;
+ get => _sharpeningLevel;
set
{
- _scale = MathF.Max(0.01f, value);
+ _sharpeningLevel = MathF.Max(0.01f, value);
}
}
diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs b/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs
index c25fe5b25..637b2fba8 100644
--- a/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Effects/ShaderHelper.cs
@@ -1,4 +1,5 @@
using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common.Logging;
namespace Ryujinx.Graphics.OpenGL.Effects
{
@@ -6,18 +7,7 @@ namespace Ryujinx.Graphics.OpenGL.Effects
{
public static int CompileProgram(string shaderCode, ShaderType shaderType)
{
- var shader = GL.CreateShader(shaderType);
- GL.ShaderSource(shader, shaderCode);
- GL.CompileShader(shader);
-
- var program = GL.CreateProgram();
- GL.AttachShader(program, shader);
- GL.LinkProgram(program);
-
- GL.DetachShader(program, shader);
- GL.DeleteShader(shader);
-
- return program;
+ return CompileProgram(new string[] { shaderCode }, shaderType);
}
public static int CompileProgram(string[] shaders, ShaderType shaderType)
@@ -26,6 +16,15 @@ namespace Ryujinx.Graphics.OpenGL.Effects
GL.ShaderSource(shader, shaders.Length, shaders, (int[])null);
GL.CompileShader(shader);
+ GL.GetShader(shader, ShaderParameter.CompileStatus, out int isCompiled);
+ if (isCompiled == 0)
+ {
+ string log = GL.GetShaderInfoLog(shader);
+ Logger.Error?.Print(LogClass.Gpu, $"Failed to compile effect shader:\n\n{log}\n");
+ GL.DeleteShader(shader);
+ return 0;
+ }
+
var program = GL.CreateProgram();
GL.AttachShader(program, shader);
GL.LinkProgram(program);
diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl
new file mode 100644
index 000000000..0fe20d3f9
--- /dev/null
+++ b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/area_scaling.glsl
@@ -0,0 +1,119 @@
+#version 430 core
+precision mediump float;
+layout (local_size_x = 16, local_size_y = 16) in;
+layout(rgba8, binding = 0, location=0) uniform image2D imgOutput;
+layout( location=1 ) uniform sampler2D Source;
+layout( location=2 ) uniform float srcX0;
+layout( location=3 ) uniform float srcX1;
+layout( location=4 ) uniform float srcY0;
+layout( location=5 ) uniform float srcY1;
+layout( location=6 ) uniform float dstX0;
+layout( location=7 ) uniform float dstX1;
+layout( location=8 ) uniform float dstY0;
+layout( location=9 ) uniform float dstY1;
+
+/***** Area Sampling *****/
+
+// By Sam Belliveau and Filippo Tarpini. Public Domain license.
+// Effectively a more accurate sharp bilinear filter when upscaling,
+// that also works as a mathematically perfect downscale filter.
+// https://entropymine.com/imageworsener/pixelmixing/
+// https://github.com/obsproject/obs-studio/pull/1715
+// https://legacy.imagemagick.org/Usage/filter/
+vec4 AreaSampling(vec2 xy)
+{
+ // Determine the sizes of the source and target images.
+ vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0));
+ vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0));
+ vec2 inverted_target_size = vec2(1.0) / target_size;
+
+ // Compute the top-left and bottom-right corners of the target pixel box.
+ vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1));
+ vec2 t_end = t_beg + vec2(1.0, 1.0);
+
+ // Convert the target pixel box to source pixel box.
+ vec2 beg = t_beg * inverted_target_size * source_size;
+ vec2 end = t_end * inverted_target_size * source_size;
+
+ // Compute the top-left and bottom-right corners of the pixel box.
+ ivec2 f_beg = ivec2(beg);
+ ivec2 f_end = ivec2(end);
+
+ // Compute how much of the start and end pixels are covered horizontally & vertically.
+ float area_w = 1.0 - fract(beg.x);
+ float area_n = 1.0 - fract(beg.y);
+ float area_e = fract(end.x);
+ float area_s = fract(end.y);
+
+ // Compute the areas of the corner pixels in the pixel box.
+ float area_nw = area_n * area_w;
+ float area_ne = area_n * area_e;
+ float area_sw = area_s * area_w;
+ float area_se = area_s * area_e;
+
+ // Initialize the color accumulator.
+ vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0);
+
+ // Accumulate corner pixels.
+ avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0);
+ avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0);
+ avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0);
+ avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0);
+
+ // Determine the size of the pixel box.
+ int x_range = int(f_end.x - f_beg.x - 0.5);
+ int y_range = int(f_end.y - f_beg.y - 0.5);
+
+ // Accumulate top and bottom edge pixels.
+ for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x)
+ {
+ avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0);
+ avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0);
+ }
+
+ // Accumulate left and right edge pixels and all the pixels in between.
+ for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y)
+ {
+ avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0);
+ avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0);
+
+ for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x)
+ {
+ avg_color += texelFetch(Source, ivec2(x, y), 0);
+ }
+ }
+
+ // Compute the area of the pixel box that was sampled.
+ float area_corners = area_nw + area_ne + area_sw + area_se;
+ float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e);
+ float area_center = float(x_range) * float(y_range);
+
+ // Return the normalized average color.
+ return avg_color / (area_corners + area_edges + area_center);
+}
+
+float insideBox(vec2 v, vec2 bLeft, vec2 tRight) {
+ vec2 s = step(bLeft, v) - step(tRight, v);
+ return s.x * s.y;
+}
+
+vec2 translateDest(vec2 pos) {
+ vec2 translatedPos = vec2(pos.x, pos.y);
+ translatedPos.x = dstX1 < dstX0 ? dstX1 - translatedPos.x : translatedPos.x;
+ translatedPos.y = dstY0 > dstY1 ? dstY0 + dstY1 - translatedPos.y - 1 : translatedPos.y;
+ return translatedPos;
+}
+
+void main()
+{
+ vec2 bLeft = vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1);
+ vec2 tRight = vec2(dstX1 > dstX0 ? dstX1 : dstX0, dstY1 > dstY0 ? dstY1 : dstY0);
+ ivec2 loc = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y);
+ if (insideBox(loc, bLeft, tRight) == 0) {
+ imageStore(imgOutput, loc, vec4(0, 0, 0, 1));
+ return;
+ }
+
+ vec4 outColor = AreaSampling(loc);
+ imageStore(imgOutput, ivec2(translateDest(loc)), vec4(outColor.rgb, 1));
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl
index 8e8755db2..3c7d485b1 100644
--- a/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl
+++ b/src/Ryujinx.Graphics.OpenGL/Effects/Shaders/fsr_scaling.glsl
@@ -85,4 +85,4 @@ void main() {
CurrFilter(gxy);
gxy.x -= 8u;
CurrFilter(gxy);
-}
\ No newline at end of file
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
index 3d64da99b..f3071f486 100644
--- a/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
+++ b/src/Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj
@@ -21,6 +21,7 @@
+
diff --git a/src/Ryujinx.Graphics.OpenGL/Window.cs b/src/Ryujinx.Graphics.OpenGL/Window.cs
index 6bcfefa4e..285ab725e 100644
--- a/src/Ryujinx.Graphics.OpenGL/Window.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Window.cs
@@ -373,6 +373,16 @@ namespace Ryujinx.Graphics.OpenGL
_isLinear = false;
_scalingFilter.Level = _scalingFilterLevel;
+ RecreateUpscalingTexture();
+ break;
+ case ScalingFilter.Area:
+ if (_scalingFilter is not AreaScalingFilter)
+ {
+ _scalingFilter?.Dispose();
+ _scalingFilter = new AreaScalingFilter(_renderer);
+ }
+ _isLinear = false;
+
RecreateUpscalingTexture();
break;
}
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs
new file mode 100644
index 000000000..87b46df80
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/AreaScalingFilter.cs
@@ -0,0 +1,101 @@
+using Ryujinx.Common;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.Shader;
+using Ryujinx.Graphics.Shader.Translation;
+using Silk.NET.Vulkan;
+using System;
+using Extent2D = Ryujinx.Graphics.GAL.Extents2D;
+using Format = Silk.NET.Vulkan.Format;
+using SamplerCreateInfo = Ryujinx.Graphics.GAL.SamplerCreateInfo;
+
+namespace Ryujinx.Graphics.Vulkan.Effects
+{
+ internal class AreaScalingFilter : IScalingFilter
+ {
+ private readonly VulkanRenderer _renderer;
+ private PipelineHelperShader _pipeline;
+ private ISampler _sampler;
+ private ShaderCollection _scalingProgram;
+ private Device _device;
+
+ public float Level { get; set; }
+
+ public AreaScalingFilter(VulkanRenderer renderer, Device device)
+ {
+ _device = device;
+ _renderer = renderer;
+
+ Initialize();
+ }
+
+ public void Dispose()
+ {
+ _pipeline.Dispose();
+ _scalingProgram.Dispose();
+ _sampler.Dispose();
+ }
+
+ public void Initialize()
+ {
+ _pipeline = new PipelineHelperShader(_renderer, _device);
+
+ _pipeline.Initialize();
+
+ var scalingShader = EmbeddedResources.Read("Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv");
+
+ var scalingResourceLayout = new ResourceLayoutBuilder()
+ .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 2)
+ .Add(ResourceStages.Compute, ResourceType.TextureAndSampler, 1)
+ .Add(ResourceStages.Compute, ResourceType.Image, 0, true).Build();
+
+ _sampler = _renderer.CreateSampler(SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear));
+
+ _scalingProgram = _renderer.CreateProgramWithMinimalLayout(new[]
+ {
+ new ShaderSource(scalingShader, ShaderStage.Compute, TargetLanguage.Spirv),
+ }, scalingResourceLayout);
+ }
+
+ public void Run(
+ TextureView view,
+ CommandBufferScoped cbs,
+ Auto destinationTexture,
+ Format format,
+ int width,
+ int height,
+ Extent2D source,
+ Extent2D destination)
+ {
+ _pipeline.SetCommandBuffer(cbs);
+ _pipeline.SetProgram(_scalingProgram);
+ _pipeline.SetTextureAndSampler(ShaderStage.Compute, 1, view, _sampler);
+
+ ReadOnlySpan dimensionsBuffer = stackalloc float[]
+ {
+ source.X1,
+ source.X2,
+ source.Y1,
+ source.Y2,
+ destination.X1,
+ destination.X2,
+ destination.Y1,
+ destination.Y2,
+ };
+
+ int rangeSize = dimensionsBuffer.Length * sizeof(float);
+ using var buffer = _renderer.BufferManager.ReserveOrCreate(_renderer, cbs, rangeSize);
+ buffer.Holder.SetDataUnchecked(buffer.Offset, dimensionsBuffer);
+
+ int threadGroupWorkRegionDim = 16;
+ int dispatchX = (width + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+ int dispatchY = (height + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
+
+ _pipeline.SetUniformBuffers(stackalloc[] { new BufferAssignment(2, buffer.Range) });
+ _pipeline.SetImage(0, destinationTexture);
+ _pipeline.DispatchCompute(dispatchX, dispatchY, 1);
+ _pipeline.ComputeBarrier();
+
+ _pipeline.Finish();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl
new file mode 100644
index 000000000..e34dd77dd
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.glsl
@@ -0,0 +1,122 @@
+// Scaling
+
+#version 430 core
+layout (local_size_x = 16, local_size_y = 16) in;
+layout( rgba8, binding = 0, set = 3) uniform image2D imgOutput;
+layout( binding = 1, set = 2) uniform sampler2D Source;
+layout( binding = 2 ) uniform dimensions{
+ float srcX0;
+ float srcX1;
+ float srcY0;
+ float srcY1;
+ float dstX0;
+ float dstX1;
+ float dstY0;
+ float dstY1;
+};
+
+/***** Area Sampling *****/
+
+// By Sam Belliveau and Filippo Tarpini. Public Domain license.
+// Effectively a more accurate sharp bilinear filter when upscaling,
+// that also works as a mathematically perfect downscale filter.
+// https://entropymine.com/imageworsener/pixelmixing/
+// https://github.com/obsproject/obs-studio/pull/1715
+// https://legacy.imagemagick.org/Usage/filter/
+vec4 AreaSampling(vec2 xy)
+{
+ // Determine the sizes of the source and target images.
+ vec2 source_size = vec2(abs(srcX1 - srcX0), abs(srcY1 - srcY0));
+ vec2 target_size = vec2(abs(dstX1 - dstX0), abs(dstY1 - dstY0));
+ vec2 inverted_target_size = vec2(1.0) / target_size;
+
+ // Compute the top-left and bottom-right corners of the target pixel box.
+ vec2 t_beg = floor(xy - vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1));
+ vec2 t_end = t_beg + vec2(1.0, 1.0);
+
+ // Convert the target pixel box to source pixel box.
+ vec2 beg = t_beg * inverted_target_size * source_size;
+ vec2 end = t_end * inverted_target_size * source_size;
+
+ // Compute the top-left and bottom-right corners of the pixel box.
+ ivec2 f_beg = ivec2(beg);
+ ivec2 f_end = ivec2(end);
+
+ // Compute how much of the start and end pixels are covered horizontally & vertically.
+ float area_w = 1.0 - fract(beg.x);
+ float area_n = 1.0 - fract(beg.y);
+ float area_e = fract(end.x);
+ float area_s = fract(end.y);
+
+ // Compute the areas of the corner pixels in the pixel box.
+ float area_nw = area_n * area_w;
+ float area_ne = area_n * area_e;
+ float area_sw = area_s * area_w;
+ float area_se = area_s * area_e;
+
+ // Initialize the color accumulator.
+ vec4 avg_color = vec4(0.0, 0.0, 0.0, 0.0);
+
+ // Accumulate corner pixels.
+ avg_color += area_nw * texelFetch(Source, ivec2(f_beg.x, f_beg.y), 0);
+ avg_color += area_ne * texelFetch(Source, ivec2(f_end.x, f_beg.y), 0);
+ avg_color += area_sw * texelFetch(Source, ivec2(f_beg.x, f_end.y), 0);
+ avg_color += area_se * texelFetch(Source, ivec2(f_end.x, f_end.y), 0);
+
+ // Determine the size of the pixel box.
+ int x_range = int(f_end.x - f_beg.x - 0.5);
+ int y_range = int(f_end.y - f_beg.y - 0.5);
+
+ // Accumulate top and bottom edge pixels.
+ for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x)
+ {
+ avg_color += area_n * texelFetch(Source, ivec2(x, f_beg.y), 0);
+ avg_color += area_s * texelFetch(Source, ivec2(x, f_end.y), 0);
+ }
+
+ // Accumulate left and right edge pixels and all the pixels in between.
+ for (int y = f_beg.y + 1; y <= f_beg.y + y_range; ++y)
+ {
+ avg_color += area_w * texelFetch(Source, ivec2(f_beg.x, y), 0);
+ avg_color += area_e * texelFetch(Source, ivec2(f_end.x, y), 0);
+
+ for (int x = f_beg.x + 1; x <= f_beg.x + x_range; ++x)
+ {
+ avg_color += texelFetch(Source, ivec2(x, y), 0);
+ }
+ }
+
+ // Compute the area of the pixel box that was sampled.
+ float area_corners = area_nw + area_ne + area_sw + area_se;
+ float area_edges = float(x_range) * (area_n + area_s) + float(y_range) * (area_w + area_e);
+ float area_center = float(x_range) * float(y_range);
+
+ // Return the normalized average color.
+ return avg_color / (area_corners + area_edges + area_center);
+}
+
+float insideBox(vec2 v, vec2 bLeft, vec2 tRight) {
+ vec2 s = step(bLeft, v) - step(tRight, v);
+ return s.x * s.y;
+}
+
+vec2 translateDest(vec2 pos) {
+ vec2 translatedPos = vec2(pos.x, pos.y);
+ translatedPos.x = dstX1 < dstX0 ? dstX1 - translatedPos.x : translatedPos.x;
+ translatedPos.y = dstY0 < dstY1 ? dstY1 + dstY0 - translatedPos.y - 1 : translatedPos.y;
+ return translatedPos;
+}
+
+void main()
+{
+ vec2 bLeft = vec2(dstX0 < dstX1 ? dstX0 : dstX1, dstY0 < dstY1 ? dstY0 : dstY1);
+ vec2 tRight = vec2(dstX1 > dstX0 ? dstX1 : dstX0, dstY1 > dstY0 ? dstY1 : dstY0);
+ ivec2 loc = ivec2(gl_GlobalInvocationID.x, gl_GlobalInvocationID.y);
+ if (insideBox(loc, bLeft, tRight) == 0) {
+ imageStore(imgOutput, loc, vec4(0, 0, 0, 1));
+ return;
+ }
+
+ vec4 outColor = AreaSampling(loc);
+ imageStore(imgOutput, ivec2(translateDest(loc)), vec4(outColor.rgb, 1));
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv b/src/Ryujinx.Graphics.Vulkan/Effects/Shaders/AreaScaling.spv
new file mode 100644
index 0000000000000000000000000000000000000000..7d097280f0c781e90318832e0a3cc8463a8beb08
GIT binary patch
literal 12428
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
C(9DGZ
literal 0
HcmV?d00001
diff --git a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
index f6a7be91e..aae28733f 100644
--- a/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
+++ b/src/Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj
@@ -15,6 +15,7 @@
+
diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs
index d67362be3..3dc6d4e19 100644
--- a/src/Ryujinx.Graphics.Vulkan/Window.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Window.cs
@@ -568,6 +568,13 @@ namespace Ryujinx.Graphics.Vulkan
_scalingFilter.Level = _scalingFilterLevel;
break;
+ case ScalingFilter.Area:
+ if (_scalingFilter is not AreaScalingFilter)
+ {
+ _scalingFilter?.Dispose();
+ _scalingFilter = new AreaScalingFilter(_gd, _device);
+ }
+ break;
}
}
}
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 3031dea0d..b3cab7f5f 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -758,10 +758,11 @@
"GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.",
"GraphicsAALabel": "Anti-Aliasing:",
"GraphicsScalingFilterLabel": "Scaling Filter:",
- "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.",
+ "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nArea scaling is recommended when downscaling resolutions that are larger than the output window. It can be used to achieve a supersampled anti-aliasing effect when downscaling by more than 2x.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.",
"GraphicsScalingFilterBilinear": "Bilinear",
"GraphicsScalingFilterNearest": "Nearest",
"GraphicsScalingFilterFsr": "FSR",
+ "GraphicsScalingFilterArea": "Area",
"GraphicsScalingFilterLevelLabel": "Level",
"GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.",
"SmaaLow": "SMAA Low",
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
index 5cffc6848..0a12575ad 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml
@@ -1,4 +1,4 @@
-
+
+
+