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.
This commit is contained in:
Geoffrey McRae
2026-07-18 03:02:19 +10:00
parent 37d7f0e48f
commit acb5c06794
10 changed files with 394 additions and 3 deletions

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View File

@@ -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;

View File

@@ -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 <stdatomic.h>
#include <stdlib.h>
// 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);
}

View File

@@ -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 <stdbool.h>
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);

View File

@@ -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;
}
}

View File

@@ -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;
}