[client] egl: make native HDR encoding-safe

Request true floating-point EGL configs for scRGB and expose the native
encodings supported by the selected surface. Fall back to software HDR
mapping when the surface cannot represent the incoming wire format.

Resample PQ in linear light, keep SDR-only effects out of native PQ
chains, and consume the cursor-specific white level without changing
binary monochrome cursor mask operations.
This commit is contained in:
Geoffrey McRae
2026-07-19 04:42:30 +10:00
parent 20225f05d1
commit 70b795c5ba
10 changed files with 160 additions and 42 deletions

View File

@@ -52,7 +52,9 @@ LG_RendererParams;
typedef enum LG_RendererSupport
{
LG_SUPPORTS_DMABUF
LG_SUPPORTS_DMABUF,
LG_SUPPORTS_HDR_PQ,
LG_SUPPORTS_HDR_SCRGB
}
LG_RendererSupport;
@@ -163,6 +165,10 @@ typedef struct LG_RendererOps
void (*onMouseColorTransform)(LG_Renderer * renderer,
const KVMFRColorTransform * transform);
/* updates the cursor-specific SDR white level reported by IddCx
* Context: cursorThread */
void (*onMouseWhiteLevel)(LG_Renderer * renderer, uint32_t sdrWhiteLevel);
/* called when the mouse has moved or changed visibillity
* Context: cursorThread */
bool (*onMouseEvent)(LG_Renderer * renderer, const bool visible, int x, int y,

View File

@@ -501,7 +501,12 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor,
void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ,
float sdrWhiteLevel)
{
atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ?
sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT);
egl_cursorSetSDRWhiteLevel(cursor, sdrWhiteLevel);
atomic_store(&cursor->wireTransfer, !hdrActive ? 0 : (hdrPQ ? 2 : 1));
}
void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel)
{
atomic_store(&cursor->sdrWhiteLevel, sdrWhiteLevel > 0.0f ?
sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT);
}

View File

@@ -58,3 +58,4 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor,
void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ,
float sdrWhiteLevel);
void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel);

View File

