Browse Source

libobs,UI: Support HLG nominal peak level

HLG output uses MovieLabs-recommended procedure.

- If peak luminance is greater than 1000, use maxRGB EETF to 1000.
- Otherwise, don't tonemap.
- Then use normal HLG conversion procedure with gamma 1.2 (1000 nits).
jpark37 3 years ago
parent
commit
338608bd67

+ 1 - 0
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.Partial="Limited"
 Basic.Settings.Advanced.Video.ColorRange.Full="Full"
 Basic.Settings.Advanced.Video.ColorRange.Full="Full"
 Basic.Settings.Advanced.Video.SdrWhiteLevel="SDR White Level (nits)"
 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="Monitoring Device"
 Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Default"
 Basic.Settings.Advanced.Audio.MonitoringDevice.Default="Default"
 Basic.Settings.Advanced.Audio.DisableAudioDucking="Disable Windows audio ducking"
 Basic.Settings.Advanced.Audio.DisableAudioDucking="Disable Windows audio ducking"

+ 26 - 0
UI/forms/OBSBasicSettings.ui

@@ -5170,6 +5170,32 @@
                        </property>
                        </property>
                       </widget>
                       </widget>
                      </item>
                      </item>
+                     <item>
+                      <widget class="QLabel" name="label_hdrNominalPeakLevel">
+                       <property name="sizePolicy">
+                        <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+                         <horstretch>0</horstretch>
+                         <verstretch>0</verstretch>
+                        </sizepolicy>
+                       </property>
+                       <property name="text">
+                        <string>Basic.Settings.Advanced.Video.HdrNominalPeakLevel</string>
+                       </property>
+                       <property name="buddy">
+                        <cstring>hdrNominalPeakLevel</cstring>
+                       </property>
+                      </widget>
+                     </item>
+                     <item>
+                      <widget class="QSpinBox" name="hdrNominalPeakLevel">
+                       <property name="minimum">
+                        <number>400</number>
+                       </property>
+                       <property name="maximum">
+                        <number>10000</number>
+                       </property>
+                      </widget>
+                     </item>
                     </layout>
                     </layout>
                    </item>
                    </item>
                    <item row="5" column="1">
                    <item row="5" column="1">

+ 7 - 2
UI/window-basic-main.cpp

@@ -1497,6 +1497,8 @@ bool OBSBasic::InitBasicConfigDefaults()
 	config_set_default_string(basicConfig, "Video", "ColorRange",
 	config_set_default_string(basicConfig, "Video", "ColorRange",
 				  "Partial");
 				  "Partial");
 	config_set_default_uint(basicConfig, "Video", "SdrWhiteLevel", 300);
 	config_set_default_uint(basicConfig, "Video", "SdrWhiteLevel", 300);
+	config_set_default_uint(basicConfig, "Video", "HdrNominalPeakLevel",
+				1000);
 
 
 	config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId",
 	config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId",
 				  "default");
 				  "default");
@@ -4414,8 +4416,11 @@ int OBSBasic::ResetVideo()
 	}
 	}
 
 
 	if (ret == OBS_VIDEO_SUCCESS) {
 	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();
 		OBSBasicStats::InitializeValues();
 		OBSProjector::UpdateMultiviewProjectors();
 		OBSProjector::UpdateMultiviewProjectors();
 	}
 	}

+ 5 - 0
UI/window-basic-settings.cpp

@@ -537,6 +537,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent)
 	HookWidget(ui->colorSpace,           COMBO_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->colorSpace,           COMBO_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->colorRange,           COMBO_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->colorRange,           COMBO_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->sdrWhiteLevel,        SCROLL_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->disableOSXVSync,      CHECK_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->resetOSXVSync,        CHECK_CHANGED,  ADV_CHANGED);
 	HookWidget(ui->resetOSXVSync,        CHECK_CHANGED,  ADV_CHANGED);
 	if (obs_audio_monitoring_available())
 	if (obs_audio_monitoring_available())
