aboutsummaryrefslogtreecommitdiff
path: root/src/video_core
diff options
context:
space:
mode:
authorTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-02 02:31:57 -0500
committerTakoroka <137927095+Takoroka@users.noreply.github.com>2026-11-02 02:31:57 -0500
commit82e34d410ff6fdffa4916c90faa05c676a5d65e4 (patch)
tree15a8fdcbf41f6f873b4843fa7565e1c57da498b3 /src/video_core
parent9c55e70d790a4cc9507d80dbb9a9d6224175574e (diff)
nx2: decode sRGB in the upscale fallback when the program does
Splatoon Raiders' upscaler program ends by decoding its result from sRGB to linear (HSETP2 against 0.04045, HMUL2 by 2.4 between MUFU.LG2 and MUFU.EX2) before its surface store. The bilinear copy that stands in for it while HMMA isn't emulated wrote the sRGB input as-is, so the later passes encoded it a second time: outdoor scenes were washed out, with a pale sky and a white haze. When the program carries those two immediates, the copy now decodes like it, through a new copy operation of the blit helper (a fragment shader with the piecewise sRGB decode). Programs without them, such as Kirby Air Riders' upscaler, keep the plain copy. The OpenGL backend ignores the operation.
Diffstat (limited to 'src/video_core')
-rw-r--r--src/video_core/engines/fermi_2d.h3
-rw-r--r--src/video_core/engines/kepler_compute.cpp26
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt1
-rw-r--r--src/video_core/host_shaders/blit_color_srgb_to_linear.frag18
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp7
-rw-r--r--src/video_core/renderer_vulkan/blit_image.h1
6 files changed, 54 insertions, 2 deletions
diff --git a/src/video_core/engines/fermi_2d.h b/src/video_core/engines/fermi_2d.h
index 7fddf9fdc1..04a12fd351 100644
--- a/src/video_core/engines/fermi_2d.h
+++ b/src/video_core/engines/fermi_2d.h
@@ -70,6 +70,9 @@ public:
ROP = 4,
SrcCopyPremult = 5,
BlendPremult = 6,
+ // Not a hardware operation: a copy that decodes sRGB-encoded values to linear, for the
+ // nx2 upscale fallback standing in for a program that decodes its output.
+ SrcCopySrgbToLinear = 7,
};
enum class MemoryLayout : u32 {
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index b8a6567907..e4f527cd58 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -665,8 +665,32 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
result.addr_lower = static_cast<u32>(tic.Address());
return result;
};
+ // Title 4's program ends by decoding its result from sRGB to linear (the 0.04045 threshold
+ // and the 2.4 exponent as half-precision immediates of HSETP2 and HMUL2). The copy of its
+ // sRGB input left the frame gamma-encoded twice, washed out: decode in the copy when the
+ // program does.
+ static std::unordered_map<GPUVAddr, bool> decodes_srgb;
+ const GPUVAddr program = regs.code_loc.Address();
+ const auto [known, inserted] = decodes_srgb.try_emplace(program, false);
+ if (inserted) {
+ bool threshold = false, exponent = false;
+ std::array<u32, 0x400> block{};
+ for (u64 offset = 0; offset < 0x20000 && !(threshold && exponent); offset += sizeof(block)) {
+ if (!memory_manager.IsFullyMappedRange(program + offset, sizeof(block))) break;
+ memory_manager.ReadBlock(program + offset, block.data(), sizeof(block));
+ for (size_t word = 0; word < block.size(); word += 4) {
+ const u32 opcode = block[word] & 0xfff;
+ threshold |= opcode == 0x434 && block[word + 1] == 0x292d292d;
+ exponent |= opcode == 0x832 && block[word + 1] == 0x40cc40cc;
+ }
+ }
+ known->second = threshold && exponent;
+ LOG_INFO(HW_GPU, "Upscale fallback for program {:#x}: {}", program,
+ known->second ? "decodes sRGB like the program" : "plain copy");
+ }
const Fermi2D::Config config{
- .operation = Fermi2D::Operation::SrcCopy,
+ .operation = known->second ? Fermi2D::Operation::SrcCopySrgbToLinear
+ : Fermi2D::Operation::SrcCopy,
.filter = Fermi2D::Filter::Bilinear,
.must_accelerate = true,
.dst_x0 = 0,
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index ada5c779dc..5193b5e2c8 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -17,6 +17,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_srgb_to_linear.frag
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/blit_depth.frag
diff --git a/src/video_core/host_shaders/blit_color_srgb_to_linear.frag b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
new file mode 100644
index 0000000000..1a38cb52a1
--- /dev/null
+++ b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#version 450
+
+layout(binding = 0) uniform sampler2D tex;
+
+layout(location = 0) in vec2 texcoord;
+layout(location = 0) out vec4 color;
+
+// Copy of an sRGB-encoded source into a linear destination: the piecewise sRGB decode on the
+// color channels, alpha unchanged.
+void main() {
+ const vec4 value = textureLod(tex, texcoord, 0);
+ const vec3 low = value.rgb / 12.92;
+ const vec3 high = pow((value.rgb + 0.055) / 1.055, vec3(2.4));
+ color = vec4(mix(high, low, lessThan(value.rgb, vec3(0.04045))), value.a);
+}
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index edd151b563..506daef11e 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -11,6 +11,7 @@
#include "common/div_ceil.h"
#include "common/settings.h"
#include "video_core/host_shaders/blit_color_float_frag_spv.h"
+#include "video_core/host_shaders/blit_color_srgb_to_linear_frag_spv.h"
#include "video_core/host_shaders/blit_color_msaa_frag_spv.h"
#include "video_core/host_shaders/blit_depth_frag_spv.h"
#include "video_core/host_shaders/blit_depth_msaa_frag_spv.h"
@@ -670,6 +671,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)),
blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)),
temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)),
+ blit_color_srgb_to_linear_frag(BuildShader(device, BLIT_COLOR_SRGB_TO_LINEAR_FRAG_SPV)),
blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)),
blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported()
? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV)
@@ -1331,7 +1333,10 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
}
blit_color_keys.push_back(key);
- const std::array stages = MakeStages(*full_screen_vert, *blit_color_to_color_frag);
+ const bool srgb_to_linear =
+ key.operation == Tegra::Engines::Fermi2D::Operation::SrcCopySrgbToLinear;
+ const std::array stages = MakeStages(
+ *full_screen_vert, srgb_to_linear ? *blit_color_srgb_to_linear_frag : *blit_color_to_color_frag);
const VkPipelineColorBlendAttachmentState blend_attachment{
.blendEnable = VK_FALSE,
.srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h
index 303beeea5c..4741f9fe45 100644
--- a/src/video_core/renderer_vulkan/blit_image.h
+++ b/src/video_core/renderer_vulkan/blit_image.h
@@ -229,6 +229,7 @@ private:
vk::ShaderModule full_screen_vert;
vk::ShaderModule blit_color_to_color_frag;
vk::ShaderModule temporal_upscale_frag;
+ vk::ShaderModule blit_color_srgb_to_linear_frag;
vk::ShaderModule blit_color_msaa_frag;
vk::ShaderModule blit_depth_stencil_frag;
vk::ShaderModule blit_depth_frag;