From 1673d8fd6ac2c88b3185334ae53e657fafa2c094 Mon Sep 17 00:00:00 2001 From: Rakete175 <64165408+Rakete175@users.noreply.github.com> Date: Fri, 10 Jul 2026 20:04:22 +0200 Subject: [PATCH] vulkan: fix washed-out and blurry 2D UI rendering (minimap, bigmap) The swapchain uses VK_FORMAT_B8G8R8A8_SRGB, which means the hardware automatically sRGB-encodes all fragment shader output before display. UI textures are uploaded as UNORM (raw sRGB-encoded bytes, sampled verbatim), matching the GL renderer's non-sRGB framebuffer behavior. In the GL renderer, UI bytes are written directly to a linear framebuffer and displayed as-is. In Vulkan, those same bytes get sRGB-encoded a second time by the swapchain attachment, causing a perceptual washed-out appearance on the minimap, bigmap, and other 2D elements. Fix by performing all color math in sRGB space (matching GL semantics), then decoding the final result to linear before output. The swapchain's automatic sRGB encoding then restores the original byte values, ensuring bit-identical output to GL. Semi-transparent blends now occur in linear space rather than sRGB space. This is visually imperceptible on soft edges and text; the alternative (byte-exact blending) would require VK_KHR_swapchain_mutable_format and a UNORM view, which is overkill for marginal gain. Separately, the flat map (minimap) texture was being rendered with linear filtering in Vulkan, causing a blurry appearance when magnified. The OpenGL renderer uses GL_NEAREST for both MIN and MAG filters on the minimap texture, producing crisp pixel-perfect rendering. This mismatch occurred because VulkanFlatMapRenderer did not configure a sampler for the map image, leaving it with the default linear sampler from VulkanRenderer::CreateImage. The bigmap was unaffected because it is typically rendered at or near 1:1 scale, where filtering artifacts are less visible. Fix by explicitly creating a nearest-neighbor sampler for the flat map texture immediately after image creation. The VulkanImageRenderer already respects per-image samplers via GetSampler(), so this change takes effect automatically without modifications to the 2D rendering pipeline. Also add a safeguard in VulkanImage::CreateSampler to destroy any existing sampler before creating a new one, preventing potential resource leaks on resampling. Fixes: minimap/bigmap rendering appearing desaturated under Vulkan Fixes: minimap appearing blurry/soft-edged under Vulkan --- Resources/Shaders/Vulkan/BasicImage.frag | 17 ++++++++++++++++- Sources/Draw/Vulkan/VulkanFlatMapRenderer.cpp | 9 +++++++++ Sources/Draw/Vulkan/VulkanImage.cpp | 4 ++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Resources/Shaders/Vulkan/BasicImage.frag b/Resources/Shaders/Vulkan/BasicImage.frag index b3db11952..9837e69d9 100644 --- a/Resources/Shaders/Vulkan/BasicImage.frag +++ b/Resources/Shaders/Vulkan/BasicImage.frag @@ -27,10 +27,25 @@ layout(location = 1) in vec2 texCoord; layout(location = 0) out vec4 fragColor; +// The swapchain attachment is VK_FORMAT_B8G8R8A8_SRGB: the hardware +// sRGB-ENCODES whatever this shader writes. But 2D UI textures are +// uploaded as UNORM (raw sRGB bytes sampled verbatim) and the GL +// renderer writes those bytes straight to a non-sRGB framebuffer. +// Without correction the value gets sRGB-encoded a second time -> +// washed-out minimap/UI. Fix: do all math in sRGB space exactly like +// GL, then DECODE to linear so the attachment's encode restores the +// original bytes. +vec3 srgbToLinear(vec3 c) { + bvec3 lo = lessThanEqual(c, vec3(0.04045)); + vec3 linLo = c / 12.92; + vec3 linHi = pow((c + 0.055) / 1.055, vec3(2.4)); + return mix(linHi, linLo, vec3(lo)); +} + void main() { vec2 flippedTexCoord = vec2(texCoord.x, 1.0 - texCoord.y); vec4 col = texture(mainTexture, flippedTexCoord); col.xyz *= col.w; col *= color; - fragColor = col; + fragColor = vec4(srgbToLinear(col.xyz), col.w); } diff --git a/Sources/Draw/Vulkan/VulkanFlatMapRenderer.cpp b/Sources/Draw/Vulkan/VulkanFlatMapRenderer.cpp index c3c55eb98..bec457af6 100644 --- a/Sources/Draw/Vulkan/VulkanFlatMapRenderer.cpp +++ b/Sources/Draw/Vulkan/VulkanFlatMapRenderer.cpp @@ -20,6 +20,8 @@ #include "VulkanFlatMapRenderer.h" #include "VulkanRenderer.h" +#include "VulkanImage.h" +#include "VulkanImageWrapper.h" #include #include #include @@ -39,6 +41,13 @@ namespace spades { Handle bmp(GenerateBitmap(0, 0, m.Width(), m.Height()), false); image = renderer.CreateImage(*bmp); + + auto* wrapper = dynamic_cast(image.GetPointerOrNull()); + if (wrapper && wrapper->GetVulkanImage()) { + wrapper->GetVulkanImage()->CreateSampler( + VK_FILTER_NEAREST, VK_FILTER_NEAREST, + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, false); + } } VulkanFlatMapRenderer::~VulkanFlatMapRenderer() {} diff --git a/Sources/Draw/Vulkan/VulkanImage.cpp b/Sources/Draw/Vulkan/VulkanImage.cpp index 1702ab268..7c70cf6bc 100644 --- a/Sources/Draw/Vulkan/VulkanImage.cpp +++ b/Sources/Draw/Vulkan/VulkanImage.cpp @@ -192,6 +192,10 @@ namespace spades { void VulkanImage::CreateSampler(VkFilter magFilter, VkFilter minFilter, VkSamplerAddressMode addressMode, bool enableAnisotropy) { + if (sampler != VK_NULL_HANDLE) { + vkDestroySampler(device->GetDevice(), sampler, nullptr); + sampler = VK_NULL_HANDLE; + } VkSamplerCreateInfo samplerInfo{}; samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; samplerInfo.magFilter = magFilter;