[client] egl: improve HDR cursor rendering, fixes alpha blending

This commit is contained in:
Geoffrey McRae
2026-07-17 16:48:57 +10:00
parent 04441bfd85
commit d41c9fdc5d
4 changed files with 53 additions and 86 deletions

View File

@@ -31,7 +31,6 @@
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
// 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);
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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)