@@ -2596,6 +2597,8 @@ void OBSBasicSettings::LoadAdvancedSettings()
 		config_get_string(main->Config(), "Video", "ColorRange");
 		config_get_string(main->Config(), "Video", "ColorRange");
 	uint32_t sdrWhiteLevel = (uint32_t)config_get_uint(
 	uint32_t sdrWhiteLevel = (uint32_t)config_get_uint(
 		main->Config(), "Video", "SdrWhiteLevel");
 		main->Config(), "Video", "SdrWhiteLevel");
+	uint32_t hdrNominalPeakLevel = (uint32_t)config_get_uint(
+		main->Config(), "Video", "HdrNominalPeakLevel");
 
 
 	QString monDevName;
 	QString monDevName;
 	QString monDevId;
 	QString monDevId;
@@ -2670,6 +2673,7 @@ void OBSBasicSettings::LoadAdvancedSettings()
 	SetComboByValue(ui->colorSpace, videoColorSpace);
 	SetComboByValue(ui->colorSpace, videoColorSpace);
 	SetComboByValue(ui->colorRange, videoColorRange);
 	SetComboByValue(ui->colorRange, videoColorRange);
 	ui->sdrWhiteLevel->setValue(sdrWhiteLevel);
 	ui->sdrWhiteLevel->setValue(sdrWhiteLevel);
+	ui->hdrNominalPeakLevel->setValue(hdrNominalPeakLevel);
 
 
 	if (!SetComboByValue(ui->bindToIP, bindIP))
 	if (!SetComboByValue(ui->bindToIP, bindIP))
 		SetInvalidValue(ui->bindToIP, bindIP, bindIP);
 		SetInvalidValue(ui->bindToIP, bindIP, bindIP);
@@ -3376,6 +3380,7 @@ void OBSBasicSettings::SaveAdvancedSettings()
 	SaveComboData(ui->colorSpace, "Video", "ColorSpace");
 	SaveComboData(ui->colorSpace, "Video", "ColorSpace");
 	SaveComboData(ui->colorRange, "Video", "ColorRange");
 	SaveComboData(ui->colorRange, "Video", "ColorRange");
 	SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel");
 	SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel");
+	SaveSpinBox(ui->hdrNominalPeakLevel, "Video", "HdrNominalPeakLevel");
 	if (obs_audio_monitoring_available()) {
 	if (obs_audio_monitoring_available()) {
 		SaveCombo(ui->monitoringDevice, "Audio",
 		SaveCombo(ui->monitoringDevice, "Audio",
 			  "MonitoringDeviceName");
 			  "MonitoringDeviceName");

+ 10 - 2
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.
 
 
 ---------------------
 ---------------------
 
 

+ 54 - 8
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);
     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);
 	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.;
+	}
+
+	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.0 / 6.0);
+	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));
 	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);
 	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));
 	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;
 	return rgb;
 }
 }

+ 9 - 7
libobs/data/format_conversion.effect

@@ -26,6 +26,8 @@ uniform float     height_d2;
 uniform float     width_x2_i;
 uniform float     width_x2_i;
 uniform float     maximum_over_sdr_white_nits;
 uniform float     maximum_over_sdr_white_nits;
 uniform float     sdr_white_nits_over_maximum;
 uniform float     sdr_white_nits_over_maximum;
