diff --git a/client/include/interface/renderer.h b/client/include/interface/renderer.h index de6fe316..33d7d04c 100644 --- a/client/include/interface/renderer.h +++ b/client/include/interface/renderer.h @@ -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, diff --git a/client/renderers/EGL/cursor.c b/client/renderers/EGL/cursor.c index 2d123fa3..060119ca 100644 --- a/client/renderers/EGL/cursor.c +++ b/client/renderers/EGL/cursor.c @@ -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); +} diff --git a/client/renderers/EGL/cursor.h b/client/renderers/EGL/cursor.h index 88f48c13..3d9b72a8 100644 --- a/client/renderers/EGL/cursor.h +++ b/client/renderers/EGL/cursor.h @@ -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); diff --git a/client/renderers/EGL/desktop.c b/client/renderers/EGL/desktop.c index 3b80334c..f43d16b5 100644 --- a/client/renderers/EGL/desktop.c +++ b/client/renderers/EGL/desktop.c @@ -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 diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index 1527d1d5..1a1cf2dc 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -32,6 +32,7 @@ #include "util.h" #include +#include #include #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, diff --git a/client/renderers/EGL/postprocess.c b/client/renderers/EGL/postprocess.c index af93f75f..ff2de624 100644 --- a/client/renderers/EGL/postprocess.c +++ b/client/renderers/EGL/postprocess.c @@ -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; diff --git a/client/renderers/EGL/postprocess.h b/client/renderers/EGL/postprocess.h index e1bee44c..27bc1baa 100644 --- a/client/renderers/EGL/postprocess.h +++ b/client/renderers/EGL/postprocess.h @@ -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); diff --git a/client/renderers/EGL/shader/cursor_mono.frag b/client/renderers/EGL/shader/cursor_mono.frag index 695b09f6..664d0230 100644 --- a/client/renderers/EGL/shader/cursor_mono.frag +++ b/client/renderers/EGL/shader/cursor_mono.frag @@ -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; } diff --git a/client/renderers/EGL/shader/desktop_rgb.frag b/client/renderers/EGL/shader/desktop_rgb.frag index 91cc47ac..3a90867d 100644 --- a/client/renderers/EGL/shader/desktop_rgb.frag +++ b/client/renderers/EGL/shader/desktop_rgb.frag @@ -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) diff --git a/client/src/main.c b/client/src/main.c index 42c310d1..5d211f0a 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -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);