diff --git a/client/renderers/EGL/CMakeLists.txt b/client/renderers/EGL/CMakeLists.txt index f5ff2415..4a8ce270 100644 --- a/client/renderers/EGL/CMakeLists.txt +++ b/client/renderers/EGL/CMakeLists.txt @@ -57,6 +57,8 @@ build_shaders( shader/damage.frag shader/hdr_overlay.vert shader/hdr_overlay.frag + shader/hdr_compose.vert + shader/hdr_compose.frag shader/basic.vert shader/convert_24bit.frag shader/ffx_cas.frag @@ -88,6 +90,7 @@ add_library(renderer_EGL STATIC cursor.c damage.c hdr_overlay.c + hdr_compose.c framebuffer.c effect.c postprocess.c diff --git a/client/renderers/EGL/cursor.c b/client/renderers/EGL/cursor.c index 060119ca..9b5ea14e 100644 --- a/client/renderers/EGL/cursor.c +++ b/client/renderers/EGL/cursor.c @@ -38,6 +38,13 @@ #include "cursor_rgb.frag.h" #include "cursor_mono.frag.h" +enum CursorTransfer +{ + CURSOR_TRANSFER_SRGB = 0, + CURSOR_TRANSFER_SCRGB = 1, + CURSOR_TRANSFER_PQ = 2, +}; + struct CursorTex { struct EGL_Texture * texture; @@ -46,11 +53,15 @@ struct CursorTex EGL_Uniform * uScale; EGL_Uniform * uRotate; EGL_Uniform * uCBMode; - EGL_Uniform * uWireTransfer; + EGL_Uniform * uSourceTransfer; EGL_Uniform * uSDRWhiteLevel; + EGL_Uniform * uMapHDRtoSDR; + EGL_Uniform * uMapHDRGain; + EGL_Uniform * uMapHDRContentPeak; EGL_Uniform * uColorTransformFlags; EGL_Uniform * uColorMatrix; EGL_Uniform * uColorScalar; + EGL_Uniform * uLinearComposition; }; struct CursorPos @@ -79,12 +90,15 @@ struct EGL_Cursor LG_RendererRotate rotate; int cbMode; - _Atomic(struct CursorPos) pos; - _Atomic(struct CursorPos) hs; - _Atomic(struct CursorSize) size; - _Atomic(float) scale; - _Atomic(int) wireTransfer; - _Atomic(float) sdrWhiteLevel; + _Atomic(struct CursorPos) pos; + _Atomic(struct CursorPos) hs; + _Atomic(struct CursorSize) size; + _Atomic(float) scale; + _Atomic(enum CursorTransfer) sourceTransfer; + _Atomic(float) sdrWhiteLevel; + _Atomic(bool) mapHDRtoSDR; + _Atomic(float) mapHDRGain; + _Atomic(float) mapHDRContentPeak; bool colorTransformUpdate; KVMFRColorTransform pendingColorTransform; @@ -119,16 +133,23 @@ 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->uWireTransfer = egl_shaderGetUniform(t->shader, "wireTransfer" ); - t->uSDRWhiteLevel = egl_shaderGetUniform(t->shader, "sdrWhiteLevel"); - t->uColorTransformFlags = + 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->uSourceTransfer = egl_shaderGetUniform(t->shader, "sourceTransfer"); + t->uSDRWhiteLevel = egl_shaderGetUniform(t->shader, "sdrWhiteLevel"); + t->uMapHDRtoSDR = egl_shaderGetUniform(t->shader, "mapHDRtoSDR" ); + t->uMapHDRGain = egl_shaderGetUniform(t->shader, "mapHDRGain" ); + t->uMapHDRContentPeak = + egl_shaderGetUniform(t->shader, "mapHDRContentPeak"); + t->uColorTransformFlags = egl_shaderGetUniform(t->shader, "colorTransformFlags"); - t->uColorMatrix = egl_shaderGetUniform(t->shader, "colorMatrix[0]"); - t->uColorScalar = egl_shaderGetUniform(t->shader, "colorTransformScalar"); + t->uColorMatrix = egl_shaderGetUniform(t->shader, "colorMatrix[0]"); + t->uColorScalar = + egl_shaderGetUniform(t->shader, "colorTransformScalar"); + t->uLinearComposition = + egl_shaderGetUniform(t->shader, "linearComposition"); egl_shaderAssocTextures(t->shader, 2); return true; @@ -136,20 +157,37 @@ static bool cursorTexInit(struct CursorTex * t, static inline void setCursorTexUniforms(EGL_Cursor * cursor, struct CursorTex * t, bool mono, float x, float y, - float w, float h, float scale) + float w, float h, float scale, bool linearComposition) { - egl_uniform4f(t->uMousePos , x, y, w, mono ? h / 2 : h); - egl_uniform1f(t->uScale , scale); - egl_uniform1i(t->uRotate , cursor->rotate); - egl_uniform1i(t->uCBMode , cursor->cbMode); - egl_uniform1i(t->uWireTransfer, - mono ? 0 : atomic_load(&cursor->wireTransfer)); - egl_uniform1f(t->uSDRWhiteLevel, atomic_load(&cursor->sdrWhiteLevel)); - egl_uniform1ui(t->uColorTransformFlags, + const enum CursorTransfer sourceTransfer = mono ? CURSOR_TRANSFER_SRGB : + atomic_load(&cursor->sourceTransfer); + egl_uniform4f (t->uMousePos , x, y, w, mono ? h / 2 : h); + egl_uniform1f (t->uScale , scale); + egl_uniform1i (t->uRotate , cursor->rotate); + // The colour-blind transform is defined for SDR signals. The desktop + // disables it while passing native HDR through, so keep the cursor on the + // same path rather than altering encoded PQ or linear scRGB independently. + egl_uniform1i (t->uCBMode , + (sourceTransfer == CURSOR_TRANSFER_SRGB || + atomic_load(&cursor->mapHDRtoSDR)) ? + cursor->cbMode : 0); + egl_uniform1i (t->uSourceTransfer , sourceTransfer); + egl_uniform1f (t->uSDRWhiteLevel , + atomic_load(&cursor->sdrWhiteLevel)); + egl_uniform1i (t->uMapHDRtoSDR , + !mono && atomic_load(&cursor->mapHDRtoSDR)); + egl_uniform1f (t->uMapHDRGain , + atomic_load(&cursor->mapHDRGain)); + egl_uniform1f (t->uMapHDRContentPeak , + atomic_load(&cursor->mapHDRContentPeak)); + egl_uniform1ui(t->uColorTransformFlags , mono ? 0 : cursor->activeColorTransform.flags); - egl_uniformSet(t->uColorMatrix, EGL_UNIFORM_TYPE_4FV, 3, GL_FALSE, + egl_uniformSet (t->uColorMatrix , EGL_UNIFORM_TYPE_4FV, 3, GL_FALSE, cursor->activeColorTransform.matrix); - egl_uniform1f(t->uColorScalar, cursor->activeColorTransform.scalar); + egl_uniform1f (t->uColorScalar , + cursor->activeColorTransform.scalar); + egl_uniform1i (t->uLinearComposition , + !mono && linearComposition); if (!mono && cursor->colorLUT) egl_stateBindTexture(1, GL_TEXTURE_2D, cursor->colorLUT); } @@ -195,13 +233,16 @@ 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)->wireTransfer, 0); - atomic_init(&(*cursor)->sdrWhiteLevel, + atomic_init(&(*cursor)->pos , pos ); + atomic_init(&(*cursor)->hs , hs ); + atomic_init(&(*cursor)->size , size ); + atomic_init(&(*cursor)->scale , 1.0f ); + atomic_init(&(*cursor)->sourceTransfer , CURSOR_TRANSFER_SRGB); + atomic_init(&(*cursor)->sdrWhiteLevel , KVMFR_SDR_WHITE_LEVEL_DEFAULT); + atomic_init(&(*cursor)->mapHDRtoSDR , false); + atomic_init(&(*cursor)->mapHDRGain , 1.0f ); + atomic_init(&(*cursor)->mapHDRContentPeak, 1.0f ); return true; } @@ -290,8 +331,12 @@ void egl_cursorSetState(EGL_Cursor * cursor, const bool visible, } struct CursorState egl_cursorRender(EGL_Cursor * cursor, - LG_RendererRotate rotate, int width, int height) + LG_RendererRotate rotate, int width, int height, + bool linearComposition, bool * logicalDeferred) { + if (logicalDeferred) + *logicalDeferred = false; + if (!cursor->visible) return (struct CursorState) { .visible = false }; @@ -443,20 +488,27 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, state.rect.x = max(0, state.rect.x - 1); state.rect.y = max(0, state.rect.y - 1); + if (linearComposition && cursor->type != LG_CURSOR_COLOR) + { + if (logicalDeferred) + *logicalDeferred = true; + return state; + } + egl_stateBlend(true); switch(cursor->type) { case LG_CURSOR_MONOCHROME: { setCursorTexUniforms(cursor, &cursor->norm, true, pos.x, pos.y, - size.w, size.h, scale); + size.w, size.h, scale, false); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_ZERO, GL_SRC_COLOR); egl_modelSetTexture(cursor->model, cursor->norm.texture); egl_modelRender(cursor->model); setCursorTexUniforms(cursor, &cursor->mono, true, pos.x, pos.y, - size.w, size.h, scale); + size.w, size.h, scale, false); egl_shaderUse(cursor->mono.shader); egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); egl_modelSetTexture(cursor->model, cursor->mono.texture); @@ -467,14 +519,14 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, case LG_CURSOR_MASKED_COLOR: { setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y, - size.w, size.h, scale); + size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); egl_modelSetTexture(cursor->model, cursor->norm.texture); egl_modelRender(cursor->model); setCursorTexUniforms(cursor, &cursor->mono, false, pos.x, pos.y, - size.w, size.h, scale); + size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->mono.shader); egl_stateBlendFunc(GL_ONE_MINUS_DST_COLOR, GL_ZERO); egl_modelSetTexture(cursor->model, cursor->mono.texture); @@ -485,7 +537,7 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, case LG_CURSOR_COLOR: { setCursorTexUniforms(cursor, &cursor->norm, false, pos.x, pos.y, - size.w, size.h, scale); + size.w, size.h, scale, linearComposition); egl_shaderUse(cursor->norm.shader); egl_stateBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); egl_modelSetTexture(cursor->model, cursor->norm.texture); @@ -498,11 +550,19 @@ struct CursorState egl_cursorRender(EGL_Cursor * cursor, return state; } -void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ, - float sdrWhiteLevel) +void egl_cursorSetHDRState(EGL_Cursor * cursor, bool sourceHDR, + bool nativeHDR, bool mapHDRtoSDR, bool hdrPQ, + float mapGain, float mapContentPeak) { - egl_cursorSetSDRWhiteLevel(cursor, sdrWhiteLevel); - atomic_store(&cursor->wireTransfer, !hdrActive ? 0 : (hdrPQ ? 2 : 1)); + // The cursor's white level is supplied independently by the pointer queue. + // Do not replace it with the frame-level fallback during every render. + atomic_store(&cursor->sourceTransfer, + !sourceHDR ? CURSOR_TRANSFER_SRGB : + (hdrPQ ? CURSOR_TRANSFER_PQ : CURSOR_TRANSFER_SCRGB)); + atomic_store(&cursor->mapHDRtoSDR, + sourceHDR && !nativeHDR && mapHDRtoSDR); + atomic_store(&cursor->mapHDRGain, mapGain); + atomic_store(&cursor->mapHDRContentPeak, mapContentPeak); } void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel) diff --git a/client/renderers/EGL/cursor.h b/client/renderers/EGL/cursor.h index 3d9b72a8..cf89fbe9 100644 --- a/client/renderers/EGL/cursor.h +++ b/client/renderers/EGL/cursor.h @@ -54,8 +54,10 @@ void egl_cursorSetState(EGL_Cursor * cursor, const bool visible, const float x, const float y, const float hx, const float hy); struct CursorState egl_cursorRender(EGL_Cursor * cursor, - LG_RendererRotate rotate, int width, int height); + LG_RendererRotate rotate, int width, int height, + bool linearComposition, bool * logicalDeferred); -void egl_cursorSetHDRState(EGL_Cursor * cursor, bool hdrActive, bool hdrPQ, - float sdrWhiteLevel); +void egl_cursorSetHDRState(EGL_Cursor * cursor, bool sourceHDR, + bool nativeHDR, bool mapHDRtoSDR, bool hdrPQ, + float mapGain, float mapContentPeak); void egl_cursorSetSDRWhiteLevel(EGL_Cursor * cursor, float sdrWhiteLevel); diff --git a/client/renderers/EGL/damage.c b/client/renderers/EGL/damage.c index e28e8a99..dcba36c5 100644 --- a/client/renderers/EGL/damage.c +++ b/client/renderers/EGL/damage.c @@ -51,6 +51,11 @@ struct EGL_Damage // uniforms EGL_Uniform * uTransform; + EGL_Uniform * uLinearOutput; + EGL_Uniform * uReferenceWhiteLevel; + + bool linearOutput; + float referenceWhiteLevel; }; void egl_damageConfigUI(EGL_Damage * damage) @@ -90,7 +95,13 @@ bool egl_damageInit(EGL_Damage ** damage) return false; } - (*damage)->uTransform = egl_shaderGetUniform((*damage)->shader, "transform"); + (*damage)->uTransform = + egl_shaderGetUniform((*damage)->shader, "transform"); + (*damage)->uLinearOutput = + egl_shaderGetUniform((*damage)->shader, "linearOutput"); + (*damage)->uReferenceWhiteLevel = + egl_shaderGetUniform((*damage)->shader, "referenceWhiteLevel"); + (*damage)->referenceWhiteLevel = 80.0f; return true; } @@ -130,6 +141,14 @@ void egl_damageResize(EGL_Damage * damage, float translateX, float translateY, update_matrix(damage); } +void egl_damageSetHDRState(EGL_Damage * damage, bool active, + float referenceWhiteLevel) +{ + damage->linearOutput = active; + damage->referenceWhiteLevel = referenceWhiteLevel > 0.0f ? + referenceWhiteLevel : 80.0f; +} + bool egl_damageRender(EGL_Damage * damage, LG_RendererRotate rotate, const struct DesktopDamage * data) { if (!damage->show) @@ -145,6 +164,9 @@ bool egl_damageRender(EGL_Damage * damage, LG_RendererRotate rotate, const struc egl_stateBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); egl_uniformMatrix3x2fv(damage->uTransform, 1, GL_FALSE, damage->transform); + egl_uniform1i(damage->uLinearOutput, damage->linearOutput); + egl_uniform1f(damage->uReferenceWhiteLevel, + damage->referenceWhiteLevel); egl_shaderUse(damage->shader); if (data && data->count != 0) diff --git a/client/renderers/EGL/damage.h b/client/renderers/EGL/damage.h index fb330aa1..f1215d6a 100644 --- a/client/renderers/EGL/damage.h +++ b/client/renderers/EGL/damage.h @@ -40,5 +40,7 @@ void egl_damageConfigUI(EGL_Damage * damage); void egl_damageSetup(EGL_Damage * damage, int width, int height); void egl_damageResize(EGL_Damage * damage, float translateX, float translateY, float scaleX, float scaleY); +void egl_damageSetHDRState(EGL_Damage * damage, bool active, + float referenceWhiteLevel); bool egl_damageRender(EGL_Damage * damage, LG_RendererRotate rotate, const struct DesktopDamage * data); diff --git a/client/renderers/EGL/desktop.c b/client/renderers/EGL/desktop.c index e2c9d4ae..6fddbffe 100644 --- a/client/renderers/EGL/desktop.c +++ b/client/renderers/EGL/desktop.c @@ -20,6 +20,7 @@ #include "desktop.h" #include "state.h" +#include "framebuffer.h" #include "common/debug.h" #include "common/option.h" #include "common/locking.h" @@ -52,7 +53,9 @@ struct DesktopShader EGL_Uniform * uIsHDR; EGL_Uniform * uMapHDRtoSDR; EGL_Uniform * uMapHDRGain; + EGL_Uniform * uMapHDRContentPeak; EGL_Uniform * uMapHDRPQ; + EGL_Uniform * uOutputHDRLinear; }; struct EGL_Desktop @@ -91,6 +94,7 @@ struct EGL_Desktop // map HDR content to SDR bool mapHDRtoSDR; bool nativeHDR; + bool linearComposition; int peakLuminance; int maxCLL; @@ -119,15 +123,28 @@ static bool egl_initDesktopShader( return false; } - shader->uDesktopSize = egl_shaderGetUniform(shader->shader, "desktopSize" ); - shader->uTransform = egl_shaderGetUniform(shader->shader, "transform" ); - shader->uScaleAlgo = egl_shaderGetUniform(shader->shader, "scaleAlgo" ); - shader->uNVGain = egl_shaderGetUniform(shader->shader, "nvGain" ); - shader->uCBMode = egl_shaderGetUniform(shader->shader, "cbMode" ); - shader->uIsHDR = egl_shaderGetUniform(shader->shader, "isHDR" ); - shader->uMapHDRtoSDR = egl_shaderGetUniform(shader->shader, "mapHDRtoSDR" ); - shader->uMapHDRGain = egl_shaderGetUniform(shader->shader, "mapHDRGain" ); - shader->uMapHDRPQ = egl_shaderGetUniform(shader->shader, "mapHDRPQ" ); + shader->uDesktopSize = + egl_shaderGetUniform(shader->shader, "desktopSize"); + shader->uTransform = + egl_shaderGetUniform(shader->shader, "transform"); + shader->uScaleAlgo = + egl_shaderGetUniform(shader->shader, "scaleAlgo"); + shader->uNVGain = + egl_shaderGetUniform(shader->shader, "nvGain"); + shader->uCBMode = + egl_shaderGetUniform(shader->shader, "cbMode"); + shader->uIsHDR = + egl_shaderGetUniform(shader->shader, "isHDR"); + shader->uMapHDRtoSDR = + egl_shaderGetUniform(shader->shader, "mapHDRtoSDR"); + shader->uMapHDRGain = + egl_shaderGetUniform(shader->shader, "mapHDRGain"); + shader->uMapHDRContentPeak = + egl_shaderGetUniform(shader->shader, "mapHDRContentPeak"); + shader->uMapHDRPQ = + egl_shaderGetUniform(shader->shader, "mapHDRPQ"); + shader->uOutputHDRLinear = + egl_shaderGetUniform(shader->shader, "outputHDRLinear"); return true; } @@ -426,7 +443,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, unsigned int outputHeight, const float x, const float y, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, LG_RendererRotate rotate, const struct DamageRects * rects, - bool * fullFrame) + bool * fullFrame, EGL_Framebuffer * target) { EGL_Texture * tex; int width, height; @@ -490,8 +507,13 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, finalSizeY = height; } - egl_stateBindFramebuffer(0); - egl_resetViewport(desktop->egl); + if (target) + egl_framebufferBind(target); + else + { + egl_stateBindFramebuffer(0); + egl_resetViewport(desktop->egl); + } egl_textureBind(texture); @@ -522,26 +544,47 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, desktop->useDMA && texture == desktop->texture ? &desktop->dmaShader : &desktop->shader; - const float mapHDRGain = + /* PQ is normalized to 10000 nits, while scRGB defines 1.0 as 80 nits. + * Convert either encoding into values relative to the selected SDR peak. */ + const float mapHDRGain = (desktop->hdrPQ ? 10000.0f : 80.0f) / + desktop->peakLuminance; + const float mapHDRContentPeak = (float)desktop->maxCLL / desktop->peakLuminance; - egl_uniform1i (shader->uScaleAlgo , scaleAlgo); - egl_uniform2f (shader->uDesktopSize, width, height); - egl_uniformMatrix3x2fv(shader->uTransform , 1, GL_FALSE, desktop->matrix); - egl_uniform1f (shader->uNVGain , desktop->nvGain); - egl_uniform1i (shader->uCBMode , desktop->cbMode); - egl_uniform1i (shader->uIsHDR , hdr); - egl_uniform1i (shader->uMapHDRtoSDR, desktop->mapHDRtoSDR && !desktop->nativeHDR); - egl_uniform1f (shader->uMapHDRGain , mapHDRGain); - egl_uniform1i (shader->uMapHDRPQ , desktop->hdrPQ); + egl_uniform1i (shader->uScaleAlgo , scaleAlgo); + egl_uniform2f (shader->uDesktopSize , width, height); + egl_uniformMatrix3x2fv(shader->uTransform , + 1, GL_FALSE, desktop->matrix); + egl_uniform1f (shader->uNVGain , desktop->nvGain); + egl_uniform1i (shader->uCBMode , desktop->cbMode); + egl_uniform1i (shader->uIsHDR , hdr); + egl_uniform1i (shader->uMapHDRtoSDR , + desktop->mapHDRtoSDR && !desktop->nativeHDR); + egl_uniform1f (shader->uMapHDRGain , mapHDRGain); + egl_uniform1f (shader->uMapHDRContentPeak, mapHDRContentPeak); + egl_uniform1i (shader->uMapHDRPQ , desktop->hdrPQ); + egl_uniform1i (shader->uOutputHDRLinear , + desktop->linearComposition); egl_shaderUse(shader->shader); egl_desktopRectsRender(desktop->mesh); return true; } -void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR) +void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR, + bool linearComposition) { - desktop->nativeHDR = nativeHDR; + desktop->nativeHDR = nativeHDR; + desktop->linearComposition = nativeHDR && linearComposition; +} + +void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled, + float * gain, float * contentPeak) +{ + *enabled = desktop->hdr && !desktop->useSpice && + desktop->mapHDRtoSDR && !desktop->nativeHDR; + *gain = (desktop->hdrPQ ? 10000.0f : 80.0f) / + desktop->peakLuminance; + *contentPeak = (float)desktop->maxCLL / desktop->peakLuminance; } void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height) diff --git a/client/renderers/EGL/desktop.h b/client/renderers/EGL/desktop.h index a3011d34..1d3596ec 100644 --- a/client/renderers/EGL/desktop.h +++ b/client/renderers/EGL/desktop.h @@ -25,7 +25,8 @@ #include "egl.h" #include "desktop_rects.h" -typedef struct EGL_Desktop EGL_Desktop; +typedef struct EGL_Desktop EGL_Desktop; +typedef struct EGL_Framebuffer EGL_Framebuffer; enum EGL_DesktopScaleType { @@ -42,7 +43,10 @@ bool egl_desktopInit(EGL * egl, EGL_Desktop ** desktop, EGLDisplay * display, void egl_desktopFree(EGL_Desktop ** desktop); void egl_desktopConfigUI(EGL_Desktop * desktop); -void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR); +void egl_desktopSetNativeHDR(EGL_Desktop * desktop, bool nativeHDR, + bool linearComposition); +void egl_desktopGetHDRMapping(EGL_Desktop * desktop, bool * enabled, + float * gain, float * contentPeak); bool egl_desktopSetup (EGL_Desktop * desktop, const LG_RendererFormat format); bool egl_desktopUpdate(EGL_Desktop * desktop, const FrameBuffer * frame, int dmaFd, const FrameDamageRect * damageRects, int damageRectsCount); @@ -51,7 +55,7 @@ bool egl_desktopRender(EGL_Desktop * desktop, unsigned int outputWidth, unsigned int outputHeight, const float x, const float y, const float scaleX, const float scaleY, enum EGL_DesktopScaleType scaleType, LG_RendererRotate rotate, const struct DamageRects * rects, - bool * fullFrame); + bool * fullFrame, EGL_Framebuffer * target); void egl_desktopSpiceConfigure(EGL_Desktop * desktop, int width, int height); void egl_desktopSpiceDrawFill(EGL_Desktop * desktop, int x, int y, int width, diff --git a/client/renderers/EGL/egl.c b/client/renderers/EGL/egl.c index 6f9c253c..e27aca94 100644 --- a/client/renderers/EGL/egl.c +++ b/client/renderers/EGL/egl.c @@ -50,6 +50,7 @@ #include "desktop.h" #include "cursor.h" #include "hdr_overlay.h" +#include "hdr_compose.h" #include "postprocess.h" #include "util.h" @@ -83,6 +84,7 @@ struct Inst EGL_Cursor * cursor; // the mouse cursor EGL_Damage * damage; // the damage display EGL_HDROverlay * hdrOverlay; + EGL_HDRCompose * hdrCompose; bool imgui; // if imgui was initialized LG_RendererFormat format; @@ -132,6 +134,9 @@ struct Inst bool surfaceSupportsSCRGB; bool hdr; // true if current frame format is HDR bool nativeHDR; // true only while the surface HDR description is active + bool nativeHDRPQ; + float nativeReferenceWhiteLevel; + bool linearHDRComposition; }; static struct Option egl_options[] = @@ -317,6 +322,7 @@ static void egl_deinitialize(LG_Renderer * renderer) egl_cursorFree (&this->cursor); egl_damageFree (&this->damage); egl_hdrOverlayFree(&this->hdrOverlay); + egl_hdrComposeFree(&this->hdrCompose); LG_LOCK_FREE(this->lock); LG_LOCK_FREE(this->desktopDamageLock); @@ -344,7 +350,8 @@ static bool egl_supports(LG_Renderer * renderer, LG_RendererSupport flag) return this->dmaSupport; case LG_SUPPORTS_HDR_PQ: - return this->surfaceSupportsPQ; + return this->surfaceSupportsPQ && + egl_hdrComposeIsConfigured(this->hdrCompose); case LG_SUPPORTS_HDR_SCRGB: return this->surfaceSupportsSCRGB; @@ -557,6 +564,9 @@ static void egl_onResize(LG_Renderer * renderer, const int width, const int heig if (!egl_hdrOverlayResize(this->hdrOverlay, this->width, this->height)) DEBUG_ERROR("Failed to resize the HDR overlay framebuffer"); + if (this->hdrCompose && + !egl_hdrComposeResize(this->hdrCompose, this->width, this->height)) + DEBUG_FATAL("Failed to resize the linear HDR composition framebuffer"); // this is needed to refresh the font atlas texture ImGui_ImplOpenGL3_Shutdown(); @@ -619,18 +629,12 @@ static bool egl_updateHDRState(struct Inst * this, bool force) bool nativeHDR = false; app_getProp(LG_DS_NATIVE_HDR, &nativeHDR); const bool surfaceCompatible = this->format.hdrPQ ? - this->surfaceSupportsPQ : this->surfaceSupportsSCRGB; + (this->surfaceSupportsPQ && + egl_hdrComposeIsConfigured(this->hdrCompose)) : + this->surfaceSupportsSCRGB; const bool useNativeHDR = !this->showSpice && this->format.hdr && surfaceCompatible && nativeHDR && !app_getHDRDescFailed(); - if (!force && this->nativeHDR == useNativeHDR) - return false; - - this->nativeHDR = useNativeHDR; - egl_desktopSetNativeHDR(this->desktop, useNativeHDR); - egl_cursorSetHDRState(this->cursor, useNativeHDR, this->format.hdrPQ, - this->format.sdrWhiteLevel); - LG_DSHDRWhiteLevels whiteLevels = { .pq = 203, @@ -638,9 +642,29 @@ static bool egl_updateHDRState(struct Inst * this, bool force) }; if (useNativeHDR && !app_getProp(LG_DS_HDR_WHITE_LEVELS, &whiteLevels)) DEBUG_WARN("Display server did not provide native HDR white levels"); + const float referenceWhiteLevel = this->format.hdrPQ ? + whiteLevels.pq : whiteLevels.scRGB; + const bool stateChanged = this->nativeHDR != useNativeHDR || + (useNativeHDR && + (this->nativeHDRPQ != this->format.hdrPQ || + this->nativeReferenceWhiteLevel != referenceWhiteLevel)); + + if (!force && !stateChanged) + return false; + + this->nativeHDR = useNativeHDR; + this->nativeHDRPQ = this->format.hdrPQ; + this->nativeReferenceWhiteLevel = referenceWhiteLevel; + this->linearHDRComposition = useNativeHDR && this->format.hdrPQ; + egl_hdrComposeSetActive(this->hdrCompose, + this->linearHDRComposition); + egl_desktopSetNativeHDR(this->desktop, useNativeHDR, + this->linearHDRComposition); egl_hdrOverlaySetState(this->hdrOverlay, useNativeHDR, - this->format.hdrPQ, - this->format.hdrPQ ? whiteLevels.pq : whiteLevels.scRGB); + this->format.hdrPQ && !this->linearHDRComposition, + referenceWhiteLevel); + egl_damageSetHDRState(this->damage, useNativeHDR, + referenceWhiteLevel); return true; } @@ -688,7 +712,9 @@ static bool egl_onFrameFormat(LG_Renderer * renderer, const LG_RendererFormat fo bool nativeHDR = false; app_getProp(LG_DS_NATIVE_HDR, &nativeHDR); const bool surfaceCompatible = format.hdrPQ ? - this->surfaceSupportsPQ : this->surfaceSupportsSCRGB; + (this->surfaceSupportsPQ && + egl_hdrComposeIsConfigured(this->hdrCompose)) : + this->surfaceSupportsSCRGB; if (!surfaceCompatible) DEBUG_WARN("HDR frames being displayed on an 8-bit EGL surface " "or an EGL surface incompatible with the source encoding"); @@ -1049,6 +1075,15 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA) return false; } + if (this->surfaceSupportsPQ && + !util_hasGLExt(gl_exts, "GL_EXT_color_buffer_half_float") && + !util_hasGLExt(gl_exts, "GL_EXT_color_buffer_float")) + { + DEBUG_WARN("Half-float render targets are unavailable; native PQ " + "composition disabled"); + this->surfaceSupportsPQ = false; + } + this->hasBufferAge = util_hasGLExt(client_exts, "EGL_EXT_buffer_age"); if (!this->hasBufferAge) DEBUG_WARN("GL_EXT_buffer_age is not supported, will not perform as well."); @@ -1130,6 +1165,14 @@ static bool egl_renderStartup(LG_Renderer * renderer, bool useDMA) return false; } + if (this->surfaceSupportsPQ && + !egl_hdrComposeInit(&this->hdrCompose)) + { + DEBUG_WARN("Native PQ output disabled: failed to initialize linear " + "HDR composition"); + this->surfaceSupportsPQ = false; + } + if (!ImGui_ImplOpenGL3_Init("#version 300 es")) { DEBUG_ERROR("Failed to initialize ImGui"); @@ -1203,6 +1246,36 @@ static int egl_clipSurfaceDamage( return out; } +static int egl_mergeSurfaceDamage(struct Rect * damage, int count) +{ + if (count <= 1) + return count; + + FrameDamageRect rects[count]; + for (int i = 0; i < count; ++i) + { + rects[i] = (FrameDamageRect) { + .x = damage[i].x, + .y = damage[i].y, + .width = damage[i].w, + .height = damage[i].h, + }; + } + + count = rectsMergeOverlapping(rects, count); + for (int i = 0; i < count; ++i) + { + damage[i] = (struct Rect) { + .x = rects[i].x, + .y = rects[i].y, + .w = rects[i].width, + .h = rects[i].height, + }; + } + + return count; +} + inline static void renderLetterBox(struct Inst * this) { bool hLB = this->destRect.x > 0; @@ -1248,12 +1321,20 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, { struct Inst * this = UPCAST(struct Inst, renderer); egl_stateCheckShared(); - EGLint bufferAge = egl_bufferAge(this); + EGLint bufferAge = egl_bufferAge(this); const bool hdrStateChanged = egl_updateHDRState(this, false); - bool renderAll = hdrStateChanged || - invalidateWindow || this->hadOverlay || - bufferAge <= 0 || bufferAge > MAX_BUFFER_AGE || - this->showSpice; + bool mapCursorHDR; + float mapCursorGain; + float mapCursorContentPeak; + egl_desktopGetHDRMapping(this->desktop, &mapCursorHDR, + &mapCursorGain, &mapCursorContentPeak); + egl_cursorSetHDRState(this->cursor, + this->format.hdr && !this->showSpice, this->nativeHDR, mapCursorHDR, + this->format.hdrPQ, mapCursorGain, mapCursorContentPeak); + bool renderAll = hdrStateChanged || + invalidateWindow || this->hadOverlay || + bufferAge <= 0 || bufferAge > MAX_BUFFER_AGE || + this->showSpice; bool hasOverlay = false; struct CursorState cursorState = { .visible = false }; @@ -1328,7 +1409,9 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, } ++this->overlayHistoryIdx; - bool fullFrame = false; + bool fullFrame = false; + const bool linearHDRComposition = egl_hdrComposeBegin(this->hdrCompose); + bool deferredLogicalCursor = false; if (likely(this->destRect.w > 0 && this->destRect.h > 0)) { if (egl_desktopRender(this->desktop, @@ -1336,11 +1419,12 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, this->translateX, this->translateY, this->scaleX , this->scaleY , this->scaleType , rotate, renderAll ? NULL : accumulated, - &fullFrame)) + &fullFrame, egl_hdrComposeGetFramebuffer(this->hdrCompose))) { cursorState = egl_cursorRender(this->cursor, (this->format.rotate + rotate) % LG_ROTATE_MAX, - this->width, this->height); + this->width, this->height, + linearHDRComposition, &deferredLogicalCursor); } else hasOverlay = true; @@ -1364,13 +1448,18 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, if (damageIdx == -1) hasOverlay = true; - const bool hdrOverlay = egl_hdrOverlayBegin(this->hdrOverlay, - damage, damageIdx); + const bool hdrOverlay = egl_hdrOverlayBegin( + this->hdrOverlay, damage, damageIdx); + const bool drawOverlay = !this->nativeHDR || hdrOverlay; ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplOpenGL3_RenderDrawData(igGetDrawData()); - egl_stateInvalidate(); + if (drawOverlay) + { + ImGui_ImplOpenGL3_RenderDrawData(igGetDrawData()); + egl_stateInvalidate(); + } if (hdrOverlay) - egl_hdrOverlayEnd(this->hdrOverlay, damage, damageIdx); + egl_hdrOverlayEnd(this->hdrOverlay, damage, damageIdx, + egl_hdrComposeGetFramebuffer(this->hdrCompose)); for (int i = 0; i < damageIdx; ++i) damage[i].y = this->height - damage[i].y - damage[i].h; @@ -1422,6 +1511,37 @@ static bool egl_render(LG_Renderer * renderer, LG_RendererRotate rotate, damageIdx = egl_clipSurfaceDamage( damage, damageIdx, this->width, this->height); + struct Rect * encodeDamage = damage; + int encodeDamageCount = + linearHDRComposition && renderAll ? 0 : damageIdx; + if (linearHDRComposition && encodeDamageCount > 0) + { + const int capacity = damageIdx + accumulated->count; + encodeDamage = alloca(capacity * sizeof(*encodeDamage)); + memcpy(encodeDamage, damage, damageIdx * sizeof(*encodeDamage)); + encodeDamageCount = damageIdx; + + double matrix[6]; + egl_desktopToScreenMatrix(matrix, + this->format.frameWidth, this->format.frameHeight, + this->translateX, this->translateY, this->scaleX, this->scaleY, rotate, + this->width, this->height); + for (int i = 0; i < accumulated->count; ++i) + encodeDamage[encodeDamageCount++] = + egl_desktopToScreen(matrix, accumulated->rects + i); + + encodeDamageCount = egl_clipSurfaceDamage( + encodeDamage, encodeDamageCount, this->width, this->height); + encodeDamageCount = egl_mergeSurfaceDamage( + encodeDamage, encodeDamageCount); + } + + egl_hdrComposeEnd(this->hdrCompose, encodeDamage, encodeDamageCount); + if (deferredLogicalCursor && cursorState.visible) + egl_cursorRender(this->cursor, + (this->format.rotate + rotate) % LG_ROTATE_MAX, + this->width, this->height, false, NULL); + this->hadOverlay = hasOverlay; this->cursorLast = cursorState; diff --git a/client/renderers/EGL/hdr_compose.c b/client/renderers/EGL/hdr_compose.c new file mode 100644 index 00000000..46667483 --- /dev/null +++ b/client/renderers/EGL/hdr_compose.c @@ -0,0 +1,176 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "hdr_compose.h" + +#include "framebuffer.h" +#include "model.h" +#include "shader.h" +#include "state.h" + +#include "common/debug.h" + +#include + +#include "hdr_compose.vert.h" +#include "hdr_compose.frag.h" + +struct EGL_HDRCompose +{ + EGL_Framebuffer * framebuffer; + EGL_Shader * shader; + EGL_Model * model; + unsigned int width; + unsigned int height; + bool configured; + bool active; +}; + +bool egl_hdrComposeInit(EGL_HDRCompose ** compose) +{ + *compose = calloc(1, sizeof(**compose)); + if (!*compose) + return false; + + EGL_HDRCompose * this = *compose; + if (!egl_framebufferInit(&this->framebuffer) || + !egl_shaderInit(&this->shader) || + !egl_shaderCompile(this->shader, + b_shader_hdr_compose_vert, b_shader_hdr_compose_vert_size, + b_shader_hdr_compose_frag, b_shader_hdr_compose_frag_size, + false, NULL) || + !egl_modelInit(&this->model)) + { + DEBUG_ERROR("Failed to initialize linear HDR composition"); + egl_hdrComposeFree(compose); + return false; + } + + egl_modelSetDefault(this->model, false); + egl_modelSetShader(this->model, this->shader); + egl_modelSetTexture(this->model, + egl_framebufferGetTexture(this->framebuffer)); + + /* Prove that an FP16 render target can actually be created before native + * PQ support is advertised. The real size is installed on the first + * resize. */ + if (!egl_framebufferSetup(this->framebuffer, EGL_PF_RGBA16F, 1, 1)) + { + DEBUG_ERROR("Failed to create the linear HDR composition target"); + egl_hdrComposeFree(compose); + return false; + } + this->width = 1; + this->height = 1; + this->configured = true; + + EGL_Texture * texture = egl_framebufferGetTexture(this->framebuffer); + glSamplerParameteri(texture->sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glSamplerParameteri(texture->sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + return true; +} + +void egl_hdrComposeFree(EGL_HDRCompose ** compose) +{ + if (!*compose) + return; + + EGL_HDRCompose * this = *compose; + egl_modelFree(&this->model); + egl_shaderFree(&this->shader); + egl_framebufferFree(&this->framebuffer); + free(this); + *compose = NULL; +} + +bool egl_hdrComposeResize(EGL_HDRCompose * this, + unsigned int width, unsigned int height) +{ + if (!this) + return false; + + const bool wasActive = this->active; + this->width = width; + this->height = height; + this->configured = false; + this->active = false; + + if (!width || !height) + return true; + + this->configured = egl_framebufferSetup(this->framebuffer, + EGL_PF_RGBA16F, width, height); + this->active = wasActive && this->configured; + return this->configured; +} + +void egl_hdrComposeSetActive(EGL_HDRCompose * this, bool active) +{ + if (!this) + return; + + this->active = active && this->configured; +} + +bool egl_hdrComposeIsConfigured(EGL_HDRCompose * this) +{ + return this && this->configured; +} + +bool egl_hdrComposeBegin(EGL_HDRCompose * this) +{ + if (!this || !this->active) + return false; + + egl_framebufferBind(this->framebuffer); + return true; +} + +void egl_hdrComposeEnd(EGL_HDRCompose * this, + const struct Rect * damage, int damageCount) +{ + if (!this || !this->active) + return; + + egl_stateBindFramebuffer(0); + egl_stateViewport(0, 0, this->width, this->height); + egl_stateBlend(false); + + if (damageCount <= 0) + { + egl_stateScissor(false); + egl_modelRender(this->model); + return; + } + + egl_stateScissor(true); + for (int i = 0; i < damageCount; ++i) + { + glScissor(damage[i].x, damage[i].y, + damage[i].w, damage[i].h); + egl_modelRender(this->model); + } + egl_stateScissor(false); +} + +EGL_Framebuffer * egl_hdrComposeGetFramebuffer(EGL_HDRCompose * this) +{ + return this && this->active ? this->framebuffer : NULL; +} diff --git a/client/renderers/EGL/hdr_compose.h b/client/renderers/EGL/hdr_compose.h new file mode 100644 index 00000000..9fe60e10 --- /dev/null +++ b/client/renderers/EGL/hdr_compose.h @@ -0,0 +1,39 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "common/types.h" + +#include + +typedef struct EGL_Framebuffer EGL_Framebuffer; +typedef struct EGL_HDRCompose EGL_HDRCompose; + +bool egl_hdrComposeInit(EGL_HDRCompose ** compose); +void egl_hdrComposeFree(EGL_HDRCompose ** compose); +bool egl_hdrComposeResize(EGL_HDRCompose * compose, + unsigned int width, unsigned int height); +void egl_hdrComposeSetActive(EGL_HDRCompose * compose, bool active); +bool egl_hdrComposeIsConfigured(EGL_HDRCompose * compose); +bool egl_hdrComposeBegin(EGL_HDRCompose * compose); +void egl_hdrComposeEnd(EGL_HDRCompose * compose, + const struct Rect * damage, int damageCount); +EGL_Framebuffer * egl_hdrComposeGetFramebuffer(EGL_HDRCompose * compose); diff --git a/client/renderers/EGL/hdr_overlay.c b/client/renderers/EGL/hdr_overlay.c index 7686873f..bfbcb7d0 100644 --- a/client/renderers/EGL/hdr_overlay.c +++ b/client/renderers/EGL/hdr_overlay.c @@ -202,10 +202,16 @@ bool egl_hdrOverlayBegin(EGL_HDROverlay * this, } void egl_hdrOverlayEnd(EGL_HDROverlay * this, - const struct Rect * damage, int damageCount) + const struct Rect * damage, int damageCount, + EGL_Framebuffer * target) { - egl_stateBindFramebuffer(0); - egl_stateViewport(0, 0, this->width, this->height); + if (target) + egl_framebufferBind(target); + else + { + egl_stateBindFramebuffer(0); + egl_stateViewport(0, 0, this->width, this->height); + } egl_uniform1i(this->uPQ, this->renderPQ); egl_uniform1f(this->uReferenceWhiteLevel, diff --git a/client/renderers/EGL/hdr_overlay.h b/client/renderers/EGL/hdr_overlay.h index db9a7ab4..264750e5 100644 --- a/client/renderers/EGL/hdr_overlay.h +++ b/client/renderers/EGL/hdr_overlay.h @@ -24,7 +24,8 @@ #include -typedef struct EGL_HDROverlay EGL_HDROverlay; +typedef struct EGL_HDROverlay EGL_HDROverlay; +typedef struct EGL_Framebuffer EGL_Framebuffer; bool egl_hdrOverlayInit(EGL_HDROverlay ** overlay); void egl_hdrOverlayFree(EGL_HDROverlay ** overlay); @@ -42,4 +43,5 @@ void egl_hdrOverlaySetState(EGL_HDROverlay * overlay, bool active, bool pq, bool egl_hdrOverlayBegin(EGL_HDROverlay * overlay, const struct Rect * damage, int damageCount); void egl_hdrOverlayEnd(EGL_HDROverlay * overlay, - const struct Rect * damage, int damageCount); + const struct Rect * damage, int damageCount, + EGL_Framebuffer * target); diff --git a/client/renderers/EGL/shader/cursor_mono.frag b/client/renderers/EGL/shader/cursor_mono.frag index 664d0230..ce48c2d5 100644 --- a/client/renderers/EGL/shader/cursor_mono.frag +++ b/client/renderers/EGL/shader/cursor_mono.frag @@ -2,15 +2,11 @@ precision highp float; precision highp int; -#include "hdr.h" - in vec2 uv; out vec4 color; uniform sampler2D sampler1; uniform float scale; -uniform int wireTransfer; // 0: sRGB, 1: scRGB, 2: PQ -uniform float sdrWhiteLevel; void main() { @@ -30,13 +26,7 @@ void main() if (tmp.rgb == vec3(0.0, 0.0, 0.0)) discard; - 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); - + // Logical cursor masks operate on the encoded destination values. Their + // binary AND/XOR operands must not be colour converted. color = tmp; } diff --git a/client/renderers/EGL/shader/cursor_rgb.frag b/client/renderers/EGL/shader/cursor_rgb.frag index e6496d43..2bb25f48 100644 --- a/client/renderers/EGL/shader/cursor_rgb.frag +++ b/client/renderers/EGL/shader/cursor_rgb.frag @@ -12,11 +12,15 @@ uniform sampler2D sampler1; uniform sampler2D sampler2; uniform float scale; uniform int cbMode; -uniform int wireTransfer; // 0: sRGB, 1: scRGB, 2: PQ +uniform int sourceTransfer; uniform float sdrWhiteLevel; +uniform bool mapHDRtoSDR; +uniform float mapHDRGain; +uniform float mapHDRContentPeak; uniform uint colorTransformFlags; uniform vec4 colorMatrix[3]; uniform float colorTransformScalar; +uniform bool linearComposition; const uint COLOR_TRANSFORM_MATRIX = 1u; const uint COLOR_TRANSFORM_LUT = 2u; @@ -47,10 +51,10 @@ vec3 xyzToBT2020(vec3 xyz) vec3 applyColorLUT(vec3 value) { - vec3 pos = clamp(value, 0.0, 1.0) * 4095.0; - ivec3 lo = ivec3(floor(pos)); - ivec3 hi = min(lo + ivec3(1), ivec3(4095)); - vec3 f = fract(pos); + vec3 pos = clamp(value, 0.0, 1.0) * 4095.0; + ivec3 lo = ivec3(floor(pos)); + ivec3 hi = min(lo + ivec3(1), ivec3(4095)); + vec3 f = fract(pos); return vec3( mix(texelFetch(sampler2, ivec2(lo.r, 0), 0).r, texelFetch(sampler2, ivec2(hi.r, 0), 0).r, f.r), @@ -74,10 +78,9 @@ void main() else color = texture(sampler1, uv); - if (cbMode > 0) - color = cbTransform(color, cbMode); - - if (color.a > 0.0 && (wireTransfer != 0 || colorTransformFlags != 0u)) + if (color.a > 0.0 && + (sourceTransfer != TRANSFER_SRGB || + colorTransformFlags != 0u || cbMode > 0)) { // Cursor pixels are premultiplied sRGB. Work on straight, linear BT.709 // values through the XYZ calibration stage, encode for the active wire @@ -90,20 +93,36 @@ void main() dot(vec4(xyz, 1.0), colorMatrix[0]), dot(vec4(xyz, 1.0), colorMatrix[1]), dot(vec4(xyz, 1.0), colorMatrix[2])) * colorTransformScalar; - value = wireTransfer == 2 ? xyzToBT2020(xyz) : xyzToBT709(xyz); + value = sourceTransfer == TRANSFER_PQ ? + xyzToBT2020(xyz) : xyzToBT709(xyz); } - else if (wireTransfer == 2) + else if (sourceTransfer == TRANSFER_PQ) value = bt709to2020(value); - if (wireTransfer == 2) + if (sourceTransfer == TRANSFER_PQ) value = lin2pq(max(value, 0.0) * (sdrWhiteLevel / 10000.0)); - else if (wireTransfer == 1) + else if (sourceTransfer == TRANSFER_SCRGB) value *= sdrWhiteLevel / 80.0; else value = lin2srgb(max(value, 0.0)); if ((colorTransformFlags & COLOR_TRANSFORM_LUT) != 0u) value = applyColorLUT(value); + + if (mapHDRtoSDR) + value = mapToSDR(value, mapHDRGain, + mapHDRContentPeak, sourceTransfer == TRANSFER_PQ); + // Native PQ surfaces are composed through a linear scRGB framebuffer. + // Preserve the guest's transfer-domain LUT by decoding it only after the + // complete guest transform has been applied. + else if (linearComposition && sourceTransfer == TRANSFER_PQ) + value = bt2020to709(pq2lin(value, 1.0)) * 125.0; + + // Apply SDR-only accessibility transforms after HDR-to-SDR mapping and + // to straight colour so translucent cursor edges remain premultiplied + // correctly. + if (cbMode > 0) + value = cbTransform(vec4(value, 1.0), cbMode).rgb; color.rgb = value * color.a; } else if (color.a == 0.0) diff --git a/client/renderers/EGL/shader/damage.frag b/client/renderers/EGL/shader/damage.frag index 80de1c40..bb0f6c83 100644 --- a/client/renderers/EGL/shader/damage.frag +++ b/client/renderers/EGL/shader/damage.frag @@ -2,9 +2,17 @@ precision highp float; precision highp int; +#include "hdr.h" + out vec4 color; +uniform bool linearOutput; +uniform float referenceWhiteLevel; + void main() { - color = vec4(1.0, 1.0, 0.0, 0.5); + vec3 value = vec3(1.0, 1.0, 0.0); + if (linearOutput) + value = srgb2lin(value) * (referenceWhiteLevel / 80.0); + color = vec4(value, 0.5); } diff --git a/client/renderers/EGL/shader/desktop_rgb.frag b/client/renderers/EGL/shader/desktop_rgb.frag index 3a90867d..d4a0c543 100644 --- a/client/renderers/EGL/shader/desktop_rgb.frag +++ b/client/renderers/EGL/shader/desktop_rgb.frag @@ -24,15 +24,17 @@ uniform int cbMode; uniform bool isHDR; uniform bool mapHDRtoSDR; uniform float mapHDRGain; +uniform float mapHDRContentPeak; uniform bool mapHDRPQ; +uniform bool outputHDRLinear; -vec4 samplePQLinear(vec2 coord) +vec3 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); + 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); @@ -44,11 +46,12 @@ vec4 samplePQLinear(vec2 coord) 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); + return max(linear, 0.0); } void main() { + bool sampledPQLinear = false; switch (scaleAlgo) { case EGL_SCALE_NEAREST: @@ -60,13 +63,28 @@ void main() case EGL_SCALE_LINEAR: { - color = isHDR && mapHDRPQ ? samplePQLinear(uv) : texture(sampler1, uv); + if (isHDR && mapHDRPQ) + { + vec3 linear = samplePQLinear(uv); + color = vec4( + outputHDRLinear ? linear : lin2pq(linear), 1.0); + sampledPQLinear = outputHDRLinear; + } + else + color = texture(sampler1, uv); break; } } if (isHDR && mapHDRtoSDR) - color.rgb = mapToSDR(color.rgb, mapHDRGain, mapHDRPQ); + color.rgb = mapToSDR(color.rgb, mapHDRGain, + mapHDRContentPeak, mapHDRPQ); + else if (isHDR && outputHDRLinear && mapHDRPQ) + { + vec3 linear2020 = sampledPQLinear ? + color.rgb : pq2lin(color.rgb, 1.0); + color.rgb = bt2020to709(linear2020) * 125.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 diff --git a/client/renderers/EGL/shader/hdr.h b/client/renderers/EGL/shader/hdr.h index 6f2aac37..ebe24c76 100644 --- a/client/renderers/EGL/shader/hdr.h +++ b/client/renderers/EGL/shader/hdr.h @@ -29,47 +29,24 @@ const float c1 = 3424.0 / 4096.0; const float c2 = 2413.0 / 128.0; const float c3 = 2392.0 / 128.0; -float minGain(vec3 pixel) { return min(pixel.r, min(pixel.g, pixel.b)); } -float maxGain(vec3 pixel) { return max(pixel.r, max(pixel.g, pixel.b)); } -float midGain(vec3 pixel) -{ - return pixel.r < pixel.g ? - (pixel.r < pixel.b ? - min(pixel.g, pixel.b) : // min = r - min(pixel.r, pixel.g)) : // min = b - (pixel.g < pixel.b ? - min(pixel.r, pixel.b) : // min = g - min(pixel.r, pixel.g)); // min = b -} +const int TRANSFER_SRGB = 0; +const int TRANSFER_SCRGB = 1; +const int TRANSFER_PQ = 2; vec3 compress(vec3 pixel) { - float maxGain = maxGain(pixel); - return pixel * (maxGain < knee ? maxGain : - knee + max(maxGain - knee, 0.0) * compressor) / maxGain; -} - -vec3 fixClip(vec3 pixel) -{ - // keep the (mid - min) / (max - min) ratio - float preMin = minGain(pixel); - float preMid = midGain(pixel); - float preMax = maxGain(pixel); - vec3 clip = clamp(pixel, 0.0, 1.0); - float postMin = minGain(clip); - float postMid = midGain(clip); - float postMax = maxGain(clip); - float ratio = (preMid - preMin) / (preMax - preMin); - float newMid = ratio * (postMax - postMin) + postMin; - return vec3(clip.r != postMid ? clip.r : newMid, - clip.g != postMid ? clip.g : newMid, - clip.b != postMid ? clip.b : newMid); + pixel = max(pixel, 0.0); + float peak = max(pixel.r, max(pixel.g, pixel.b)); + if (peak <= 0.0) + return vec3(0.0); + return pixel * (peak < knee ? peak : + knee + (peak - knee) * compressor) / peak; } // Returns luminance in nits vec3 pq2lin(vec3 pq, float gain) { - vec3 p = pow(pq, vec3(m2inv)); + vec3 p = pow(max(pq, 0.0), vec3(m2inv)); vec3 d = max(p - c1, vec3(0.0)) / (c2 - c3 * p); return pow(d, vec3(m1inv)) * gain; } @@ -77,25 +54,25 @@ vec3 pq2lin(vec3 pq, float gain) vec3 lin2pq(vec3 linear) { // ST.2084 uses absolute luminance normalized to 10000 cd/m². - vec3 p = pow(linear, vec3(m1)); + vec3 p = pow(max(linear, 0.0), vec3(m1)); return pow((c1 + c2 * p) / (1.0 + c3 * p), vec3(m2)); } vec3 srgb2lin(vec3 c) { - vec3 v = c / 12.92; - vec3 v2 = pow((c + vec3(0.055)) / 1.055, vec3(2.4)); + vec3 v = c / 12.92; + vec3 v2 = pow((c + vec3(0.055)) / 1.055, vec3(2.4)); vec3 threshold = vec3(0.04045); - vec3 result = mix(v, v2, greaterThanEqual(c, threshold)); + vec3 result = mix(v, v2, greaterThanEqual(c, threshold)); return result; } vec3 lin2srgb(vec3 c) { - vec3 v = c * 12.92; - vec3 v2 = pow(c, vec3(1.0/2.4)) * 1.055 - 0.055; + vec3 v = c * 12.92; + vec3 v2 = pow(max(c, 0.0), vec3(1.0/2.4)) * 1.055 - 0.055; vec3 threshold = vec3(0.0031308); - vec3 result = mix(v, v2, greaterThanEqual(c, threshold)); + vec3 result = mix(v, v2, greaterThanEqual(c, threshold)); return result; } @@ -103,29 +80,42 @@ vec3 lin2srgb(vec3 c) vec3 bt2020to709(vec3 bt2020) { return vec3( - bt2020.r * 1.6605 + bt2020.g * -0.5876 + bt2020.b * -0.0728, - bt2020.r * -0.1246 + bt2020.g * 1.1329 + bt2020.b * -0.0083, - bt2020.r * -0.0182 + bt2020.g * -0.1006 + bt2020.b * 1.1187); + bt2020.r * 1.6604910 + bt2020.g * -0.5876411 + bt2020.b * -0.0728499, + bt2020.r * -0.1245505 + bt2020.g * 1.1328999 + bt2020.b * -0.0083494, + bt2020.r * -0.0181508 + bt2020.g * -0.1005789 + bt2020.b * 1.1187297); } // 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); + bt709.r * 0.6274039 + bt709.g * 0.3292830 + bt709.b * 0.0433131, + bt709.r * 0.0690973 + bt709.g * 0.9195404 + bt709.b * 0.0113623, + bt709.r * 0.0163914 + bt709.g * 0.0880133 + bt709.b * 0.8955953); } -vec3 mapToSDR(vec3 color, float gain, bool pq) +vec3 mapToSDR(vec3 color, float gain, float contentPeak, bool pq) { if (pq) { - // HDR10: PQ-encoded BT.2020. Linearise then convert the gamut to BT.709. + // HDR10: PQ-encoded BT.2020. Decode the absolute 10000-nit PQ range into + // values relative to the configured SDR display peak. MaxCLL constrains + // content luminance without changing that absolute unit conversion. color = pq2lin(color.rgb, gain); + float luminance2020 = + dot(color, vec3(0.2627002, 0.6779981, 0.0593017)); + if (contentPeak > 0.0 && luminance2020 > contentPeak) + color *= contentPeak / luminance2020; color = bt2020to709(color); } - // else: scRGB is already linear BT.709 (1.0 == SDR white), so no EOTF or - // gamut conversion is required - only highlight compression below. - return lin2srgb(compress(color)); + else + { + // scRGB is linear BT.709 with 1.0 fixed at 80 nits. Use the same + // absolute-luminance mapping as PQ before compressing the highlights. + color *= gain; + float luminance709 = dot(color, vec3(0.2126390, 0.7151687, 0.0721923)); + if (contentPeak > 0.0 && luminance709 > contentPeak) + color *= contentPeak / luminance709; + } + return lin2srgb(clamp(compress(color), 0.0, 1.0)); } diff --git a/client/renderers/EGL/shader/hdr_compose.frag b/client/renderers/EGL/shader/hdr_compose.frag new file mode 100644 index 00000000..084c8e86 --- /dev/null +++ b/client/renderers/EGL/shader/hdr_compose.frag @@ -0,0 +1,18 @@ +#version 300 es +precision highp float; + +#include "hdr.h" + +in vec2 fragCoord; +out vec4 color; + +uniform sampler2D sampler1; + +void main() +{ + vec3 scRGB = texture(sampler1, fragCoord).rgb; + // Preserve negative BT.709 components used to represent colours outside + // the BT.709 gamut. Clamp only after rotating back into BT.2020. + vec3 linear2020 = bt709to2020(scRGB * (80.0 / 10000.0)); + color = vec4(lin2pq(max(linear2020, 0.0)), 1.0); +} diff --git a/client/renderers/EGL/shader/hdr_compose.vert b/client/renderers/EGL/shader/hdr_compose.vert new file mode 100644 index 00000000..08e6ab30 --- /dev/null +++ b/client/renderers/EGL/shader/hdr_compose.vert @@ -0,0 +1,13 @@ +#version 300 es +precision highp float; + +layout(location = 0) in vec3 vertex; +layout(location = 1) in vec2 coord; + +out vec2 fragCoord; + +void main() +{ + gl_Position = vec4(vertex, 1.0); + fragCoord = coord; +}