[client] egl: compose native PQ output in linear light

Decode PQ/BT.2020 desktop pixels into an FP16 linear scRGB target before
compositing the color cursor, ImGui, damage overlay, and letterboxing.
Encode the damaged result to PQ once at the surface boundary. Keep logical
AND/XOR cursor masks in the encoded domain.

This preserves premultiplied cursor edges, prevents encoded-domain
blending, uses the Wayland output reference white for local overlays, and
keeps guest cursor white and display calibration independent from frame
SDR white. It also corrects HDR-to-SDR absolute luminance scaling, gamut
matrices, black handling, and buffer-age repair for the extra pass.

Suppress overlays when their conversion framebuffer is unavailable rather
than allowing unconverted sRGB draws into an HDR target.

Use named transfer constants for cursor shader state. Convert clipped
surface rectangles explicitly before using the frame-damage merge helper,
which avoids mixing its unsigned type with the EGL surface rectangle type.

Only advertise native PQ when the FP16 composition target is available.
SDR and native scRGB retain their direct single-pass paths.
This commit is contained in:
Geoffrey McRae
2026-07-19 17:34:27 +10:00
parent 29cfa58e3b
commit 79b031f061
19 changed files with 726 additions and 191 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <stdlib.h>
#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;
}

View File

@@ -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 <stdbool.h>
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);

View File

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

View File

@@ -24,7 +24,8 @@
#include <stdbool.h>
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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