diff --git a/obs/effect/cursor.effect b/obs/effect/cursor.effect index ffdf2f76..8cc9014a 100644 --- a/obs/effect/cursor.effect +++ b/obs/effect/cursor.effect @@ -1,6 +1,15 @@ uniform float4x4 ViewProj; uniform texture2d image; +uniform texture2d colorLUT; uniform float multiplier; +uniform float sdrWhiteLevel; +uniform int outputTransfer; /* 0: sRGB, 1: scRGB, 2: PQ decoded to scRGB */ +uniform bool matrixEnabled; +uniform bool lutEnabled; +uniform float4 colorMatrix0; +uniform float4 colorMatrix1; +uniform float4 colorMatrix2; +uniform float colorScalar; sampler_state def_sampler { @@ -9,6 +18,13 @@ sampler_state def_sampler AddressV = Clamp; }; +sampler_state lut_sampler +{ + Filter = Point; + AddressU = Clamp; + AddressV = Clamp; +}; + struct VertData { float4 pos : POSITION; @@ -37,6 +53,99 @@ float3 srgbToLinear(float3 color) srgbToLinearChannel(color.b)); } +float3 pow3(float3 value, float exponent) +{ + return pow(value, float3(exponent, exponent, exponent)); +} + +float3 linearToSrgb(float3 value) +{ + float3 low = value * 12.92; + float3 high = 1.055 * pow3(max(value, 0.0), 1.0 / 2.4) - 0.055; + return lerp(low, high, step(0.0031308, value)); +} + +float3 bt709ToXYZ(float3 rgb) +{ + return float3( + dot(rgb, float3(0.4123908, 0.3575843, 0.1804808)), + dot(rgb, float3(0.2126390, 0.7151687, 0.0721923)), + dot(rgb, float3(0.0193308, 0.1191948, 0.9505322))); +} + +float3 xyzToBT709(float3 xyz) +{ + return float3( + dot(xyz, float3( 3.2409699, -1.5373832, -0.4986108)), + dot(xyz, float3(-0.9692436, 1.8759675, 0.0415551)), + dot(xyz, float3( 0.0556301, -0.2039770, 1.0569715))); +} + +float3 xyzToBT2020(float3 xyz) +{ + return float3( + dot(xyz, float3( 1.7166512, -0.3556708, -0.2533663)), + dot(xyz, float3(-0.6666844, 1.6164812, 0.0157685)), + dot(xyz, float3( 0.0176399, -0.0427706, 0.9421031))); +} + +float3 bt709ToBT2020(float3 rgb) +{ + return xyzToBT2020(bt709ToXYZ(rgb)); +} + +float3 bt2020ToBT709(float3 rgb) +{ + return float3( + dot(rgb, float3( 1.6604910, -0.5876411, -0.0728499)), + dot(rgb, float3(-0.1245505, 1.1328999, -0.0083494)), + dot(rgb, float3(-0.0181508, -0.1005789, 1.1187297))); +} + +float3 linearToPQ(float3 value) +{ + const float m1 = 0.1593017578125; + const float m2 = 78.84375; + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + float3 p = pow3(max(value, 0.0), m1); + return pow3((c1 + c2 * p) / (1.0 + c3 * p), m2); +} + +float3 pqToLinear(float3 value) +{ + const float m1 = 0.1593017578125; + const float m2 = 78.84375; + const float c1 = 0.8359375; + const float c2 = 18.8515625; + const float c3 = 18.6875; + float3 p = pow3(max(value, 0.0), 1.0 / m2); + return pow3(max(p - c1, 0.0) / max(c2 - c3 * p, 0.000001), + 1.0 / m1); +} + +float3 applyLUT(float3 value) +{ + float3 pos = clamp(value, 0.0, 1.0) * 4095.0; + float3 lo = floor(pos); + float3 hi = min(lo + 1.0, 4095.0); + float3 f = frac(pos); + float2 rlo = float2((lo.r + 0.5) / 4096.0, 0.5); + float2 rhi = float2((hi.r + 0.5) / 4096.0, 0.5); + float2 glo = float2((lo.g + 0.5) / 4096.0, 0.5); + float2 ghi = float2((hi.g + 0.5) / 4096.0, 0.5); + float2 blo = float2((lo.b + 0.5) / 4096.0, 0.5); + float2 bhi = float2((hi.b + 0.5) / 4096.0, 0.5); + return float3( + lerp(colorLUT.Sample(lut_sampler, rlo).r, + colorLUT.Sample(lut_sampler, rhi).r, f.r), + lerp(colorLUT.Sample(lut_sampler, glo).g, + colorLUT.Sample(lut_sampler, ghi).g, f.g), + lerp(colorLUT.Sample(lut_sampler, blo).b, + colorLUT.Sample(lut_sampler, bhi).b, f.b)); +} + float4 PSCursor(VertData vert_in) : TARGET { float4 pixel = image.Sample(def_sampler, vert_in.uv); @@ -46,7 +155,40 @@ float4 PSCursor(VertData vert_in) : TARGET /* Cursor RGB is premultiplied in nonlinear sRGB. Recover the straight * color before decoding it, then premultiply again in linear light. */ float3 srgb = clamp(pixel.rgb / pixel.a, 0.0, 1.0); - pixel.rgb = srgbToLinear(srgb) * pixel.a * multiplier; + float3 value = srgbToLinear(srgb); + if (matrixEnabled) + { + float3 xyz = bt709ToXYZ(value); + xyz = float3( + dot(float4(xyz, 1.0), colorMatrix0), + dot(float4(xyz, 1.0), colorMatrix1), + dot(float4(xyz, 1.0), colorMatrix2)) * colorScalar; + value = outputTransfer == 2 ? xyzToBT2020(xyz) : xyzToBT709(xyz); + } + else if (outputTransfer == 2) + value = bt709ToBT2020(value); + + if (outputTransfer == 2) + { + value = linearToPQ(value * (sdrWhiteLevel / 10000.0)); + if (lutEnabled) + value = applyLUT(value); + value = bt2020ToBT709(pqToLinear(value)) * 125.0; + } + else if (outputTransfer == 1) + { + value *= multiplier; + if (lutEnabled) + value = applyLUT(value); + } + else + { + value = linearToSrgb(value); + if (lutEnabled) + value = applyLUT(value); + } + + pixel.rgb = value * pixel.a; } else pixel.rgb = float3(0.0, 0.0, 0.0); diff --git a/obs/lg.c b/obs/lg.c index 2d5ce853..95693fb8 100644 --- a/obs/lg.c +++ b/obs/lg.c @@ -105,7 +105,7 @@ typedef struct bool hideMouse; bool hdr; bool hdrPQ; - float sdrWhiteLevel; // source SDR white level in nits + _Atomic(float) sdrWhiteLevel; // source cursor white level in nits struct vec3 colorMatrix[3]; // source primaries -> BT.709 (linear) float hdrScale; // scRGB reference white scaling #if LIBOBS_API_MAJOR_VER >= 27 @@ -132,6 +132,11 @@ typedef struct os_sem_t * cursorSem; atomic_uint cursorVer; unsigned int cursorCurVer; + KVMFRColorTransform pendingCursorTransform; + KVMFRColorTransform activeCursorTransform; + atomic_uint cursorTransformVer; + unsigned int cursorTransformCurVer; + gs_texture_t * cursorLUT; uint32_t cursorSize; uint32_t * cursorData; @@ -149,6 +154,13 @@ typedef struct gs_effect_t * cursorEffect; gs_eparam_t * cursorImage; gs_eparam_t * cursorMultiplier; + gs_eparam_t * cursorSDRWhiteLevel; + gs_eparam_t * cursorOutputTransfer; + gs_eparam_t * cursorMatrixEnabled; + gs_eparam_t * cursorLUTEnabled; + gs_eparam_t * cursorColorMatrix[3]; + gs_eparam_t * cursorColorScalar; + gs_eparam_t * cursorColorLUT; } LGPlugin; @@ -231,11 +243,31 @@ static void * lgCreate(obs_data_t * settings, obs_source_t * context) this->cursorEffect, "image" ); this->cursorMultiplier = gs_effect_get_param_by_name( this->cursorEffect, "multiplier"); + this->cursorSDRWhiteLevel = gs_effect_get_param_by_name( + this->cursorEffect, "sdrWhiteLevel"); + this->cursorOutputTransfer = gs_effect_get_param_by_name( + this->cursorEffect, "outputTransfer"); + this->cursorMatrixEnabled = gs_effect_get_param_by_name( + this->cursorEffect, "matrixEnabled"); + this->cursorLUTEnabled = gs_effect_get_param_by_name( + this->cursorEffect, "lutEnabled"); + this->cursorColorMatrix[0] = gs_effect_get_param_by_name( + this->cursorEffect, "colorMatrix0"); + this->cursorColorMatrix[1] = gs_effect_get_param_by_name( + this->cursorEffect, "colorMatrix1"); + this->cursorColorMatrix[2] = gs_effect_get_param_by_name( + this->cursorEffect, "colorMatrix2"); + this->cursorColorScalar = gs_effect_get_param_by_name( + this->cursorEffect, "colorScalar"); + this->cursorColorLUT = gs_effect_get_param_by_name( + this->cursorEffect, "colorLUT"); obs_leave_graphics(); os_sem_init (&this->frameSem , 0); os_sem_init (&this->cursorSem, 1); atomic_store(&this->cursorVer, 0); + atomic_store(&this->cursorTransformVer, 0); + atomic_store(&this->sdrWhiteLevel, KVMFR_SDR_WHITE_LEVEL_DEFAULT); lgUpdate(this, settings); return this; } @@ -353,6 +385,21 @@ static void deinit(LGPlugin * this) this->cursorXorTex = NULL; } + if (this->cursorLUT) + { + gs_effect_set_texture(this->cursorColorLUT, NULL); + gs_texture_destroy(this->cursorLUT); + this->cursorLUT = NULL; + } + memset(&this->pendingCursorTransform, 0, + sizeof(this->pendingCursorTransform)); + memset(&this->activeCursorTransform, 0, + sizeof(this->activeCursorTransform)); + this->pendingCursorTransform.scalar = 1.0f; + this->activeCursorTransform.scalar = 1.0f; + atomic_store(&this->cursorTransformVer, 0); + this->cursorTransformCurVer = 0; + #if LIBOBS_API_MAJOR_VER >= 27 dmabufReset(this); #endif @@ -488,8 +535,13 @@ static void * pointerThread(void * data) } const KVMFRCursor * const cursor = (const KVMFRCursor * const)msg.mem; - this->cursorVisible = this->hideMouse ? - 0 : msg.udata & CURSOR_FLAG_VISIBLE; + if (msg.udata & CURSOR_FLAG_VISIBLE_VALID) + { + this->cursorVisible = this->hideMouse ? + 0 : msg.udata & CURSOR_FLAG_VISIBLE; + if (cursor->sdrWhiteLevel) + atomic_store(&this->sdrWhiteLevel, cursor->sdrWhiteLevel); + } if (msg.udata & CURSOR_FLAG_SHAPE) { @@ -576,6 +628,21 @@ static void * pointerThread(void * data) os_sem_post(this->cursorSem); } + if (msg.udata & CURSOR_FLAG_COLOR_TRANSFORM) + { + const size_t shapeSize = msg.udata & CURSOR_FLAG_SHAPE ? + (size_t)cursor->height * cursor->pitch : 0; + const KVMFRColorTransform * transform = + (const KVMFRColorTransform *)((const uint8_t *)(cursor + 1) + + shapeSize); + + os_sem_wait(this->cursorSem); + this->pendingCursorTransform = *transform; + atomic_fetch_add_explicit( + &this->cursorTransformVer, 1, memory_order_release); + os_sem_post(this->cursorSem); + } + if (msg.udata & CURSOR_FLAG_POSITION) { this->cursor.x = cursor->x; @@ -882,8 +949,8 @@ static void lgFormatInit(LGPlugin * this, const KVMFRFrame * frame, this->unpack = false; this->hdr = frame->flags & FRAME_FLAG_HDR; this->hdrPQ = frame->flags & FRAME_FLAG_HDR_PQ; - this->sdrWhiteLevel = frame->sdrWhiteLevel ? - frame->sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT; + atomic_store(&this->sdrWhiteLevel, frame->sdrWhiteLevel ? + frame->sdrWhiteLevel : KVMFR_SDR_WHITE_LEVEL_DEFAULT); if (this->hdr && this->hdrPQ) lgComputeColorMatrix(this); @@ -1113,6 +1180,36 @@ static void lgVideoTick(void * data, float seconds) os_sem_post(this->cursorSem); } + const unsigned int cursorTransformVer = atomic_load_explicit( + &this->cursorTransformVer, memory_order_acquire); + if (cursorTransformVer != this->cursorTransformCurVer) + { + os_sem_wait(this->cursorSem); + this->activeCursorTransform = this->pendingCursorTransform; + + obs_enter_graphics(); + if (this->cursorLUT) + { + gs_effect_set_texture(this->cursorColorLUT, NULL); + gs_texture_destroy(this->cursorLUT); + this->cursorLUT = NULL; + } + + if (this->activeCursorTransform.flags & KVMFR_COLOR_TRANSFORM_LUT) + { + const uint8_t * lut = + (const uint8_t *)this->activeCursorTransform.lut; + this->cursorLUT = gs_texture_create( + 4096, 1, GS_RGBA32F, 1, &lut, GS_DYNAMIC); + if (!this->cursorLUT) + blog(LOG_ERROR, "Failed to create cursor colour-transform LUT"); + } + obs_leave_graphics(); + + this->cursorTransformCurVer = cursorTransformVer; + os_sem_post(this->cursorSem); + } + if ((status = lgmpClientAdvanceToLast(this->frameQueue)) != LGMP_OK) { if (status != LGMP_ERR_QUEUE_EMPTY) @@ -1206,6 +1303,29 @@ static void lgVideoTick(void * data, float seconds) obs_leave_graphics(); } +static void lgSetupCursorEffect(LGPlugin * this, int outputTransfer) +{ + const KVMFRColorTransform * transform = &this->activeCursorTransform; + const bool matrixEnabled = + transform->flags & KVMFR_COLOR_TRANSFORM_MATRIX; + const bool lutEnabled = + (transform->flags & KVMFR_COLOR_TRANSFORM_LUT) && this->cursorLUT; + const float whiteLevel = atomic_load(&this->sdrWhiteLevel); + + gs_effect_set_float(this->cursorMultiplier, + whiteLevel / LG_SCRGB_REFERENCE_WHITE); + gs_effect_set_float(this->cursorSDRWhiteLevel, whiteLevel); + gs_effect_set_int(this->cursorOutputTransfer, outputTransfer); + gs_effect_set_bool(this->cursorMatrixEnabled, matrixEnabled); + gs_effect_set_bool(this->cursorLUTEnabled, lutEnabled); + for (int i = 0; i < 3; ++i) + gs_effect_set_val(this->cursorColorMatrix[i], + transform->matrix[i], sizeof(transform->matrix[i])); + gs_effect_set_float(this->cursorColorScalar, transform->scalar); + if (lutEnabled) + gs_effect_set_texture(this->cursorColorLUT, this->cursorLUT); +} + static void lgVideoRender(void * data, gs_effect_t * effect) { LGPlugin * this = (LGPlugin *)data; @@ -1275,20 +1395,24 @@ static void lgVideoRender(void * data, gs_effect_t * effect) * linear scRGB space, decode them and reproduce the source display's SDR * white level reported by the IDD. */ bool mapCursorToScRGB = false; - float cursorScale = 1.0f; #if LIBOBS_API_MAJOR_VER >= 28 mapCursorToScRGB = this->colorSpace == GS_CS_709_SCRGB; - if (mapCursorToScRGB) - cursorScale = this->sdrWhiteLevel / LG_SCRGB_REFERENCE_WHITE; #endif - effect = mapCursorToScRGB && !this->cursorMono ? + const bool hasCursorTransform = + this->activeCursorTransform.flags != 0; + const bool useCursorEffect = !this->cursorMono && + (mapCursorToScRGB || hasCursorTransform); + const int cursorOutputTransfer = !mapCursorToScRGB ? 0 : + (this->hdrPQ ? 2 : 1); + + effect = useCursorEffect ? this->cursorEffect : obs_get_base_effect(OBS_EFFECT_DEFAULT); - gs_eparam_t * image = mapCursorToScRGB && !this->cursorMono ? + gs_eparam_t * image = useCursorEffect ? this->cursorImage : gs_effect_get_param_by_name(effect, "image"); gs_effect_set_texture(image, this->cursorTex); - if (mapCursorToScRGB && !this->cursorMono) - gs_effect_set_float(this->cursorMultiplier, cursorScale); + if (useCursorEffect) + lgSetupCursorEffect(this, cursorOutputTransfer); gs_matrix_push(); gs_matrix_translate3f( @@ -1310,13 +1434,13 @@ static void lgVideoRender(void * data, gs_effect_t * effect) if (this->cursor.type == CURSOR_TYPE_MASKED_COLOR && this->cursorXorTex) { - effect = mapCursorToScRGB ? + effect = useCursorEffect ? this->cursorEffect : obs_get_base_effect(OBS_EFFECT_DEFAULT); - image = mapCursorToScRGB ? + image = useCursorEffect ? this->cursorImage : gs_effect_get_param_by_name(effect, "image"); gs_effect_set_texture(image, this->cursorXorTex); - if (mapCursorToScRGB) - gs_effect_set_float(this->cursorMultiplier, cursorScale); + if (useCursorEffect) + lgSetupCursorEffect(this, cursorOutputTransfer); gs_blend_function_separate( GS_BLEND_INVDSTCOLOR, GS_BLEND_INVSRCALPHA, GS_BLEND_ZERO , GS_BLEND_ONE);