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