@@ -468,7 +468,8 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth,
egl_postProcessConfigModified(desktop->pp);
if (processFrame &&
egl_postProcessRun(desktop->pp, tex, desktop->mesh,
width, height, outputWidth, outputHeight, dma) &&
width, height, outputWidth, outputHeight, dma,
desktop->hdr && desktop->hdrPQ) &&
egl_postProcessNeedsFullFrame(desktop->pp))
{
/* The filter output may have changed everywhere, but this only applies to

View File

@@ -32,6 +32,7 @@
#include "util.h"
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl32.h>
#include "cimgui.h"
@@ -126,7 +127,8 @@ struct Inst
bool showSpice;
int spiceWidth, spiceHeight;
bool configSupportsHDR; // true if we probed FP16 or 10-bit EGL config
bool surfaceSupportsPQ;
bool surfaceSupportsSCRGB;
bool hdr; // true if current frame format is HDR
bool nativeHDR; // true only while the surface HDR description is active
};
@@ -340,6 +342,12 @@ static bool egl_supports(LG_Renderer * renderer, LG_RendererSupport flag)
case LG_SUPPORTS_DMABUF:
return this->dmaSupport;
case LG_SUPPORTS_HDR_PQ:
return this->surfaceSupportsPQ;
case LG_SUPPORTS_HDR_SCRGB:
return this->surfaceSupportsSCRGB;
default:
return false;
}
@@ -585,6 +593,13 @@ static void egl_onMouseColorTransform(LG_Renderer * renderer,
egl_cursorSetColorTransform(this->cursor, transform);
}
static void egl_onMouseWhiteLevel(LG_Renderer * renderer,
uint32_t sdrWhiteLevel)
{
struct Inst * this = UPCAST(struct Inst, renderer);
egl_cursorSetSDRWhiteLevel(this->cursor, sdrWhiteLevel);
}
static bool egl_onMouseEvent(LG_Renderer * renderer, const bool visible,
int x, int y, const int hx, const int hy)
{
@@ -602,7 +617,9 @@ static bool egl_updateHDRState(struct Inst * this, bool force)
{
bool nativeHDR = false;
app_getProp(LG_DS_NATIVE_HDR, &nativeHDR);
const bool useNativeHDR = this->format.hdr && nativeHDR &&
const bool surfaceCompatible = this->format.hdrPQ ?
this->surfaceSupportsPQ : this->surfaceSupportsSCRGB;
const bool useNativeHDR = this->format.hdr && surfaceCompatible && nativeHDR &&
!app_getHDRDescFailed();
if (!force && this->nativeHDR == useNativeHDR)
@@ -669,9 +686,11 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo
{
bool nativeHDR = false;
app_getProp(LG_DS_NATIVE_HDR, &nativeHDR);
if (!this->configSupportsHDR && !nativeHDR)
const bool surfaceCompatible = format.hdrPQ ?
this->surfaceSupportsPQ : this->surfaceSupportsSCRGB;
if (!surfaceCompatible)
DEBUG_WARN("HDR frames being displayed on an 8-bit EGL surface "
"without a color-managed compositor; tones may be incorrect");
"or an EGL surface incompatible with the source encoding");
}
egl_updateHDRState(this, true);
@@ -844,31 +863,57 @@ 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[] =
// Probe for best available color depth: floating-point FP16, then fixed
// 10-bit, then fixed 8-bit. Component type defaults to fixed-point in EGL,
// so it must be requested explicitly for an scRGB-capable surface.
#ifndef EGL_COLOR_COMPONENT_TYPE_EXT
#define EGL_COLOR_COMPONENT_TYPE_EXT 0x3339
#define EGL_COLOR_COMPONENT_TYPE_FIXED_EXT 0x333A
#define EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT 0x333B
#endif
static const struct
{
{ 16, 16, 16, 0, 48, "FP16 (RGBA16F)" },
{ 10, 10, 10, 2, 32, "10-bit (RGBA10)" },
{ 8, 8, 8, 0, 24, "8-bit (RGBA8)" },
EGLint r, g, b, a, depth, componentType;
bool pq, scRGB;
const char * desc;
} configs[] =
{
{ 16, 16, 16, 0, 48, EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT,
true, true, "FP16 (RGBA16F)" },
{ 10, 10, 10, 2, 32, EGL_COLOR_COMPONENT_TYPE_FIXED_EXT,
true, false, "10-bit (RGBA10)" },
{ 8, 8, 8, 0, 24, EGL_COLOR_COMPONENT_TYPE_FIXED_EXT,
false, false, "8-bit (RGBA8)" },
};
const char * eglExtensions = eglQueryString(this->display, EGL_EXTENSIONS);
const bool hasFloatConfigs = eglExtensions &&
util_hasGLExt(eglExtensions, "EGL_EXT_pixel_format_float");
EGLint num_config;
const char * configDesc = NULL;
int chosen = -1;
for (int i = 0; i < (int)(sizeof(configs) / sizeof(configs[0])); ++i)
{
EGLint attr[] =
{
EGL_RED_SIZE , configs[i].r,
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_SAMPLES , maxSamples,
EGL_NONE
};
if (configs[i].componentType == EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT &&
!hasFloatConfigs)
continue;
EGLint attr[23];
int ai = 0;
#define EGL_CONFIG_ATTR(name, value) \
do { attr[ai++] = (name); attr[ai++] = (value); } while (0)
EGL_CONFIG_ATTR(EGL_RED_SIZE , configs[i].r);
EGL_CONFIG_ATTR(EGL_GREEN_SIZE , configs[i].g);
EGL_CONFIG_ATTR(EGL_BLUE_SIZE , configs[i].b);
EGL_CONFIG_ATTR(EGL_ALPHA_SIZE , configs[i].a);
EGL_CONFIG_ATTR(EGL_BUFFER_SIZE , configs[i].depth);
if (hasFloatConfigs)
EGL_CONFIG_ATTR(EGL_COLOR_COMPONENT_TYPE_EXT, configs[i].componentType);
EGL_CONFIG_ATTR(EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT);
EGL_CONFIG_ATTR(EGL_SAMPLE_BUFFERS , maxSamples > 0 ? 1 : 0);
EGL_CONFIG_ATTR(EGL_SAMPLES , maxSamples);
attr[ai] = EGL_NONE;
#undef EGL_CONFIG_ATTR
if (eglChooseConfig(this->display, attr, &this->configs, 1, &num_config) &&
num_config > 0)
@@ -885,9 +930,10 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA)
return false;
}
this->configSupportsHDR = chosen <= 1; // FP16 or 10-bit
this->surfaceSupportsPQ = configs[chosen].pq;
this->surfaceSupportsSCRGB = configs[chosen].scRGB;
DEBUG_INFO("EGL config: %s%s", configDesc,
this->configSupportsHDR ? " (HDR capable)" : "");
this->surfaceSupportsPQ ? " (HDR capable)" : "");
const EGLint surfattr[] =
{
@@ -1468,6 +1514,7 @@ struct LG_RendererOps LGR_EGL =
.onResize = egl_onResize,
.onMouseShape = egl_onMouseShape,
.onMouseColorTransform = egl_onMouseColorTransform,
.onMouseWhiteLevel = egl_onMouseWhiteLevel,
.onMouseEvent = egl_onMouseEvent,
.onFrameFormat = egl_onFrameFormat,
.onFrame = egl_onFrame,

View File

@@ -62,6 +62,7 @@ struct EGL_PostProcess
int desktopWidth, desktopHeight;
unsigned int targetX, targetY;
bool useDMA;
bool hdrPQ;
unsigned int outputX, outputY;
bool fullFrame;
}
@@ -705,7 +706,7 @@ bool egl_postProcessNeedsFullFrame(EGL_PostProcess * this)
static bool configMatches(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
unsigned int inputX, unsigned int inputY,
int desktopWidth, int desktopHeight,
unsigned int targetX, unsigned int targetY, bool useDMA)
unsigned int targetX, unsigned int targetY, bool useDMA, bool hdrPQ)
{
return
this->config.valid &&
@@ -716,13 +717,14 @@ static bool configMatches(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
this->config.desktopHeight == desktopHeight &&
this->config.targetX == targetX &&
this->config.targetY == targetY &&
this->config.useDMA == useDMA;
this->config.useDMA == useDMA &&
this->config.hdrPQ == hdrPQ;
}
static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
unsigned int inputX, unsigned int inputY,
int desktopWidth, int desktopHeight,
unsigned int targetX, unsigned int targetY, bool useDMA)
unsigned int targetX, unsigned int targetY, bool useDMA, bool hdrPQ)
{
this->config.valid = false;
vector_clear(&this->activeFilters);
@@ -744,6 +746,13 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
for(const Vector ** filters = lists; *filters; ++filters)
vector_forEach(filter, *filters)
{
// These filters operate on their input signal directly. PQ must be
// decoded to linear light before resampling or enhancement, and none of
// the current external filters declares such support. Keep only the
// mandatory format-conversion filters in a PQ chain.
if (hdrPQ && filter->ops.type != EGL_FILTER_TYPE_INTERNAL)
continue;
egl_filterSetOutputResHint(filter, targetX, targetY);
if (!egl_filterSetup(filter, outputFormat, outputX, outputY,
@@ -777,6 +786,7 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
this->config.targetX = targetX;
this->config.targetY = targetY;
this->config.useDMA = useDMA;
this->config.hdrPQ = hdrPQ;
this->config.outputX = outputX;
this->config.outputY = outputY;
this->config.fullFrame = fullFrame;
@@ -786,7 +796,7 @@ static bool configureChain(EGL_PostProcess * this, EGL_PixelFormat pixFmt,
bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex,
EGL_DesktopRects * rects, int desktopWidth, int desktopHeight,
unsigned int targetX, unsigned int targetY, bool useDMA)
unsigned int targetX, unsigned int targetY, bool useDMA, bool hdrPQ)
{
if (targetX == 0 && targetY == 0)
DEBUG_FATAL("targetX || targetY == 0");
@@ -802,7 +812,7 @@ bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex,
atomic_load_explicit(&this->modified, memory_order_acquire);
const bool reconfigure = modified ||
!configMatches(this, pixFmt, inputX, inputY,
desktopWidth, desktopHeight, targetX, targetY, useDMA);
desktopWidth, desktopHeight, targetX, targetY, useDMA, hdrPQ);
if (reconfigure)
{
@@ -811,7 +821,7 @@ bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex,
&this->modified, false, memory_order_acq_rel);
if (!configureChain(this, pixFmt, inputX, inputY,
desktopWidth, desktopHeight, targetX, targetY, useDMA))
desktopWidth, desktopHeight, targetX, targetY, useDMA, hdrPQ))
{
egl_postProcessInvalidate(this);
return false;

View File

@@ -47,7 +47,7 @@ bool egl_postProcessNeedsFullFrame(EGL_PostProcess * this);
* targetX/Y is the final target output dimension hint if scalers are present */
bool egl_postProcessRun(EGL_PostProcess * this, EGL_Texture * tex,
EGL_DesktopRects * rects, int desktopWidth, int desktopHeight,
unsigned int targetX, unsigned int targetY, bool useDMA);
unsigned int targetX, unsigned int targetY, bool useDMA, bool hdrPQ);
EGL_Texture * egl_postProcessGetOutput(EGL_PostProcess * this,
unsigned int * outputX, unsigned int * outputY);