+uniform float     hlg_exponent;
+uniform float     hlg_lw;
 
 
 uniform float4    color_vec0;
 uniform float4    color_vec0;
 uniform float4    color_vec1;
 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;
 	float3 rgb = image.Load(int3(frag_in.pos.xy, 0)).rgb * sdr_white_nits_over_maximum;
 	rgb = rec709_to_rec2020(rgb);
 	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;
 	float y = dot(color_vec0.xyz, rgb) + color_vec0.w;
 	y = (65472. / 65535.) * y + (32. / 65535.); // set up truncation to 10 bits
 	y = (65472. / 65535.) * y + (32. / 65535.); // set up truncation to 10 bits
 	return y;
 	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;
 	float3 rgb = image.Load(int3(frag_in.pos.xy, 0)).rgb * sdr_white_nits_over_maximum;
 	rgb = rec709_to_rec2020(rgb);
 	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;
 	float y = dot(color_vec0.xyz, rgb) + color_vec0.w;
 	return y * (1023. / 65535.);
 	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_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);
 	float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum);
 	rgb = rec709_to_rec2020(rgb);
 	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 u = dot(color_vec1.xyz, rgb) + color_vec1.w;
 	float v = dot(color_vec2.xyz, rgb) + color_vec2.w;
 	float v = dot(color_vec2.xyz, rgb) + color_vec2.w;
 	float2 uv = float2(u, v);
 	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_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);
 	float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum);
 	rgb = rec709_to_rec2020(rgb);
 	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 u = dot(color_vec1.xyz, rgb) + color_vec1.w;
 	return u * (1023. / 65535.);
 	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_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);
 	float3 rgb = (rgb_topleft + rgb_topright + rgb_bottomleft + rgb_bottomright) * (0.25 * sdr_white_nits_over_maximum);
 	rgb = rec709_to_rec2020(rgb);
 	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;
 	float v = dot(color_vec2.xyz, rgb) + color_vec2.w;
 	return v * (1023. / 65535.);
 	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;
 	float cr = image2.Load(xy0_chroma).x * ratio;
 	float3 yuv = float3(y, cb, cr);
 	float3 yuv = float3(y, cb, cr);
 	float3 hlg = YUV_to_RGB(yuv);
 	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);
 	float3 rgb = rec2020_to_rec709(hdr2020);
 	return float4(rgb, 1.0);
 	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;
 	float2 cbcr = image1.Load(int3(frag_in.uv, 0)).xy;
 	float3 yuv = float3(y, cbcr);
 	float3 yuv = float3(y, cbcr);
 	float3 hlg = YUV_to_RGB(yuv);
 	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);
 	float3 rgb = rec2020_to_rec709(hdr2020);
 	return float4(rgb, 1.0);
 	return float4(rgb, 1.0);
 }
 }

+ 2 - 2
libobs/obs-internal.h

