mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
GS: FidelityFX Super Resolution 1 as an output-scaling mode
Adds FSR1 (EASU upscale + RCAS sharpen, two compute passes) to the Vulkan backend, so a game rendered below display size can be upscaled properly instead of bilinear-stretched at present. Slots in beside the existing MetalFX branch in GSRenderer rather than introducing a parallel abstraction: GSUpscaler and the non-pure DoXxx virtuals already occupy that design space, and OpenGL and Metal inherit a false return and need no change. FSR1 is MIT (AMD, 2021) and the tree already ships ffx_a.h and ffx_cas.h under the identical grant, so the headers are vendored verbatim with their licence blocks intact. ★ ffx_a.h is NOT replaced. FSR1 wants the 2021 header, ours is 2019, and the 2019 one has been locally patched for Metal Shading Language (A16, A_MSL, A_MAYBE_UNUSED) with ffx_cas.h depending on those. Swapping it would break the Metal backend. The 2021 copy ships alongside as ffx_a_fsr1.h, used only for GPU-side string substitution. The CPU-side FsrEasuConOffset/FsrRcasCon compile against the existing 2019 header — verified by compiling a probe, not by grepping, because AU1_AF1 and AU1_AH2_AF2 are functions and a grep for a #define reports a false negative. Two shader modules, not two specializations. FSR_EASU_F and FSR_RCAS_F are preprocessor gates deciding which function bodies ffx_fsr1.h emits at all, and specialization constants resolve after preprocessing, so CAS's constant_id trick would produce a shader calling undefined functions. Confirmed distinct: disassembly shows EASU with three OpImageGather and RCAS with none. Both passes push the full 80-byte constant block. With all five uvec4 declared so one layout serves both, Sample decorates to byte offset 64 — pushing the 32 bytes RCAS nominally needs would leave it undefined, and Sample gates a gamma-squaring branch, so garbage there squares the image. Binding 0 is a combined image sampler, unlike CAS's plain sampled image, because EASU uses textureGather. The EASU intermediate stays in GENERAL with explicit compute-to-compute barriers. Layout::ShaderReadOnly targets the FRAGMENT stage and TransitionToLayout early-outs when the layout already matches, so neither of the usual tools makes a compute write visible to a compute read. The barrier also covers frame N+1's EASU write against frame N's RCAS read, since the image is parked across frames. FSR and CAS are alternatives, not a chain: RCAS is itself a sharpener. Selecting FSR hides the CAS rows. Pipeline compilation failure is non-fatal and leaves Features().fsr1 false, matching the CAS path that exists because of an Adreno 650 crash. GSUpscaler::FSR1 is appended, not inserted, since the enum is persisted as an integer. Android clamps to the enum's own maximum rather than the count of options its picker shows — clamping to the picker would have rewritten FSR1 back to Off on every save, because MetalFX occupies value 1 and is never displayed. Verified: build clean, no C++ or Kotlin errors; all three resource files packaged into the APK; FSR code present in the core. NOT verified: anything on a GPU. No visual check, no perf numbers, and in particular no confirmation that textureGather in a compute shader works on the Adreno drivers this targets.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
// Based on the AMD FidelityFX Super Resolution 1.0 sample.
|
||||
// Copyright(c) 2021 Advanced Micro Devices, Inc.All rights reserved.
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files(the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions :
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
// The #version line and FSR_PASS_EASU come from the backend, because FSR_EASU_F/FSR_RCAS_F are
|
||||
// preprocessor gates deciding which function bodies ffx_fsr1.h emits at all. Selecting the pass
|
||||
// with a specialization constant the way cas.glsl does would leave both calls unresolved.
|
||||
|
||||
#define A_GPU 1
|
||||
#define A_GLSL 1
|
||||
|
||||
// FSR needs the 2021 revision of the portability header; ffx_a.h in this directory is the 2019 one
|
||||
// that ffx_cas.h and the Metal backend are patched against, and must not be used here.
|
||||
#include "ffx_a_fsr1.h"
|
||||
|
||||
// Both passes share this block so that one pipeline layout serves both. RCAS only reads Const0 and
|
||||
// Sample, but Sample sits at offset 64 regardless, so the host has to push all five vectors.
|
||||
layout(push_constant) uniform const_buffer
|
||||
{
|
||||
uvec4 Const0;
|
||||
uvec4 Const1;
|
||||
uvec4 Const2;
|
||||
uvec4 Const3;
|
||||
uvec4 Sample;
|
||||
};
|
||||
|
||||
// EASU reads through textureGather, so unlike CAS this has to be a combined image sampler rather
|
||||
// than a plain sampled image.
|
||||
layout(set=0, binding=0) uniform sampler2D imgSrc;
|
||||
layout(set=0, binding=1, rgba8) uniform writeonly image2D imgDst;
|
||||
|
||||
#if FSR_PASS_EASU
|
||||
#define FSR_EASU_F 1
|
||||
AF4 FsrEasuRF(AF2 p) { return textureGather(imgSrc, p, 0); }
|
||||
AF4 FsrEasuGF(AF2 p) { return textureGather(imgSrc, p, 1); }
|
||||
AF4 FsrEasuBF(AF2 p) { return textureGather(imgSrc, p, 2); }
|
||||
#else
|
||||
#define FSR_RCAS_F 1
|
||||
AF4 FsrRcasLoadF(ASU2 p) { return texelFetch(imgSrc, ASU2(p), 0); }
|
||||
// Our input is already linear and between 0 and 1, so nothing to transform. See ffx_fsr1.h
|
||||
void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {}
|
||||
#endif
|
||||
|
||||
#include "ffx_fsr1.h"
|
||||
|
||||
void CurrFilter(AU2 pos)
|
||||
{
|
||||
AF3 c;
|
||||
#if FSR_PASS_EASU
|
||||
FsrEasuF(c, pos, Const0, Const1, Const2, Const3);
|
||||
#else
|
||||
FsrRcasF(c.r, c.g, c.b, pos, Const0);
|
||||
#endif
|
||||
|
||||
// Sample.x selects AMD's gamma2 output path, which we never want - the host zeroes it. It is
|
||||
// read unconditionally, so a short push constant write would square the whole image.
|
||||
if (Sample.x == 1u)
|
||||
c *= c;
|
||||
|
||||
imageStore(imgDst, ASU2(pos), AF4(c, 1.0));
|
||||
}
|
||||
|
||||
layout(local_size_x=64) in;
|
||||
void main()
|
||||
{
|
||||
// Do remapping of local xy in workgroup for a more PS-like swizzle pattern.
|
||||
AU2 gxy = ARmp8x8(gl_LocalInvocationID.x)+AU2(gl_WorkGroupID.x<<4u,gl_WorkGroupID.y<<4u);
|
||||
|
||||
CurrFilter(gxy);
|
||||
gxy.x += 8u;
|
||||
|
||||
CurrFilter(gxy);
|
||||
gxy.y += 8u;
|
||||
|
||||
CurrFilter(gxy);
|
||||
gxy.x -= 8u;
|
||||
|
||||
CurrFilter(gxy);
|
||||
}
|
||||
@@ -479,6 +479,9 @@ enum class GSUpscaler : u8
|
||||
{
|
||||
Off, ///< Plain bilinear present-time stretch (default).
|
||||
MetalFXSpatial, ///< Apple MetalFX spatial upscaler (Metal backend, macOS 13+).
|
||||
// Appended rather than inserted: this enum is persisted as an integer, so renumbering
|
||||
// MetalFXSpatial would silently re-point every existing config at a different upscaler.
|
||||
FSR1, ///< AMD FidelityFX Super Resolution 1 (EASU + RCAS compute passes, Vulkan).
|
||||
};
|
||||
|
||||
enum class GSHWAutoFlushLevel : u8
|
||||
@@ -1069,6 +1072,9 @@ struct Pcsx2Config
|
||||
u8 LsfgFlowScale = 100;
|
||||
|
||||
u8 CAS_Sharpness = 50;
|
||||
// FSR1's RCAS pass, 0..100. Mapped to AMD's "stops" scale in GSDevice::FSR1Upscale,
|
||||
// where 0 stops is maximum sharpening - it is not the same curve as CAS_Sharpness.
|
||||
u8 FSR_Sharpness = 50;
|
||||
u8 ShadeBoost_Brightness = DEFAULT_SHADEBOOST_BRIGHTNESS;
|
||||
u8 ShadeBoost_Contrast = DEFAULT_SHADEBOOST_CONTRAST;
|
||||
u8 ShadeBoost_Saturation = DEFAULT_SHADEBOOST_SATURATION;
|
||||
|
||||
@@ -293,7 +293,7 @@ GSDevice::GSDevice()
|
||||
GSDevice::~GSDevice()
|
||||
{
|
||||
// should've been cleaned up in Destroy()
|
||||
pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas && !m_mfx_output);
|
||||
pxAssert(m_pool[0].empty() && m_pool[1].empty() && !m_merge && !m_weavebob && !m_blend && !m_mad && !m_target_tmp && !m_cas && !m_mfx_output && !m_fsr1_easu && !m_fsr1_output);
|
||||
}
|
||||
|
||||
GSVector2i GSDevice::GetPresentationSize() const
|
||||
@@ -1228,6 +1228,8 @@ void GSDevice::ClearCurrent()
|
||||
delete m_target_tmp;
|
||||
delete m_cas;
|
||||
delete m_mfx_output;
|
||||
delete m_fsr1_easu;
|
||||
delete m_fsr1_output;
|
||||
|
||||
m_merge = nullptr;
|
||||
m_weavebob = nullptr;
|
||||
@@ -1236,6 +1238,8 @@ void GSDevice::ClearCurrent()
|
||||
m_target_tmp = nullptr;
|
||||
m_cas = nullptr;
|
||||
m_mfx_output = nullptr;
|
||||
m_fsr1_easu = nullptr;
|
||||
m_fsr1_output = nullptr;
|
||||
}
|
||||
|
||||
void GSDevice::Merge(GSTexture* sTex[3], GSVector4* sRect, GSVector4* dRect, const GSVector2i& fs, const GSRegPMODE& PMODE, const GSRegEXTBUF& EXTBUF, u32 c)
|
||||
@@ -1474,6 +1478,11 @@ void GSDevice::EndDSAsRT()
|
||||
#define A_CPU 1
|
||||
#include "bin/resources/shaders/common/ffx_a.h"
|
||||
#include "bin/resources/shaders/common/ffx_cas.h"
|
||||
// FSR needs the 2021 revision of ffx_a.h on the GPU side, but its CPU-side constant setup
|
||||
// (FsrEasuConOffset/FsrRcasCon) is satisfied by the 2019 header above - which is the one
|
||||
// PCSX2 patched locally for Metal (A16/A_MSL/A_MAYBE_UNUSED) and that ffx_cas.h depends on.
|
||||
// So only the shader gets the 2021 copy; nothing here includes ffx_a_fsr1.h.
|
||||
#include "bin/resources/shaders/common/ffx_fsr1.h"
|
||||
|
||||
#if defined(__clang__)
|
||||
#pragma clang diagnostic pop
|
||||
@@ -1494,6 +1503,25 @@ bool GSDevice::GetCASShaderSource(std::string* source)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GSDevice::GetFSR1ShaderSource(std::string* source, bool easu_pass)
|
||||
{
|
||||
std::optional<std::string> ffx_a_source = ReadShaderSource("shaders/common/ffx_a_fsr1.h");
|
||||
std::optional<std::string> ffx_fsr1_source = ReadShaderSource("shaders/common/ffx_fsr1.h");
|
||||
if (!ffx_a_source.has_value() || !ffx_fsr1_source.has_value())
|
||||
return false;
|
||||
|
||||
// FSR_EASU_F/FSR_RCAS_F gate which function bodies ffx_fsr1.h emits at all, so the pass has
|
||||
// to be chosen before the preprocessor runs. That is why this takes easu_pass rather than
|
||||
// letting the backend pick with a specialization constant the way cas.glsl does.
|
||||
source->insert(0, easu_pass ? "#version 460 core\n#define FSR_PASS_EASU 1\n"
|
||||
: "#version 460 core\n#define FSR_PASS_EASU 0\n");
|
||||
|
||||
// Same cheeky string replace as CAS above - our shader compilers don't support includes.
|
||||
StringUtil::ReplaceAll(source, "#include \"ffx_a_fsr1.h\"", ffx_a_source.value());
|
||||
StringUtil::ReplaceAll(source, "#include \"ffx_fsr1.h\"", ffx_fsr1_source.value());
|
||||
return true;
|
||||
}
|
||||
|
||||
void GSDevice::CAS(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect, bool sharpen_only)
|
||||
{
|
||||
FlushDeferredDraws();
|
||||
@@ -1565,6 +1593,74 @@ void GSDevice::MetalFXUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4&
|
||||
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
void GSDevice::FSR1Upscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect)
|
||||
{
|
||||
FlushDeferredDraws();
|
||||
const int dst_width = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
||||
const int dst_height = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
||||
if (dst_width <= 0 || dst_height <= 0)
|
||||
return;
|
||||
|
||||
GSTexture* src_tex = tex;
|
||||
|
||||
// Two targets, not one: RCAS is a separate dispatch that reads EASU's whole output, so it
|
||||
// cannot write in place.
|
||||
if (!m_fsr1_easu || m_fsr1_easu->GetWidth() != dst_width || m_fsr1_easu->GetHeight() != dst_height)
|
||||
{
|
||||
delete m_fsr1_easu;
|
||||
m_fsr1_easu = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
||||
if (!m_fsr1_easu)
|
||||
{
|
||||
Console.Error("Failed to allocate FSR1 EASU texture.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!m_fsr1_output || m_fsr1_output->GetWidth() != dst_width || m_fsr1_output->GetHeight() != dst_height)
|
||||
{
|
||||
delete m_fsr1_output;
|
||||
m_fsr1_output = CreateSurface(GSTexture::ShaderWriteTexture, dst_width, dst_height, 1, GSTexture::Format::Color);
|
||||
if (!m_fsr1_output)
|
||||
{
|
||||
Console.Error("Failed to allocate FSR1 RCAS texture.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Zero-initialised, and pushed whole by both passes: the shader reads the fifth vector
|
||||
// ("Sample") unconditionally to pick AMD's gamma2 output path, which we never want.
|
||||
std::array<u32, NUM_FSR1_CONSTANTS> consts = {};
|
||||
|
||||
// EASU distinguishes the displayed region from the resource holding it: the viewport is the
|
||||
// cropped src_rect, the size is the whole texture, and the offset puts the two together.
|
||||
FsrEasuConOffset(&consts[0], &consts[4], &consts[8], &consts[12],
|
||||
static_cast<AF1>(src_rect.width()), static_cast<AF1>(src_rect.height()),
|
||||
static_cast<AF1>(src_tex->GetWidth()), static_cast<AF1>(src_tex->GetHeight()),
|
||||
static_cast<AF1>(dst_width), static_cast<AF1>(dst_height),
|
||||
static_cast<AF1>(src_rect.x), static_cast<AF1>(src_rect.y));
|
||||
|
||||
if (!DoFSR1EASU(src_tex, m_fsr1_easu, consts))
|
||||
{
|
||||
// leave textures intact if we failed
|
||||
Console.Warning("Applying FSR1 EASU failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
// RCAS takes sharpness in stops - 0 is the maximum and each stop halves it - so the 0..100
|
||||
// slider runs backwards across the 2..0 range AMD's own sample exposes.
|
||||
std::array<u32, NUM_FSR1_CONSTANTS> rcas_consts = {};
|
||||
FsrRcasCon(&rcas_consts[0], 2.0f - (static_cast<float>(GSConfig.FSR_Sharpness) * 0.02f));
|
||||
|
||||
if (!DoFSR1RCAS(m_fsr1_easu, m_fsr1_output, rcas_consts))
|
||||
{
|
||||
Console.Warning("Applying FSR1 RCAS failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
tex = m_fsr1_output;
|
||||
src_rect = GSVector4i(0, 0, dst_width, dst_height);
|
||||
src_uv = GSVector4(0.0f, 0.0f, 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
bool GSHWDrawConfig::BlendState::IsEffective(ColorMaskSelector colormask) const
|
||||
{
|
||||
return enable && (((colormask.key & 7u) && (src_factor != GSDevice::CONST_ONE || dst_factor != GSDevice::CONST_ZERO)) ||
|
||||
|
||||
@@ -1416,6 +1416,7 @@ public:
|
||||
bool aa1 : 1; ///< Supports the GS AA1 feature.
|
||||
bool rov : 1; ///< Supports rasterizer ordered views for both depth and color.
|
||||
bool metalfx_spatial : 1; ///< Supports Apple MetalFX spatial upscaling (Metal backend, macOS 13+).
|
||||
bool fsr1 : 1; ///< Supports AMD FidelityFX Super Resolution 1 (two compute passes).
|
||||
bool dual_source_blend : 1; ///< Supports a second fragment output (SRC1) as a hardware blend factor.
|
||||
bool broken_mad_deinterlace : 1; ///< Driver can't reliably preserve/read the two-bank FastMAD history target.
|
||||
FeatureSupport()
|
||||
@@ -1513,6 +1514,10 @@ protected:
|
||||
static constexpr u32 MAX_POOLED_TEXTURES = 300;
|
||||
static constexpr u32 MAX_TEXTURE_AGE = 10;
|
||||
static constexpr u32 NUM_CAS_CONSTANTS = 12; // 8 plus src offset x/y, 16 byte alignment
|
||||
// Five uvec4s: EASU's con0..con3 plus FSR's "Sample" vector. RCAS only reads con0 and
|
||||
// Sample, but Sample still decorates to byte offset 64, so both passes push all 80 bytes
|
||||
// - a short push leaves Sample undefined and the shader squares the whole image.
|
||||
static constexpr u32 NUM_FSR1_CONSTANTS = 20;
|
||||
static constexpr u32 EXPAND_BUFFER_SIZE = sizeof(u16) * 16383 * 6;
|
||||
|
||||
WindowInfo m_window_info;
|
||||
@@ -1529,6 +1534,8 @@ protected:
|
||||
GSTexture* m_current = nullptr;
|
||||
GSTexture* m_cas = nullptr;
|
||||
GSTexture* m_mfx_output = nullptr; ///< MetalFX spatial upscale destination (Metal backend).
|
||||
GSTexture* m_fsr1_easu = nullptr; ///< FSR1 EASU output, at display size; RCAS reads it back.
|
||||
GSTexture* m_fsr1_output = nullptr; ///< FSR1 RCAS output, the texture actually presented.
|
||||
GSTexture* m_colclip_rt = nullptr; ///< Temp hw colclip texture
|
||||
GSTexture* m_ds_as_rt = nullptr; ///< Depth as color
|
||||
|
||||
@@ -1582,6 +1589,17 @@ protected:
|
||||
/// Base implementation is a no-op; only the Metal backend overrides it.
|
||||
virtual bool DoMetalFXSpatial(GSTexture* sTex, GSTexture* dTex) { return false; }
|
||||
|
||||
/// Resolves FSR1 shader includes for the specified source, and prepends the #version line
|
||||
/// plus the FSR_PASS_EASU gate. The pass cannot be a specialization constant: it decides
|
||||
/// which function bodies ffx_fsr1.h emits at all, which is a preprocessor-time question.
|
||||
static bool GetFSR1ShaderSource(std::string* source, bool easu_pass);
|
||||
|
||||
/// FSR1 pass 1 (EASU): edge-adaptive spatial upsample from sTex to dTex's size.
|
||||
/// FSR1 pass 2 (RCAS): robust contrast-adaptive sharpen, same size in and out.
|
||||
/// Both no-op in the base class so only the backends that gate Features().fsr1 on need them.
|
||||
virtual bool DoFSR1EASU(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants) { return false; }
|
||||
virtual bool DoFSR1RCAS(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants) { return false; }
|
||||
|
||||
/// Perform texture operations for ImGui
|
||||
void UpdateImGuiTextures();
|
||||
|
||||
@@ -2004,6 +2022,9 @@ public:
|
||||
/// tex/src_rect/src_uv to point at the upscaled result, mirroring CAS().
|
||||
void MetalFXUpscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect);
|
||||
|
||||
/// Same contract as MetalFXUpscale(), via FSR1's two compute passes.
|
||||
void FSR1Upscale(GSTexture*& tex, GSVector4i& src_rect, GSVector4& src_uv, const GSVector4& draw_rect);
|
||||
|
||||
bool ResizeRenderTarget(GSTexture** t, int w, int h, bool preserve_contents, bool recycle);
|
||||
|
||||
void AgePool();
|
||||
|
||||
@@ -1073,7 +1073,29 @@ void GSRenderer::VSync(u32 field, bool registers_written, bool idle_frame)
|
||||
}
|
||||
}
|
||||
|
||||
if (GSConfig.CASMode != GSCASMode::Disabled)
|
||||
// FSR1 runs here for the same reason MetalFX does - its passes are compute, and the
|
||||
// present render pass is already open by the time DoBeginPresent runs.
|
||||
// It is CAS's `if`, not a second branch beside it: FSR's second pass *is* RCAS, a
|
||||
// contrast-adaptive sharpener, so letting CAS run afterward sharpens twice.
|
||||
if (GSConfig.Upscaler == GSUpscaler::FSR1)
|
||||
{
|
||||
static bool fsr1_log_once = false;
|
||||
if (g_gs_device->Features().fsr1)
|
||||
{
|
||||
const int draw_w = static_cast<int>(std::ceil(draw_rect.z - draw_rect.x));
|
||||
const int draw_h = static_cast<int>(std::ceil(draw_rect.w - draw_rect.y));
|
||||
if (current->GetWidth() < draw_w && current->GetHeight() < draw_h)
|
||||
g_gs_device->FSR1Upscale(current, src_rect, src_uv, draw_rect);
|
||||
}
|
||||
else if (!fsr1_log_once)
|
||||
{
|
||||
Host::AddIconOSDMessage("FSR1Unsupported", ICON_FA_TRIANGLE_EXCLAMATION,
|
||||
TRANSLATE_SV("GS", "FSR1 upscaling is not available, your graphics driver does not support the required functionality."),
|
||||
10.0f);
|
||||
fsr1_log_once = true;
|
||||
}
|
||||
}
|
||||
else if (GSConfig.CASMode != GSCASMode::Disabled)
|
||||
{
|
||||
static bool cas_log_once = false;
|
||||
if (g_gs_device->Features().cas_sharpening)
|
||||
|
||||
@@ -2633,6 +2633,15 @@ bool GSDeviceVK::Create(GSVSyncMode vsync_mode, bool allow_present_throttle)
|
||||
m_features.cas_sharpening = false;
|
||||
}
|
||||
|
||||
// Same non-fatal treatment as CAS above, and for the same reason: FSR1 is two more compute
|
||||
// pipelines, so a driver that chokes on CAS's will likely choke on these too. Leaving
|
||||
// Features().fsr1 false makes GSRenderer fall back to the plain bilinear present.
|
||||
if (!CompileFSR1Pipelines())
|
||||
{
|
||||
Console.Warning("VK: FSR1 pipeline compilation failed - disabling FSR1 upscaling.");
|
||||
m_features.fsr1 = false;
|
||||
}
|
||||
|
||||
if (!CompileImGuiPipeline())
|
||||
return false;
|
||||
|
||||
@@ -6040,6 +6049,54 @@ bool GSDeviceVK::CompileCASPipelines()
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GSDeviceVK::CompileFSR1Pipelines()
|
||||
{
|
||||
VkDevice dev = m_device;
|
||||
Vulkan::DescriptorSetLayoutBuilder dslb;
|
||||
Vulkan::PipelineLayoutBuilder plb;
|
||||
|
||||
if (m_use_push_descriptors)
|
||||
dslb.SetPushFlag();
|
||||
// Combined image sampler, not SAMPLED_IMAGE as CAS uses: EASU reads through textureGather,
|
||||
// which needs a sampler bound to the image.
|
||||
dslb.AddBinding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT);
|
||||
dslb.AddBinding(1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT);
|
||||
if ((m_fsr1_ds_layout = dslb.Create(dev)) == VK_NULL_HANDLE)
|
||||
return false;
|
||||
Vulkan::SetObjectName(dev, m_fsr1_ds_layout, "FSR1 descriptor layout");
|
||||
|
||||
plb.AddPushConstants(VK_SHADER_STAGE_COMPUTE_BIT, 0, NUM_FSR1_CONSTANTS * sizeof(u32));
|
||||
plb.AddDescriptorSet(m_fsr1_ds_layout);
|
||||
if ((m_fsr1_pipeline_layout = plb.Create(dev)) == VK_NULL_HANDLE)
|
||||
return false;
|
||||
Vulkan::SetObjectName(dev, m_fsr1_pipeline_layout, "FSR1 pipeline layout");
|
||||
|
||||
// Two modules from two differently-#define'd copies of the same file, where CAS gets away
|
||||
// with one module and a specialization constant: FSR_EASU_F/FSR_RCAS_F decide which function
|
||||
// bodies ffx_fsr1.h emits, so a specialization constant would leave both calls unresolved.
|
||||
for (u8 easu_pass = 0; easu_pass < NUM_FSR1_PIPELINES; easu_pass++)
|
||||
{
|
||||
std::optional<std::string> fsr1_source = ReadShaderSource("shaders/vulkan/fsr1.glsl");
|
||||
if (!fsr1_source.has_value() || !GetFSR1ShaderSource(&fsr1_source.value(), easu_pass != 0))
|
||||
return false;
|
||||
|
||||
VkShaderModule mod = g_vulkan_shader_cache->GetComputeShader(fsr1_source->c_str());
|
||||
if (mod == VK_NULL_HANDLE)
|
||||
return false;
|
||||
ScopedGuard mod_guard = [this, &mod]() { vkDestroyShaderModule(m_device, mod, nullptr); };
|
||||
|
||||
Vulkan::ComputePipelineBuilder cpb;
|
||||
cpb.SetPipelineLayout(m_fsr1_pipeline_layout);
|
||||
cpb.SetShader(mod, "main");
|
||||
m_fsr1_pipelines[easu_pass] = cpb.Create(dev, g_vulkan_shader_cache->GetPipelineCache(true), false);
|
||||
if (!m_fsr1_pipelines[easu_pass])
|
||||
return false;
|
||||
}
|
||||
|
||||
m_features.fsr1 = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GSDeviceVK::CompileImGuiPipeline()
|
||||
{
|
||||
const std::optional<std::string> glsl = ReadShaderSource("shaders/vulkan/imgui.glsl");
|
||||
@@ -6312,6 +6369,108 @@ bool GSDeviceVK::DoCAS(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GSDeviceVK::DoFSR1EASU(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants)
|
||||
{
|
||||
return DoFSR1Pass(sTex, dTex, true, constants);
|
||||
}
|
||||
|
||||
bool GSDeviceVK::DoFSR1RCAS(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants)
|
||||
{
|
||||
return DoFSR1Pass(sTex, dTex, false, constants);
|
||||
}
|
||||
|
||||
bool GSDeviceVK::DoFSR1Pass(
|
||||
GSTexture* sTex, GSTexture* dTex, bool easu_pass, const std::array<u32, NUM_FSR1_CONSTANTS>& constants)
|
||||
{
|
||||
g_perfmon.Put(GSPerfMon::TextureCopies, 1);
|
||||
|
||||
EndRenderPass();
|
||||
|
||||
GSTextureVK* const sTexVK = static_cast<GSTextureVK*>(sTex);
|
||||
GSTextureVK* const dTexVK = static_cast<GSTextureVK*>(dTex);
|
||||
VkCommandBuffer cmdbuf = GetCurrentCommandBuffer();
|
||||
|
||||
// The EASU intermediate is handed to RCAS in compute and so never leaves GENERAL, which
|
||||
// defeats both of the backend's usual tools: Layout::ShaderReadOnly's barrier targets
|
||||
// VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT and would not make compute stores visible to a
|
||||
// compute read, and TransitionToLayout() early-outs when the layout already matches, so
|
||||
// re-requesting ComputeReadWriteImage emits nothing at all. Anything already in GENERAL
|
||||
// therefore needs its compute<->compute dependency stated by hand.
|
||||
const auto compute_barrier = [cmdbuf](GSTextureVK* tex, VkAccessFlags src_access, VkAccessFlags dst_access) {
|
||||
const VkImageMemoryBarrier barrier = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, nullptr, src_access, dst_access,
|
||||
VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED,
|
||||
tex->GetImage(), {VK_IMAGE_ASPECT_COLOR_BIT, 0u, 1u, 0u, 1u}};
|
||||
vkCmdPipelineBarrier(cmdbuf, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, 0,
|
||||
nullptr, 0, nullptr, 1, &barrier);
|
||||
};
|
||||
|
||||
if (sTexVK->GetLayout() == GSTextureVK::Layout::ComputeReadWriteImage)
|
||||
{
|
||||
// RCAS reading EASU's output. Without this it reads undefined data.
|
||||
compute_barrier(sTexVK, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
// EASU's input is the merged display texture, arriving from a colour-attachment write
|
||||
// exactly as CAS's does.
|
||||
sTexVK->TransitionToLayout(cmdbuf, GSTextureVK::Layout::ShaderReadOnly);
|
||||
}
|
||||
|
||||
if (dTexVK->GetLayout() == GSTextureVK::Layout::ComputeReadWriteImage)
|
||||
{
|
||||
// Every frame after the first: the intermediate was left in GENERAL for RCAS to read, so
|
||||
// this is what orders EASU's writes against the previous frame's RCAS reads of it.
|
||||
compute_barrier(dTexVK, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_WRITE_BIT);
|
||||
}
|
||||
else
|
||||
{
|
||||
dTexVK->TransitionToLayout(cmdbuf, GSTextureVK::Layout::ComputeReadWriteImage);
|
||||
}
|
||||
|
||||
// EASU gathers with normalised coordinates, so it needs the linear/clamp-to-edge sampler;
|
||||
// RCAS only texelFetches and ignores the sampler entirely.
|
||||
const VkSampler sampler = easu_pass ? m_linear_sampler : m_point_sampler;
|
||||
|
||||
// only happening once a frame, so the update isn't a huge deal.
|
||||
Vulkan::DescriptorSetUpdateBuilder dsub;
|
||||
if (m_use_push_descriptors)
|
||||
{
|
||||
dsub.AddCombinedImageSamplerDescriptorWrite(VK_NULL_HANDLE, 0, sTexVK->GetView(), sampler, sTexVK->GetVkLayout());
|
||||
dsub.AddStorageImageDescriptorWrite(VK_NULL_HANDLE, 1, dTexVK->GetView(), dTexVK->GetVkLayout());
|
||||
dsub.PushUpdate(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_fsr1_pipeline_layout, 0, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
VkDescriptorSet ds = AllocateDescriptorSetFromFramePool(m_fsr1_ds_layout);
|
||||
if (ds == VK_NULL_HANDLE) [[unlikely]]
|
||||
return false; // two allocs per frame after EndRenderPass - exhaustion implausible; skip the pass
|
||||
dsub.AddCombinedImageSamplerDescriptorWrite(ds, 0, sTexVK->GetView(), sampler, sTexVK->GetVkLayout());
|
||||
dsub.AddStorageImageDescriptorWrite(ds, 1, dTexVK->GetView(), dTexVK->GetVkLayout());
|
||||
dsub.Update(m_device);
|
||||
vkCmdBindDescriptorSets(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_fsr1_pipeline_layout, 0, 1, &ds, 0, nullptr);
|
||||
}
|
||||
|
||||
static const int threadGroupWorkRegionDim = 16;
|
||||
const int dispatchX = (dTex->GetWidth() + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
|
||||
const int dispatchY = (dTex->GetHeight() + (threadGroupWorkRegionDim - 1)) / threadGroupWorkRegionDim;
|
||||
|
||||
// Full 80 bytes for both passes. RCAS only reads Const0, but the shared block puts `Sample`
|
||||
// at byte 64 either way, and it is read unconditionally - a 32-byte push leaves it undefined
|
||||
// and the shader squares the image.
|
||||
vkCmdPushConstants(cmdbuf, m_fsr1_pipeline_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0,
|
||||
NUM_FSR1_CONSTANTS * sizeof(u32), constants.data());
|
||||
vkCmdBindPipeline(cmdbuf, VK_PIPELINE_BIND_POINT_COMPUTE, m_fsr1_pipelines[static_cast<u8>(easu_pass)]);
|
||||
vkCmdDispatch(cmdbuf, dispatchX, dispatchY, 1);
|
||||
|
||||
// The EASU target goes straight into RCAS in compute, so leave it in GENERAL and let the
|
||||
// barrier at the top of the next pass order the two dispatches. Only RCAS's output is handed
|
||||
// to the present pass, which samples it from the fragment stage.
|
||||
if (!easu_pass)
|
||||
dTexVK->TransitionToLayout(GSTextureVK::Layout::ShaderReadOnly);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void GSDeviceVK::DestroyResources()
|
||||
{
|
||||
if (m_tfx_ubo_descriptor_set != VK_NULL_HANDLE)
|
||||
@@ -6375,6 +6534,17 @@ void GSDeviceVK::DestroyResources()
|
||||
vkDestroyPipelineLayout(m_device, m_cas_pipeline_layout, nullptr);
|
||||
if (m_cas_ds_layout != VK_NULL_HANDLE)
|
||||
vkDestroyDescriptorSetLayout(m_device, m_cas_ds_layout, nullptr);
|
||||
|
||||
for (VkPipeline it : m_fsr1_pipelines)
|
||||
{
|
||||
if (it != VK_NULL_HANDLE)
|
||||
vkDestroyPipeline(m_device, it, nullptr);
|
||||
}
|
||||
if (m_fsr1_pipeline_layout != VK_NULL_HANDLE)
|
||||
vkDestroyPipelineLayout(m_device, m_fsr1_pipeline_layout, nullptr);
|
||||
if (m_fsr1_ds_layout != VK_NULL_HANDLE)
|
||||
vkDestroyDescriptorSetLayout(m_device, m_fsr1_ds_layout, nullptr);
|
||||
|
||||
if (m_imgui_pipeline != VK_NULL_HANDLE)
|
||||
vkDestroyPipeline(m_device, m_imgui_pipeline, nullptr);
|
||||
|
||||
|
||||
@@ -449,6 +449,7 @@ public:
|
||||
CONVERT_PUSH_CONSTANTS_SIZE = 96,
|
||||
|
||||
NUM_CAS_PIPELINES = 2,
|
||||
NUM_FSR1_PIPELINES = 2, // [0] RCAS, [1] EASU
|
||||
};
|
||||
enum TFX_DESCRIPTOR_SET : u32
|
||||
{
|
||||
@@ -541,6 +542,9 @@ private:
|
||||
VkDescriptorSetLayout m_cas_ds_layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_cas_pipeline_layout = VK_NULL_HANDLE;
|
||||
std::array<VkPipeline, NUM_CAS_PIPELINES> m_cas_pipelines = {};
|
||||
VkDescriptorSetLayout m_fsr1_ds_layout = VK_NULL_HANDLE;
|
||||
VkPipelineLayout m_fsr1_pipeline_layout = VK_NULL_HANDLE;
|
||||
std::array<VkPipeline, NUM_FSR1_PIPELINES> m_fsr1_pipelines = {};
|
||||
VkPipeline m_imgui_pipeline = VK_NULL_HANDLE;
|
||||
|
||||
GSHWDrawConfig::VSConstantBuffer m_vs_cb_cache;
|
||||
@@ -578,6 +582,13 @@ private:
|
||||
bool DoCAS(
|
||||
GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array<u32, NUM_CAS_CONSTANTS>& constants) final;
|
||||
|
||||
bool DoFSR1EASU(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants) final;
|
||||
bool DoFSR1RCAS(GSTexture* sTex, GSTexture* dTex, const std::array<u32, NUM_FSR1_CONSTANTS>& constants) final;
|
||||
/// Shared body of the two above: same layout, same push range, different pipeline and
|
||||
/// different input-side synchronisation.
|
||||
bool DoFSR1Pass(
|
||||
GSTexture* sTex, GSTexture* dTex, bool easu_pass, const std::array<u32, NUM_FSR1_CONSTANTS>& constants);
|
||||
|
||||
VkSampler GetSampler(GSHWDrawConfig::SamplerSelector ss);
|
||||
void ClearSamplerCache() final;
|
||||
|
||||
@@ -602,6 +613,7 @@ private:
|
||||
bool CompileMergePipelines();
|
||||
bool CompilePostProcessingPipelines();
|
||||
bool CompileCASPipelines();
|
||||
bool CompileFSR1Pipelines();
|
||||
|
||||
bool CompileImGuiPipeline();
|
||||
void RenderImGui();
|
||||
|
||||
@@ -899,6 +899,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const
|
||||
OpEqu(BackThreadMode) &&
|
||||
|
||||
OpEqu(CAS_Sharpness) &&
|
||||
OpEqu(FSR_Sharpness) &&
|
||||
OpEqu(ShadeBoost_Brightness) &&
|
||||
OpEqu(ShadeBoost_Contrast) &&
|
||||
OpEqu(ShadeBoost_Saturation) &&
|
||||
@@ -1155,6 +1156,8 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap)
|
||||
SettingsWrapIntEnumEx(CASMode, "CASMode");
|
||||
SettingsWrapIntEnumEx(Upscaler, "Upscaler");
|
||||
SettingsWrapBitfieldEx(CAS_Sharpness, "CASSharpness");
|
||||
// Bitfield, not Entry: FSR_Sharpness is a u8, same as CAS_Sharpness above.
|
||||
SettingsWrapBitfieldEx(FSR_Sharpness, "FSRSharpness");
|
||||
SettingsWrapBitfieldEx(Dithering, "dithering_ps2");
|
||||
SettingsWrapBitfieldEx(MaxAnisotropy, "MaxAnisotropy");
|
||||
SettingsWrapBitfieldEx(SWExtraThreads, "extrathreads");
|
||||
|
||||
@@ -602,6 +602,13 @@ data class Settings(
|
||||
val casMode: Int = 0,
|
||||
/** EmuCore/GS/CASSharpness — sharpening strength 0..100 (%). */
|
||||
val casSharpness: Int = 50,
|
||||
/** EmuCore/GS/Upscaler — GSUpscaler: 0 Off / 1 MetalFX (Apple only) / 2 FSR1.
|
||||
* 1 is unreachable from this UI; the values are the core enum's, and it is persisted
|
||||
* as an integer, so they must not be renumbered to close the gap. */
|
||||
val upscaler: Int = 0,
|
||||
/** EmuCore/GS/FSRSharpness — FSR1 RCAS strength 0..100 (%). Separate from casSharpness:
|
||||
* RCAS runs on a different curve, so the two sliders are not interchangeable. */
|
||||
val fsrSharpness: Int = 50,
|
||||
/** EmuCore/GS/LoadTextureReplacements. */
|
||||
val loadTextureReplacements: Boolean = false,
|
||||
/** EmuCore/GS/LoadTextureReplacementsAsync. */
|
||||
@@ -1186,6 +1193,8 @@ data class Settings(
|
||||
} ?: this.shaderChainParams,
|
||||
casMode = intAt("EmuCore/GS/CASMode") ?: this.casMode,
|
||||
casSharpness = intAt("EmuCore/GS/CASSharpness") ?: this.casSharpness,
|
||||
upscaler = intAt("EmuCore/GS/Upscaler") ?: this.upscaler,
|
||||
fsrSharpness = intAt("EmuCore/GS/FSRSharpness") ?: this.fsrSharpness,
|
||||
loadTextureReplacements = boolAt("EmuCore/GS/LoadTextureReplacements") ?: this.loadTextureReplacements,
|
||||
loadTextureReplacementsAsync = boolAt("EmuCore/GS/LoadTextureReplacementsAsync") ?: this.loadTextureReplacementsAsync,
|
||||
precacheTextureReplacements = boolAt("EmuCore/GS/PrecacheTextureReplacements") ?: this.precacheTextureReplacements,
|
||||
@@ -1399,6 +1408,10 @@ data class Settings(
|
||||
ShaderParams.push(shaderChainPreset, shaderChainParams[shaderChainPreset].orEmpty())
|
||||
put("EmuCore/GS", "CASMode", "int", casMode.coerceIn(0, 2).toString())
|
||||
put("EmuCore/GS", "CASSharpness", "int", casSharpness.coerceIn(0, 100).toString())
|
||||
// Upper bound is UPSCALER_FSR1, not the count of options this UI shows — clamping to
|
||||
// the visible choices would silently rewrite FSR1 back to Off.
|
||||
put("EmuCore/GS", "Upscaler", "int", upscaler.coerceIn(UPSCALER_OFF, UPSCALER_FSR1).toString())
|
||||
put("EmuCore/GS", "FSRSharpness", "int", fsrSharpness.coerceIn(0, 100).toString())
|
||||
put("EmuCore/GS", "LoadTextureReplacements", "bool", loadTextureReplacements.toString())
|
||||
put("EmuCore/GS", "LoadTextureReplacementsAsync", "bool", loadTextureReplacementsAsync.toString())
|
||||
put("EmuCore/GS", "PrecacheTextureReplacements", "bool", precacheTextureReplacements.toString())
|
||||
@@ -1570,6 +1583,8 @@ data class Settings(
|
||||
lsfgFlowScale != other.lsfgFlowScale ||
|
||||
casMode != other.casMode ||
|
||||
casSharpness != other.casSharpness ||
|
||||
upscaler != other.upscaler ||
|
||||
fsrSharpness != other.fsrSharpness ||
|
||||
accurateBlendingUnit != other.accurateBlendingUnit ||
|
||||
hwMipmap != other.hwMipmap ||
|
||||
triFilter != other.triFilter ||
|
||||
@@ -1794,6 +1809,8 @@ data class Settings(
|
||||
put("lsfgFlowScale", lsfgFlowScale)
|
||||
put("casMode", casMode)
|
||||
put("casSharpness", casSharpness)
|
||||
put("upscaler", upscaler)
|
||||
put("fsrSharpness", fsrSharpness)
|
||||
put("loadTextureReplacements", loadTextureReplacements)
|
||||
put("loadTextureReplacementsAsync", loadTextureReplacementsAsync)
|
||||
put("precacheTextureReplacements", precacheTextureReplacements)
|
||||
@@ -1857,6 +1874,12 @@ data class Settings(
|
||||
@JvmStatic
|
||||
internal var emitSink: ((String, String, String, String) -> Unit)? = null
|
||||
|
||||
/** [upscaler] values, straight from the core's GSUpscaler. Named because 1 is Apple's
|
||||
* MetalFX and never appears in this UI, so FSR1's value (2) does NOT line up with its
|
||||
* position in any Android picker — writing the picker index would select MetalFX. */
|
||||
const val UPSCALER_OFF = 0
|
||||
const val UPSCALER_FSR1 = 2
|
||||
|
||||
/** One-tap "Low-End" performance snapshot applied on top of [base].
|
||||
* Only cheap, safe-for-most levers that already exist as fields:
|
||||
* - accurate_blending_unit = Minimum (0) — cheapest blend path
|
||||
@@ -2073,6 +2096,8 @@ data class Settings(
|
||||
lsfgFlowScale = json.optInt("lsfgFlowScale", def.lsfgFlowScale),
|
||||
casMode = json.optInt("casMode", def.casMode),
|
||||
casSharpness = json.optInt("casSharpness", def.casSharpness),
|
||||
upscaler = json.optInt("upscaler", def.upscaler),
|
||||
fsrSharpness = json.optInt("fsrSharpness", def.fsrSharpness),
|
||||
loadTextureReplacements = json.optBoolean("loadTextureReplacements", def.loadTextureReplacements),
|
||||
loadTextureReplacementsAsync = json.optBoolean("loadTextureReplacementsAsync", def.loadTextureReplacementsAsync),
|
||||
precacheTextureReplacements = json.optBoolean("precacheTextureReplacements", def.precacheTextureReplacements),
|
||||
@@ -2316,6 +2341,8 @@ data class Settings(
|
||||
if (current.lsfgFlowScale != base.lsfgFlowScale) j.put("lsfgFlowScale", current.lsfgFlowScale)
|
||||
if (current.casMode != base.casMode) j.put("casMode", current.casMode)
|
||||
if (current.casSharpness != base.casSharpness) j.put("casSharpness", current.casSharpness)
|
||||
if (current.upscaler != base.upscaler) j.put("upscaler", current.upscaler)
|
||||
if (current.fsrSharpness != base.fsrSharpness) j.put("fsrSharpness", current.fsrSharpness)
|
||||
if (current.loadTextureReplacements != base.loadTextureReplacements) j.put("loadTextureReplacements", current.loadTextureReplacements)
|
||||
if (current.loadTextureReplacementsAsync != base.loadTextureReplacementsAsync) j.put("loadTextureReplacementsAsync", current.loadTextureReplacementsAsync)
|
||||
if (current.precacheTextureReplacements != base.precacheTextureReplacements) j.put("precacheTextureReplacements", current.precacheTextureReplacements)
|
||||
@@ -2576,6 +2603,8 @@ data class Settings(
|
||||
lsfgFlowScale = if (overrides.has("lsfgFlowScale")) overrides.getInt("lsfgFlowScale") else base.lsfgFlowScale,
|
||||
casMode = if (overrides.has("casMode")) overrides.getInt("casMode") else base.casMode,
|
||||
casSharpness = if (overrides.has("casSharpness")) overrides.getInt("casSharpness") else base.casSharpness,
|
||||
upscaler = if (overrides.has("upscaler")) overrides.getInt("upscaler") else base.upscaler,
|
||||
fsrSharpness = if (overrides.has("fsrSharpness")) overrides.getInt("fsrSharpness") else base.fsrSharpness,
|
||||
loadTextureReplacements = if (overrides.has("loadTextureReplacements")) overrides.getBoolean("loadTextureReplacements") else base.loadTextureReplacements,
|
||||
loadTextureReplacementsAsync = if (overrides.has("loadTextureReplacementsAsync")) overrides.getBoolean("loadTextureReplacementsAsync") else base.loadTextureReplacementsAsync,
|
||||
precacheTextureReplacements = if (overrides.has("precacheTextureReplacements")) overrides.getBoolean("precacheTextureReplacements") else base.precacheTextureReplacements,
|
||||
|
||||
@@ -1231,6 +1231,9 @@ val EN: Map<String, String> = mapOf(
|
||||
"renderer.cas.sharpen" to "Sharpen",
|
||||
"renderer.cas.sharpenResize" to "Sharpen + Resize",
|
||||
"renderer.cas.sharpness.label" to "CAS Sharpness",
|
||||
"renderer.fsr1.description" to "AMD FidelityFX Super Resolution 1 — upscales the frame to the screen with edge-adaptive sharpening instead of a plain stretch. Replaces CAS.",
|
||||
"renderer.fsr1.label" to "FSR 1 Upscaling",
|
||||
"renderer.fsr1.sharpness.label" to "FSR Sharpness",
|
||||
"renderer.fxaa.description" to "Fast post-process anti-aliasing — smooths jagged edges with a light blur.",
|
||||
"renderer.fxaa.label" to "FXAA",
|
||||
"renderer.deinterlacing.description" to "Changes how interlaced video is displayed. Auto is safest.",
|
||||
|
||||
@@ -399,24 +399,49 @@ fun RendererTab(state: MutableState<Settings>) {
|
||||
apply(s.copy(fxaa = it))
|
||||
}
|
||||
SettingsDivider()
|
||||
SegmentedRow(
|
||||
label = str("renderer.cas.label"),
|
||||
options = listOf(str("fixes.opt.off"), str("renderer.cas.sharpen"), str("renderer.cas.sharpenResize")),
|
||||
selectedIndex = s.casMode.coerceIn(0, 2),
|
||||
description = str("renderer.cas.description"),
|
||||
onChange = { apply(s.copy(casMode = it)) },
|
||||
)
|
||||
if (s.casMode != 0) {
|
||||
val fsr1On = s.upscaler == Settings.UPSCALER_FSR1
|
||||
ToggleRow(
|
||||
str("renderer.fsr1.label"),
|
||||
fsr1On,
|
||||
description = str("renderer.fsr1.description"),
|
||||
) {
|
||||
apply(s.copy(upscaler = if (it) Settings.UPSCALER_FSR1 else Settings.UPSCALER_OFF))
|
||||
}
|
||||
if (fsr1On) {
|
||||
SettingsDivider()
|
||||
IntSliderRow(
|
||||
label = str("renderer.cas.sharpness.label"),
|
||||
value = s.casSharpness.coerceIn(0, 100),
|
||||
label = str("renderer.fsr1.sharpness.label"),
|
||||
value = s.fsrSharpness.coerceIn(0, 100),
|
||||
min = 0,
|
||||
max = 100,
|
||||
valueFormatter = { "$it%" },
|
||||
onChange = { apply(s.copy(casSharpness = it)) },
|
||||
onChange = { apply(s.copy(fsrSharpness = it)) },
|
||||
)
|
||||
}
|
||||
// FSR's second pass IS RCAS, a contrast-adaptive sharpener, so the core runs one or
|
||||
// the other and never both. Showing CAS while FSR is on would offer a slider that
|
||||
// does nothing.
|
||||
if (!fsr1On) {
|
||||
SettingsDivider()
|
||||
SegmentedRow(
|
||||
label = str("renderer.cas.label"),
|
||||
options = listOf(str("fixes.opt.off"), str("renderer.cas.sharpen"), str("renderer.cas.sharpenResize")),
|
||||
selectedIndex = s.casMode.coerceIn(0, 2),
|
||||
description = str("renderer.cas.description"),
|
||||
onChange = { apply(s.copy(casMode = it)) },
|
||||
)
|
||||
if (s.casMode != 0) {
|
||||
SettingsDivider()
|
||||
IntSliderRow(
|
||||
label = str("renderer.cas.sharpness.label"),
|
||||
value = s.casSharpness.coerceIn(0, 100),
|
||||
min = 0,
|
||||
max = 100,
|
||||
valueFormatter = { "$it%" },
|
||||
onChange = { apply(s.copy(casSharpness = it)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
SettingsDivider()
|
||||
// RetroArch (.slangp) chains run last in the post-process order (after
|
||||
// ShadeBoost/FXAA/CAS), so they close out Display Effects. Presentation-only
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = map
|
||||
SettingsCategory.Graphics to listOf(
|
||||
"accurateBlendingUnit", "adrenoFbFetch", "aspectRatio", "casMode", "casSharpness",
|
||||
"customAspectRatio", "deinterlaceMode", "displayBilinear", "dumpReplaceableTextures", "fmvAspectRatio",
|
||||
"forceMaliFbFetch", "fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode",
|
||||
"forceMaliFbFetch", "fsrSharpness", "fxaa", "gpuProfile", "gsBackThreadMode", "hardwareDownloadMode",
|
||||
"hwAa1", "hwAccurateAlphaTest", "hwMipmap", "hwRov", "loadTextureReplacements",
|
||||
"loadTextureReplacementsAsync", "maxAnisotropy", "orientation",
|
||||
"osdShowTextureReplacements", "portraitRenderTop", "landscapeRenderTop", "autoProgressiveScan",
|
||||
@@ -47,7 +47,7 @@ internal val SETTINGS_CATEGORY_FIELDS: Map<SettingsCategory, List<String>> = map
|
||||
"shadeBoost", "shadeBoostBrightness", "shadeBoostContrast", "shadeBoostGamma",
|
||||
"shadeBoostSaturation", "shaderChainEnabled", "shaderChainParams", "shaderChainPreset",
|
||||
"textureFiltering", "texturePreloading", "triFilter", "tvShader", "upscaleFloat",
|
||||
"vsyncEnable",
|
||||
"upscaler", "vsyncEnable",
|
||||
),
|
||||
// AudioTab.kt
|
||||
SettingsCategory.Audio to listOf(
|
||||
|
||||
@@ -106,6 +106,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("renderer.contrast.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.saturation.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.gamma.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.fsr1.sharpness.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.cas.sharpness.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.displayMode.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.fmvAspect.label", true, SettingsCategory.Graphics),
|
||||
@@ -116,6 +117,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("renderer.textureFiltering.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.texturePreloading.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.displayFilter.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.fsr1.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.cas.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.blendingAccuracy.label", true, SettingsCategory.Graphics),
|
||||
SettingsSearchEntry("renderer.trilinear.label", true, SettingsCategory.Graphics),
|
||||
|
||||
Reference in New Issue
Block a user