[client] egl: implement SDR to HDR mapping for cursors

This commit is contained in:
Geoffrey McRae
2026-07-17 04:44:38 +10:00
parent f77986b863
commit 9803f5b12a
3 changed files with 84 additions and 2 deletions

View File

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

View File

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

View File

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