From 364485ded8560bf6311293bc68f893dbddf6b87c Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Wed, 15 Jul 2026 19:48:31 +1000 Subject: [PATCH] [client] wayland: implement HDR support --- client/displayservers/Wayland/CMakeLists.txt | 1 + client/displayservers/Wayland/colormgmt.c | 105 ++++++ client/displayservers/Wayland/gl.c | 302 +++++++++++++++++- .../Wayland/protocol/CMakeLists.txt | 3 + client/displayservers/Wayland/registry.c | 3 + client/displayservers/Wayland/wayland.c | 29 ++ client/displayservers/Wayland/wayland.h | 57 +++- client/include/app.h | 7 + client/include/interface/displayserver.h | 15 + client/include/interface/renderer.h | 8 + client/renderers/EGL/desktop.c | 12 +- client/renderers/EGL/desktop.h | 1 + client/renderers/EGL/egl.c | 70 +++- client/src/app.c | 5 + client/src/main.c | 30 ++ client/src/main.h | 5 + 16 files changed, 623 insertions(+), 30 deletions(-) create mode 100644 client/displayservers/Wayland/colormgmt.c diff --git a/client/displayservers/Wayland/CMakeLists.txt b/client/displayservers/Wayland/CMakeLists.txt index d130d889..359d62e7 100644 --- a/client/displayservers/Wayland/CMakeLists.txt +++ b/client/displayservers/Wayland/CMakeLists.txt @@ -13,6 +13,7 @@ find_program(WAYLAND_SCANNER_EXECUTABLE NAMES wayland-scanner) add_library(displayserver_Wayland STATIC activation.c clipboard.c + colormgmt.c cursor.c gl.c idle.c diff --git a/client/displayservers/Wayland/colormgmt.c b/client/displayservers/Wayland/colormgmt.c new file mode 100644 index 00000000..6ca5cfb7 --- /dev/null +++ b/client/displayservers/Wayland/colormgmt.c @@ -0,0 +1,105 @@ +/** + * Looking Glass + * Copyright (C) 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 "wayland.h" + +#include + +#include + +#include "common/debug.h" + +static void cmFeature(void * data, struct wp_color_manager_v1 * cm, uint32_t feature) +{ + switch (feature) + { + case WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC: + wlWm.cmHasParametric = true; + break; + case WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES: + wlWm.cmHasLuminances = true; + break; + case WP_COLOR_MANAGER_V1_FEATURE_SET_MASTERING_DISPLAY_PRIMARIES: + wlWm.cmHasMasteringPrimaries = true; + break; + default: + break; + } +} + +static void cmTFNamed(void * data, struct wp_color_manager_v1 * cm, uint32_t tf) +{ + if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ) + wlWm.cmHasTFSt2084PQ = true; + else if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR) + wlWm.cmHasTFExtLinear = true; +} + +static void cmPrimariesNamed(void * data, struct wp_color_manager_v1 * cm, + uint32_t primaries) +{ + if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_BT2020) + wlWm.cmHasPrimariesBT2020 = true; + else if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB) + wlWm.cmHasPrimariesSRGB = true; +} + +static void cmDone(void * data, struct wp_color_manager_v1 * cm) +{ + wlWm.cmFeaturesDone = true; + wlWm.cmCanDoHDR = wlWm.cmHasParametric && + ((wlWm.cmHasTFSt2084PQ && wlWm.cmHasPrimariesBT2020) || + (wlWm.cmHasTFExtLinear && wlWm.cmHasPrimariesSRGB)); + DEBUG_INFO("Color management features: parametric:%d luminances:%d " + "mastering_primaries:%d st2084_pq:%d ext_linear:%d bt2020:%d srgb:%d " + "can_do_hdr:%d", + wlWm.cmHasParametric, wlWm.cmHasLuminances, + wlWm.cmHasMasteringPrimaries, + wlWm.cmHasTFSt2084PQ, wlWm.cmHasTFExtLinear, + wlWm.cmHasPrimariesBT2020, wlWm.cmHasPrimariesSRGB, + wlWm.cmCanDoHDR); +} + +static const struct wp_color_manager_v1_listener cmListener = { + .supported_intent = NULL, + .supported_feature = cmFeature, + .supported_tf_named = cmTFNamed, + .supported_primaries_named = cmPrimariesNamed, + .done = cmDone, +}; + +bool waylandColorMgmtInit(void) +{ + if (!wlWm.colorManager) + return true; + + wp_color_manager_v1_add_listener(wlWm.colorManager, &cmListener, NULL); + wl_display_roundtrip(wlWm.display); + return true; +} + +void waylandColorMgmtFree(void) +{ + if (!wlWm.colorManager) + return; + + wp_color_manager_v1_destroy(wlWm.colorManager); + wlWm.colorManager = NULL; +} diff --git a/client/displayservers/Wayland/gl.c b/client/displayservers/Wayland/gl.c index 8614995a..cd1c2d02 100644 --- a/client/displayservers/Wayland/gl.c +++ b/client/displayservers/Wayland/gl.c @@ -70,6 +70,52 @@ EGLDisplay waylandGetEGLDisplay(void) return eglGetDisplay(native); } +static void applyHDRPending(void) +{ + if (wlWm.pendingHDRApply) + { + wlWm.pendingHDRApply = false; + atomic_thread_fence(memory_order_acquire); + waylandSetHDRImageDescription( + wlWm.pendingHDRDisplayPrimary, wlWm.pendingHDRWhitePoint, + wlWm.pendingHDRMaxDisplayLuminance, + wlWm.pendingHDRMinDisplayLuminance, + wlWm.pendingHDRMaxCLL, + wlWm.pendingHDRMaxFALL, + wlWm.pendingHDRPQ); + } + else if (wlWm.pendingHDRClear) + { + wlWm.pendingHDRClear = false; + waylandClearHDRImageDescription(); + } +} + +void waylandClearHDRImageDescription(void) +{ + if (!wlWm.hdrActive) + return; + + if (wlWm.hdrImageCreator) + { + wp_image_description_creator_params_v1_destroy(wlWm.hdrImageCreator); + wlWm.hdrImageCreator = NULL; + } + if (wlWm.hdrImageDesc) + { + wp_image_description_v1_destroy(wlWm.hdrImageDesc); + wlWm.hdrImageDesc = NULL; + } + if (wlWm.colorSurface) + { + wp_color_management_surface_v1_destroy(wlWm.colorSurface); + wlWm.colorSurface = NULL; + } + + wlWm.hdrActive = false; + DEBUG_INFO("HDR image description cleared"); +} + void waylandEGLSwapBuffers(EGLDisplay display, EGLSurface surface, const struct Rect * damage, int count) { if (!wlWm.swapWithDamage.init) @@ -84,6 +130,7 @@ void waylandEGLSwapBuffers(EGLDisplay display, EGLSurface surface, const struct } waylandPresentationFrame(); + applyHDRPending(); swapWithDamage(&wlWm.swapWithDamage, display, surface, damage, count); if (wlWm.needsResize) @@ -149,23 +196,226 @@ EGLNativeWindowType waylandGetEGLNativeWindow(void) } #endif +// HDR color management support +void waylandSetHDRImageDescription(const uint16_t displayPrimary[3][2], + const uint16_t whitePoint[2], uint32_t maxDisplayLuminance, + uint32_t minDisplayLuminance, uint32_t maxCLL, uint32_t maxFALL, + bool hdrPQ) +{ + if (!wlWm.colorManager) + return; + + if (!wlWm.cmFeaturesDone) + { + DEBUG_WARN("Color management features not yet advertised, deferring HDR"); + return; + } + + if (!wlWm.cmHasParametric) + { + DEBUG_WARN("Compositor does not support parametric image descriptions"); + return; + } + + // Verify the compositor supports the transfer function we need + if (hdrPQ && !wlWm.cmHasTFSt2084PQ) + { + DEBUG_WARN("Compositor does not support ST2084_PQ transfer function"); + return; + } + if (!hdrPQ && !wlWm.cmHasTFExtLinear) + { + DEBUG_WARN("Compositor does not support EXT_LINEAR transfer function"); + return; + } + + // Verify primaries support for the target color space + if (hdrPQ && !wlWm.cmHasPrimariesBT2020) + { + DEBUG_WARN("Compositor does not support BT.2020 primaries"); + return; + } + if (!hdrPQ && !wlWm.cmHasPrimariesSRGB) + { + DEBUG_WARN("Compositor does not support sRGB primaries"); + return; + } + + // Clean up any previous description (allows re-application with updated metadata) + if (wlWm.hdrImageCreator) + { + wp_image_description_creator_params_v1_destroy(wlWm.hdrImageCreator); + wlWm.hdrImageCreator = NULL; + } + if (wlWm.hdrImageDesc) + { + wp_image_description_v1_destroy(wlWm.hdrImageDesc); + wlWm.hdrImageDesc = NULL; + } + if (wlWm.colorSurface) + { + wp_color_management_surface_v1_destroy(wlWm.colorSurface); + wlWm.colorSurface = NULL; + } + + wlWm.hdrActive = false; + + wlWm.hdrImageCreator = + wp_color_manager_v1_create_parametric_creator(wlWm.colorManager); + if (!wlWm.hdrImageCreator) + { + DEBUG_WARN("Failed to create parametric image description creator"); + return; + } + + // Set primaries: BT.2020 for PQ HDR10, sRGB for scRGB/FP16 + wp_image_description_creator_params_v1_set_primaries_named( + wlWm.hdrImageCreator, + hdrPQ ? WP_COLOR_MANAGER_V1_PRIMARIES_BT2020 + : WP_COLOR_MANAGER_V1_PRIMARIES_SRGB); + + // Select transfer function: PQ for PQ-encoded content, linear for scRGB/FP16 + wp_image_description_creator_params_v1_set_tf_named( + wlWm.hdrImageCreator, + hdrPQ ? WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ + : WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); + + // Set luminance range from metadata (values already in 0.0001 cd/m² units) + wp_image_description_creator_params_v1_set_luminances( + wlWm.hdrImageCreator, + minDisplayLuminance > 0 ? minDisplayLuminance : 50, + maxDisplayLuminance > 0 ? maxDisplayLuminance : 10000000, + hdrPQ ? 2030000 : 800000); + + // Set mastering display primaries from frame HDR metadata. + // Always set when the compositor supports it, falling back to the + // primaries from the metadata (even if zero, in which case the + // compositor uses its own defaults). + if (wlWm.cmHasMasteringPrimaries) + wp_image_description_creator_params_v1_set_mastering_display_primaries( + wlWm.hdrImageCreator, + displayPrimary[0][0], displayPrimary[0][1], + displayPrimary[1][0], displayPrimary[1][1], + displayPrimary[2][0], displayPrimary[2][1], + whitePoint[0], whitePoint[1]); + + if (maxCLL > 0) + wp_image_description_creator_params_v1_set_max_cll(wlWm.hdrImageCreator, maxCLL); + if (maxFALL > 0) + wp_image_description_creator_params_v1_set_max_fall(wlWm.hdrImageCreator, maxFALL); + + wlWm.hdrImageDesc = + wp_image_description_creator_params_v1_create(wlWm.hdrImageCreator); + wlWm.hdrImageCreator = NULL; // consumed by create + + if (!wlWm.hdrImageDesc) + { + DEBUG_WARN("Failed to create HDR image description"); + return; + } + + wlWm.colorSurface = + wp_color_manager_v1_get_surface(wlWm.colorManager, wlWm.surface); + if (!wlWm.colorSurface) + { + DEBUG_WARN("Failed to get color management surface"); + wp_image_description_v1_destroy(wlWm.hdrImageDesc); + wlWm.hdrImageDesc = NULL; + return; + } + + wp_color_management_surface_v1_set_image_description( + wlWm.colorSurface, wlWm.hdrImageDesc, + WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + + wlWm.hdrActive = true; + DEBUG_INFO("HDR image description set on surface (%s, %s, %u nits)", + hdrPQ ? "PQ" : "scRGB", hdrPQ ? "BT.2020" : "sRGB", maxDisplayLuminance); +} + +bool waylandRequestHDR(const uint16_t displayPrimary[3][2], + const uint16_t whitePoint[2], uint32_t maxDisplayLuminance, + uint32_t minDisplayLuminance, uint32_t maxCLL, uint32_t maxFALL, + bool hdrPQ) +{ + if (!wlWm.cmFeaturesDone || !wlWm.cmHasParametric) + return false; + if (hdrPQ && !wlWm.cmHasTFSt2084PQ) + return false; + if (!hdrPQ && !wlWm.cmHasTFExtLinear) + return false; + if (hdrPQ && !wlWm.cmHasPrimariesBT2020) + return false; + if (!hdrPQ && !wlWm.cmHasPrimariesSRGB) + return false; + + wlWm.pendingHDRPQ = hdrPQ; + + // Write all metadata before setting the apply flag to ensure the + // render thread sees the complete HDR metadata when it observes + // pendingHDRApply == true. + memcpy(wlWm.pendingHDRDisplayPrimary, displayPrimary, + sizeof(wlWm.pendingHDRDisplayPrimary)); + memcpy(wlWm.pendingHDRWhitePoint, whitePoint, + sizeof(wlWm.pendingHDRWhitePoint)); + wlWm.pendingHDRMaxDisplayLuminance = maxDisplayLuminance; + wlWm.pendingHDRMinDisplayLuminance = minDisplayLuminance; + wlWm.pendingHDRMaxCLL = maxCLL; + wlWm.pendingHDRMaxFALL = maxFALL; + + wlWm.pendingHDRClear = false; + atomic_thread_fence(memory_order_release); + wlWm.pendingHDRApply = true; + return true; +} + +void waylandRequestClearHDR(void) +{ + wlWm.pendingHDRClear = true; + wlWm.pendingHDRApply = false; +} + #ifdef ENABLE_OPENGL + +static const EGLint eglBaseAttrs[] = +{ + EGL_CONFORMANT , EGL_OPENGL_BIT, + EGL_RENDERABLE_TYPE , EGL_OPENGL_BIT, + EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, + EGL_SAMPLE_BUFFERS , 0, + EGL_SAMPLES , 0, + EGL_NONE +}; + +static bool eglChooseConfigWithDepth(EGLDisplay display, EGLint red, + EGLint green, EGLint blue, EGLint alpha, EGLint depth, + EGLConfig * config, const char ** desc) +{ + // Build attr array with the base attrs plus color depth + EGLint attr[32]; + int ai = 0; + attr[ai++] = EGL_RED_SIZE ; attr[ai++] = red; + attr[ai++] = EGL_GREEN_SIZE; attr[ai++] = green; + attr[ai++] = EGL_BLUE_SIZE ; attr[ai++] = blue; + attr[ai++] = EGL_ALPHA_SIZE; attr[ai++] = alpha; + attr[ai++] = EGL_BUFFER_SIZE; attr[ai++] = depth; + for (int i = 0; eglBaseAttrs[i] != EGL_NONE; ++i) + attr[ai++] = eglBaseAttrs[i]; + attr[ai] = EGL_NONE; + + EGLint num_config; + if (eglChooseConfig(display, attr, config, 1, &num_config) && num_config > 0) + { + if (desc) + *desc = red >= 16 ? "FP16 (RGBA16F)" : + red >= 10 ? "10-bit (RGBA10)" : "8-bit (RGBA8)"; + return true; + } + return false; +} + bool waylandOpenGLInit(void) { - EGLint attr[] = - { - EGL_BUFFER_SIZE , 24, - EGL_CONFORMANT , EGL_OPENGL_BIT, - EGL_RENDERABLE_TYPE , EGL_OPENGL_BIT, - EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, - EGL_RED_SIZE , 8, - EGL_GREEN_SIZE , 8, - EGL_BLUE_SIZE , 8, - EGL_SAMPLE_BUFFERS , 0, - EGL_SAMPLES , 0, - EGL_NONE - }; - wlWm.glDisplay = waylandGetEGLDisplay(); int maj, min; @@ -181,13 +431,31 @@ bool waylandOpenGLInit(void) return false; } - EGLint num_config; - if (!eglChooseConfig(wlWm.glDisplay, attr, &wlWm.glConfig, 1, &num_config)) + EGLConfig config; + const char * configDesc = NULL; + + // Probe for best available color depth: FP16 → 10-bit → 8-bit + if (!eglChooseConfigWithDepth(wlWm.glDisplay, + 16, 16, 16, 0, 48, &config, &configDesc) && + !eglChooseConfigWithDepth(wlWm.glDisplay, + 10, 10, 10, 2, 32, &config, &configDesc) && + !eglChooseConfigWithDepth(wlWm.glDisplay, + 8, 8, 8, 0, 24, &config, &configDesc)) { - DEBUG_ERROR("Failed to choose config (eglError: 0x%x)", eglGetError()); + DEBUG_ERROR("Failed to choose any EGL config"); return false; } + wlWm.glConfig = config; + DEBUG_INFO("EGL config: %s", configDesc); + + // Also store the 8-bit fallback for SDR contexts + if (configDesc && strcmp(configDesc, "8-bit (RGBA8)") != 0) + eglChooseConfigWithDepth(wlWm.glDisplay, + 8, 8, 8, 0, 24, &wlWm.glConfigSDR, NULL); + else + wlWm.glConfigSDR = wlWm.glConfig; + wlWm.glSurface = eglCreateWindowSurface(wlWm.glDisplay, wlWm.glConfig, wlWm.eglWindow, NULL); if (wlWm.glSurface == EGL_NO_SURFACE) { diff --git a/client/displayservers/Wayland/protocol/CMakeLists.txt b/client/displayservers/Wayland/protocol/CMakeLists.txt index 31d802ba..c2756a2a 100644 --- a/client/displayservers/Wayland/protocol/CMakeLists.txt +++ b/client/displayservers/Wayland/protocol/CMakeLists.txt @@ -66,6 +66,9 @@ wayland_generate( wayland_generate( "${WAYLAND_PROTOCOLS_BASE}/staging/content-type/content-type-v1.xml" "${CMAKE_BINARY_DIR}/wayland/wayland-content-type-v1-client-protocol") +wayland_generate( + "${WAYLAND_PROTOCOLS_BASE}/staging/color-management/color-management-v1.xml" + "${CMAKE_BINARY_DIR}/wayland/wayland-color-management-v1-client-protocol") target_link_libraries(wayland_protocol PkgConfig::WAYLAND diff --git a/client/displayservers/Wayland/registry.c b/client/displayservers/Wayland/registry.c index bc8045ad..a659fd7a 100644 --- a/client/displayservers/Wayland/registry.c +++ b/client/displayservers/Wayland/registry.c @@ -74,6 +74,9 @@ static void registryGlobalHandler(void * data, struct wl_registry * registry, else if (!strcmp(interface, wp_content_type_manager_v1_interface.name)) wlWm.contentTypeManager = wl_registry_bind(wlWm.registry, name, &wp_content_type_manager_v1_interface, 1); + else if (!strcmp(interface, wp_color_manager_v1_interface.name)) + wlWm.colorManager = wl_registry_bind(wlWm.registry, name, + &wp_color_manager_v1_interface, 1); else if (wlWm.desktop->registryGlobalHandler( data, registry, name, interface, version)) return; diff --git a/client/displayservers/Wayland/wayland.c b/client/displayservers/Wayland/wayland.c index b7b00eba..9b65e54f 100644 --- a/client/displayservers/Wayland/wayland.c +++ b/client/displayservers/Wayland/wayland.c @@ -28,6 +28,7 @@ #include "common/debug.h" #include "common/option.h" +#include "interface/renderer.h" #include "dynamic/wayland_desktops.h" @@ -143,6 +144,9 @@ static bool waylandInit(const LG_DSInitParams params) if (!waylandRegistryInit()) return false; + if (!waylandColorMgmtInit()) + return false; + if (!waylandActivationInit()) return false; @@ -189,6 +193,7 @@ static void waylandFree(void) waylandPresentationFree(); waylandInputFree(); waylandOutputFree(); + waylandColorMgmtFree(); waylandRegistryFree(); waylandCursorFree(); wl_display_disconnect(wlWm.display); @@ -202,6 +207,12 @@ static bool waylandGetProp(LG_DSProperty prop, void * ret) return true; } + if (prop == LG_DS_NATIVE_HDR) + { + *(bool *)ret = wlWm.cmCanDoHDR; + return true; + } + return false; } @@ -227,6 +238,22 @@ static void waylandMinimize(void) wlWm.desktop->minimize(); } +static bool waylandHDRCallback(const void * rendererFormat) +{ + if (!wlWm.colorManager) + return false; + + const LG_RendererFormat * format = (const LG_RendererFormat *)rendererFormat; + if (format->hdr) + return waylandRequestHDR(format->hdrDisplayPrimary, format->hdrWhitePoint, + format->hdrMaxDisplayLuminance, format->hdrMinDisplayLuminance, + format->hdrMaxContentLightLevel, format->hdrMaxFrameAverageLightLevel, + format->hdrPQ); + else + waylandRequestClearHDR(); + return true; +} + struct LG_DisplayServerOps LGDS_Wayland = { .name = "Wayland", @@ -276,6 +303,8 @@ struct LG_DisplayServerOps LGDS_Wayland = .getFullscreen = waylandGetFullscreen, .minimize = waylandMinimize, + .setHDRImageDescription = waylandHDRCallback, + .cbInit = waylandCBInit, .cbNotice = waylandCBNotice, .cbRelease = waylandCBRelease, diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index 8d14436e..c5cf74bc 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -49,6 +49,7 @@ #include "wayland-xdg-activation-v1-client-protocol.h" #include "wayland-fractional-scale-v1-client-protocol.h" #include "wayland-content-type-v1-client-protocol.h" +#include "wayland-color-management-v1-client-protocol.h" #include "scale.h" @@ -120,7 +121,8 @@ struct WaylandDSState #ifdef ENABLE_OPENGL EGLDisplay glDisplay; - EGLConfig glConfig; + EGLConfig glConfig; // primary config (best bit depth probed) + EGLConfig glConfigSDR; // fallback 8-bit SDR config EGLSurface glSurface; #endif @@ -180,6 +182,41 @@ struct WaylandDSState struct wp_content_type_manager_v1 * contentTypeManager; struct wp_content_type_v1 * contentType; + // Color management (HDR) + struct wp_color_manager_v1 * colorManager; + struct wp_color_management_surface_v1 * colorSurface; + struct wp_image_description_v1 * hdrImageDesc; + struct wp_image_description_creator_params_v1 * hdrImageCreator; + + // Set to true when an HDR image description is active on the surface + bool hdrActive; + + // wp_color_manager_v1 feature advertisement tracking. + // Set to true after the done event for the color-manager has been received + // and all compositor capabilities have been recorded. + bool cmFeaturesDone; + bool cmHasParametric; + bool cmHasLuminances; + bool cmHasMasteringPrimaries; + bool cmHasTFSt2084PQ; + bool cmHasTFExtLinear; + bool cmHasPrimariesBT2020; + bool cmHasPrimariesSRGB; + bool cmCanDoHDR; // true if compositor supports features needed for HDR + + // Pending HDR format to apply (set by frame thread, applied in swap buffers). + // pendingHDRApply and pendingHDRClear are accessed from multiple threads + // without a lock, so they must be atomic. + _Atomic(bool) pendingHDRApply; + _Atomic(bool) pendingHDRClear; + bool pendingHDRPQ; + uint16_t pendingHDRDisplayPrimary[3][2]; + uint16_t pendingHDRWhitePoint[2]; + uint32_t pendingHDRMaxDisplayLuminance; + uint32_t pendingHDRMinDisplayLuminance; + uint32_t pendingHDRMaxCLL; + uint32_t pendingHDRMaxFALL; + LGEvent * frameEvent; struct wl_list poll; // WaylandPoll::link @@ -262,6 +299,24 @@ void waylandGLSetSwapInterval(int interval); void waylandGLSwapBuffers(void); #endif +// HDR color management (Wayland color-management-v1) +bool waylandColorMgmtInit(void); +void waylandColorMgmtFree(void); +void waylandSetHDRImageDescription(const uint16_t displayPrimary[3][2], + const uint16_t whitePoint[2], uint32_t maxDisplayLuminance, + uint32_t minDisplayLuminance, uint32_t maxCLL, uint32_t maxFALL, + bool hdrPQ); +void waylandClearHDRImageDescription(void); + +// Queue HDR change from any thread (applied in waylandEGLSwapBuffers). +// Returns true if the request was queued, false if HDR is not available +// (no color manager, missing features, or unsupported TF/primaries). +bool waylandRequestHDR(const uint16_t displayPrimary[3][2], + const uint16_t whitePoint[2], uint32_t maxDisplayLuminance, + uint32_t minDisplayLuminance, uint32_t maxCLL, uint32_t maxFALL, + bool hdrPQ); +void waylandRequestClearHDR(void); + // idle module bool waylandIdleInit(void); void waylandIdleFree(void); diff --git a/client/include/app.h b/client/include/app.h index ec585e12..83d87570 100644 --- a/client/include/app.h +++ b/client/include/app.h @@ -72,6 +72,13 @@ void app_setFullscreen(bool fs); bool app_getFullscreen(void); bool app_getProp(LG_DSProperty prop, void * ret); +/** + * Returns true if the last call to setHDRImageDescription() failed. + * When this returns true, the renderer should fall back to software + * tone-mapping even on a compositor that reports LG_DS_NATIVE_HDR. + */ +bool app_getHDRDescFailed(void); + #ifdef ENABLE_EGL EGLDisplay app_getEGLDisplay(void); EGLNativeWindowType app_getEGLNativeWindow(void); diff --git a/client/include/interface/displayserver.h b/client/include/interface/displayserver.h index 93c9631c..d495cf47 100644 --- a/client/include/interface/displayserver.h +++ b/client/include/interface/displayserver.h @@ -53,6 +53,14 @@ typedef enum LG_DSProperty * return data type: bool */ LG_DS_WARP_SUPPORT, + + /** + * returns true if the display server supports native HDR via + * setHDRImageDescription (i.e., the compositor actually has the + * required color-management features to process HDR content). + * return data type: bool + */ + LG_DS_NATIVE_HDR, } LG_DSProperty; @@ -213,6 +221,13 @@ struct LG_DisplayServerOps void (*setFullscreen)(bool fs); void (*minimize)(void); + /* HDR color management - set display image description. + * Optional, if not supported set to NULL. + * Returns true on success, false if the description could not be applied + * (e.g., compositor doesn't support the required transfer function or + * primaries, or the image description creation failed). */ + bool (*setHDRImageDescription)(const void * rendererFormat); + /* clipboard support, optional, if not supported set to NULL */ bool (*cbInit)(void); void (*cbNotice)(LG_ClipboardData type); diff --git a/client/include/interface/renderer.h b/client/include/interface/renderer.h index 1249b457..a1ad6a32 100644 --- a/client/include/interface/renderer.h +++ b/client/include/interface/renderer.h @@ -84,6 +84,14 @@ typedef struct LG_RendererFormat unsigned int pitch; // scanline bytes (or compressed size) unsigned int bpp; // bits per pixel (zero if compressed) LG_RendererRotate rotate; // guest rotation + + // HDR static metadata (from KVMFRFrame, valid when hdr is true) + uint16_t hdrDisplayPrimary[3][2]; + uint16_t hdrWhitePoint[2]; + uint32_t hdrMaxDisplayLuminance; + uint32_t hdrMinDisplayLuminance; + uint32_t hdrMaxContentLightLevel; + uint32_t hdrMaxFrameAverageLightLevel; } LG_RendererFormat; diff --git a/client/renderers/EGL/desktop.c b/client/renderers/EGL/desktop.c index e31844c4..be1b08a5 100644 --- a/client/renderers/EGL/desktop.c +++ b/client/renderers/EGL/desktop.c @@ -91,6 +91,7 @@ struct EGL_Desktop // map HDR content to SDR bool mapHDRtoSDR; + bool nativeHDR; int peakLuminance; int maxCLL; @@ -548,7 +549,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, { .type = EGL_UNIFORM_TYPE_1I, .location = shader->uCBMode, - .f = { desktop->cbMode } + .i = { desktop->cbMode } }, { .type = EGL_UNIFORM_TYPE_1I, @@ -558,7 +559,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, { .type = EGL_UNIFORM_TYPE_1I, .location = shader->uMapHDRtoSDR, - .i = { desktop->mapHDRtoSDR } + .i = { desktop->mapHDRtoSDR && !desktop->nativeHDR } }, { .type = EGL_UNIFORM_TYPE_1F, @@ -568,7 +569,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, { .type = EGL_UNIFORM_TYPE_1I, .location = shader->uMapHDRPQ, - .f = { desktop->hdrPQ } + .i = { desktop->hdrPQ } } }; @@ -579,6 +580,11 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, return true; } +void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR) +{ + desktop->nativeHDR = nativeHDR; +} + void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height) { if (!desktop->spiceTexture) diff --git a/client/renderers/EGL/desktop.h b/client/renderers/EGL/desktop.h index ffb2389c..44ee24a8 100644 --- a/client/renderers/EGL/desktop.h +++ b/client/renderers/EGL/desktop.h @@ -42,6 +42,7 @@ bool egl_desktopInit(EGL * egl, EGL_Desktop ** desktop, EGLDisplay * display, void egl_desktopFree(EGL_Desktop ** desktop); void egl_desktopConfigUI(EGL_Desktop * desktop); +void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR); bool egl_desktopSetup (EGL_Desktop * desktop, const LG_RendererFormat format); bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd, const FrameDamageRect * damageRects, int damageRectsCount); diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index 2bf7266e..c2158ba3 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -121,6 +121,9 @@ struct Inst bool showSpice; int spiceWidth, spiceHeight; + + bool configSupportsHDR; // true if we probed FP16 or 10-bit EGL config + bool hdr; // true if current frame format is HDR }; static struct Option egl_options[] = @@ -607,6 +610,26 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo egl_cursorSetScale(this->cursor, scale); } + // Track HDR state and warn if HDR frames arrive without any HDR path + this->hdr = format.hdr; + if (format.hdr) + { + bool nativeHDR = false; + app_getProp(LG_DS_NATIVE_HDR, &nativeHDR); + if (!this->configSupportsHDR && !nativeHDR) + DEBUG_WARN("HDR frames being displayed on an 8-bit EGL surface " + "without a color-managed compositor; tones may be incorrect"); + } + + // If the display server supports native HDR (via setHDRImageDescription), + // disable software tone-mapping to avoid double tone-mapping. + // However, if the last call to setHDRImageDescription actually failed + // (e.g., compositor missing required primaries), keep tone-mapping on. + bool nativeHDR = false; + app_getProp(LG_DS_NATIVE_HDR, &nativeHDR); + egl_desktopSetNativeHDR(this->desktop, + format.hdr && nativeHDR && !app_getHDRDescFailed()); + egl_update_scale_type(this); egl_damageSetup(this->damage, format.frameWidth, format.frameHeight); @@ -774,22 +797,51 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA) } } - EGLint attr[] = + // Probe for best available color depth: FP16 → 10-bit → 8-bit + static const struct { EGLint r, g, b, a, depth; const char * desc; } configs[] = { - EGL_BUFFER_SIZE , 30, - EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, - EGL_SAMPLE_BUFFERS , maxSamples > 0 ? 1 : 0, - EGL_SAMPLES , maxSamples, - EGL_NONE + { 16, 16, 16, 0, 48, "FP16 (RGBA16F)" }, + { 10, 10, 10, 2, 32, "10-bit (RGBA10)" }, + { 8, 8, 8, 0, 24, "8-bit (RGBA8)" }, }; + EGLint num_config; + const char * configDesc = NULL; + int chosen = -1; - EGLint num_config; - if (!eglChooseConfig(this->display, attr, &this->configs, 1, &num_config)) + for (int i = 0; i < (int)(sizeof(configs) / sizeof(configs[0])); ++i) { - DEBUG_ERROR("Failed to choose config (eglError: 0x%x)", eglGetError()); + EGLint attr[] = + { + EGL_RED_SIZE , configs[i].r, + EGL_GREEN_SIZE , configs[i].g, + EGL_BLUE_SIZE , configs[i].b, + EGL_ALPHA_SIZE , configs[i].a, + EGL_BUFFER_SIZE , configs[i].depth, + EGL_RENDERABLE_TYPE , EGL_OPENGL_ES2_BIT, + EGL_SAMPLE_BUFFERS , maxSamples > 0 ? 1 : 0, + EGL_SAMPLES , maxSamples, + EGL_NONE + }; + + if (eglChooseConfig(this->display, attr, &this->configs, 1, &num_config) && + num_config > 0) + { + configDesc = configs[i].desc; + chosen = i; + break; + } + } + + if (chosen < 0) + { + DEBUG_ERROR("Failed to choose any EGL config"); return false; } + this->configSupportsHDR = chosen <= 1; // FP16 or 10-bit + DEBUG_INFO("EGL config: %s%s", configDesc, + this->configSupportsHDR ? " (HDR capable)" : ""); + const EGLint surfattr[] = { EGL_RENDER_BUFFER, this->opt.doubleBuffer ? EGL_BACK_BUFFER : EGL_SINGLE_BUFFER, diff --git a/client/src/app.c b/client/src/app.c index 530a73db..6fc3f79c 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -618,6 +618,11 @@ bool app_getProp(LG_DSProperty prop, void * ret) return g_state.ds->getProp(prop, ret); } +bool app_getHDRDescFailed(void) +{ + return atomic_load(&g_state.hdrDescFailed); +} + #ifdef ENABLE_EGL EGLDisplay app_getEGLDisplay(void) { diff --git a/client/src/main.c b/client/src/main.c index 3174f333..bedec53e 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -641,6 +641,25 @@ int main_frameThread(void * unused) lgrFormat.hdr = frame->flags & FRAME_FLAG_HDR; lgrFormat.hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; + if (lgrFormat.hdr) + { + memcpy(lgrFormat.hdrDisplayPrimary, frame->hdrDisplayPrimary, sizeof(lgrFormat.hdrDisplayPrimary)); + memcpy(lgrFormat.hdrWhitePoint , frame->hdrWhitePoint , sizeof(lgrFormat.hdrWhitePoint )); + lgrFormat.hdrMaxDisplayLuminance = frame->hdrMaxDisplayLuminance; + lgrFormat.hdrMinDisplayLuminance = frame->hdrMinDisplayLuminance; + lgrFormat.hdrMaxContentLightLevel = frame->hdrMaxContentLightLevel; + lgrFormat.hdrMaxFrameAverageLightLevel = frame->hdrMaxFrameAverageLightLevel; + } + else + { + memset(lgrFormat.hdrDisplayPrimary, 0, sizeof(lgrFormat.hdrDisplayPrimary)); + memset(lgrFormat.hdrWhitePoint , 0, sizeof(lgrFormat.hdrWhitePoint )); + lgrFormat.hdrMaxDisplayLuminance = 0; + lgrFormat.hdrMinDisplayLuminance = 0; + lgrFormat.hdrMaxContentLightLevel = 0; + lgrFormat.hdrMaxFrameAverageLightLevel = 0; + } + if (frame->flags & FRAME_FLAG_TRUNCATED) { const float needed = @@ -728,6 +747,17 @@ int main_frameThread(void * unused) } LG_UNLOCK(g_state.lgrLock); + if (g_state.ds->setHDRImageDescription) + { + if (!g_state.ds->setHDRImageDescription(&lgrFormat)) + { + DEBUG_WARN("Display server failed to apply HDR image description"); + atomic_store(&g_state.hdrDescFailed, true); + } + else + atomic_store(&g_state.hdrDescFailed, false); + } + g_state.srcSize.x = lgrFormat.screenWidth; g_state.srcSize.y = lgrFormat.screenHeight; g_state.haveSrcSize = true; diff --git a/client/src/main.h b/client/src/main.h index 3e1ae409..ed6d4405 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -154,6 +154,11 @@ struct AppState enum MicDefaultState micDefaultState; int spiceHotX, spiceHotY; + + // Set to true when setHDRImageDescription() returns false, indicating + // the display server could not apply the HDR image description. + // Checked by the renderer to fall back to software tone-mapping. + _Atomic(bool) hdrDescFailed; }; struct AppParams