SGSR upscaling

Snapdragon Game Super Resolution 1.0, mobile variant: a single-pass
edge-directed spatial upscaler Qualcomm wrote for Adreno. Against FSR1 it is
one dispatch instead of two and one target instead of two, which is what makes
it worth having on a phone -- cheaper, not better.

Licensing is the reason this is a reimplementation rather than a port.
Suggested by CamilleLaVey, who made the same filter work in Eden, but Eden's
glue is GPL-3.0-or-later and RPCS3 is GPL-2.0-ONLY, so none of it is used --
the same blocker that stopped the LSFG adoption, and permission cannot fix it
because Eden has other contributors. What IS used is Qualcomm's BSD-3-Clause
release, which is GPL-2.0 compatible, with the copyright notice kept. The crop
mapping and the widened sharpness range are reimplemented from a description
of what they do, which is not copyrightable.

Qualcomm ship it as a fragment shader over a fullscreen triangle; this is a
compute pass because that is what the VK device layer already schedules. The
interpolated texcoord becomes a UV from the invocation id and the fragment
output becomes an imageStore, with a bounds check because a dispatch rounds up
to whole workgroups.

The push constant offsets were read out of the compiled SPIR-V rather than
derived from the struct -- 0/8/16/24/32/40, 44 bytes -- because a mismatch
there produces garbage that looks exactly like a shader bug. glslc also
type-checks the GLSL, which the native build cannot: shaders here are compiled
at runtime, so a broken one builds fine and fails on device.

No vendor gate, deliberately: its requirements are a strict subset of FSR1's
(textureGather with a constant component and no offset, an rgba8 storage
image, one descriptor set), so anywhere FSR1 runs, this runs. It no-ops when
the frame is already at or above output resolution, which is correct and
indistinguishable from broken, so the setting text says so.

