From acb5c067942fd31bc08000994cb5f06b8d06b73d Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 18 Jul 2026 03:02:19 +1000 Subject: [PATCH] egl: correct ImGui overlay luminance in HDR Render ImGui to an intermediate SDR framebuffer and map it into the native PQ or scRGB output using the Wayland surface reference white. Preserve premultiplied alpha during conversion and limit composition to damaged overlay regions. --- client/displayservers/Wayland/gl.c | 6 +- client/displayservers/Wayland/wayland.c | 13 +- client/displayservers/Wayland/wayland.h | 3 + client/include/interface/displayserver.h | 15 ++ client/renderers/EGL/CMakeLists.txt | 3 + client/renderers/EGL/egl.c | 26 ++ client/renderers/EGL/hdr_overlay.c | 238 +++++++++++++++++++ client/renderers/EGL/hdr_overlay.h | 45 ++++ client/renderers/EGL/shader/hdr_overlay.frag | 35 +++ client/renderers/EGL/shader/hdr_overlay.vert | 13 + 10 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 client/renderers/EGL/hdr_overlay.c create mode 100644 client/renderers/EGL/hdr_overlay.h create mode 100644 client/renderers/EGL/shader/hdr_overlay.frag create mode 100644 client/renderers/EGL/shader/hdr_overlay.vert diff --git a/client/displayservers/Wayland/gl.c b/client/displayservers/Wayland/gl.c index 883fbc1e..04611404 100644 --- a/client/displayservers/Wayland/gl.c +++ b/client/displayservers/Wayland/gl.c @@ -284,13 +284,15 @@ void waylandSetHDRImageDescription(const uint16_t displayPrimary[3][2], // min_lum : 0.0001 cd/m² (source already scaled) -> pass through // max_lum : cd/m² (used only for scRGB; ignored for PQ, where the // compositor forces it to min_lum + 10000 cd/m²) - // reference_lum : cd/m² -> BT.2408 diffuse white (203 PQ / 80 scRGB) + // reference_lum : cd/m² -> locally configured reference white if (wlWm.cmHasLuminances) wp_image_description_creator_params_v1_set_luminances( wlWm.hdrImageCreator, minDisplayLuminance > 0 ? minDisplayLuminance : 50, maxDisplayLuminance > 0 ? maxDisplayLuminance : 1000, - hdrPQ ? 203 : 80); + hdrPQ ? + wlWm.hdrWhiteLevels.pq : + wlWm.hdrWhiteLevels.scRGB); // Advertise the content primaries (BT.2020 for PQ). These describe the real // colour gamut of the signal and are safe to forward. diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index 9b6e91cd..93aa2fc2 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -108,7 +108,12 @@ static bool getCompositor(char * dst, size_t size) static bool waylandInit(const LG_DSInitParams params) { memset(&wlWm, 0, sizeof(wlWm)); - wlWm.desktop = WL_Desktops[0]; + wlWm.desktop = WL_Desktops[0]; + wlWm.hdrWhiteLevels = (LG_DSHDRWhiteLevels) + { + .pq = 203, + .scRGB = 80, + }; wlWm.display = wl_display_connect(NULL); if (!wlWm.display) @@ -215,6 +220,12 @@ static bool waylandGetProp(LG_DSProperty prop, void * ret) return true; } + if (prop == LG_DS_HDR_WHITE_LEVELS) + { + *(LG_DSHDRWhiteLevels *)ret = wlWm.hdrWhiteLevels; + return true; + } + return false; } diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index 2d912f65..0eaebaa9 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -206,6 +206,9 @@ struct WaylandDSState bool cmHasPerceptualIntent; bool cmCanDoHDR; // true if compositor supports features needed for HDR + // Reference white configured on native HDR surface image descriptions. + LG_DSHDRWhiteLevels hdrWhiteLevels; + // toplevel icon manager struct xdg_toplevel_icon_manager_v1 * iconManager; diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index d495cf47..cefdddd9 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -61,9 +61,24 @@ typedef enum LG_DSProperty * return data type: bool */ LG_DS_NATIVE_HDR, + + /** + * returns the local reference-white luminance configured for each native + * HDR transfer function. This describes locally rendered content, not the + * guest's SDR white level. + * return data type: LG_DSHDRWhiteLevels + */ + LG_DS_HDR_WHITE_LEVELS, } LG_DSProperty; +typedef struct LG_DSHDRWhiteLevels +{ + uint32_t pq; + uint32_t scRGB; +} +LG_DSHDRWhiteLevels; + enum LG_DSWarpSupport { LG_DS_WARP_NONE, diff --git a/client/renderers/EGL/CMakeLists.txt b/client/renderers/EGL/CMakeLists.txt index 794be2ab..17b374e3 100644 --- a/client/renderers/EGL/CMakeLists.txt +++ b/client/renderers/EGL/CMakeLists.txt @@ -55,6 +55,8 @@ build_shaders( shader/cursor_mono.frag shader/damage.vert shader/damage.frag + shader/hdr_overlay.vert + shader/hdr_overlay.frag shader/basic.vert shader/convert_24bit.frag shader/ffx_cas.frag @@ -85,6 +87,7 @@ add_library(renderer_EGL STATIC desktop_rects.c cursor.c damage.c + hdr_overlay.c framebuffer.c effect.c postprocess.c diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index 6ff184ce..d925d4af 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -47,6 +47,7 @@ #include "damage.h" #include "desktop.h" #include "cursor.h" +#include "hdr_overlay.h" #include "postprocess.h" #include "util.h" @@ -78,6 +79,7 @@ struct Inst EGL_Desktop * desktop; // the desktop EGL_Cursor * cursor; // the mouse cursor EGL_Damage * damage; // the damage display + EGL_HDROverlay * hdrOverlay; bool imgui; // if imgui was initialized LG_RendererFormat format; @@ -309,6 +311,7 @@ static void egl_deinitialize(LG_Renderer * renderer) egl_desktopFree(&this->desktop); egl_cursorFree (&this->cursor); egl_damageFree (&this->damage); + egl_hdrOverlayFree(&this->hdrOverlay); LG_LOCK_FREE(this->lock); LG_LOCK_FREE(this->desktopDamageLock); @@ -541,6 +544,9 @@ static void egl_onResize(LG_Renderer * renderer, const int width, const int heig this->desktopDamage[this->desktopDamageIdx].count = -1; }); + if (!egl_hdrOverlayResize(this->hdrOverlay, this->width, this->height)) + DEBUG_ERROR("Failed to resize the HDR overlay framebuffer"); + // this is needed to refresh the font atlas texture ImGui_ImplOpenGL3_Shutdown(); ImGui_ImplOpenGL3_Init("#version 300 es"); @@ -644,6 +650,16 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo egl_cursorSetHDRState(this->cursor, useNativeHDR, format.hdrPQ, format.sdrWhiteLevel); + LG_DSHDRWhiteLevels whiteLevels = + { + .pq = 203, + .scRGB = 80, + }; + if (useNativeHDR && !app_getProp(LG_DS_HDR_WHITE_LEVELS, &whiteLevels)) + DEBUG_WARN("Display server did not provide native HDR white levels"); + egl_hdrOverlaySetState(this->hdrOverlay, useNativeHDR, format.hdrPQ, + format.hdrPQ ? whiteLevels.pq : whiteLevels.scRGB); + egl_update_scale_type(this); egl_damageSetup(this->damage, format.frameWidth, format.frameHeight); @@ -1045,6 +1061,12 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA) return false; } + if (!egl_hdrOverlayInit(&this->hdrOverlay)) + { + DEBUG_ERROR("Failed to initialize the HDR overlay"); + return false; + } + if (!ImGui_ImplOpenGL3_Init("#version 300 es")) { DEBUG_ERROR("Failed to initialize ImGui"); @@ -1225,9 +1247,13 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, if (damageIdx == -1) hasOverlay = true; + const bool hdrOverlay = egl_hdrOverlayBegin(this->hdrOverlay, + damage, damageIdx); ImGui_ImplOpenGL3_NewFrame(); ImGui_ImplOpenGL3_RenderDrawData(igGetDrawData()); egl_stateInvalidate(); + if (hdrOverlay) + egl_hdrOverlayEnd(this->hdrOverlay, damage, damageIdx); for (int i = 0; i < damageIdx; ++i) damage[i].y = this->height - damage[i].y - damage[i].h; diff --git a/client/renderers/EGL/hdr_overlay.c b/client/renderers/EGL/hdr_overlay.c new file mode 100644 index 00000000..7686873f --- /dev/null +++ b/client/renderers/EGL/hdr_overlay.c @@ -0,0 +1,238 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "hdr_overlay.h" + +#include "framebuffer.h" +#include "model.h" +#include "shader.h" +#include "state.h" +#include "texture.h" + +#include "common/debug.h" +#include "common/rects.h" +#include "common/util.h" + +#include +#include + +// these headers are auto generated by cmake +#include "hdr_overlay.vert.h" +#include "hdr_overlay.frag.h" + +struct EGL_HDROverlay +{ + EGL_Framebuffer * framebuffer; + EGL_Shader * shader; + EGL_Model * model; + + EGL_Uniform * uPQ; + EGL_Uniform * uReferenceWhiteLevel; + + unsigned int width; + unsigned int height; + bool configured; + + _Atomic(bool) active; + _Atomic(bool) pq; + _Atomic(float) referenceWhiteLevel; + + bool renderPQ; + float renderReferenceWhiteLevel; +}; + +bool egl_hdrOverlayInit(EGL_HDROverlay ** overlay) +{ + *overlay = calloc(1, sizeof(**overlay)); + if (!*overlay) + { + DEBUG_ERROR("Failed to allocate the HDR overlay"); + return false; + } + + EGL_HDROverlay * this = *overlay; + if (!egl_framebufferInit(&this->framebuffer) || + !egl_shaderInit(&this->shader) || + !egl_shaderCompile(this->shader, + b_shader_hdr_overlay_vert, b_shader_hdr_overlay_vert_size, + b_shader_hdr_overlay_frag, b_shader_hdr_overlay_frag_size, + false, NULL) || + !egl_modelInit(&this->model)) + { + DEBUG_ERROR("Failed to initialize the HDR overlay"); + egl_hdrOverlayFree(overlay); + return false; + } + + this->uPQ = egl_shaderGetUniform(this->shader, "pq"); + this->uReferenceWhiteLevel = + egl_shaderGetUniform(this->shader, "referenceWhiteLevel"); + + egl_modelSetDefault(this->model, false); + egl_modelSetShader(this->model, this->shader); + egl_modelSetTexture(this->model, + egl_framebufferGetTexture(this->framebuffer)); + + /* The conversion pass is a 1:1 copy; filtering can sample stale pixels just + * outside a partial-damage region. */ + EGL_Texture * texture = egl_framebufferGetTexture(this->framebuffer); + glSamplerParameteri(texture->sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(texture->sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + atomic_init(&this->active, false); + atomic_init(&this->pq, false); + atomic_init(&this->referenceWhiteLevel, 80.0f); + return true; +} + +void egl_hdrOverlayFree(EGL_HDROverlay ** overlay) +{ + if (!*overlay) + return; + + EGL_HDROverlay * this = *overlay; + egl_modelFree(&this->model); + egl_shaderFree(&this->shader); + egl_framebufferFree(&this->framebuffer); + free(this); + *overlay = NULL; +} + +bool egl_hdrOverlayResize(EGL_HDROverlay * this, unsigned int width, + unsigned int height) +{ + this->width = width; + this->height = height; + this->configured = false; + + if (!width || !height) + return true; + + this->configured = egl_framebufferSetup(this->framebuffer, EGL_PF_RGBA, + width, height); + return this->configured; +} + +void egl_hdrOverlaySetState(EGL_HDROverlay * this, bool active, bool pq, + float referenceWhiteLevel) +{ + atomic_store(&this->referenceWhiteLevel, + referenceWhiteLevel > 0.0f ? referenceWhiteLevel : 80.0f); + atomic_store(&this->pq, pq); + atomic_store(&this->active, active); +} + +static int convertDamage(EGL_HDROverlay * this, const struct Rect * damage, + int damageCount, FrameDamageRect * output) +{ + int count = 0; + for (int i = 0; i < damageCount; ++i) + { + const int x1 = clamp(damage[i].x, 0, (int)this->width); + const int y1 = clamp(damage[i].y, 0, (int)this->height); + const int x2 = clamp(damage[i].x + damage[i].w, 0, (int)this->width); + const int y2 = clamp(damage[i].y + damage[i].h, 0, (int)this->height); + if (x2 <= x1 || y2 <= y1) + continue; + + output[count++] = (FrameDamageRect) + { + .x = x1, + .y = this->height - y2, + .width = x2 - x1, + .height = y2 - y1, + }; + } + + return rectsMergeOverlapping(output, count); +} + +bool egl_hdrOverlayBegin(EGL_HDROverlay * this, + const struct Rect * damage, int damageCount) +{ + if (!this->configured || !atomic_load(&this->active)) + return false; + + this->renderPQ = atomic_load(&this->pq); + this->renderReferenceWhiteLevel = + atomic_load(&this->referenceWhiteLevel); + + egl_framebufferBind(this->framebuffer); + egl_stateBlend(false); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + + if (damageCount < 0) + { + egl_stateScissor(false); + glClear(GL_COLOR_BUFFER_BIT); + } + else + { + FrameDamageRect rects[damageCount]; + const int count = convertDamage(this, damage, damageCount, rects); + + egl_stateScissor(true); + for (int i = 0; i < count; ++i) + { + glScissor(rects[i].x, rects[i].y, + rects[i].width, rects[i].height); + glClear(GL_COLOR_BUFFER_BIT); + } + egl_stateScissor(false); + } + + return true; +} + +void egl_hdrOverlayEnd(EGL_HDROverlay * this, + const struct Rect * damage, int damageCount) +{ + egl_stateBindFramebuffer(0); + egl_stateViewport(0, 0, this->width, this->height); + + egl_uniform1i(this->uPQ, this->renderPQ); + egl_uniform1f(this->uReferenceWhiteLevel, + this->renderReferenceWhiteLevel); + + egl_stateBlend(true); + egl_stateBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + + if (damageCount < 0) + { + egl_stateScissor(false); + egl_modelRender(this->model); + } + else + { + FrameDamageRect rects[damageCount]; + const int count = convertDamage(this, damage, damageCount, rects); + + egl_stateScissor(true); + for (int i = 0; i < count; ++i) + { + glScissor(rects[i].x, rects[i].y, + rects[i].width, rects[i].height); + egl_modelRender(this->model); + } + egl_stateScissor(false); + } + + egl_stateBlend(false); +} diff --git a/client/renderers/EGL/hdr_overlay.h b/client/renderers/EGL/hdr_overlay.h new file mode 100644 index 00000000..db9a7ab4 --- /dev/null +++ b/client/renderers/EGL/hdr_overlay.h @@ -0,0 +1,45 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "common/types.h" + +#include + +typedef struct EGL_HDROverlay EGL_HDROverlay; + +bool egl_hdrOverlayInit(EGL_HDROverlay ** overlay); +void egl_hdrOverlayFree(EGL_HDROverlay ** overlay); + +bool egl_hdrOverlayResize(EGL_HDROverlay * overlay, unsigned int width, + unsigned int height); +void egl_hdrOverlaySetState(EGL_HDROverlay * overlay, bool active, bool pq, + float referenceWhiteLevel); + +/* + * Begin returns true when ImGui should be rendered into the HDR overlay + * framebuffer. The caller must then call egl_hdrOverlayEnd after ImGui has + * finished rendering and the EGL state cache has been invalidated. + */ +bool egl_hdrOverlayBegin(EGL_HDROverlay * overlay, + const struct Rect * damage, int damageCount); +void egl_hdrOverlayEnd(EGL_HDROverlay * overlay, + const struct Rect * damage, int damageCount); diff --git a/client/renderers/EGL/shader/hdr_overlay.frag b/client/renderers/EGL/shader/hdr_overlay.frag new file mode 100644 index 00000000..4a2ccaa9 --- /dev/null +++ b/client/renderers/EGL/shader/hdr_overlay.frag @@ -0,0 +1,35 @@ +#version 300 es +precision highp float; + +#include "hdr.h" + +in vec2 fragCoord; +out vec4 color; + +uniform sampler2D sampler1; +uniform bool pq; +uniform float referenceWhiteLevel; + +void main() +{ + color = texture(sampler1, fragCoord); + if (color.a <= 0.0) + { + color.rgb = vec3(0.0); + return; + } + + // ImGui's framebuffer contains premultiplied sRGB. Convert the straight + // color, then premultiply again for composition onto the HDR framebuffer. + vec3 linear = srgb2lin(clamp(color.rgb / color.a, 0.0, 1.0)); + if (pq) + { + linear = bt709to2020(linear); + color.rgb = lin2pq(linear * (referenceWhiteLevel / 10000.0)) * color.a; + } + else + { + // scRGB is linear sRGB and defines 1.0 as 80 nits. + color.rgb = linear * (referenceWhiteLevel / 80.0) * color.a; + } +} diff --git a/client/renderers/EGL/shader/hdr_overlay.vert b/client/renderers/EGL/shader/hdr_overlay.vert new file mode 100644 index 00000000..08e6ab30 --- /dev/null +++ b/client/renderers/EGL/shader/hdr_overlay.vert @@ -0,0 +1,13 @@ +#version 300 es +precision highp float; + +layout(location = 0) in vec3 vertex; +layout(location = 1) in vec2 coord; + +out vec2 fragCoord; + +void main() +{ + gl_Position = vec4(vertex, 1.0); + fragCoord = coord; +}