Files
ARMSX3/rpcs3/Emu/RSX/Program/Upscalers/SGSR/sgsr_shader.glsl
T
jpolo1224 f53a76c0fc 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.
2026-08-24 16:46:30 -04:00

115 lines
4.7 KiB
GLSL

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);
}
)"