From ffaa5e8001141a6754f218d8b7bfda8b1673f71e Mon Sep 17 00:00:00 2001 From: Nexoniarz Date: Tue, 20 Oct 2026 19:01:07 +0200 Subject: [PATCH] =?UTF-8?q?Pictures=20in=20the=20UI=20read=20as=20ImGui's?= =?UTF-8?q?=20colours=20are:=20an=20sRGB=20picture=20was=20turned=20linear?= =?UTF-8?q?=20and=20showed=20too=20dark=20(the=20logo=20in=20About,=20the?= =?UTF-8?q?=20Picture=20panel's=20paint)=20=E2=80=94=20a=20UNORM=20view=20?= =?UTF-8?q?on=20Vulkan,=20a=20skip-decode=20sampler=20on=20OpenGL=204.6=20?= =?UTF-8?q?and=204.0,=20the=20texture's=20own=20setting=20before=203.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- engine/render/backend.h | 13 ++++++++++++ engine/render/renderer.h | 1 + engine/rhi/gl/gl_backend.cpp | 19 +++++++++++++++-- engine/rhi/gl/gl_legacy.cpp | 36 ++++++++++++++++++++++++++++++-- engine/rhi/gl/gl_loader.h | 1 + engine/rhi/vulkan/vk_backend.cpp | 14 ++++++++++++- 6 files changed, 79 insertions(+), 5 deletions(-) diff --git a/engine/render/backend.h b/engine/render/backend.h index 5665f96..3cf9b49 100644 --- a/engine/render/backend.h +++ b/engine/render/backend.h @@ -14,8 +14,20 @@ #include #include +struct ImDrawList; +struct ImDrawCmd; + namespace keel { +// Draw callbacks round an sRGB picture drawn in the UI, so it's read as the plain numbers ImGui's +// own colours are (read as sRGB it turns linear, and looks too dark). None when uiTexture already +// gives such a view (Vulkan). +struct UiRawRead { + void (*begin)(const ImDrawList*, const ImDrawCmd*) = nullptr; + void (*end)(const ImDrawList*, const ImDrawCmd*) = nullptr; + void* data = nullptr; +}; + class Window; using TextureId = uint32_t; @@ -217,6 +229,7 @@ public: virtual void uploadModel(uint32_t model, const KmdlModel& kmdl) = 0; // A texture as Dear ImGui's ImTextureID, for drawing it in the UI (0 = none). virtual uint64_t uiTexture(TextureId slot) = 0; + virtual UiRawRead uiRawRead(TextureId) { return {}; } // Starts a frame; false when nothing can be drawn (minimised). Sets the drawable size. virtual bool beginFrame(uint32_t& width, uint32_t& height) = 0; // Renders the packet, then ImGui's draw data, and presents. diff --git a/engine/render/renderer.h b/engine/render/renderer.h index a22da7d..89bc5bd 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -76,6 +76,7 @@ public: // A loaded texture for ImGui::Image (ImTextureID); 0 if it isn't one. uint64_t uiTexture(TextureId texture) { return backend_ ? backend_->uiTexture(texture) : 0; } + UiRawRead uiRawRead(TextureId texture) { return backend_ ? backend_->uiRawRead(texture) : UiRawRead{}; } // Pixel size of a loaded texture. glm::ivec2 textureSize(TextureId texture) const { const auto it = textureSizes_.find(texture); diff --git a/engine/rhi/gl/gl_backend.cpp b/engine/rhi/gl/gl_backend.cpp index e3169fb..a57b972 100644 --- a/engine/rhi/gl/gl_backend.cpp +++ b/engine/rhi/gl/gl_backend.cpp @@ -232,6 +232,12 @@ public: gl::SamplerParameteri(clampSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); gl::SamplerParameteri(clampSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl::CreateSamplers(1, &uiRawSampler_); // (UI pictures: sRGB read as plain numbers, as ImGui's colours are) + gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + gl::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + gl::SamplerParameteri(uiRawSampler_, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A4A /* GL_SKIP_DECODE_EXT */); gl::CreateSamplers(1, &compareSampler_); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR); gl::SamplerParameteri(compareSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); @@ -320,7 +326,7 @@ public: } for (GLuint b : {textureTable_, shadowTable_, draws_, joints_, frame_, mirrorFrame_, lights_, views_, cells_, indices_, push_, empty_}) gl::DeleteBuffers(1, &b); - for (GLuint s : {repeatSampler_, clampSampler_, compareSampler_}) gl::DeleteSamplers(1, &s); + for (GLuint s : {repeatSampler_, clampSampler_, compareSampler_, uiRawSampler_}) gl::DeleteSamplers(1, &s); for (GLuint p : {meshProgram_, skinnedProgram_, shadowMeshProgram_, shadowSkinnedProgram_, boxProgram_, tonemapProgram_, skyProgram_, hzbProgram_, ssaoProgram_, ssaoBlurProgram_, ssrProgram_, adaptProgram_, volumetricProgram_, spriteProgram_, bloomDownProgram_, bloomUpProgram_, taaProgram_}) gl::DeleteProgram(p); @@ -358,6 +364,7 @@ public: gl::TextureSubImage2D(t.texture, GLint(level), 0, 0, GLsizei(std::max(1u, w >> level)), GLsizei(std::max(1u, h >> level)), GL_RGBA, GL_UNSIGNED_BYTE, mips[level].data()); } + t.srgb = srgb; if (auto it = textures_.find(slot); it != textures_.end()) retiredTextures_.emplace_back(frameNumber_, it->second); // queued draws may use it setHandles(slot, t, repeatSampler_, false); // like Vulkan: one repeating sampler for every texture textures_[slot] = t; @@ -385,6 +392,13 @@ public: const auto it = textures_.find(slot); return it == textures_.end() ? 0 : uint64_t(it->second.texture); } + UiRawRead uiRawRead(TextureId slot) override { + const auto it = textures_.find(slot); + if (it == textures_.end() || !it->second.srgb) return {}; + // (A sampler, not the texture's own setting: that's fixed once it has a bindless handle.) + return {[](const ImDrawList*, const ImDrawCmd* c) { gl::BindSampler(0, GLuint(uintptr_t(c->UserCallbackData))); }, + [](const ImDrawList*, const ImDrawCmd*) { gl::BindSampler(0, 0); }, reinterpret_cast(uintptr_t(uiRawSampler_))}; + } bool beginFrame(uint32_t& width, uint32_t& height) override { int w = 0, h = 0; @@ -424,6 +438,7 @@ private: struct Texture { GLuint texture = 0; GLuint64 handle = 0, shadowHandle = 0; + bool srgb = false; }; struct Model { GLuint vertices = 0, indices = 0, skin = 0; @@ -529,7 +544,7 @@ private: SDL_Window* window_ = nullptr; SDL_GLContext context_ = nullptr; bool imgui_ = false; - GLuint repeatSampler_ = 0, clampSampler_ = 0, compareSampler_ = 0; + GLuint repeatSampler_ = 0, clampSampler_ = 0, compareSampler_ = 0, uiRawSampler_ = 0; GLuint mirrorFrame_ = 0; // the mirror's pass's frame data GLuint textureTable_ = 0, shadowTable_ = 0, draws_ = 0, joints_ = 0, frame_ = 0, lights_ = 0, views_ = 0, cells_ = 0, indices_ = 0; GLuint push_ = 0, empty_ = 0, vao_ = 0; diff --git a/engine/rhi/gl/gl_legacy.cpp b/engine/rhi/gl/gl_legacy.cpp index cd10eb5..ed5b466 100644 --- a/engine/rhi/gl/gl_legacy.cpp +++ b/engine/rhi/gl/gl_legacy.cpp @@ -100,7 +100,11 @@ namespace { X(void, DrawBuffer, (GLenum)) \ X(void, DrawBuffers, (GLsizei, const GLenum*)) \ X(void, TexBuffer, (GLenum, GLenum, GLuint)) \ - X(void, BindFragDataLocation, (GLuint, GLuint, const GLchar*)) + X(void, BindFragDataLocation, (GLuint, GLuint, const GLchar*)) \ + X(void, GenSamplers, (GLsizei, GLuint*)) \ + X(void, DeleteSamplers, (GLsizei, const GLuint*)) \ + X(void, BindSampler, (GLuint, GLuint)) \ + X(void, SamplerParameteri, (GLuint, GLenum, GLint)) namespace glx { #define KEEL_GLX_DECLARE(ret, name, params) using name##_t = ret(APIENTRY*) params; name##_t name = nullptr; @@ -499,6 +503,14 @@ public: ImGui::GetIO().IniFilename = nullptr; ImGui::StyleColorsDark(); imgui_ = ImGui_ImplSDL3_InitForOpenGL(window_, context_) && ImGui_ImplOpenGL3_Init(es_ ? "#version 300 es" : "#version 140"); + if (glx::GenSamplers && glx::BindSampler && glx::SamplerParameteri) { // (3.3 on: UI pictures read as plain numbers, as ImGui's colours are) + glx::GenSamplers(1, &uiRawSampler_); + glx::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glx::SamplerParameteri(uiRawSampler_, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glx::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glx::SamplerParameteri(uiRawSampler_, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glx::SamplerParameteri(uiRawSampler_, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A4A /* GL_SKIP_DECODE_EXT */); + } log::info("Fallback renderer: tier {} ({})", tier_, tier_ >= 2 ? "shadows, normal maps, highlights, sky reflections, 16 lights" : "lightmaps, sun shadows, sky"); return imgui_; } @@ -515,6 +527,7 @@ public: for (GLuint t : textures_) if (t) glx::DeleteTextures(1, &t); for (Model& m : models_) releaseModel(m); + if (uiRawSampler_) glx::DeleteSamplers(1, &uiRawSampler_); releaseTargets(); for (GLuint* t : {&shadowMap_, &jointTexture_}) glx::DeleteTextures(1, t); glx::DeleteFramebuffers(1, &shadowFbo_); @@ -538,7 +551,8 @@ public: // No block compression here (old GPUs): the renderer decodes BC7 to RGBA8 first. void createTexture(TextureId slot, uint32_t w, uint32_t h, bool srgb, const std::vector>& mips, TexFormat = TexFormat::RGBA8) override { - if (slot >= textures_.size()) textures_.resize(slot + 1, 0); + if (slot >= textures_.size()) textures_.resize(slot + 1, 0), srgb_.resize(slot + 1, false); + srgb_[slot] = srgb; GLuint& t = textures_[slot]; if (!t) glx::GenTextures(1, &t); glx::BindTexture(GL_TEXTURE_2D, t); @@ -585,6 +599,22 @@ public: } uint64_t uiTexture(TextureId slot) override { return slot < textures_.size() ? uint64_t(textures_[slot]) : 0; } + // (A sampler object where there are any; else the texture's own setting, switched for the draw and back.) + UiRawRead uiRawRead(TextureId slot) override { + if (slot >= textures_.size() || !srgb_[slot]) return {}; + if (uiRawSampler_) + return {[](const ImDrawList*, const ImDrawCmd* c) { glx::BindSampler(0, GLuint(uintptr_t(c->UserCallbackData))); }, + [](const ImDrawList*, const ImDrawCmd*) { glx::BindSampler(0, 0); }, reinterpret_cast(uintptr_t(uiRawSampler_))}; + return {[](const ImDrawList*, const ImDrawCmd* c) { + glx::BindTexture(GL_TEXTURE_2D, GLuint(uintptr_t(c->UserCallbackData))); + glx::TexParameteri(GL_TEXTURE_2D, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A4A /* GL_SKIP_DECODE_EXT */); + }, + [](const ImDrawList*, const ImDrawCmd* c) { + glx::BindTexture(GL_TEXTURE_2D, GLuint(uintptr_t(c->UserCallbackData))); + glx::TexParameteri(GL_TEXTURE_2D, 0x8A48 /* GL_TEXTURE_SRGB_DECODE_EXT */, 0x8A49 /* GL_DECODE_EXT */); + }, + reinterpret_cast(uintptr_t(textures_[slot]))}; + } bool beginFrame(uint32_t& width, uint32_t& height) override { int w = 0, h = 0; @@ -722,6 +752,8 @@ private: bool imgui_ = false; Program mesh_, depth_, sky_, box_, tonemap_, sprite_; std::vector textures_; + std::vector srgb_; + GLuint uiRawSampler_ = 0; std::vector models_; GLuint frameUbo_ = 0, jointBuffer_ = 0, jointTexture_ = 0, emptyVao_ = 0; GLuint shadowMap_ = 0, shadowFbo_ = 0; diff --git a/engine/rhi/gl/gl_loader.h b/engine/rhi/gl/gl_loader.h index 0319edf..f59388b 100644 --- a/engine/rhi/gl/gl_loader.h +++ b/engine/rhi/gl/gl_loader.h @@ -34,6 +34,7 @@ GLenum format, GLsizei size, const void* data)) \ X(void, CreateSamplers, (GLsizei n, GLuint* samplers)) \ X(void, DeleteSamplers, (GLsizei n, const GLuint* samplers)) \ + X(void, BindSampler, (GLuint unit, GLuint sampler)) \ X(void, SamplerParameteri, (GLuint sampler, GLenum pname, GLint param)) \ X(void, SamplerParameterf, (GLuint sampler, GLenum pname, GLfloat param)) \ X(GLuint64, GetTextureSamplerHandleARB, (GLuint texture, GLuint sampler)) \ diff --git a/engine/rhi/vulkan/vk_backend.cpp b/engine/rhi/vulkan/vk_backend.cpp index e586d6c..e599075 100644 --- a/engine/rhi/vulkan/vk_backend.cpp +++ b/engine/rhi/vulkan/vk_backend.cpp @@ -425,6 +425,7 @@ public: ici.arrayLayers = 1; ici.samples = VK_SAMPLE_COUNT_1_BIT; ici.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if (srgb) ici.flags = VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT; // (the UI reads it as plain numbers: uiView) VmaAllocationCreateInfo aci{}; aci.usage = VMA_MEMORY_USAGE_AUTO; VK_CHECK(vmaCreateImage(device_.allocator(), &ici, &aci, &t.image, &t.alloc, nullptr)); @@ -454,6 +455,12 @@ public: vci.format = format; vci.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, uint32_t(mips.size()), 0, 1}; VK_CHECK(vkCreateImageView(device_.device(), &vci, nullptr, &t.view)); + // ImGui's colours are sRGB numbers drawn as they are, so the UI must read the picture without + // turning it linear first (else it shows too dark): the same image, seen as UNORM. + if (srgb) { + vci.format = fmt == TexFormat::BC7 ? VK_FORMAT_BC7_UNORM_BLOCK : VK_FORMAT_R8G8B8A8_UNORM; + VK_CHECK(vkCreateImageView(device_.device(), &vci, nullptr, &t.uiView)); + } if (textures_.size() <= slot) textures_.resize(slot + 1); // Replaced (hot reload): frames in flight may still sample the old one. if (textures_[slot].image) retiredTextures_.emplace_back(frameNumber_, textures_[slot]); @@ -484,7 +491,10 @@ public: uiTextures_.erase(it), it = uiTextures_.end(); } if (it == uiTextures_.end()) - it = uiTextures_.emplace(slot, UiTexture{uint64_t(ImGui_ImplVulkan_AddTexture(sampler_, textures_[slot].view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)), textures_[slot].view}).first; + it = uiTextures_.emplace(slot, UiTexture{uint64_t(ImGui_ImplVulkan_AddTexture(sampler_, textures_[slot].uiView ? textures_[slot].uiView : textures_[slot].view, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)), + textures_[slot].view}) + .first; return it->second.set; } @@ -535,6 +545,7 @@ private: VkImage image = VK_NULL_HANDLE; VmaAllocation alloc = VK_NULL_HANDLE; VkImageView view = VK_NULL_HANDLE; + VkImageView uiView = VK_NULL_HANDLE; // (an sRGB picture seen as UNORM, for the UI) }; // A ray-tracing shape (bottom-level acceleration structure): one per submesh, by first index. struct Blas { @@ -549,6 +560,7 @@ private: void destroyTexture(Texture& t) { if (t.view) vkDestroyImageView(device_.device(), t.view, nullptr); + if (t.uiView) vkDestroyImageView(device_.device(), t.uiView, nullptr); if (t.image) vmaDestroyImage(device_.allocator(), t.image, t.alloc); t = {}; }