diff --git a/client/renderers/EGL/cursor.c b/client/renderers/EGL/cursor.c index 2fa2f155..c8d7a777 100644 --- a/client/renderers/EGL/cursor.c +++ b/client/renderers/EGL/cursor.c @@ -31,7 +31,6 @@ #include #include #include -#include // these headers are auto generated by cmake #include "cursor.vert.h" @@ -46,6 +45,7 @@ struct CursorTex GLuint uScale; GLuint uRotate; GLuint uCBMode; + GLint uMapSDRtoPQ; }; struct CursorPos @@ -74,72 +74,17 @@ 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; _Atomic(float) scale; + _Atomic(bool) mapSDRtoPQ; struct CursorTex norm; struct CursorTex mono; 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) @@ -163,10 +108,11 @@ static bool cursorTexInit(struct CursorTex * t, return false; } - t->uMousePos = egl_shaderGetUniform(t->shader, "mouse" ); - t->uScale = egl_shaderGetUniform(t->shader, "scale" ); - t->uRotate = egl_shaderGetUniform(t->shader, "rotate" ); - t->uCBMode = egl_shaderGetUniform(t->shader, "cbMode" ); + t->uMousePos = egl_shaderGetUniform(t->shader, "mouse" ); + t->uScale = egl_shaderGetUniform(t->shader, "scale" ); + t->uRotate = egl_shaderGetUniform(t->shader, "rotate" ); + t->uCBMode = egl_shaderGetUniform(t->shader, "cbMode" ); + t->uMapSDRtoPQ = egl_shaderGetUniform(t->shader, "mapSDRtoPQ"); return true; } @@ -175,10 +121,11 @@ static inline void setCursorTexUniforms(EGL_Cursor * cursor, struct CursorTex * t, bool mono, float x, float y, float w, float h, float scale) { - glUniform4f(t->uMousePos, x, y, w, mono ? h / 2 : h); - glUniform1f(t->uScale , scale); - glUniform1i(t->uRotate , cursor->rotate); - glUniform1i(t->uCBMode , cursor->cbMode); + glUniform4f(t->uMousePos , x, y, w, mono ? h / 2 : h); + glUniform1f(t->uScale , scale); + glUniform1i(t->uRotate , cursor->rotate); + glUniform1i(t->uCBMode , cursor->cbMode); + glUniform1i(t->uMapSDRtoPQ, !mono && atomic_load(&cursor->mapSDRtoPQ)); } static void cursorTexFree(struct CursorTex * t) @@ -222,10 +169,11 @@ bool egl_cursorInit(EGL_Cursor ** cursor) struct CursorPos pos = { .x = 0, .y = 0 }; struct CursorPos hs = { .x = 0, .y = 0 }; struct CursorSize size = { .w = 0, .h = 0 }; - atomic_init(&(*cursor)->pos , pos ); - atomic_init(&(*cursor)->hs , hs ); - atomic_init(&(*cursor)->size , size); - atomic_init(&(*cursor)->scale, 1.0f); + atomic_init(&(*cursor)->pos , pos ); + atomic_init(&(*cursor)->hs , hs ); + atomic_init(&(*cursor)->size , size ); + atomic_init(&(*cursor)->scale , 1.0f ); + atomic_init(&(*cursor)->mapSDRtoPQ, false); return true; } @@ -313,8 +261,6 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, cursor->update = false; uint8_t * data = cursor->data; - bool needsPQ = cursor->hdrActive && cursor->hdrPQ; - switch(cursor->type) { case LG_CURSOR_MASKED_COLOR: @@ -342,13 +288,6 @@ 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); @@ -498,11 +437,5 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, 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; - } + atomic_store(&cursor->mapSDRtoPQ, hdrActive && hdrPQ); } diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index bd427717..47fc756a 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -630,7 +630,7 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo 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 + // Tell the cursor shader whether to map SDR cursor colors into PQ egl_cursorSetHDRState(this->cursor, useNativeHDR, format.hdrPQ); egl_update_scale_type(this); diff --git a/client/renderers/EGL/shader/cursor_rgb.frag b/client/renderers/EGL/shader/cursor_rgb.frag index 303c1ed1..f6754bf0 100644 --- a/client/renderers/EGL/shader/cursor_rgb.frag +++ b/client/renderers/EGL/shader/cursor_rgb.frag @@ -3,6 +3,7 @@ precision highp float; precision highp int; #include "color_blind.h" +#include "hdr.h" in vec2 uv; out vec4 color; @@ -10,6 +11,7 @@ out vec4 color; uniform sampler2D sampler1; uniform float scale; uniform int cbMode; +uniform bool mapSDRtoPQ; void main() { @@ -27,4 +29,18 @@ void main() if (cbMode > 0) color = cbTransform(color, cbMode); + + if (mapSDRtoPQ) + { + if (color.a > 0.0) + { + // Cursor pixels are premultiplied. Convert the straight color to PQ, + // then premultiply again so the blend operation remains valid. + vec3 srgb = clamp(color.rgb / color.a, 0.0, 1.0); + vec3 linear = bt709to2020(srgb2lin(srgb)); + color.rgb = lin2pq(linear * (203.0 / 10000.0)) * color.a; + } + else + color.rgb = vec3(0.0); + } } diff --git a/client/renderers/EGL/shader/hdr.h b/client/renderers/EGL/shader/hdr.h index da250e15..6f2aac37 100644 --- a/client/renderers/EGL/shader/hdr.h +++ b/client/renderers/EGL/shader/hdr.h @@ -21,6 +21,8 @@ const float ratio = 4.0; // Compressor ratio: 1 = disabled, <1 = exp const float compressor = 1.0 / ratio; // PQ constants +const float m1 = 2610.0 / 16384.0; +const float m2 = 2523.0 / 32.0; const float m1inv = 16384.0 / 2610.0; const float m2inv = 32.0 / 2523.0; const float c1 = 3424.0 / 4096.0; @@ -72,6 +74,13 @@ vec3 pq2lin(vec3 pq, float gain) return pow(d, vec3(m1inv)) * gain; } +vec3 lin2pq(vec3 linear) +{ + // ST.2084 uses absolute luminance normalized to 10000 cd/m². + vec3 p = pow(linear, vec3(m1)); + return pow((c1 + c2 * p) / (1.0 + c3 * p), vec3(m2)); +} + vec3 srgb2lin(vec3 c) { vec3 v = c / 12.92; @@ -99,6 +108,15 @@ vec3 bt2020to709(vec3 bt2020) bt2020.r * -0.0182 + bt2020.g * -0.1006 + bt2020.b * 1.1187); } +// in linear space +vec3 bt709to2020(vec3 bt709) +{ + return vec3( + bt709.r * 0.6274 + bt709.g * 0.3293 + bt709.b * 0.0433, + bt709.r * 0.0691 + bt709.g * 0.9195 + bt709.b * 0.0114, + bt709.r * 0.0164 + bt709.g * 0.0880 + bt709.b * 0.8956); +} + vec3 mapToSDR(vec3 color, float gain, bool pq) { if (pq)