Wired at all five places the mode is represented: the enum (appended, never
inserted -- it is serialised by ordinal in savestates), the fmt_class_string
case (a missing one serialises as 'unknown' and the mode is silently never
selectable), the VK dispatch, both Kotlin pickers, and the persisted clamp
that would otherwise rewrite the new value straight back to FSR.
This commit is contained in:
jpolo1224
2026-08-24 16:46:30 -04:00
parent 13dcec9e66
commit f53a76c0fc
13 changed files with 513 additions and 6 deletions
@@ -1844,7 +1844,7 @@ data class Settings(
put("EmuCore/GS", "fxaa", "bool", fxaa.toString())
// Scaling Mode writes Output Scaling Mode unconditionally, the shader chain only
// when it is on, so CAS has to go first for the chain to keep the last word.
put("EmuCore/GS", "CASMode", "int", casMode.coerceIn(0, 2).toString())
put("EmuCore/GS", "CASMode", "int", casMode.coerceIn(0, 3).toString())
put("EmuCore/GS", "CASSharpness", "int", casSharpness.coerceIn(0, 100).toString())
put("EmuCore/GS", "ShaderChainEnabled", "bool", shaderChainEnabled.toString())
put("EmuCore/GS", "ShaderChainPreset", "string", shaderChainPreset)
@@ -909,6 +909,7 @@ val EN: Map<String, String> = mapOf(
"overlay.toggle.temps.description" to
"Show CPU, GPU and battery temperature on the performance overlay. Not every device exposes these \u2014 one that doesn't simply shows nothing.",
"overlay.tempInterval.label" to "Temperature poll interval",
"renderer.outputScaling.sgsr" to "SGSR",
"pad.rumble.label" to "Rumble / Vibration",
"pad.rumblePhone.label" to "Vibrate the phone",
"pad.rumblePhone.description" to
@@ -884,7 +884,7 @@ private fun GraphicsPane(state: EmulationMenuUiState, viewModel: EmulationMenuVi
title = str("renderer.outputScaling.label"),
options = listOf(
str("renderer.outputScaling.nearest"), str("renderer.outputScaling.bilinear"),
str("renderer.outputScaling.fsr"),
str("renderer.outputScaling.fsr"), str("renderer.outputScaling.sgsr"),
).mapIndexed { index, label -> index to label },
selected = settings.casMode,
onSelect = { v -> viewModel.updateSettings { it.copy(casMode = v) } },
@@ -338,9 +338,13 @@ fun RendererTab(state: MutableState<Settings>) {
str("renderer.outputScaling.nearest"),
str("renderer.outputScaling.bilinear"),
str("renderer.outputScaling.fsr"),
str("renderer.outputScaling.sgsr"),
),
selectedIndex = s.casMode.coerceIn(0, 2),
columns = 3,
// Bound raised with the option. A clamp left at the old maximum silently rewrites
// the new choice back to the previous one, which reads as the setting refusing to
// take.
selectedIndex = s.casMode.coerceIn(0, 3),
columns = 2,
description = str("renderer.outputScaling.description"),
onChange = { apply(s.copy(casMode = it)) },
)
@@ -528,6 +528,9 @@ object Rpcs3Bridge {
when (asInt(value)) {
0 -> "Nearest"
2 -> "FidelityFX Super Resolution"
// 3 skips the librashader chain, which is ordinal 3 in the native enum
// but is driven by its own toggle rather than this picker.
3 -> "Snapdragon Game Super Resolution"
else -> "Bilinear"
},
)
+1
View File
@@ -629,6 +629,7 @@ endif()
if(TARGET 3rdparty_vulkan)
target_sources(rpcs3_emu PRIVATE
RSX/VK/upscalers/fsr1/fsr_pass.cpp
RSX/VK/upscalers/sgsr/sgsr_pass.cpp
RSX/VK/vkutils/barriers.cpp
RSX/VK/vkutils/buffer_object.cpp
RSX/VK/vkutils/chip_class.cpp
@@ -0,0 +1,114 @@
R"(
#version 450
// Snapdragon Game Super Resolution 1.0, "mobile" variant.
//
// SPDX-FileCopyrightText: Copyright (c) 2025, Qualcomm Innovation Center, Inc. All rights reserved.
// SPDX-License-Identifier: BSD-3-Clause
//
// The filter body is Qualcomm's, unchanged in substance. What differs from their sample is the
// shape around it: theirs is a fragment shader over a fullscreen triangle, and this is a compute
// pass, because that is what the VK device layer already schedules (see fsr_ubershader.glsl). So
// the interpolated texcoord becomes an explicit UV computed from the invocation id, and the
// fragment output becomes an imageStore.
//
// Suggested by CamilleLaVey, who made the same filter work in Eden. Eden's glue is
// GPL-3.0-or-later and RPCS3 is GPL-2.0-only, so none of it is used here: the algorithm below is
// Qualcomm's BSD-3-Clause release, which is GPL-2.0 compatible, and the crop mapping and widened
// sharpness range are reimplemented from the description of what they do rather than copied.
#define EDGE_THRESHOLD (8.0 / 255.0)
layout(push_constant) uniform const_buffer
{
// Output extent, for the bounds check. A dispatch is rounded up to whole workgroups, so the
// last one runs partly outside the image.
uvec2 dstSize;
// The displayed region inside the source texture, normalised. The RSX output target is
// larger than the picture in it; without this the filter would upscale the padding too.
vec2 uvOffset;
vec2 uvScale;
// Source texture dimensions and their reciprocal. Qualcomm's "size" and "scale".
vec2 srcSize;
vec2 invSrcSize;
// 0..2. 1.0 is Qualcomm's own default; the range reaches 2.0 because it is too tight to be
// useful at the top end otherwise.
float edgeSharpness;
};
layout(set = 0, binding = 0) uniform sampler2D InputTexture;
layout(set = 0, binding = 1, rgba8) uniform writeonly image2D OutputTexture;
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
}
layout(local_size_x = 8, local_size_y = 8) in;
void main()
{
const uvec2 pos = gl_GlobalInvocationID.xy;
if (pos.x >= dstSize.x || pos.y >= dstSize.y)
return;
// Centre of this output pixel, mapped into the displayed region of the source.
const vec2 texcoord = uvOffset + ((vec2(pos) + vec2(0.5f)) / vec2(dstSize)) * uvScale;
vec4 color = textureLod(InputTexture, texcoord, 0.0f);
// image coord
vec2 icoord = (texcoord * srcSize + vec2(-0.5f, 0.5f));
vec2 icoord_pixel = floor(icoord);
vec2 coord = icoord_pixel * invSrcSize;
vec2 pl = icoord - icoord_pixel;
// left: 0, right: 1, upDown: 2
mat3x4 dg = mat3x4(
textureGather(InputTexture, coord, 1),
textureGather(InputTexture, coord + vec2(2.f * invSrcSize.x, 0.0f), 1),
vec4(
textureGather(InputTexture, coord + vec2(invSrcSize.x, -invSrcSize.y), 1).wz,
textureGather(InputTexture, coord + vec2(invSrcSize.x, +invSrcSize.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) * edgeSharpness, 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
imageStore(OutputTexture, ivec2(pos), color);
}
)"
+7
View File
@@ -11,6 +11,9 @@
#include "upscalers/bilinear_pass.hpp"
#include "upscalers/fsr_pass.h"
#ifdef __ANDROID__
#include "upscalers/sgsr_pass.h"
#endif
#include "upscalers/nearest_pass.hpp"
#ifdef __ANDROID__
#include "upscalers/librashader_pass.h"
@@ -1454,6 +1457,10 @@ void VKGSRender::flip(const rsx::display_flip_info_t& info)
{
m_upscaler = std::make_unique<vk::librashader_upscale_pass>();
}
else if (m_output_scaling == output_scaling_mode::sgsr)
{
m_upscaler = std::make_unique<vk::sgsr_upscale_pass>();
}
#endif
else
{
@@ -0,0 +1,296 @@
#include "../../vkutils/barriers.h"
#include "../../VKHelpers.h"
#include "../../VKResourceManager.h"
#include "../sgsr_pass.h"
#include "Emu/system_config.h"
namespace vk
{
namespace SGSR
{
sgsr_pass::sgsr_pass()
{
const char* shader_core =
#include "Emu/RSX/Program/Upscalers/SGSR/sgsr_shader.glsl"
;
m_src = shader_core;
// Fill with 0 to avoid sending incomplete/unused variables to the GPU
std::fill(m_constants_buf.begin(), m_constants_buf.end(), 0u);
ssbo_count = 0;
use_push_constants = true;
push_constants_size = 44;
create();
}
std::vector<glsl::program_input> sgsr_pass::get_inputs()
{
std::vector<vk::glsl::program_input> inputs =
{
glsl::program_input::make(
::glsl::program_domain::glsl_compute_program,
"InputTexture",
vk::glsl::input_type_texture,
0,
0
),
glsl::program_input::make(
::glsl::program_domain::glsl_compute_program,
"OutputTexture",
vk::glsl::input_type_storage_texture,
0,
1
),
};
auto result = compute_task::get_inputs();
result.insert(result.end(), inputs.begin(), inputs.end());
return result;
}
void sgsr_pass::bind_resources(const vk::command_buffer& /*cmd*/)
{
if (!m_sampler)
{
const auto pdev = vk::get_current_renderer();
m_sampler = std::make_unique<vk::sampler>(*pdev,
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
VK_FALSE, 0.f, 1.f, 0.f, 0.f, VK_FILTER_LINEAR, VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_NEAREST, VK_BORDER_COLOR_FLOAT_OPAQUE_BLACK);
}
m_program->bind_uniform({ *m_input_image, *m_sampler }, 0, 0);
m_program->bind_uniform({ *m_output_image }, 0, 1);
}
void sgsr_pass::run(const vk::command_buffer& cmd,
vk::viewable_image* src,
vk::viewable_image* dst,
const size2u& input_size,
const size2u& output_size,
const f32 uv_offset[2],
const f32 uv_scale[2])
{
m_input_image = src->get_view(rsx::default_remap_vector.with_encoding(VK_REMAP_IDENTITY));
m_output_image = dst->get_view(rsx::default_remap_vector.with_encoding(VK_REMAP_IDENTITY));
m_input_size = input_size;
m_output_size = output_size;
m_uv_offset[0] = uv_offset[0];
m_uv_offset[1] = uv_offset[1];
m_uv_scale[0] = uv_scale[0];
m_uv_scale[1] = uv_scale[1];
// Laid out by hand against the offsets read out of the compiled SPIR-V. Writing it
// through a struct would be tidier and would also be the thing that silently breaks
// if the shader's members are ever reordered.
const f32 src_w = static_cast<f32>(src->width());
const f32 src_h = static_cast<f32>(src->height());
auto write_u32 = [this](usz word, u32 v) { m_constants_buf[word] = v; };
auto write_f32 = [this](usz word, f32 v) { m_constants_buf[word] = std::bit_cast<u32>(v); };
write_u32(0, output_size.width); // dstSize.x @0
write_u32(1, output_size.height); // dstSize.y @4
write_f32(2, m_uv_offset[0]); // uvOffset @8
write_f32(3, m_uv_offset[1]);
write_f32(4, m_uv_scale[0]); // uvScale @16
write_f32(5, m_uv_scale[1]);
write_f32(6, src_w); // srcSize @24
write_f32(7, src_h);
write_f32(8, 1.f / src_w); // invSrcSize @32
write_f32(9, 1.f / src_h);
// Qualcomm's edge_sharpness is 0..2 with 1.0 as their default. The existing 0..100
// sharpening slider maps onto it directly, so 50 is Qualcomm's default and 100 is the
// widened top end -- one control for both upscalers, because they want the same thing
// from the user and a second one only lets them disagree.
write_f32(10, g_cfg.video.rcas_sharpening_intensity / 50.f);
if (!m_program)
{
load_program(cmd);
}
ensure(push_constants_size <= (m_constants_buf.size() * sizeof(decltype(m_constants_buf)::value_type)));
vkCmdPushConstants(cmd, m_program->layout(), VK_SHADER_STAGE_COMPUTE_BIT, 0, push_constants_size, m_constants_buf.data());
// 8x8 workgroups, rounded up. The shader bounds-checks its invocation because of this.
constexpr auto wg_size = 8;
const auto invocations_x = utils::aligned_div(output_size.width, wg_size);
const auto invocations_y = utils::aligned_div(output_size.height, wg_size);
compute_task::run(cmd, invocations_x, invocations_y, 1);
}
}
void sgsr_upscale_pass::dispose_images()
{
auto safe_delete = [](auto& data)
{
if (data) vk::get_resource_manager()->dispose(data);
};
safe_delete(m_output_left);
safe_delete(m_output_right);
}
void sgsr_upscale_pass::initialize_image(u32 output_w, u32 output_h, rsx::flags32_t mode)
{
dispose_images();
const auto pdev = vk::get_current_renderer();
auto initialize_image_impl = [pdev, output_w, output_h](VkImageUsageFlags usage, VkFormat format)
{
return std::make_unique<vk::viewable_image>(
*pdev,
pdev->get_memory_mapping().device_local,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
VK_IMAGE_TYPE_2D,
format,
output_w, output_h, 1, 1, 1, VK_SAMPLE_COUNT_1_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_TILING_OPTIMAL,
usage,
VK_IMAGE_CREATE_ALLOW_NULL_RPCS3,
VMM_ALLOCATION_POOL_SWAPCHAIN,
RSX_FORMAT_CLASS_COLOR);
};
const VkFlags usage_mask_output = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT;
VkFormat data_format = VK_FORMAT_UNDEFINED;
bool failed = true;
std::array<VkFormat, 2> supported_formats = { VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM };
for (const auto& format : supported_formats)
{
const VkFlags all_required_bits = VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_SRC_BIT;
if ((pdev->get_format_properties(format).optimalTilingFeatures & all_required_bits) == all_required_bits)
{
data_format = format;
failed = false;
break;
}
}
if (failed)
{
rsx_log.error("SGSR: no writable output format is available; falling back to bilinear.");
return;
}
if (mode & UPSCALE_LEFT_VIEW)
{
m_output_left = initialize_image_impl(usage_mask_output, data_format);
}
if (mode & UPSCALE_RIGHT_VIEW)
{
m_output_right = initialize_image_impl(usage_mask_output, data_format);
}
}
vk::viewable_image* sgsr_upscale_pass::scale_output(
const vk::command_buffer& cmd,
vk::viewable_image* src,
VkImage present_surface,
VkImageLayout present_surface_layout,
const VkImageBlit& request,
rsx::flags32_t mode)
{
size2u input_size, output_size;
input_size.width = std::abs(request.srcOffsets[1].x - request.srcOffsets[0].x);
input_size.height = std::abs(request.srcOffsets[1].y - request.srcOffsets[0].y);
output_size.width = std::abs(request.dstOffsets[1].x - request.dstOffsets[0].x);
output_size.height = std::abs(request.dstOffsets[1].y - request.dstOffsets[0].y);
auto src_image = src;
auto output_request = request;
// Only does anything when the rendered frame is SMALLER than the output. At or above the
// output resolution there is nothing to reconstruct, so it correctly does nothing -- which
// is indistinguishable from broken from the outside, hence the note in the setting text.
if (input_size.width < output_size.width && input_size.height < output_size.height)
{
ensure((mode & (UPSCALE_LEFT_VIEW | UPSCALE_RIGHT_VIEW)) != (UPSCALE_LEFT_VIEW | UPSCALE_RIGHT_VIEW));
auto& m_output_data = (mode & UPSCALE_LEFT_VIEW) ? m_output_left : m_output_right;
if (!m_output_data || m_output_data->width() != output_size.width || m_output_data->height() != output_size.height)
{
initialize_image(output_size.width, output_size.height, mode);
}
if (m_output_data)
{
auto cs_task = vk::get_compute_task<vk::SGSR::sgsr_pass>();
// The displayed picture is a rectangle inside a larger target, so hand the shader
// the crop rather than letting it assume the whole texture. Taken from the blit
// request, which is the only thing that knows where the picture actually is.
const f32 src_w = static_cast<f32>(src->width());
const f32 src_h = static_cast<f32>(src->height());
const f32 x0 = static_cast<f32>(std::min(request.srcOffsets[0].x, request.srcOffsets[1].x));
const f32 y0 = static_cast<f32>(std::min(request.srcOffsets[0].y, request.srcOffsets[1].y));
const f32 uv_offset[2] = { x0 / src_w, y0 / src_h };
const f32 uv_scale[2] = { input_size.width / src_w, input_size.height / src_h };
src->push_layout(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
m_output_data->change_layout(cmd, VK_IMAGE_LAYOUT_GENERAL);
cs_task->run(cmd, src, m_output_data.get(), input_size, output_size, uv_offset, uv_scale);
src->pop_layout(cmd);
src_image = m_output_data.get();
if (mode & UPSCALE_AND_COMMIT)
{
vk::insert_image_memory_barrier(cmd,
m_output_data->value,
m_output_data->current_layout, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_ACCESS_SHADER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
{ VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 });
m_output_data->current_layout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
output_request.srcOffsets[0].x = 0;
output_request.srcOffsets[1].x = output_size.width;
output_request.srcOffsets[0].y = 0;
output_request.srcOffsets[1].y = output_size.height;
// Preserve mirroring/flipping
if (request.srcOffsets[0].x > request.srcOffsets[1].x)
{
std::swap(output_request.srcOffsets[0].x, output_request.srcOffsets[1].x);
}
if (request.srcOffsets[0].y > request.srcOffsets[1].y)
{
std::swap(output_request.srcOffsets[0].y, output_request.srcOffsets[1].y);
}
}
}
}
if (mode & UPSCALE_AND_COMMIT)
{
src_image->push_layout(cmd, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL);
vkCmdBlitImage(cmd, src_image->value, src_image->current_layout, present_surface, present_surface_layout, 1, &output_request, VK_FILTER_LINEAR);
src_image->pop_layout(cmd);
return nullptr;
}
return src_image;
}
}
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "../vkutils/sampler.h"
#include "../VKCompute.h"
#include "upscaling.h"
namespace vk
{
// Snapdragon Game Super Resolution 1.0, mobile variant.
//
// A single-pass edge-directed spatial upscaler, written by Qualcomm for Adreno. Against FSR1
// it is one dispatch instead of two and one target instead of two, which is what makes it
// worth having on a phone -- the filter is cheaper, not better.
//
// Its driver requirements are a strict subset of FSR1's: textureGather with a constant
// component and no offset (core Vulkan 1.0; only gather-with-offsets needs
// shaderImageGatherExtended), an rgba8 storage image (mandatory format support), one
// descriptor set and 44 bytes of push constants. So there is deliberately no vendor gate --
// anywhere FSR1 runs, this runs.
namespace SGSR
{
class sgsr_pass : public compute_task
{
std::unique_ptr<vk::sampler> m_sampler;
const vk::image_view* m_input_image = nullptr;
const vk::image_view* m_output_image = nullptr;
// 11 words. The layout is verified against the compiled SPIR-V rather than derived
// from the struct, because a mismatch here produces garbage that looks exactly like a
// shader bug: dstSize@0, uvOffset@8, uvScale@16, srcSize@24, invSrcSize@32,
// edgeSharpness@40.
std::array<u32, 11> m_constants_buf{};
size2u m_input_size{};
size2u m_output_size{};
f32 m_uv_offset[2]{};
f32 m_uv_scale[2]{};
std::vector<glsl::program_input> get_inputs() override;
void bind_resources(const vk::command_buffer&) override;
public:
sgsr_pass();
// uv_offset/uv_scale map the DISPLAYED region inside the source texture. The RSX
// output target is larger than the picture in it, so without this the filter would
// upscale the padding as well as the image.
void run(const vk::command_buffer& cmd,
vk::viewable_image* src,
vk::viewable_image* dst,
const size2u& input_size,
const size2u& output_size,
const f32 uv_offset[2],
const f32 uv_scale[2]);
};
}
class sgsr_upscale_pass : public upscaler
{
std::unique_ptr<vk::viewable_image> m_output_left;
std::unique_ptr<vk::viewable_image> m_output_right;
void dispose_images();
void initialize_image(u32 output_w, u32 output_h, rsx::flags32_t mode);
public:
vk::viewable_image* scale_output(
const vk::command_buffer& cmd,
vk::viewable_image* src,
VkImage present_surface,
VkImageLayout present_surface_layout,
const VkImageBlit& request,
rsx::flags32_t mode
) override;
};
}
+1
View File
@@ -724,6 +724,7 @@ void fmt_class_string<output_scaling_mode>::format(std::string& out, u64 arg)
// Missing case here would serialise as `unknown` and the mode would
// silently never be selectable -- same trap as pad_handler::virtual_pad.
case output_scaling_mode::shader: return "Shader chain (librashader)";
case output_scaling_mode::sgsr: return "Snapdragon Game Super Resolution";
#endif
}
+4 -1
View File
@@ -370,7 +370,10 @@ enum class output_scaling_mode
// ARMSX3: RetroArch (.slangp) chain via librashader, run as the output pass.
// Appended, never inserted -- these are serialised by ordinal in savestates
// and per-game configs, so inserting would silently remap existing settings.
shader
shader,
// Snapdragon Game Super Resolution: one dispatch and one target against FSR1's two,
// written by Qualcomm for Adreno. Same driver requirements as FSR1 or fewer.
sgsr
#endif
};