View File

@@ -9,7 +9,7 @@ out vec4 color;
uniform sampler2D sampler1;
uniform float scale;
uniform bool mapSDRtoPQ;
uniform int wireTransfer; // 0: sRGB, 1: scRGB, 2: PQ
uniform float sdrWhiteLevel;
void main()
@@ -30,11 +30,13 @@ void main()
if (tmp.rgb == vec3(0.0, 0.0, 0.0))
discard;
if (mapSDRtoPQ)
if (wireTransfer == 2)
{
vec3 linear = bt709to2020(srgb2lin(tmp.rgb));
tmp.rgb = lin2pq(linear * (sdrWhiteLevel / 10000.0));
}
else if (wireTransfer == 1)
tmp.rgb = srgb2lin(tmp.rgb) * (sdrWhiteLevel / 80.0);
color = tmp;
}

View File

@@ -26,6 +26,27 @@ uniform bool mapHDRtoSDR;
uniform float mapHDRGain;
uniform bool mapHDRPQ;
vec4 samplePQLinear(vec2 coord)
{
ivec2 size = textureSize(sampler1, 0);
vec2 samplePos = coord * vec2(size) - 0.5;
ivec2 base = ivec2(floor(samplePos));
vec2 f = fract(samplePos);
ivec2 limit = size - ivec2(1);
vec3 c00 = pq2lin(texelFetch(sampler1,
clamp(base, ivec2(0), limit), 0).rgb, 1.0);
vec3 c10 = pq2lin(texelFetch(sampler1,
clamp(base + ivec2(1, 0), ivec2(0), limit), 0).rgb, 1.0);
vec3 c01 = pq2lin(texelFetch(sampler1,
clamp(base + ivec2(0, 1), ivec2(0), limit), 0).rgb, 1.0);
vec3 c11 = pq2lin(texelFetch(sampler1,
clamp(base + ivec2(1, 1), ivec2(0), limit), 0).rgb, 1.0);
vec3 linear = mix(mix(c00, c10, f.x), mix(c01, c11, f.x), f.y);
return vec4(lin2pq(max(linear, 0.0)), 1.0);
}
void main()
{
switch (scaleAlgo)
@@ -39,7 +60,7 @@ void main()
case EGL_SCALE_LINEAR:
{
color = texture(sampler1, uv);
color = isHDR && mapHDRPQ ? samplePQLinear(uv) : texture(sampler1, uv);
break;
}
}
@@ -47,10 +68,13 @@ void main()
if (isHDR && mapHDRtoSDR)
color.rgb = mapToSDR(color.rgb, mapHDRGain, mapHDRPQ);
if (cbMode > 0)
// The legacy effects are defined for an SDR signal. Do not apply them to a
// native HDR signal where their nonlinear operations would alter luminance
// and gamut incorrectly. They remain available after HDR-to-SDR mapping.
if (cbMode > 0 && (!isHDR || mapHDRtoSDR))
color = cbTransform(color, cbMode);
if (nvGain > 0.0)
if (nvGain > 0.0 && (!isHDR || mapHDRtoSDR))
{
highp float lumi = (0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b);
if (lumi < 0.5)

View File

@@ -459,8 +459,8 @@ int main_cursorThread(void * unused)
memcpy(cursor, msg.mem, neededSize);
lgmpClientMessageDone(g_state.pointerQueue);
g_cursor.guest.visible =
msg.udata & CURSOR_FLAG_VISIBLE;
if (msg.udata & CURSOR_FLAG_VISIBLE_VALID)
g_cursor.guest.visible = msg.udata & CURSOR_FLAG_VISIBLE;
if (msg.udata & CURSOR_FLAG_SHAPE)
{
@@ -500,6 +500,11 @@ int main_cursorThread(void * unused)
g_state.lgr->ops.onMouseColorTransform(g_state.lgr, transform);
}
if ((msg.udata & CURSOR_FLAG_VISIBLE_VALID) &&
cursor->sdrWhiteLevel && g_state.lgr->ops.onMouseWhiteLevel)
g_state.lgr->ops.onMouseWhiteLevel(
g_state.lgr, cursor->sdrWhiteLevel);
if (msg.udata & CURSOR_FLAG_POSITION)
{
bool valid = g_cursor.guest.valid;
@@ -764,11 +769,28 @@ int main_frameThread(void * unused)
LG_UNLOCK(g_state.lgrLock);
break;
}
// Renderers which expose HDR surface capabilities must match the wire
// encoding. Legacy renderers do not advertise this interface, so retain
// their existing display-server behaviour rather than silently forcing
// them through a conversion path they may not implement.
const bool rendererSupportsNativeHDR = !lgrFormat.hdr ||
!g_state.lgr->ops.supports || RENDERER(supports,
lgrFormat.hdrPQ ? LG_SUPPORTS_HDR_PQ : LG_SUPPORTS_HDR_SCRGB);
LG_UNLOCK(g_state.lgrLock);
if (g_state.ds->setHDRImageDescription)
{
if (!g_state.ds->setHDRImageDescription(&lgrFormat))
if (lgrFormat.hdr && !rendererSupportsNativeHDR)
{
LG_RendererFormat sdrSurfaceFormat = lgrFormat;
sdrSurfaceFormat.hdr = false;
g_state.ds->setHDRImageDescription(&sdrSurfaceFormat);
DEBUG_WARN("Renderer surface cannot represent the native HDR "
"encoding; using software HDR mapping");
atomic_store(&g_state.hdrDescFailed, true);
}
else if (!g_state.ds->setHDRImageDescription(&lgrFormat))
{
DEBUG_WARN("Display server failed to apply HDR image description");
atomic_store(&g_state.hdrDescFailed, true);