diff --git a/client/renderers/EGL/cursor.c b/client/renderers/EGL/cursor.c index 1ee218cb..2fa2f155 100644 --- a/client/renderers/EGL/cursor.c +++ b/client/renderers/EGL/cursor.c @@ -31,6 +31,7 @@ #include #include #include +#include // these headers are auto generated by cmake #include "cursor.vert.h" @@ -73,6 +74,10 @@ struct EGL_Cursor LG_RendererRotate rotate; int cbMode; + // HDR state for native compositor HDR with PQ + bool hdrActive; + bool hdrPQ; + _Atomic(struct CursorPos) pos; _Atomic(struct CursorPos) hs; _Atomic(struct CursorSize) size; @@ -83,6 +88,58 @@ struct EGL_Cursor struct EGL_Model * model; }; +// PQ constants (ST.2084) +#define PQ_M1 (2610.0f / 16384.0f) +#define PQ_M2 (2523.0f / 32.0f) +#define PQ_C1 (3424.0f / 4096.0f) +#define PQ_C2 (2413.0f / 128.0f) +#define PQ_C3 (2392.0f / 128.0f) +#define SDR_WHITE_NITS 203.0f + +static float srgb2lin_f(float c) +{ + c /= 255.0f; + if (c <= 0.04045f) + return c / 12.92f; + return powf((c + 0.055f) / 1.055f, 2.4f); +} + +static float lin2pq_f(float nits) +{ + float y = powf(nits / 10000.0f, PQ_M1); + float p = (PQ_C1 + PQ_C2 * y) / (1.0f + PQ_C3 * y); + return powf(p, PQ_M2); +} + +static void convertCursorDataToPQ(uint8_t * data, int width, int height, + int stride) +{ + for (int y = 0; y < height; ++y) + { + uint8_t * row = data + (size_t)stride * y; + for (int x = 0; x < width; ++x) + { + uint8_t * px = row + (size_t)x * 4; + // BGRA order: px[0]=B, px[1]=G, px[2]=R, px[3]=A + float b_lin = srgb2lin_f(px[0]); + float g_lin = srgb2lin_f(px[1]); + float r_lin = srgb2lin_f(px[2]); + + float b_nits = b_lin * SDR_WHITE_NITS; + float g_nits = g_lin * SDR_WHITE_NITS; + float r_nits = r_lin * SDR_WHITE_NITS; + + float b_pq = lin2pq_f(b_nits); + float g_pq = lin2pq_f(g_nits); + float r_pq = lin2pq_f(r_nits); + + px[0] = (uint8_t)(b_pq * 255.0f + 0.5f); + px[1] = (uint8_t)(g_pq * 255.0f + 0.5f); + px[2] = (uint8_t)(r_pq * 255.0f + 0.5f); + } + } +} + static bool cursorTexInit(struct CursorTex * t, const char * vertex_code , size_t vertex_size, const char * fragment_code, size_t fragment_size) @@ -256,6 +313,7 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, cursor->update = false; uint8_t * data = cursor->data; + bool needsPQ = cursor->hdrActive && cursor->hdrPQ; switch(cursor->type) { @@ -284,6 +342,13 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, case LG_CURSOR_COLOR: { + // Convert color data from sRGB to PQ when compositor HDR PQ is active. + // This is done CPU-side because the shader also handles mask textures + // where PQ conversion would break the GL_ZERO/GL_SRC_COLOR blend math. + if (needsPQ) + convertCursorDataToPQ(data, cursor->width, cursor->height, + cursor->stride); + egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, cursor->stride); egl_textureUpdate(cursor->norm.texture, data, true); @@ -310,6 +375,7 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, } } + // Monochrome cursors use AND/XOR mask textures - never convert to PQ egl_textureSetup(cursor->norm.texture, EGL_PF_BGRA, cursor->width, cursor->height, cursor->width, sizeof(and[0])); egl_textureSetup(cursor->mono.texture, EGL_PF_BGRA, @@ -429,3 +495,14 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, return state; } + +void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ) +{ + if (cursor->hdrActive != hdrActive || cursor->hdrPQ != hdrPQ) + { + cursor->hdrActive = hdrActive; + cursor->hdrPQ = hdrPQ; + // Force a re-upload of the cursor texture with the new color space + cursor->update = true; + } +} diff --git a/client/renderers/EGL/cursor.h b/client/renderers/EGL/cursor.h index 6bc4da3f..74d62f92 100644 --- a/client/renderers/EGL/cursor.h +++ b/client/renderers/EGL/cursor.h @@ -52,3 +52,5 @@ void egl_cursorSetState(EGL_Cursor * cursor, const bool visible, struct CursorState egl_cursorRender(EGL_Cursor * cursor, LG_RendererRotate rotate, int width, int height); + +void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ); diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index c2158ba3..bd427717 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -627,8 +627,11 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo // (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()); + bool useNativeHDR = format.hdr && nativeHDR && !app_getHDRDescFailed(); + egl_desktopSetNativeHDR(this->desktop, useNativeHDR); + + // Tell the cursor shader about HDR state so it can do SDR→PQ conversion + egl_cursorSetHDRState(this->cursor, useNativeHDR, format.hdrPQ); egl_update_scale_type(this); egl_damageSetup(this->damage, format.frameWidth, format.frameHeight);