@@ -305,7 +305,6 @@ struct obs_core_video {
 	bool conversion_needed;
 	bool conversion_needed;
 	float conversion_width_i;
 	float conversion_width_i;
 	float conversion_height_i;
 	float conversion_height_i;
-	float maximum_nits;
 
 
 	uint32_t output_width;
 	uint32_t output_width;
 	uint32_t output_height;
 	uint32_t output_height;
@@ -326,7 +325,8 @@ struct obs_core_video {
 	gs_effect_t *deinterlace_yadif_2x_effect;
 	gs_effect_t *deinterlace_yadif_2x_effect;
 
 
 	struct obs_video_info ovi;
 	struct obs_video_info ovi;
-	uint32_t sdr_white_level;
+	float sdr_white_level;
+	float hdr_nominal_peak_level;
 
 
 	pthread_mutex_t task_mutex;
 	pthread_mutex_t task_mutex;
 	struct circlebuf tasks;
 	struct circlebuf tasks;

+ 10 - 2
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, "height_d2", (float)cy * 0.5f);
 		set_eparam(conv, "width_x2_i", 0.5f / (float)cx);
 		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",
 		set_eparam(conv, "maximum_over_sdr_white_nits",
 			   maximum_nits / obs_get_video_sdr_white_level());
 			   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;
 		struct vec4 vec0, vec1, vec2;
 		vec4_set(&vec0, frame->color_matrix[0], frame->color_matrix[1],
 		vec4_set(&vec0, frame->color_matrix[0], frame->color_matrix[1],

+ 8 - 1
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 *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(
 	gs_eparam_t *sdr_white_nits_over_maximum = gs_effect_get_param_by_name(
 		effect, "sdr_white_nits_over_maximum");
 		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;
 	struct vec4 vec0, vec1, vec2;
 	vec4_set(&vec0, video->color_matrix[4], video->color_matrix[5],
 	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);
 	gs_enable_blending(false);
 
 
 	if (convert_textures[0]) {
 	if (convert_textures[0]) {
+		const float hdr_nominal_peak_level =
+			video->hdr_nominal_peak_level;
 		const float multiplier =
 		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_texture(image, texture);
 		gs_effect_set_vec4(color_vec0, &vec0);
 		gs_effect_set_vec4(color_vec0, &vec0);
 		gs_effect_set_float(sdr_white_nits_over_maximum, multiplier);
 		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],
 		render_convert_plane(effect, convert_textures[0],
 				     video->conversion_techs[0]);
 				     video->conversion_techs[0]);
 
 
@@ -346,6 +350,7 @@ static void render_convert_texture(struct obs_core_video *video,
 					    video->conversion_height_i);
 					    video->conversion_height_i);
 			gs_effect_set_float(sdr_white_nits_over_maximum,
 			gs_effect_set_float(sdr_white_nits_over_maximum,
 					    multiplier);
 					    multiplier);
+			gs_effect_set_float(hlg_lw, hdr_nominal_peak_level);
 			render_convert_plane(effect, convert_textures[1],
 			render_convert_plane(effect, convert_textures[1],
 					     video->conversion_techs[1]);
 					     video->conversion_techs[1]);
 
 
@@ -358,6 +363,8 @@ static void render_convert_texture(struct obs_core_video *video,
 						    video->conversion_height_i);
 						    video->conversion_height_i);
 				gs_effect_set_float(sdr_white_nits_over_maximum,
 				gs_effect_set_float(sdr_white_nits_over_maximum,
 						    multiplier);
 						    multiplier);
+				gs_effect_set_float(hlg_lw,
+						    hdr_nominal_peak_level);
 				render_convert_plane(
 				render_convert_plane(
 					effect, convert_textures[2],
 					effect, convert_textures[2],
 					video->conversion_techs[2]);
 					video->conversion_techs[2]);

+ 9 - 5
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_techs[2] = NULL;
 	video->conversion_width_i = 0.f;
 	video->conversion_width_i = 0.f;
 	video->conversion_height_i = 0.f;
 	video->conversion_height_i = 0.f;
-	video->maximum_nits = 10000.f;
 
 
 	switch ((uint32_t)ovi->output_format) {
 	switch ((uint32_t)ovi->output_format) {
 	case VIDEO_FORMAT_I420:
 	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[0] = "I010_HLG_Y";
 			video->conversion_techs[1] = "I010_HLG_U";
 			video->conversion_techs[1] = "I010_HLG_U";
 			video->conversion_techs[2] = "I010_HLG_V";
 			video->conversion_techs[2] = "I010_HLG_V";
-			video->maximum_nits = 1000.f;
 		} else {
 		} else {
 			video->conversion_techs[0] = "I010_SRGB_Y";
 			video->conversion_techs[0] = "I010_SRGB_Y";
 			video->conversion_techs[1] = "I010_SRGB_U";
 			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) {
 		} else if (ovi->colorspace == VIDEO_CS_2100_HLG) {
 			video->conversion_techs[0] = "P010_HLG_Y";
 			video->conversion_techs[0] = "P010_HLG_Y";
 			video->conversion_techs[1] = "P010_HLG_UV";
 			video->conversion_techs[1] = "P010_HLG_UV";
-			video->maximum_nits = 1000.f;
 		} else {
 		} else {
 			video->conversion_techs[0] = "P010_SRGB_Y";
 			video->conversion_techs[0] = "P010_SRGB_Y";
 			video->conversion_techs[1] = "P010_SRGB_UV";
 			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;
 	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;
 	struct obs_core_video *video = &obs->video;
 	assert(video->graphics);
 	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)
 bool obs_get_audio_info(struct obs_audio_info *oai)

+ 7 - 3
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 */
 /** Gets the current video settings, returns false if no video */
 EXPORT bool obs_get_video_info(struct obs_video_info *ovi);
 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);
 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 */
 /** Gets the current audio settings, returns false if no audio */
 EXPORT bool obs_get_audio_info(struct obs_audio_info *oai);
 EXPORT bool obs_get_audio_info(struct obs_audio_info *oai);