[client] wayland: implement HDR support

This commit is contained in:
Geoffrey McRae
2026-07-15 19:48:31 +10:00
parent 92d0383fd7
commit e2f6de77e7
16 changed files with 623 additions and 30 deletions

View File

@@ -13,6 +13,7 @@ find_program(WAYLAND_SCANNER_EXECUTABLE NAMES wayland-scanner)
add_library(displayserver_Wayland STATIC add_library(displayserver_Wayland STATIC
activation.c activation.c
clipboard.c clipboard.c
colormgmt.c
cursor.c cursor.c
gl.c gl.c
idle.c idle.c

View File

@@ -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 <stdbool.h>
#include <wayland-client.h>
#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;
}

View File

@@ -70,6 +70,52 @@ EGLDisplay waylandGetEGLDisplay(void)
return eglGetDisplay(native); 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) void waylandEGLSwapBuffers(EGLDisplay display, EGLSurface surface, const struct Rect * damage, int count)
{ {
if (!wlWm.swapWithDamage.init) if (!wlWm.swapWithDamage.init)
@@ -84,6 +130,7 @@ void waylandEGLSwapBuffers(EGLDisplay display, EGLSurface surface, const struct
} }
waylandPresentationFrame(); waylandPresentationFrame();
applyHDRPending();
swapWithDamage(&wlWm.swapWithDamage, display, surface, damage, count); swapWithDamage(&wlWm.swapWithDamage, display, surface, damage, count);
if (wlWm.needsResize) if (wlWm.needsResize)
@@ -149,23 +196,226 @@ EGLNativeWindowType waylandGetEGLNativeWindow(void)
} }
#endif #endif
#ifdef ENABLE_OPENGL // HDR color management support
bool waylandOpenGLInit(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)
{ {
EGLint attr[] = if (!wlWm.colorManager)
return;
if (!wlWm.cmFeaturesDone)
{ {
EGL_BUFFER_SIZE , 24, 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_CONFORMANT , EGL_OPENGL_BIT,
EGL_RENDERABLE_TYPE , EGL_OPENGL_BIT, EGL_RENDERABLE_TYPE , EGL_OPENGL_BIT,
EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER, EGL_COLOR_BUFFER_TYPE, EGL_RGB_BUFFER,
EGL_RED_SIZE , 8,
EGL_GREEN_SIZE , 8,
EGL_BLUE_SIZE , 8,
EGL_SAMPLE_BUFFERS , 0, EGL_SAMPLE_BUFFERS , 0,
EGL_SAMPLES , 0, EGL_SAMPLES , 0,
EGL_NONE 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)
{
wlWm.glDisplay = waylandGetEGLDisplay(); wlWm.glDisplay = waylandGetEGLDisplay();
int maj, min; int maj, min;
@@ -181,13 +431,31 @@ bool waylandOpenGLInit(void)
return false; return false;
} }
EGLint num_config; EGLConfig config;
if (!eglChooseConfig(wlWm.glDisplay, attr, &wlWm.glConfig, 1, &num_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; 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); wlWm.glSurface = eglCreateWindowSurface(wlWm.glDisplay, wlWm.glConfig, wlWm.eglWindow, NULL);
if (wlWm.glSurface == EGL_NO_SURFACE) if (wlWm.glSurface == EGL_NO_SURFACE)
{ {

View File

@@ -66,6 +66,9 @@ wayland_generate(
wayland_generate( wayland_generate(
"${WAYLAND_PROTOCOLS_BASE}/staging/content-type/content-type-v1.xml" "${WAYLAND_PROTOCOLS_BASE}/staging/content-type/content-type-v1.xml"
"${CMAKE_BINARY_DIR}/wayland/wayland-content-type-v1-client-protocol") "${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 target_link_libraries(wayland_protocol
PkgConfig::WAYLAND PkgConfig::WAYLAND

View File

@@ -74,6 +74,9 @@ static void registryGlobalHandler(void * data, struct wl_registry * registry,
else if (!strcmp(interface, wp_content_type_manager_v1_interface.name)) else if (!strcmp(interface, wp_content_type_manager_v1_interface.name))
wlWm.contentTypeManager = wl_registry_bind(wlWm.registry, name, wlWm.contentTypeManager = wl_registry_bind(wlWm.registry, name,
&wp_content_type_manager_v1_interface, 1); &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( else if (wlWm.desktop->registryGlobalHandler(
data, registry, name, interface, version)) data, registry, name, interface, version))
return; return;

View File

@@ -28,6 +28,7 @@
#include "common/debug.h" #include "common/debug.h"
#include "common/option.h" #include "common/option.h"
#include "interface/renderer.h"
#include "dynamic/wayland_desktops.h" #include "dynamic/wayland_desktops.h"
@@ -143,6 +144,9 @@ static bool waylandInit(const LG_DSInitParams params)
if (!waylandRegistryInit()) if (!waylandRegistryInit())
return false; return false;
if (!waylandColorMgmtInit())
return false;
if (!waylandActivationInit()) if (!waylandActivationInit())
return false; return false;
@@ -189,6 +193,7 @@ static void waylandFree(void)
waylandPresentationFree(); waylandPresentationFree();
waylandInputFree(); waylandInputFree();
waylandOutputFree(); waylandOutputFree();
waylandColorMgmtFree();
waylandRegistryFree(); waylandRegistryFree();
waylandCursorFree(); waylandCursorFree();
wl_display_disconnect(wlWm.display); wl_display_disconnect(wlWm.display);
@@ -202,6 +207,12 @@ static bool waylandGetProp(LG_DSProperty prop, void * ret)
return true; return true;
} }
if (prop == LG_DS_NATIVE_HDR)
{
*(bool *)ret = wlWm.cmCanDoHDR;
return true;
}
return false; return false;
} }
@@ -227,6 +238,22 @@ static void waylandMinimize(void)
wlWm.desktop->minimize(); 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 = struct LG_DisplayServerOps LGDS_Wayland =
{ {
.name = "Wayland", .name = "Wayland",
@@ -276,6 +303,8 @@ struct LG_DisplayServerOps LGDS_Wayland =
.getFullscreen = waylandGetFullscreen, .getFullscreen = waylandGetFullscreen,
.minimize = waylandMinimize, .minimize = waylandMinimize,
.setHDRImageDescription = waylandHDRCallback,
.cbInit = waylandCBInit, .cbInit = waylandCBInit,
.cbNotice = waylandCBNotice, .cbNotice = waylandCBNotice,
.cbRelease = waylandCBRelease, .cbRelease = waylandCBRelease,

View File

@@ -49,6 +49,7 @@
#include "wayland-xdg-activation-v1-client-protocol.h" #include "wayland-xdg-activation-v1-client-protocol.h"
#include "wayland-fractional-scale-v1-client-protocol.h" #include "wayland-fractional-scale-v1-client-protocol.h"
#include "wayland-content-type-v1-client-protocol.h" #include "wayland-content-type-v1-client-protocol.h"
#include "wayland-color-management-v1-client-protocol.h"
#include "scale.h" #include "scale.h"
@@ -120,7 +121,8 @@ struct WaylandDSState
#ifdef ENABLE_OPENGL #ifdef ENABLE_OPENGL
EGLDisplay glDisplay; EGLDisplay glDisplay;
EGLConfig glConfig; EGLConfig glConfig; // primary config (best bit depth probed)
EGLConfig glConfigSDR; // fallback 8-bit SDR config
EGLSurface glSurface; EGLSurface glSurface;
#endif #endif
@@ -180,6 +182,41 @@ struct WaylandDSState
struct wp_content_type_manager_v1 * contentTypeManager; struct wp_content_type_manager_v1 * contentTypeManager;
struct wp_content_type_v1 * contentType; 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; LGEvent * frameEvent;
struct wl_list poll; // WaylandPoll::link struct wl_list poll; // WaylandPoll::link
@@ -262,6 +299,24 @@ void waylandGLSetSwapInterval(int interval);
void waylandGLSwapBuffers(void); void waylandGLSwapBuffers(void);
#endif #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 // idle module
bool waylandIdleInit(void); bool waylandIdleInit(void);
void waylandIdleFree(void); void waylandIdleFree(void);

View File

@@ -72,6 +72,13 @@ void app_setFullscreen(bool fs);
bool app_getFullscreen(void); bool app_getFullscreen(void);
bool app_getProp(LG_DSProperty prop, void * ret); 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 #ifdef ENABLE_EGL
EGLDisplay app_getEGLDisplay(void); EGLDisplay app_getEGLDisplay(void);
EGLNativeWindowType app_getEGLNativeWindow(void); EGLNativeWindowType app_getEGLNativeWindow(void);

View File

@@ -53,6 +53,14 @@ typedef enum LG_DSProperty
* return data type: bool * return data type: bool
*/ */
LG_DS_WARP_SUPPORT, 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; LG_DSProperty;
@@ -213,6 +221,13 @@ struct LG_DisplayServerOps
void (*setFullscreen)(bool fs); void (*setFullscreen)(bool fs);
void (*minimize)(void); 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 */ /* clipboard support, optional, if not supported set to NULL */
bool (*cbInit)(void); bool (*cbInit)(void);
void (*cbNotice)(LG_ClipboardData type); void (*cbNotice)(LG_ClipboardData type);

View File

@@ -84,6 +84,14 @@ typedef struct LG_RendererFormat
unsigned int pitch; // scanline bytes (or compressed size) unsigned int pitch; // scanline bytes (or compressed size)
unsigned int bpp; // bits per pixel (zero if compressed) unsigned int bpp; // bits per pixel (zero if compressed)
LG_RendererRotate rotate; // guest rotation 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; LG_RendererFormat;

View File

@@ -91,6 +91,7 @@ struct EGL_Desktop
// map HDR content to SDR // map HDR content to SDR
bool mapHDRtoSDR; bool mapHDRtoSDR;
bool nativeHDR;
int peakLuminance; int peakLuminance;
int maxCLL; int maxCLL;
@@ -548,7 +549,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
{ {
.type = EGL_UNIFORM_TYPE_1I, .type = EGL_UNIFORM_TYPE_1I,
.location = shader->uCBMode, .location = shader->uCBMode,
.f = { desktop->cbMode } .i = { desktop->cbMode }
}, },
{ {
.type = EGL_UNIFORM_TYPE_1I, .type = EGL_UNIFORM_TYPE_1I,
@@ -558,7 +559,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
{ {
.type = EGL_UNIFORM_TYPE_1I, .type = EGL_UNIFORM_TYPE_1I,
.location = shader->uMapHDRtoSDR, .location = shader->uMapHDRtoSDR,
.i = { desktop->mapHDRtoSDR } .i = { desktop->mapHDRtoSDR && !desktop->nativeHDR }
}, },
{ {
.type = EGL_UNIFORM_TYPE_1F, .type = EGL_UNIFORM_TYPE_1F,
@@ -568,7 +569,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
{ {
.type = EGL_UNIFORM_TYPE_1I, .type = EGL_UNIFORM_TYPE_1I,
.location = shader->uMapHDRPQ, .location = shader->uMapHDRPQ,
.f = { desktop->hdrPQ } .i = { desktop->hdrPQ }
} }
}; };
@@ -579,6 +580,11 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
return true; return true;
} }
void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR)
{
desktop->nativeHDR = nativeHDR;
}
void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height) void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height)
{ {
if (!desktop->spiceTexture) if (!desktop->spiceTexture)

View File

@@ -42,6 +42,7 @@ bool egl_desktopInit(EGL * egl, EGL_Desktop ** desktop, EGLDisplay * display,
void egl_desktopFree(EGL_Desktop ** desktop); void egl_desktopFree(EGL_Desktop ** desktop);
void egl_desktopConfigUI(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_desktopSetup (EGL_Desktop * desktop, const LG_RendererFormat format);
bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd, bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd,
const FrameDamageRect * damageRects, int damageRectsCount); const FrameDamageRect * damageRects, int damageRectsCount);

View File

@@ -121,6 +121,9 @@ struct Inst
bool showSpice; bool showSpice;
int spiceWidth, spiceHeight; 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[] = 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); 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_update_scale_type(this);
egl_damageSetup(this->damage, format.frameWidth, format.frameHeight); egl_damageSetup(this->damage, format.frameWidth, format.frameHeight);
@@ -774,22 +797,51 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA)
} }
} }
// Probe for best available color depth: FP16 → 10-bit → 8-bit
static const struct { EGLint r, g, b, a, depth; const char * desc; } configs[] =
{
{ 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;
for (int i = 0; i < (int)(sizeof(configs) / sizeof(configs[0])); ++i)
{
EGLint attr[] = EGLint attr[] =
{ {
EGL_BUFFER_SIZE , 30, EGL_RED_SIZE , configs[i].r,
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, 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_SAMPLE_BUFFERS , maxSamples > 0 ? 1 : 0,
EGL_SAMPLES , maxSamples, EGL_SAMPLES , maxSamples,
EGL_NONE EGL_NONE
}; };
EGLint num_config; if (eglChooseConfig(this->display, attr, &this->configs, 1, &num_config) &&
if (!eglChooseConfig(this->display, attr, &this->configs, 1, &num_config)) num_config > 0)
{ {
DEBUG_ERROR("Failed to choose config (eglError: 0x%x)", eglGetError()); configDesc = configs[i].desc;
chosen = i;
break;
}
}
if (chosen < 0)
{
DEBUG_ERROR("Failed to choose any EGL config");
return false; return false;
} }
this->configSupportsHDR = chosen <= 1; // FP16 or 10-bit
DEBUG_INFO("EGL config: %s%s", configDesc,
this->configSupportsHDR ? " (HDR capable)" : "");
const EGLint surfattr[] = const EGLint surfattr[] =
{ {
EGL_RENDER_BUFFER, this->opt.doubleBuffer ? EGL_BACK_BUFFER : EGL_SINGLE_BUFFER, EGL_RENDER_BUFFER, this->opt.doubleBuffer ? EGL_BACK_BUFFER : EGL_SINGLE_BUFFER,

View File

@@ -618,6 +618,11 @@ bool app_getProp(LG_DSProperty prop, void * ret)
return g_state.ds->getProp(prop, ret); return g_state.ds->getProp(prop, ret);
} }
bool app_getHDRDescFailed(void)
{
return atomic_load(&g_state.hdrDescFailed);
}
#ifdef ENABLE_EGL #ifdef ENABLE_EGL
EGLDisplay app_getEGLDisplay(void) EGLDisplay app_getEGLDisplay(void)
{ {

View File

@@ -641,6 +641,25 @@ int main_frameThread(void * unused)
lgrFormat.hdr = frame->flags & FRAME_FLAG_HDR; lgrFormat.hdr = frame->flags & FRAME_FLAG_HDR;
lgrFormat.hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; 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) if (frame->flags & FRAME_FLAG_TRUNCATED)
{ {
const float needed = const float needed =
@@ -728,6 +747,17 @@ int main_frameThread(void * unused)
} }
LG_UNLOCK(g_state.lgrLock); 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.x = lgrFormat.screenWidth;
g_state.srcSize.y = lgrFormat.screenHeight; g_state.srcSize.y = lgrFormat.screenHeight;
g_state.haveSrcSize = true; g_state.haveSrcSize = true;

View File

@@ -154,6 +154,11 @@ struct AppState
enum MicDefaultState micDefaultState; enum MicDefaultState micDefaultState;
int spiceHotX, spiceHotY; 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 struct AppParams