From 0c74a495f534078487ae5413a17d53ba84e70b8c Mon Sep 17 00:00:00 2001 From: lizzie Date: Sun, 24 May 2026 01:38:19 +0200 Subject: [PATCH] [video_core/host_shaders] add Snapdragon GSRv1 fragment shaders (#3307) Signed-off-by: lizzie Co-authored-by: CamilleLaVey Co-authored-by: xbzk Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3307 Reviewed-by: MaranBr Reviewed-by: CamilleLaVey --- docs/user/Graphics.md | 4 + .../settings/ui/SettingsFragmentPresenter.kt | 21 ++- .../yuzu_emu/fragments/EmulationFragment.kt | 23 ++- .../app/src/main/res/values/arrays.xml | 4 + .../app/src/main/res/values/strings.xml | 6 +- src/common/settings_enums.h | 2 +- src/qt_common/config/shared_translation.cpp | 4 +- src/qt_common/config/shared_translation.h | 2 + src/video_core/CMakeLists.txt | 2 + src/video_core/host_shaders/CMakeLists.txt | 7 +- src/video_core/host_shaders/sgsr1_shader.vert | 19 +++ .../host_shaders/sgsr1_shader_mobile.frag | 82 ++++++++++ .../sgsr1_shader_mobile_edge_direction.frag | 115 ++++++++++++++ .../renderer_opengl/gl_blit_screen.cpp | 4 +- .../renderer_vulkan/present/layer.cpp | 12 +- .../renderer_vulkan/present/layer.h | 3 +- .../renderer_vulkan/present/sgsr.cpp | 143 ++++++++++++++++++ src/video_core/renderer_vulkan/present/sgsr.h | 50 ++++++ .../renderer_vulkan/vk_blit_screen.cpp | 2 + src/yuzu/configuration/configure_graphics.cpp | 2 +- src/yuzu/main_window.cpp | 3 +- 21 files changed, 483 insertions(+), 27 deletions(-) create mode 100644 src/video_core/host_shaders/sgsr1_shader.vert create mode 100644 src/video_core/host_shaders/sgsr1_shader_mobile.frag create mode 100644 src/video_core/host_shaders/sgsr1_shader_mobile_edge_direction.frag create mode 100644 src/video_core/renderer_vulkan/present/sgsr.cpp create mode 100644 src/video_core/renderer_vulkan/present/sgsr.h diff --git a/docs/user/Graphics.md b/docs/user/Graphics.md index ad359b9049..4d3b1da3c7 100644 --- a/docs/user/Graphics.md +++ b/docs/user/Graphics.md @@ -44,6 +44,10 @@ Various graphical filters exist - each of them aimed at a specific target/image - **MMPX**: Nearest-neighbour filter aimed at providing higher pixel-art quality. - **Pros**: Offers decent pixel-art upscaling. - **Cons**: Only works for pixel-art. +- **SGSR**: Uses Snapdragon Studios Game Super Resolution to enhance image quality (similar to FSR, but for Adreno devices). + - **Pros**: Optimized for Adreno devices. + - **Cons**: Doesn't play nicely with non-Adreno devices. +- **SGSR Edge**: Almost the same pipeline as SGSR, but with improved edge detection. ### Anisotropy values diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 92a4a19fcd..161579927c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -76,18 +76,25 @@ class SettingsFragmentPresenter( } } - private fun isFsrScalingFilterSelected(): Boolean { - val fsrFilterValue = resolveFsrScalingFilterValue() ?: return false + private fun isSharpnessScalingFilterSelected(): Boolean { val needsGlobal = getNeedsGlobalForKey(IntSetting.RENDERER_SCALING_FILTER.key) val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal) - return selectedFilter == fsrFilterValue + return selectedFilter in resolveSharpnessScalingFilterValues() } - private fun resolveFsrScalingFilterValue(): Int? { + private fun resolveSharpnessScalingFilterValues(): Set { val names = context.resources.getStringArray(R.array.rendererScalingFilterNames) val values = context.resources.getIntArray(R.array.rendererScalingFilterValues) - val fsrIndex = names.indexOf(context.getString(R.string.scaling_filter_fsr)) - return if (fsrIndex in values.indices) values[fsrIndex] else null + val sharpnessFilterNames = setOf( + context.getString(R.string.scaling_filter_fsr), + context.getString(R.string.scaling_filter_sgsr), + context.getString(R.string.scaling_filter_sgsr_edge), + ) + return names.asSequence() + .mapIndexedNotNull { index, name -> + if (name in sharpnessFilterNames && index in values.indices) values[index] else null + } + .toSet() } // Allows you to show/hide abstract settings based on the paired setting key @@ -267,7 +274,7 @@ class SettingsFragmentPresenter( add(IntSetting.RENDERER_RESOLUTION.key) add(IntSetting.RENDERER_VSYNC.key) add(IntSetting.RENDERER_SCALING_FILTER.key) - if (isFsrScalingFilterSelected()) { + if (isSharpnessScalingFilterSelected()) { add(IntSetting.FSR_SHARPENING_SLIDER.key) } add(IntSetting.RENDERER_ANTI_ALIASING.key) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt index 120bafdd14..0069e169b3 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt @@ -1090,7 +1090,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { private fun addQuickSettings() { binding.quickSettingsSheet.apply { val container = binding.quickSettingsSheet.findViewById(R.id.quick_settings_container) - val isFsrSelected = isFsrScalingFilterSelected() + val isSharpnessFilterSelected = isSharpnessScalingFilterSelected() container.removeAllViews() @@ -1176,7 +1176,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { addQuickSettings() } - if (isFsrSelected) { + if (isSharpnessFilterSelected) { quickSettings.addSliderSetting( R.string.fsr_sharpness, container, @@ -1197,17 +1197,24 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } - private fun isFsrScalingFilterSelected(): Boolean { - val fsrFilterValue = resolveFsrScalingFilterValue() ?: return false + private fun isSharpnessScalingFilterSelected(): Boolean { val selectedFilter = IntSetting.RENDERER_SCALING_FILTER.getInt(needsGlobal = false) - return selectedFilter == fsrFilterValue + return selectedFilter in resolveSharpnessScalingFilterValues() } - private fun resolveFsrScalingFilterValue(): Int? { + private fun resolveSharpnessScalingFilterValues(): Set { val names = resources.getStringArray(R.array.rendererScalingFilterNames) val values = resources.getIntArray(R.array.rendererScalingFilterValues) - val fsrIndex = names.indexOf(getString(R.string.scaling_filter_fsr)) - return if (fsrIndex in values.indices) values[fsrIndex] else null + val sharpnessFilterNames = setOf( + getString(R.string.scaling_filter_fsr), + getString(R.string.scaling_filter_sgsr), + getString(R.string.scaling_filter_sgsr_edge), + ) + return names.asSequence() + .mapIndexedNotNull { index, name -> + if (name in sharpnessFilterNames && index in values.indices) values[index] else null + } + .toSet() } private fun openQuickSettingsMenu() { diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml index 99838b98cc..f3a2a069e7 100644 --- a/src/android/app/src/main/res/values/arrays.xml +++ b/src/android/app/src/main/res/values/arrays.xml @@ -240,6 +240,8 @@ @string/scaling_filter_bspline @string/scaling_filter_mitchell @string/scaling_filter_spline1 + @string/scaling_filter_sgsr + @string/scaling_filter_sgsr_edge @@ -256,6 +258,8 @@ 10 11 12 + 13 + 14 diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 815660fe5c..77be250537 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -468,8 +468,8 @@ Resolution (Handheld/Docked) VSync mode Window adapting filter - FSR sharpness - Determines how sharpened the image will look while using FSR\'s dynamic contrast + FSR/SGSR sharpness + Determines how sharpened the image will look while using FSR or SGSR filters Anti-aliasing method @@ -1080,6 +1080,8 @@ B-Spline Mitchell MMPX + Snapdragon GSR + Snapdragon GSR EdgeDir None diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 9a406e432b..da142e8e1c 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -145,7 +145,7 @@ ENUM(ConfirmStop, Ask_Always, Ask_Based_On_Game, Ask_Never); ENUM(FullscreenMode, Borderless, Exclusive); ENUM(NvdecEmulation, Off, Cpu, Gpu); ENUM(ResolutionSetup, Res1_4X, Res1_2X, Res3_4X, Res1X, Res5_4X, Res3_2X, Res2X, Res3X, Res4X, Res5X, Res6X, Res7X, Res8X); -ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, MaxEnum); +ENUM(ScalingFilter, NearestNeighbor, Bilinear, Bicubic, Gaussian, Lanczos, ScaleForce, Fsr, Area, ZeroTangent, BSpline, Mitchell, Spline1, Mmpx, Sgsr, SgsrEdge, MaxEnum); ENUM(AntiAliasing, None, Fxaa, Smaa, MaxEnum); ENUM(AspectRatio, R16_9, R4_3, R21_9, R16_10, Stretch); ENUM(ConsoleMode, Handheld, Docked); diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 2429a2632f..5c63732a3e 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -149,7 +149,7 @@ std::unique_ptr InitializeTranslations(QObject* parent) { "Options lower than 1X can cause artifacts.")); INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QString()); INSERT(Settings, fsr_sharpening_slider, tr("FSR Sharpness:"), - tr("Determines how sharpened the image will look using FSR's dynamic contrast.")); + tr("Determines how sharpened the image will look using FSR's or SGSR's dynamic contrast.")); 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.")); @@ -492,6 +492,8 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, BSpline, tr("B-Spline")), PAIR(ScalingFilter, Mitchell, tr("Mitchell")), PAIR(ScalingFilter, Spline1, tr("Spline-1")), + PAIR(ScalingFilter, Sgsr, tr("Snapdragon Game Super Resolution")), + PAIR(ScalingFilter, SgsrEdge, tr("Snapdragon Game Super Resolution EdgeDir")), }}); translations->insert({Settings::EnumMetadata::Index(), { diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index 6529c7bf40..c34b5162c4 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -53,6 +53,8 @@ static const std::map scaling_filter_texts_map {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR"))}, {Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Area"))}, {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, + {Settings::ScalingFilter::Sgsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR"))}, + {Settings::ScalingFilter::SgsrEdge, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR EdgeDir"))}, }; static const std::map use_docked_mode_texts_map = { diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index be53b71393..53b0d1638b 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -130,6 +130,8 @@ add_library(video_core STATIC renderer_vulkan/present/present_push_constants.h renderer_vulkan/present/smaa.cpp renderer_vulkan/present/smaa.h + renderer_vulkan/present/sgsr.cpp + renderer_vulkan/present/sgsr.h renderer_vulkan/present/util.cpp renderer_vulkan/present/util.h renderer_vulkan/present/window_adapt_pass.cpp diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 60b399ccba..9e8d76b104 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -76,6 +76,11 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_quad_indexed.comp ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_turbo_mode.comp ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_uint8.comp + + # Snapdragon Game Super Resolution + ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader.vert + ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile.frag + ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag ) if (PLATFORM_HAIKU) @@ -90,7 +95,7 @@ if ("${GLSLANGVALIDATOR}" STREQUAL "GLSLANGVALIDATOR-NOTFOUND") message(FATAL_ERROR "Required program `glslangValidator` not found.") endif() -set(GLSL_FLAGS "") +set(GLSL_FLAGS "-DUseUniformBlock=1") set(SPIR_V_VERSION "spirv1.3") set(QUIET_FLAG "--quiet") diff --git a/src/video_core/host_shaders/sgsr1_shader.vert b/src/video_core/host_shaders/sgsr1_shader.vert new file mode 100644 index 0000000000..0d2a935a9d --- /dev/null +++ b/src/video_core/host_shaders/sgsr1_shader.vert @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 450 + +layout(push_constant) uniform constants { + vec2 scale; + vec2 size; + vec2 resize_factor; + float edge_sharpness; +}; +layout(location = 0) out highp vec2 texcoord; + +void main() { + float x = float((gl_VertexIndex & 1) << 2); + float y = float((gl_VertexIndex & 2) << 1); + gl_Position = vec4(x - 1.0f, y - 1.0f, 0.0, 1.0f) * vec4(sign(resize_factor), 1.f, 1.f); + texcoord = vec2(x, y) * abs(resize_factor) * 0.5; +} diff --git a/src/video_core/host_shaders/sgsr1_shader_mobile.frag b/src/video_core/host_shaders/sgsr1_shader_mobile.frag new file mode 100644 index 0000000000..2e62d60af3 --- /dev/null +++ b/src/video_core/host_shaders/sgsr1_shader_mobile.frag @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +#version 460 core + +precision highp float; +precision highp int; + +// Operation modes: RGBA -> 1, RGBY -> 3, LERP -> 4 +#define OPERATION_MODE 1 +#define EDGE_THRESHOLD (8.0 / 255.0) + +layout(push_constant) uniform constants { + vec2 scale; + vec2 size; + vec2 resize_factor; + float edge_sharpness; +}; +layout(set = 0, binding = 0) uniform sampler2D sampler0; +layout(location=0) in vec2 texcoord; +layout(location=0) out vec4 frag_color; + +vec4 weightY(vec4 dx, vec4 dy, vec4 std) { + vec4 x = ((dx * dx) + (dy * dy)) * 0.55f + std; + return (x - 1.f) * (x - 4.f) * 3.8125f; // approx. of (x - 1) * (x - 4)^3 +} + +void main() { + vec4 color = textureLod(sampler0, texcoord.xy, 0.0f); + // image coord + vec2 icoord = (texcoord * size + vec2(-0.5f, 0.5f)); + vec2 icoord_pixel = floor(icoord); + vec2 coord = icoord_pixel * scale; + vec2 pl = icoord - icoord_pixel; + // left: 0, right: 1, upDown: 2 + mat3x4 dg = mat3x4( + textureGather(sampler0, coord, 1), + textureGather(sampler0, coord + vec2(2.f * scale.x, 0.0f), 1), + vec4( + textureGather(sampler0, coord + vec2(scale.x, -scale.y), 1).wz, + textureGather(sampler0, coord + vec2(scale.x, +scale.y), 1).yx + ) + ); + float edgeVote = abs(dg[0].z - dg[0].y) + abs(color.y - dg[0].y) + abs(color.y - dg[0].z); + if (edgeVote > EDGE_THRESHOLD) { + float mean = (dg[0].y + dg[0].z + dg[1].x + dg[1].w) * 0.25f; + dg = dg - mean; + vec4 sum = abs(dg[0]) + abs(dg[1]) + abs(dg[2]); + float std = 2.181818f / (sum.x + sum.y + sum.z + sum.w); + mat2x4 w = mat2x4( + weightY( + pl.xxxx + vec4(+1.0f, +0.0f, +0.0f, +1.0f), + pl.yyyy + vec4(-1.0f, -1.0f, +0.0f, +0.0f), + clamp(abs(dg[0]) * std, 0.0f, 1.0f) + ) + weightY( + pl.xxxx + vec4(-1.0f, -2.0f, -2.0f, -1.0f), + pl.yyyy + vec4(-1.0f, -1.0f, +0.0f, +0.0f), + clamp(abs(dg[1]) * std, 0.0f, 1.0f) + ) + weightY( + pl.xxxx + vec4(+0.0f, -1.0f, -1.0f, +0.0f), + pl.yyyy + vec4(+1.0f, +1.0f, -2.0f, -2.0f), + clamp(abs(dg[2]) * std, 0.0f, 1.0f) + ), + dg[0] + dg[1] + dg[2] + ); + // compute final y with bounds + vec2 yb = vec2( + min(min(dg[0].y, dg[0].z), min(dg[1].x, dg[1].w)), // min + max(max(dg[0].y, dg[0].z), max(dg[1].x, dg[1].w)) // max + ); + vec2 fvy = vec2( + w[0].x + w[0].y + w[0].z + w[0].w, + w[1].x + w[1].y + w[1].z + w[1].w + ); + float fy = clamp((fvy.y / fvy.x) * edge_sharpness, yb[0], yb[1]); + // Smooth high contrast input + float dy = clamp(fy - color.y + mean, -23.0f / 255.0f, 23.0f / 255.0f); + color = clamp(color + dy, 0.0f, 1.0f); + } + color.w = 1.0f; //assume alpha channel is not used + frag_color.xyzw = color; +} \ No newline at end of file diff --git a/src/video_core/host_shaders/sgsr1_shader_mobile_edge_direction.frag b/src/video_core/host_shaders/sgsr1_shader_mobile_edge_direction.frag new file mode 100644 index 0000000000..10a8ff8e00 --- /dev/null +++ b/src/video_core/host_shaders/sgsr1_shader_mobile_edge_direction.frag @@ -0,0 +1,115 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved. +// SPDX-License-Identifier: BSD-3-Clause + +#version 460 core + +//precision float; +//precision int; + +// Operation modes: RGBA -> 1, RGBY -> 3, LERP -> 4 +#define OperationMode 1 +#define EdgeThreshold 8.0/255.0 + +layout( push_constant ) uniform constants { + vec4 ViewportInfo[1]; + vec2 ResizeFactor; + float EdgeSharpness; +}; +layout(set = 0, binding = 0) uniform sampler2D ps0; +layout(location=0) in vec2 in_TEXCOORD0; +layout(location=0) out vec4 out_Target0; + +float fastLanczos2(float x) { + float wA = x-4.0; + float wB = x*wA-wA; + wA *= wA; + return wB*wA; +} + +vec2 weightY(float dx, float dy, float c, vec3 data) { + float std = data.x; + vec2 dir = data.yz; + float edgeDis = ((dx*dir.y)+(dy*dir.x)); + float x = (((dx*dx)+(dy*dy))+((edgeDis*edgeDis)*((clamp(((c*c)*std),0.0,1.0)*0.7)+-1.0))); + float w = fastLanczos2(x); + return vec2(w, w * c); +} + +vec2 edgeDirection(vec4 left, vec4 right) { + vec2 dir; + float RxLz = (right.x + (-left.z)); + float RwLy = (right.w + (-left.y)); + vec2 delta; + delta.x = (RxLz + RwLy); + delta.y = (RxLz + (-RwLy)); + float lengthInv = inversesqrt((delta.x * delta.x+ 3.075740e-05) + (delta.y * delta.y)); + dir.x = (delta.x * lengthInv); + dir.y = (delta.y * lengthInv); + return dir; +} + +void main() { + vec4 color; + if(OperationMode == 1) + color.xyz = textureLod(ps0, in_TEXCOORD0.xy, 0.0).xyz; + else + color.xyzw = textureLod(ps0, in_TEXCOORD0.xy, 0.0).xyzw; + + if ( OperationMode!=4) { + vec2 imgCoord = ((in_TEXCOORD0.xy*ViewportInfo[0].zw)+vec2(-0.5,0.5)); + vec2 imgCoordPixel = floor(imgCoord); + vec2 coord = (imgCoordPixel*ViewportInfo[0].xy); + vec2 pl = imgCoord - imgCoordPixel; + vec4 left = textureGather(ps0, coord, OperationMode); + float edgeVote = abs(left.z - left.y) + abs(color[OperationMode] - left.y) + abs(color[OperationMode] - left.z) ; + if(edgeVote > EdgeThreshold) { + coord.x += ViewportInfo[0].x; + + vec2 IR_highp_vec2_0 = coord + vec2(ViewportInfo[0].x, 0.0); + vec4 right = textureGather(ps0, IR_highp_vec2_0, OperationMode); + vec4 upDown; + vec2 IR_highp_vec2_1 = coord + vec2(0.0, -ViewportInfo[0].y); + upDown.xy = textureGather(ps0, IR_highp_vec2_1, OperationMode).wz; + vec2 IR_highp_vec2_2 = coord + vec2(0.0, ViewportInfo[0].y); + upDown.zw = textureGather(ps0, IR_highp_vec2_2, OperationMode).yx; + + float mean = (left.y+left.z+right.x+right.w)*0.25; + left = left - vec4(mean); + right = right - vec4(mean); + upDown = upDown - vec4(mean); + color.w =color[OperationMode] - mean; + + float sum = (((((abs(left.x)+abs(left.y))+abs(left.z))+abs(left.w))+(((abs(right.x)+abs(right.y))+abs(right.z))+abs(right.w)))+(((abs(upDown.x)+abs(upDown.y))+abs(upDown.z))+abs(upDown.w))); + float sumMean = 1.014185e+01/sum; + float std = (sumMean*sumMean); + + vec3 data = vec3(std, edgeDirection(left, right)); + vec2 aWY = weightY(pl.x, pl.y+1.0, upDown.x,data); + aWY += weightY(pl.x-1.0, pl.y+1.0, upDown.y,data); + aWY += weightY(pl.x-1.0, pl.y-2.0, upDown.z,data); + aWY += weightY(pl.x, pl.y-2.0, upDown.w,data); + aWY += weightY(pl.x+1.0, pl.y-1.0, left.x,data); + aWY += weightY(pl.x, pl.y-1.0, left.y,data); + aWY += weightY(pl.x, pl.y, left.z,data); + aWY += weightY(pl.x+1.0, pl.y, left.w,data); + aWY += weightY(pl.x-1.0, pl.y-1.0, right.x,data); + aWY += weightY(pl.x-2.0, pl.y-1.0, right.y,data); + aWY += weightY(pl.x-2.0, pl.y, right.z,data); + aWY += weightY(pl.x-1.0, pl.y, right.w,data); + + float finalY = aWY.y/aWY.x; + float maxY = max(max(left.y,left.z),max(right.x,right.w)); + float minY = min(min(left.y,left.z),min(right.x,right.w)); + float deltaY = clamp(EdgeSharpness*finalY, minY, maxY) -color.w; + + //smooth high contrast input + deltaY = clamp(deltaY, -23.0 / 255.0, 23.0 / 255.0); + + color.x = clamp((color.x+deltaY),0.0,1.0); + color.y = clamp((color.y+deltaY),0.0,1.0); + color.z = clamp((color.z+deltaY),0.0,1.0); + } + } + color.w = 1.0; //assume alpha channel is not used + out_Target0.xyzw = color; +} \ No newline at end of file diff --git a/src/video_core/renderer_opengl/gl_blit_screen.cpp b/src/video_core/renderer_opengl/gl_blit_screen.cpp index 4b75e1b949..23e5eb7481 100644 --- a/src/video_core/renderer_opengl/gl_blit_screen.cpp +++ b/src/video_core/renderer_opengl/gl_blit_screen.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2024 Torzu Emulator Project @@ -115,6 +115,8 @@ void BlitScreen::CreateWindowAdapt() { window_adapt = MakeMmpx(device); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Sgsr: + case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: default: window_adapt = MakeBilinear(device); diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index b462c672cc..e20473d2af 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -15,6 +15,7 @@ #include "common/settings.h" #include "video_core/framebuffer_config.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" #include "video_core/renderer_vulkan/present/present_push_constants.h" @@ -63,7 +64,11 @@ Layer::Layer(const Device& device_, MemoryAllocator& memory_allocator_, Schedule CreateDescriptorPool(); CreateDescriptorSets(layout); if (filters.get_scaling_filter() == Settings::ScalingFilter::Fsr) { - fsr.emplace(device, memory_allocator, image_count, output_size); + sr_filter.emplace(device, memory_allocator, image_count, output_size); + } else if (filters.get_scaling_filter() == Settings::ScalingFilter::Sgsr) { + sr_filter.emplace(device, memory_allocator, image_count, output_size, false); + } else if (filters.get_scaling_filter() == Settings::ScalingFilter::SgsrEdge) { + sr_filter.emplace(device, memory_allocator, image_count, output_size, true); } } @@ -114,9 +119,12 @@ void Layer::ConfigureDraw(PresentPushConstants* out_push_constants, .height = scaled_height, }; - if (fsr) { + if (auto* fsr = std::get_if(&sr_filter)) { source_image_view = fsr->Draw(scheduler, image_index, source_image, source_image_view, render_extent, crop_rect); crop_rect = {0, 0, 1, 1}; + } else if (auto* sgsr = std::get_if(&sr_filter)) { + source_image_view = sgsr->Draw(scheduler, image_index, source_image, source_image_view, render_extent, crop_rect); + crop_rect = {0, 0, 1, 1}; } SetMatrixData(*out_push_constants, layout); diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index d38b81823e..47a6a69218 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -13,6 +13,7 @@ #include "video_core/host1x/gpu_device_memory_manager.h" #include "video_core/vulkan_common/vulkan_wrapper.h" #include "video_core/renderer_vulkan/present/fsr.h" +#include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/smaa.h" @@ -95,7 +96,7 @@ private: Settings::AntiAliasing anti_alias_setting{}; std::variant anti_alias{}; - std::optional fsr{}; + std::variant sr_filter{}; std::vector resource_ticks{}; }; diff --git a/src/video_core/renderer_vulkan/present/sgsr.cpp b/src/video_core/renderer_vulkan/present/sgsr.cpp new file mode 100644 index 0000000000..4175190690 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/sgsr.cpp @@ -0,0 +1,143 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/common_types.h" +#include "common/div_ceil.h" +#include "common/settings.h" + +//#include "video_core/sgsr.h" +#include "video_core/host_shaders/sgsr1_shader_mobile_frag_spv.h" +#include "video_core/host_shaders/sgsr1_shader_mobile_edge_direction_frag_spv.h" +#include "video_core/host_shaders/sgsr1_shader_vert_spv.h" +#include "video_core/renderer_vulkan/present/sgsr.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_shader_util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +using PushConstants = std::array; + +SGSR::SGSR(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, VkExtent2D extent, bool edge_dir) + : m_device{device} + , m_memory_allocator{memory_allocator} + , m_image_count{image_count} + , m_extent{extent} + , m_edge_dir{edge_dir} +{ + // Not finished yet initializing at ctor time? + m_dynamic_images.resize(m_image_count); + for (auto& images : m_dynamic_images) { + images.image = CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); + images.image_view = CreateWrappedImageView(m_device, images.image, VK_FORMAT_R16G16B16A16_SFLOAT); + } + + m_renderpass = CreateWrappedRenderPass(m_device, VK_FORMAT_R16G16B16A16_SFLOAT); + for (auto& images : m_dynamic_images) + images.framebuffer = CreateWrappedFramebuffer(m_device, m_renderpass, images.image_view, m_extent); + + m_sampler = CreateBilinearSampler(m_device); + m_vert_shader = BuildShader(m_device, SGSR1_SHADER_VERT_SPV); + m_stage_shader = m_edge_dir + ? BuildShader(m_device, SGSR1_SHADER_MOBILE_EDGE_DIRECTION_FRAG_SPV) + : BuildShader(m_device, SGSR1_SHADER_MOBILE_FRAG_SPV); + // 2 descriptors, 2 descriptor sets per invocation + m_descriptor_pool = CreateWrappedDescriptorPool(m_device, m_image_count, m_image_count); + m_descriptor_set_layout = CreateWrappedDescriptorSetLayout(m_device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); + + VkDescriptorSetLayout layout = *m_descriptor_set_layout; + for (auto& images : m_dynamic_images) + images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layout); + + const VkPushConstantRange range{ + .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + .offset = 0, + .size = sizeof(PushConstants), + }; + VkPipelineLayoutCreateInfo ci{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = 1, + .pSetLayouts = m_descriptor_set_layout.address(), + .pushConstantRangeCount = 1, + .pPushConstantRanges = &range, + }; + m_pipeline_layout = m_device.GetLogical().CreatePipelineLayout(ci); + m_stage_pipeline = CreateWrappedPipeline(m_device, m_renderpass, m_pipeline_layout, std::tie(m_vert_shader, m_stage_shader)); +} + +void SGSR::UpdateDescriptorSets(VkImageView image_view, size_t image_index) { + Images& images = m_dynamic_images[image_index]; + std::vector image_infos; + std::vector updates; + image_infos.reserve(1); + updates.push_back(CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, images.descriptor_sets[0], 0)); + m_device.GetLogical().UpdateDescriptorSets(updates, {}); +} + +void SGSR::UploadImages(Scheduler& scheduler) { + if (!m_images_ready) { + scheduler.Record([&](vk::CommandBuffer cmdbuf) { + for (auto& image : m_dynamic_images) + ClearColorImage(cmdbuf, *image.image); + }); + scheduler.Finish(); + m_images_ready = true; + } +} + +VkImageView SGSR::Draw(Scheduler& scheduler, size_t image_index, VkImage source_image, VkImageView source_image_view, VkExtent2D input_image_extent, const Common::Rectangle& crop_rect) { + Images& images = m_dynamic_images[image_index]; + auto const output_image = *images.image; + auto const descriptor_set = images.descriptor_sets[0]; + auto const framebuffer = *images.framebuffer; + auto const pipeline = *m_stage_pipeline; + + VkPipelineLayout layout = *m_pipeline_layout; + VkRenderPass renderpass = *m_renderpass; + VkExtent2D extent = m_extent; + + const f32 input_image_width = f32(input_image_extent.width); + const f32 input_image_height = f32(input_image_extent.height); + const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; + const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; + // expected [0, 2] + const f32 sharpening = f32(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f; + + // p = (tex * viewport) / input = [0,n] (normalized texcoords) + // p * input = [0,1024], [0,768] + // layout( push_constant ) uniform constants { + // highp vec4 ViewportInfo[1]; + // highp vec2 ResizeFactor; + // highp float EdgeSharpness; + // }; + PushConstants viewport_con{}; + viewport_con[0] = std::bit_cast(std::abs(1.f / viewport_width)); + viewport_con[1] = std::bit_cast(std::abs(1.f / viewport_height)); + viewport_con[2] = std::bit_cast(std::abs(viewport_width)); + viewport_con[3] = std::bit_cast(std::abs(viewport_height)); + viewport_con[4] = std::bit_cast(viewport_width / input_image_width); + viewport_con[5] = std::bit_cast(viewport_height / input_image_height); + viewport_con[6] = std::bit_cast(sharpening); + + UploadImages(scheduler); + UpdateDescriptorSets(source_image_view, image_index); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, output_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, descriptor_set, {}); + cmdbuf.PushConstants(layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, viewport_con); + cmdbuf.Draw(3, 1, 0, 0); + cmdbuf.EndRenderPass(); + TransitionImageLayout(cmdbuf, output_image, VK_IMAGE_LAYOUT_GENERAL); + }); + return *images.image_view; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/sgsr.h b/src/video_core/renderer_vulkan/present/sgsr.h new file mode 100644 index 0000000000..67b25be75d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/sgsr.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "common/math_util.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 SGSR { +public: + static constexpr size_t SGSR_STAGE_COUNT = 1; + explicit SGSR(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, VkExtent2D extent, bool edge_dir); + VkImageView Draw(Scheduler& scheduler, size_t image_index, VkImage source_image, VkImageView source_image_view, VkExtent2D input_image_extent, const Common::Rectangle& crop_rect); +private: + void Initialize(); + void UploadImages(Scheduler& scheduler); + void UpdateDescriptorSets(VkImageView image_view, size_t image_index); + + const Device& m_device; + MemoryAllocator& m_memory_allocator; + const size_t m_image_count; + const VkExtent2D m_extent; + + vk::DescriptorPool m_descriptor_pool; + vk::DescriptorSetLayout m_descriptor_set_layout; + vk::PipelineLayout m_pipeline_layout; + vk::ShaderModule m_vert_shader; + vk::ShaderModule m_stage_shader; + vk::Pipeline m_stage_pipeline; + vk::RenderPass m_renderpass; + vk::Sampler m_sampler; + + struct Images { + vk::DescriptorSets descriptor_sets; + vk::Image image; + vk::ImageView image_view; + vk::Framebuffer framebuffer; + }; + std::vector m_dynamic_images; + bool m_images_ready{}; + bool m_edge_dir{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_blit_screen.cpp b/src/video_core/renderer_vulkan/vk_blit_screen.cpp index 75a8c3bf91..57f096db79 100644 --- a/src/video_core/renderer_vulkan/vk_blit_screen.cpp +++ b/src/video_core/renderer_vulkan/vk_blit_screen.cpp @@ -77,6 +77,8 @@ void BlitScreen::SetWindowAdaptPass() { window_adapt = MakeMmpx(device, swapchain_view_format); break; case Settings::ScalingFilter::Fsr: + case Settings::ScalingFilter::Sgsr: + case Settings::ScalingFilter::SgsrEdge: case Settings::ScalingFilter::Bilinear: default: window_adapt = MakeBilinear(device, swapchain_view_format); diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp index 344cbe2406..f294403397 100644 --- a/src/yuzu/configuration/configure_graphics.cpp +++ b/src/yuzu/configuration/configure_graphics.cpp @@ -221,7 +221,7 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) { // FSR needs a reversed slider and a 0.5 multiplier return builder.BuildWidget( setting, apply_funcs, ConfigurationShared::RequestType::ReverseSlider, true, - 0.5f, nullptr, tr("%", "FSR sharpening percentage (e.g. 50%)")); + 0.5f, nullptr, tr("%", "FSR/SGSR sharpening percentage (e.g. 50%)")); } else { return builder.BuildWidget(setting, apply_funcs); } diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp index 4d5a3c43f9..5d60bd3a8f 100644 --- a/src/yuzu/main_window.cpp +++ b/src/yuzu/main_window.cpp @@ -4164,8 +4164,7 @@ void MainWindow::UpdateAPIText() { void MainWindow::UpdateFilterText() { const auto filter = Settings::values.scaling_filter.GetValue(); const auto filter_text = ConfigurationShared::scaling_filter_texts_map.find(filter)->second; - filter_status_button->setText(filter == Settings::ScalingFilter::Fsr ? tr("FSR") - : filter_text.toUpper()); + filter_status_button->setText(filter_text.toUpper()); } void MainWindow::UpdateAAText() {