diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index fd074879f..b2892163f 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -1056,6 +1056,7 @@ Basic.Settings.Advanced.Video.ColorRange="Color Range" Basic.Settings.Advanced.Video.ColorRange.Partial="Limited" Basic.Settings.Advanced.Video.ColorRange.Full="Full" Basic.Settings.Advanced.Video.SdrWhiteLevel="SDR White Level (nits)" +Basic.Settings.Advanced.Video.HdrNominalPeakLevel="HDR Nominal Peak Level (nits)" Basic.Settings.Advanced.Audio.MonitoringDevice="Monitoring Device" Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Default" Basic.Settings.Advanced.Audio.DisableAudioDucking="Disable Windows audio ducking" diff --git a/UI/forms/OBSBasicSettings.ui b/UI/forms/OBSBasicSettings.ui index c6bf8146f..596017e42 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -5170,6 +5170,32 @@ + + + + + 0 + 0 + + + + Basic.Settings.Advanced.Video.HdrNominalPeakLevel + + + hdrNominalPeakLevel + + + + + + + 400 + + + 10000 + + + diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index ea943d742..eb8811d35 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1497,6 +1497,8 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_string(basicConfig, "Video", "ColorRange", "Partial"); config_set_default_uint(basicConfig, "Video", "SdrWhiteLevel", 300); + config_set_default_uint(basicConfig, "Video", "HdrNominalPeakLevel", + 1000); config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId", "default"); @@ -4414,8 +4416,11 @@ int OBSBasic::ResetVideo() } if (ret == OBS_VIDEO_SUCCESS) { - obs_set_video_sdr_white_level((float)config_get_uint( - basicConfig, "Video", "SdrWhiteLevel")); + const float sdr_white_level = (float)config_get_uint( + basicConfig, "Video", "SdrWhiteLevel"); + const float hdr_nominal_peak_level = (float)config_get_uint( + basicConfig, "Video", "HdrNominalPeakLevel"); + obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); OBSBasicStats::InitializeValues(); OBSProjector::UpdateMultiviewProjectors(); } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index f7940828e..509d05463 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -537,6 +537,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->colorSpace, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->sdrWhiteLevel, SCROLL_CHANGED, ADV_CHANGED); + HookWidget(ui->hdrNominalPeakLevel, SCROLL_CHANGED, ADV_CHANGED); HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED); HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED); if (obs_audio_monitoring_available()) @@ -2596,6 +2597,8 @@ void OBSBasicSettings::LoadAdvancedSettings() config_get_string(main->Config(), "Video", "ColorRange"); uint32_t sdrWhiteLevel = (uint32_t)config_get_uint( main->Config(), "Video", "SdrWhiteLevel"); + uint32_t hdrNominalPeakLevel = (uint32_t)config_get_uint( + main->Config(), "Video", "HdrNominalPeakLevel"); QString monDevName; QString monDevId; @@ -2670,6 +2673,7 @@ void OBSBasicSettings::LoadAdvancedSettings() SetComboByValue(ui->colorSpace, videoColorSpace); SetComboByValue(ui->colorRange, videoColorRange); ui->sdrWhiteLevel->setValue(sdrWhiteLevel); + ui->hdrNominalPeakLevel->setValue(hdrNominalPeakLevel); if (!SetComboByValue(ui->bindToIP, bindIP)) SetInvalidValue(ui->bindToIP, bindIP, bindIP); @@ -3376,6 +3380,7 @@ void OBSBasicSettings::SaveAdvancedSettings() SaveComboData(ui->colorSpace, "Video", "ColorSpace"); SaveComboData(ui->colorRange, "Video", "ColorRange"); SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel"); + SaveSpinBox(ui->hdrNominalPeakLevel, "Video", "HdrNominalPeakLevel"); if (obs_audio_monitoring_available()) { SaveCombo(ui->monitoringDevice, "Audio", "MonitoringDeviceName"); diff --git a/docs/sphinx/reference-core.rst b/docs/sphinx/reference-core.rst index 79ba786c5..95444ef45 100644 --- a/docs/sphinx/reference-core.rst +++ b/docs/sphinx/reference-core.rst @@ -160,9 +160,17 @@ Initialization, Shutdown, and Information --------------------- -.. function:: void obs_set_video_sdr_white_level(float sdr_white_level) +.. function:: float obs_get_video_hdr_nominal_peak_level(void) - Sets the current SDR white level. + Gets the current HDR nominal peak level. + + :return: HDR nominal peak level, 1000.f if no video + +--------------------- + +.. function:: void obs_set_video_sdr_white_level(float sdr_white_level, float hdr_nominal_peak_level) + + Sets the current video levels. --------------------- diff --git a/libobs/data/color.effect b/libobs/data/color.effect index f606e4900..6ac04c2a1 100644 --- a/libobs/data/color.effect +++ b/libobs/data/color.effect @@ -71,15 +71,61 @@ float linear_to_hlg_channel(float u) return (u <= (1.0 /12.0)) ? sqrt(3.0 * u) : ((log2((12.0 * u) - 0.28466892) * m) + 0.55991073); } -float3 linear_to_hlg(float3 rgb) +float eetf_0_1000(float Lw, float maxRGB1_pq) +{ + float Lw_pq = linear_to_st2084_channel(Lw / 10000.); + float E1 = maxRGB1_pq / Lw_pq; + float maxLum = linear_to_st2084_channel(.1) / Lw_pq; + float KS = (1.5 * maxLum) - 0.5; + float E2 = E1; + if (E1 > KS) + { + float T = (E1 - KS) / (1. - KS); + float Tsquared = T * T; + float Tcubed = Tsquared * T; + float P = (2. * Tcubed - 3. * Tsquared + 1.) * KS + (Tcubed - 2. * Tsquared + T) * (1. - KS) + (-2. * Tcubed + 3. * Tsquared) * maxLum; + E2 = P; + } + float E3 = E2; + float E4 = E3 * Lw_pq; + return E4; +} + +float3 maxRGB_eetf(float3 rgb, float Lw, float Lmax) +{ + float maxRGB_linear = max(max(rgb.r, rgb.g), rgb.b); + float maxRGB1_pq = linear_to_st2084_channel(maxRGB_linear); + float maxRGB2_pq = eetf_0_1000(Lw, maxRGB1_pq); + float maxRGB2_linear = st2084_to_linear_channel(maxRGB2_pq); + float scaling_ratio = maxRGB2_linear / maxRGB_linear; + + // scaling_ratio could be NaN + scaling_ratio = max(0., scaling_ratio); + + rgb *= scaling_ratio; + return rgb; +} + +float3 linear_to_hlg(float3 rgb, float Lw) { rgb = saturate(rgb); - float yd = (0.2627 * rgb.r) + (0.678 * rgb.g) + (0.0593 * rgb.b); - // pow(0, exponent) can lead to NaN, use smallest positive normal number - yd = max(6.10352e-5, yd); + if (Lw > 1000.) + { + rgb = maxRGB_eetf(rgb, Lw, 1000.); + rgb *= 10000. / Lw; + } + else + { + rgb *= 10.; + } - rgb *= pow(yd, -1.0 / 6.0); + float Yd = dot(rgb, float3(0.2627, 0.678, 0.0593)); + + // pow(0., exponent) can lead to NaN, use smallest positive normal number + Yd = max(6.10352e-5, Yd); + + rgb *= pow(Yd, -1. / 6.); return float3(linear_to_hlg_channel(rgb.r), linear_to_hlg_channel(rgb.g), linear_to_hlg_channel(rgb.b)); } @@ -91,10 +137,10 @@ float hlg_to_linear_channel(float u) return (u <= 0.5) ? ((u * u) / 3.0) : ((exp2(u * m + a) + 0.28466892) / 12.0); } -float3 hlg_to_linear(float3 v) +float3 hlg_to_linear(float3 v, float exponent) { float3 rgb = float3(hlg_to_linear_channel(v.r), hlg_to_linear_channel(v.g), hlg_to_linear_channel(v.b)); - float ys = dot(rgb, float3(0.2627, 0.678, 0.0593)); - rgb *= pow(ys, 0.2); + float Ys = dot(rgb, float3(0.2627, 0.678, 0.0593)); + rgb *= pow(Ys, exponent); return rgb; } diff --git a/libobs/data/format_conversion.effect b/libobs/data/format_conversion.effect index 84dd478b5..2688e76ae 100644 --- a/libobs/data/format_conversion.effect +++ b/libobs/data/format_conversion.effect @@ -26,6 +26,8 @@ uniform float height_d2; uniform float width_x2_i; uniform float maximum_over_sdr_white_nits; uniform float sdr_white_nits_over_maximum; +uniform float hlg_exponent; +uniform float hlg_lw; uniform float4 color_vec0; uniform float4 color_vec1; @@ -213,7 +215,7 @@ float PS_HLG_Y_709_2020(FragPos frag_in) : TARGET { float3 rgb = image.Load(int3(frag_in.pos.xy, 0)).rgb * sdr_white_nits_over_maximum; rgb = rec709_to_rec2020(rgb); - rgb = linear_to_hlg(rgb); + rgb = linear_to_hlg(rgb, hlg_lw); float y = dot(color_vec0.xyz, rgb) + color_vec0.w; y = (65472. / 65535.) * y + (32. / 65535.); // set up truncation to 10 bits return y; @@ -241,7 +243,7 @@ float PS_I010_HLG_Y_709_2020(FragPos frag_in) : TARGET { float3 rgb = image.Load(int3(frag_in.pos.xy, 0)).rgb * sdr_white_nits_over_maximum; rgb = rec709_to_rec2020(rgb); - rgb = linear_to_hlg(rgb); + rgb = linear_to_hlg(rgb, hlg_lw); float y = dot(color_vec0.xyz, rgb) + color_vec0.w; return y * (1023. / 65535.); } @@ -288,7 +290,7 @@ float2 PS_HLG_UV_709_2020_WideWide(FragTexWideWide frag_in) : TARGET float3 rgb_bottomright = image.Sample(def_sampler, frag_in.uuvv.yw).rgb; float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum); rgb = rec709_to_rec2020(rgb); - rgb = linear_to_hlg(rgb); + rgb = linear_to_hlg(rgb, hlg_lw); float u = dot(color_vec1.xyz, rgb) + color_vec1.w; float v = dot(color_vec2.xyz, rgb) + color_vec2.w; float2 uv = float2(u, v); @@ -364,7 +366,7 @@ float PS_I010_HLG_U_709_2020_WideWide(FragTexWideWide frag_in) : TARGET float3 rgb_bottomright = image.Sample(def_sampler, frag_in.uuvv.yw).rgb; float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum); rgb = rec709_to_rec2020(rgb); - rgb = linear_to_hlg(rgb); + rgb = linear_to_hlg(rgb, hlg_lw); float u = dot(color_vec1.xyz, rgb) + color_vec1.w; return u * (1023. / 65535.); } @@ -402,7 +404,7 @@ float PS_I010_HLG_V_709_2020_WideWide(FragTexWideWide frag_in) : TARGET float3 rgb_bottomright = image.Sample(def_sampler, frag_in.uuvv.yw).rgb; float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum); rgb = rec709_to_rec2020(rgb); - rgb = linear_to_hlg(rgb); + rgb = linear_to_hlg(rgb, hlg_lw); float v = dot(color_vec2.xyz, rgb) + color_vec2.w; return v * (1023. / 65535.); } @@ -587,7 +589,7 @@ float4 PSI010_HLG_2020_709_Reverse(VertTexPos frag_in) : TARGET float cr = image2.Load(xy0_chroma).x * ratio; float3 yuv = float3(y, cb, cr); float3 hlg = YUV_to_RGB(yuv); - float3 hdr2020 = hlg_to_linear(hlg) * maximum_over_sdr_white_nits; + float3 hdr2020 = hlg_to_linear(hlg, hlg_exponent) * maximum_over_sdr_white_nits; float3 rgb = rec2020_to_rec709(hdr2020); return float4(rgb, 1.0); } @@ -619,7 +621,7 @@ float4 PSP010_HLG_2020_709_Reverse(VertTexPos frag_in) : TARGET float2 cbcr = image1.Load(int3(frag_in.uv, 0)).xy; float3 yuv = float3(y, cbcr); float3 hlg = YUV_to_RGB(yuv); - float3 hdr2020 = hlg_to_linear(hlg) * maximum_over_sdr_white_nits; + float3 hdr2020 = hlg_to_linear(hlg, hlg_exponent) * maximum_over_sdr_white_nits; float3 rgb = rec2020_to_rec709(hdr2020); return float4(rgb, 1.0); } diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index a8802cf43..1ced6fe1c 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -305,7 +305,6 @@ struct obs_core_video { bool conversion_needed; float conversion_width_i; float conversion_height_i; - float maximum_nits; uint32_t output_width; uint32_t output_height; @@ -326,7 +325,8 @@ struct obs_core_video { gs_effect_t *deinterlace_yadif_2x_effect; struct obs_video_info ovi; - uint32_t sdr_white_level; + float sdr_white_level; + float hdr_nominal_peak_level; pthread_mutex_t task_mutex; struct circlebuf tasks; diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 6f1c3f573..f94a04f42 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -2159,10 +2159,18 @@ static bool update_async_texrender(struct obs_source *source, set_eparam(conv, "height_d2", (float)cy * 0.5f); set_eparam(conv, "width_x2_i", 0.5f / (float)cx); - const float maximum_nits = - (frame->trc == VIDEO_TRC_HLG) ? 1000.f : 10000.f; + const float hdr_nominal_peak_level = + obs->video.hdr_nominal_peak_level; + const float maximum_nits = (frame->trc == VIDEO_TRC_HLG) + ? hdr_nominal_peak_level + : 10000.f; set_eparam(conv, "maximum_over_sdr_white_nits", maximum_nits / obs_get_video_sdr_white_level()); + const float hlg_gamma = + 1.2f + + (0.42f * log10f(hdr_nominal_peak_level / 1000.f)); + const float hlg_exponent = hlg_gamma - 1.f; + set_eparam(conv, "hlg_exponent", hlg_exponent); struct vec4 vec0, vec1, vec2; vec4_set(&vec0, frame->color_matrix[0], frame->color_matrix[1], diff --git a/libobs/obs-video.c b/libobs/obs-video.c index 2cf6d0640..a9c2527d0 100644 --- a/libobs/obs-video.c +++ b/libobs/obs-video.c @@ -316,6 +316,7 @@ static void render_convert_texture(struct obs_core_video *video, gs_eparam_t *height_i = gs_effect_get_param_by_name(effect, "height_i"); gs_eparam_t *sdr_white_nits_over_maximum = gs_effect_get_param_by_name( effect, "sdr_white_nits_over_maximum"); + gs_eparam_t *hlg_lw = gs_effect_get_param_by_name(effect, "hlg_lw"); struct vec4 vec0, vec1, vec2; vec4_set(&vec0, video->color_matrix[4], video->color_matrix[5], @@ -328,11 +329,14 @@ static void render_convert_texture(struct obs_core_video *video, gs_enable_blending(false); if (convert_textures[0]) { + const float hdr_nominal_peak_level = + video->hdr_nominal_peak_level; const float multiplier = - obs_get_video_sdr_white_level() / video->maximum_nits; + obs_get_video_sdr_white_level() / 10000.f; gs_effect_set_texture(image, texture); gs_effect_set_vec4(color_vec0, &vec0); gs_effect_set_float(sdr_white_nits_over_maximum, multiplier); + gs_effect_set_float(hlg_lw, hdr_nominal_peak_level); render_convert_plane(effect, convert_textures[0], video->conversion_techs[0]); @@ -346,6 +350,7 @@ static void render_convert_texture(struct obs_core_video *video, video->conversion_height_i); gs_effect_set_float(sdr_white_nits_over_maximum, multiplier); + gs_effect_set_float(hlg_lw, hdr_nominal_peak_level); render_convert_plane(effect, convert_textures[1], video->conversion_techs[1]); @@ -358,6 +363,8 @@ static void render_convert_texture(struct obs_core_video *video, video->conversion_height_i); gs_effect_set_float(sdr_white_nits_over_maximum, multiplier); + gs_effect_set_float(hlg_lw, + hdr_nominal_peak_level); render_convert_plane( effect, convert_textures[2], video->conversion_techs[2]); diff --git a/libobs/obs.c b/libobs/obs.c index ff6775170..913277fe0 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -54,7 +54,6 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi) video->conversion_techs[2] = NULL; video->conversion_width_i = 0.f; video->conversion_height_i = 0.f; - video->maximum_nits = 10000.f; switch ((uint32_t)ovi->output_format) { case VIDEO_FORMAT_I420: @@ -88,7 +87,6 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi) video->conversion_techs[0] = "I010_HLG_Y"; video->conversion_techs[1] = "I010_HLG_U"; video->conversion_techs[2] = "I010_HLG_V"; - video->maximum_nits = 1000.f; } else { video->conversion_techs[0] = "I010_SRGB_Y"; video->conversion_techs[1] = "I010_SRGB_U"; @@ -105,7 +103,6 @@ static inline void calc_gpu_conversion_sizes(const struct obs_video_info *ovi) } else if (ovi->colorspace == VIDEO_CS_2100_HLG) { video->conversion_techs[0] = "P010_HLG_Y"; video->conversion_techs[1] = "P010_HLG_UV"; - video->maximum_nits = 1000.f; } else { video->conversion_techs[0] = "P010_SRGB_Y"; video->conversion_techs[1] = "P010_SRGB_UV"; @@ -1443,12 +1440,19 @@ float obs_get_video_sdr_white_level(void) return video->graphics ? video->sdr_white_level : 300.f; } -void obs_set_video_sdr_white_level(float sdr_white_level) +float obs_get_video_hdr_nominal_peak_level(void) +{ + struct obs_core_video *video = &obs->video; + return video->graphics ? video->hdr_nominal_peak_level : 1000.f; +} + +void obs_set_video_levels(float sdr_white_level, float hdr_nominal_peak_level) { struct obs_core_video *video = &obs->video; assert(video->graphics); - video->sdr_white_level = (uint32_t)sdr_white_level; + video->sdr_white_level = sdr_white_level; + video->hdr_nominal_peak_level = hdr_nominal_peak_level; } bool obs_get_audio_info(struct obs_audio_info *oai) diff --git a/libobs/obs.h b/libobs/obs.h index 7cc5f1f81..312c853bc 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -417,11 +417,15 @@ EXPORT bool obs_reset_audio(const struct obs_audio_info *oai); /** Gets the current video settings, returns false if no video */ EXPORT bool obs_get_video_info(struct obs_video_info *ovi); -/** Gets the SDR white level, returns 300.0 if no video */ +/** Gets the SDR white level, returns 300.f if no video */ EXPORT float obs_get_video_sdr_white_level(void); -/** Sets the SDR white level */ -EXPORT void obs_set_video_sdr_white_level(float sdr_white_level); +/** Gets the HDR nominal peak level, returns 1000.f if no video */ +EXPORT float obs_get_video_hdr_nominal_peak_level(void); + +/** Sets the video levels */ +EXPORT void obs_set_video_levels(float sdr_white_level, + float hdr_nominal_peak_level); /** Gets the current audio settings, returns false if no audio */ EXPORT bool obs_get_audio_info(struct obs_audio_info *oai);