From 7d308882e8d09970cef06a158183303bd1665557 Mon Sep 17 00:00:00 2001 From: dfattal Date: Wed, 3 Jun 2026 19:43:39 -0700 Subject: [PATCH] fix(compositor-gl): sRGB passthrough for in-process GL swapchains In-process OpenGL apps rendered ~2.2x too dark versus the IPC/service path. The native GL compositor creates app swapchains as GL_SRGB8_ALPHA8 and samples them during compose; the GPU auto-decodes sRGB->linear on the sample, but compose writes to a non-sRGB atlas with no matching re-encode, so the Leia DP (which expects sRGB-encoded bytes) received linearized, too-dark content. Set GL_TEXTURE_SRGB_DECODE_EXT=GL_SKIP_DECODE_EXT on the swapchain texture (guarded by a runtime GL_EXT_texture_sRGB_decode check) so the compositor's sample passes the stored bytes through unchanged, matching the desktop, other OpenXR runtimes, and the service path. Correct whether or not the app enables GL_FRAMEBUFFER_SRGB, and affects only runtime-side sampling, never the app's own rendering. Verified on-display with a GL Gaussian-splat player (cube/me_zoom sequence): midtones/shadows now match the desktop build. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/xrt/compositor/gl/comp_gl_compositor.cpp | 43 ++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/xrt/compositor/gl/comp_gl_compositor.cpp b/src/xrt/compositor/gl/comp_gl_compositor.cpp index 953a867ad..0f0d4307d 100644 --- a/src/xrt/compositor/gl/comp_gl_compositor.cpp +++ b/src/xrt/compositor/gl/comp_gl_compositor.cpp @@ -448,6 +448,35 @@ xrt_format_to_gl_internal(int64_t fmt) } } +// GL_EXT_texture_sRGB_decode — not in our GLAD spec, so define the enums. +#ifndef GL_TEXTURE_SRGB_DECODE_EXT +#define GL_TEXTURE_SRGB_DECODE_EXT 0x8A48 +#endif +#ifndef GL_SKIP_DECODE_EXT +#define GL_SKIP_DECODE_EXT 0x8A4A +#endif + +// True if GL_EXT_texture_sRGB_decode is present (cached; needs a current context). +static bool +gl_has_srgb_decode_ext(void) +{ + static int cached = -1; + if (cached >= 0) { + return cached != 0; + } + cached = 0; + GLint n = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &n); + for (GLint i = 0; i < n; i++) { + const GLubyte *e = glGetStringi(GL_EXTENSIONS, (GLuint)i); + if (e != NULL && strcmp((const char *)e, "GL_EXT_texture_sRGB_decode") == 0) { + cached = 1; + break; + } + } + return cached != 0; +} + /* * @@ -580,6 +609,20 @@ gl_compositor_create_swapchain(struct xrt_compositor *xc, glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + // sRGB passthrough: apps write display-referred bytes into the sRGB + // swapchain image (typically with GL_FRAMEBUFFER_SRGB off). When the + // compositor later samples it, a GL_SRGB8_ALPHA8 texture would auto + // decode sRGB->linear, and since compose writes to a non-sRGB atlas + // with no re-encode the DP receives ~2.2x-too-dark content. The Leia + // DP expects sRGB-encoded bytes, so skip the sample-time decode and + // pass the stored bytes through unchanged. This is correct for apps + // that DO enable GL_FRAMEBUFFER_SRGB too (their encoded bytes also + // pass through). Only the in-process native GL path samples these + // textures, so this never affects app-side rendering. + if (internal_format == GL_SRGB8_ALPHA8 && gl_has_srgb_decode_ext()) { + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SRGB_DECODE_EXT, GL_SKIP_DECODE_EXT); + } + // Store GL texture name in the swapchain_gl images array // (this is what the state tracker reads via xrt_swapchain_gl) sc->base.images[i] = sc->textures[i];