From 82e34d410ff6fdffa4916c90faa05c676a5d65e4 Mon Sep 17 00:00:00 2001 From: Takoroka <137927095+Takoroka@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:31:57 -0500 Subject: 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. --- src/video_core/engines/fermi_2d.h | 3 +++ src/video_core/engines/kepler_compute.cpp | 26 +++++++++++++++++++++- src/video_core/host_shaders/CMakeLists.txt | 1 + .../host_shaders/blit_color_srgb_to_linear.frag | 18 +++++++++++++++ src/video_core/renderer_vulkan/blit_image.cpp | 7 +++++- src/video_core/renderer_vulkan/blit_image.h | 1 + 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/video_core/host_shaders/blit_color_srgb_to_linear.frag 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(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 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 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; -- cgit v1.2.3