diff --git a/UI/data/locale/en-US.ini b/UI/data/locale/en-US.ini index 9993538e1..9f553bcaf 100644 --- a/UI/data/locale/en-US.ini +++ b/UI/data/locale/en-US.ini @@ -1036,6 +1036,7 @@ Basic.Settings.Advanced.Video.ColorSpace="Color Space" Basic.Settings.Advanced.Video.ColorRange="Color Range" Basic.Settings.Advanced.Video.ColorRange.Partial="Partial" Basic.Settings.Advanced.Video.ColorRange.Full="Full" +Basic.Settings.Advanced.Video.SdrWhiteLevel="SDR White 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 2f4c02e4a..b7b9712f6 100644 --- a/UI/forms/OBSBasicSettings.ui +++ b/UI/forms/OBSBasicSettings.ui @@ -4989,35 +4989,15 @@ - - - - 0 + + + + Basic.Settings.Advanced.Video.ColorSpace - - 0 + + colorSpace - - 0 - - - 0 - - - - - DisableOSXVSync - - - - - - - ResetOSXVSyncOnExit - - - - + @@ -5073,17 +5053,73 @@ - - + + - Basic.Settings.Advanced.Video.ColorSpace + Basic.Settings.Advanced.Video.SdrWhiteLevel - colorSpace + sdrWhiteLevel - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 80 + + + 480 + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + DisableOSXVSync + + + + + + + ResetOSXVSyncOnExit + + + + + + Qt::Horizontal diff --git a/UI/qt-display.cpp b/UI/qt-display.cpp index 7dca7ead4..06414c007 100644 --- a/UI/qt-display.cpp +++ b/UI/qt-display.cpp @@ -221,6 +221,14 @@ QPaintEngine *OBSQTDisplay::paintEngine() const return nullptr; } -void OBSQTDisplay::OnMove() {} +void OBSQTDisplay::OnMove() +{ + if (display) + obs_display_update_color_space(display); +} -void OBSQTDisplay::OnDisplayChange() {} +void OBSQTDisplay::OnDisplayChange() +{ + if (display) + obs_display_update_color_space(display); +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index b36c26f0e..98418aebd 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -1490,6 +1490,7 @@ bool OBSBasic::InitBasicConfigDefaults() config_set_default_string(basicConfig, "Video", "ColorSpace", "709"); config_set_default_string(basicConfig, "Video", "ColorRange", "Partial"); + config_set_default_uint(basicConfig, "Video", "SdrWhiteLevel", 300); config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId", "default"); @@ -4392,6 +4393,8 @@ int OBSBasic::ResetVideo() } if (ret == OBS_VIDEO_SUCCESS) { + obs_set_video_sdr_white_level((float)config_get_uint( + basicConfig, "Video", "SdrWhiteLevel")); OBSBasicStats::InitializeValues(); OBSProjector::UpdateMultiviewProjectors(); } diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 5f2587ac6..c6d99d74b 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -531,6 +531,7 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) HookWidget(ui->colorFormat, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->colorSpace, COMBO_CHANGED, ADV_CHANGED); HookWidget(ui->colorRange, COMBO_CHANGED, ADV_CHANGED); + HookWidget(ui->sdrWhiteLevel, SCROLL_CHANGED, ADV_CHANGED); HookWidget(ui->disableOSXVSync, CHECK_CHANGED, ADV_CHANGED); HookWidget(ui->resetOSXVSync, CHECK_CHANGED, ADV_CHANGED); if (obs_audio_monitoring_available()) @@ -2521,6 +2522,8 @@ void OBSBasicSettings::LoadAdvancedSettings() config_get_string(main->Config(), "Video", "ColorSpace"); const char *videoColorRange = config_get_string(main->Config(), "Video", "ColorRange"); + uint32_t sdrWhiteLevel = (uint32_t)config_get_uint( + main->Config(), "Video", "SdrWhiteLevel"); QString monDevName; QString monDevId; @@ -2592,6 +2595,7 @@ void OBSBasicSettings::LoadAdvancedSettings() SetComboByName(ui->colorFormat, videoColorFormat); SetComboByName(ui->colorSpace, videoColorSpace); SetComboByValue(ui->colorRange, videoColorRange); + ui->sdrWhiteLevel->setValue(sdrWhiteLevel); if (!SetComboByValue(ui->bindToIP, bindIP)) SetInvalidValue(ui->bindToIP, bindIP, bindIP); @@ -3297,6 +3301,7 @@ void OBSBasicSettings::SaveAdvancedSettings() SaveCombo(ui->colorFormat, "Video", "ColorFormat"); SaveCombo(ui->colorSpace, "Video", "ColorSpace"); SaveComboData(ui->colorRange, "Video", "ColorRange"); + SaveSpinBox(ui->sdrWhiteLevel, "Video", "SdrWhiteLevel"); 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 a01b61667..79ba786c5 100644 --- a/docs/sphinx/reference-core.rst +++ b/docs/sphinx/reference-core.rst @@ -147,11 +147,25 @@ Initialization, Shutdown, and Information .. function:: bool obs_get_video_info(struct obs_video_info *ovi) Gets the current video settings. - + :return: *false* if no video --------------------- +.. function:: float obs_get_video_sdr_white_level(void) + + Gets the current SDR white level. + + :return: SDR white level, 300.f if no video + +--------------------- + +.. function:: void obs_set_video_sdr_white_level(float sdr_white_level) + + Sets the current SDR white level. + +--------------------- + .. function:: bool obs_get_audio_info(struct obs_audio_info *oai) Gets the current audio settings. diff --git a/docs/sphinx/reference-libobs-graphics-graphics.rst b/docs/sphinx/reference-libobs-graphics-graphics.rst index d1f83bb28..b1245acca 100644 --- a/docs/sphinx/reference-libobs-graphics-graphics.rst +++ b/docs/sphinx/reference-libobs-graphics-graphics.rst @@ -47,6 +47,14 @@ Graphics Enumerations - GS_BGRA_UNORM - BGRA, 8 bits per channel, no SRGB aliasing - GS_RG16 - RG, 16 bits per channel +.. type:: enum gs_color_space + + Color space. Can be one of the following values: + + - GS_CS_SRGB - sRGB + - GS_CS_709_EXTENDED - Canvas, Mac EDR (HDR) + - GS_CS_709_SCRGB - 1.0 = 80 nits, Windows/Linux HDR + .. type:: enum gs_zstencil_format Z-Stencil buffer format. Can be one of the following values: @@ -542,6 +550,13 @@ Swap Chains --------------------- +.. function:: void gs_update_color_space(void) + + Updates the color space of the swap chain based on the HDR status of + the nearest monitor + +--------------------- + .. function:: void gs_get_size(uint32_t *cx, uint32_t *cy) Gets the size of the currently active swap chain @@ -613,6 +628,12 @@ Resource Loading Draw Functions -------------- +.. function:: enum gs_color_space gs_get_color_space(void) + + :return: The currently active color space + +--------------------- + .. function:: gs_texture_t *gs_get_render_target(void) :return: The currently active render target @@ -627,13 +648,23 @@ Draw Functions .. function:: void gs_set_render_target(gs_texture_t *tex, gs_zstencil_t *zstencil) - Sets the active render target + Sets the active render target with implicit GS_CS_SRGB color space :param tex: Texture to set as the active render target :param zstencil: Z-stencil to use as the active render target --------------------- +.. function:: void gs_set_render_target_with_color_space(gs_texture_t *tex, gs_zstencil_t *zstencil, enum gs_color_space space) + + Sets the active render target along with color space + + :param tex: Texture to set as the active render target + :param zstencil: Z-stencil to use as the active render target + :param space: Color space of the render target + +--------------------- + .. function:: void gs_set_cube_render_target(gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil) Sets a cubemap side as the active render target @@ -1475,6 +1506,14 @@ Display Duplicator (Windows Only) --------------------- +Monitor Functions +--------------------------------- + +.. function:: bool gs_is_monitor_hdr(void *monitor) + +--------------------- + + Render Helper Functions ----------------------- diff --git a/libobs-d3d11/d3d11-subsystem.cpp b/libobs-d3d11/d3d11-subsystem.cpp index 40d52627b..3b858b8a1 100644 --- a/libobs-d3d11/d3d11-subsystem.cpp +++ b/libobs-d3d11/d3d11-subsystem.cpp @@ -66,21 +66,78 @@ gs_obj::~gs_obj() next->prev_next = prev_next; } -static inline void make_swap_desc(DXGI_SWAP_CHAIN_DESC &desc, - const gs_init_data *data, - DXGI_SWAP_EFFECT effect, UINT flags) +static bool screen_supports_hdr(gs_device_t *device, HMONITOR hMonitor) { + IDXGIFactory1 *factory1 = device->factory; + if (!factory1->IsCurrent()) { + device->InitFactory(); + factory1 = device->factory; + } + + ComPtr adapter; + ComPtr output; + ComPtr output6; + for (UINT adapterIndex = 0; + SUCCEEDED(factory1->EnumAdapters(adapterIndex, &adapter)); + ++adapterIndex) { + for (UINT outputIndex = 0; + SUCCEEDED(adapter->EnumOutputs(outputIndex, &output)); + ++outputIndex) { + if (SUCCEEDED(output->QueryInterface(&output6))) { + DXGI_OUTPUT_DESC1 desc1; + if (SUCCEEDED(output6->GetDesc1(&desc1)) && + desc1.Monitor == hMonitor) { + return desc1.ColorSpace == + DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + } + } + } + } + + return false; +} + +static enum gs_color_space get_next_space(gs_device_t *device, HWND hwnd) +{ + enum gs_color_space next_space = GS_CS_SRGB; + const HMONITOR hMonitor = + MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + if (hMonitor) { + if (screen_supports_hdr(device, hMonitor)) + next_space = GS_CS_709_SCRGB; + } + + return next_space; +} + +static enum gs_color_format +get_swap_format_from_space(gs_color_space space, gs_color_format sdr_format) +{ + return (space == GS_CS_709_SCRGB) ? GS_RGBA16F : sdr_format; +} + +static inline enum gs_color_space +make_swap_desc(gs_device *device, DXGI_SWAP_CHAIN_DESC &desc, + const gs_init_data *data, DXGI_SWAP_EFFECT effect, UINT flags) +{ + const HWND hwnd = (HWND)data->window.hwnd; + const enum gs_color_space space = get_next_space(device, hwnd); + const gs_color_format format = + get_swap_format_from_space(space, data->format); + memset(&desc, 0, sizeof(desc)); desc.BufferDesc.Width = data->cx; desc.BufferDesc.Height = data->cy; - desc.BufferDesc.Format = ConvertGSTextureFormatView(data->format); + desc.BufferDesc.Format = ConvertGSTextureFormatView(format); desc.SampleDesc.Count = 1; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.BufferCount = data->num_backbuffers; - desc.OutputWindow = (HWND)data->window.hwnd; + desc.OutputWindow = hwnd; desc.Windowed = TRUE; desc.SwapEffect = effect; desc.Flags = flags; + + return space; } void gs_swap_chain::InitTarget(uint32_t cx, uint32_t cy) @@ -128,7 +185,7 @@ void gs_swap_chain::InitZStencilBuffer(uint32_t cx, uint32_t cy) } } -void gs_swap_chain::Resize(uint32_t cx, uint32_t cy) +void gs_swap_chain::Resize(uint32_t cx, uint32_t cy, gs_color_format format) { RECT clientRect; HRESULT hr; @@ -150,25 +207,40 @@ void gs_swap_chain::Resize(uint32_t cx, uint32_t cy) cy = clientRect.bottom; } - hr = swap->ResizeBuffers(swapDesc.BufferCount, cx, cy, - DXGI_FORMAT_UNKNOWN, swapDesc.Flags); + const DXGI_FORMAT dxgi_format = ConvertGSTextureFormatView(format); + hr = swap->ResizeBuffers(swapDesc.BufferCount, cx, cy, dxgi_format, + swapDesc.Flags); if (FAILED(hr)) throw HRError("Failed to resize swap buffers", hr); + ComQIPtr swap3 = swap; + if (swap3) { + const DXGI_COLOR_SPACE_TYPE dxgi_space = + (format == GS_RGBA16F) + ? DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 + : DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709; + hr = swap3->SetColorSpace1(dxgi_space); + if (FAILED(hr)) + throw HRError("Failed to set color space", hr); + } + target.dxgiFormatResource = ConvertGSTextureFormatResource(format); + target.dxgiFormatView = dxgi_format; + target.dxgiFormatViewLinear = ConvertGSTextureFormatViewLinear(format); InitTarget(cx, cy); InitZStencilBuffer(cx, cy); } void gs_swap_chain::Init() { + const gs_color_format format = get_swap_format_from_space( + get_next_space(device, hwnd), initData.format); + target.device = device; target.isRenderTarget = true; target.format = initData.format; - target.dxgiFormatResource = - ConvertGSTextureFormatResource(initData.format); - target.dxgiFormatView = ConvertGSTextureFormatView(initData.format); - target.dxgiFormatViewLinear = - ConvertGSTextureFormatViewLinear(initData.format); + target.dxgiFormatResource = ConvertGSTextureFormatResource(format); + target.dxgiFormatView = ConvertGSTextureFormatView(format); + target.dxgiFormatViewLinear = ConvertGSTextureFormatViewLinear(format); InitTarget(initData.cx, initData.cy); zs.device = device; @@ -180,7 +252,8 @@ void gs_swap_chain::Init() gs_swap_chain::gs_swap_chain(gs_device *device, const gs_init_data *data) : gs_obj(device, gs_type::gs_swap_chain), hwnd((HWND)data->window.hwnd), - initData(*data) + initData(*data), + space(GS_CS_SRGB) { DXGI_SWAP_EFFECT effect = DXGI_SWAP_EFFECT_DISCARD; UINT flags = 0; @@ -203,7 +276,7 @@ gs_swap_chain::gs_swap_chain(gs_device *device, const gs_init_data *data) } } - make_swap_desc(swapDesc, &initData, effect, flags); + space = make_swap_desc(device, swapDesc, &initData, effect, flags); HRESULT hr = device->factory->CreateSwapChain(device->device, &swapDesc, swap.Assign()); if (FAILED(hr)) @@ -1340,6 +1413,24 @@ gs_swapchain_t *device_swapchain_create(gs_device_t *device, return swap; } +static void device_resize_internal(gs_device_t *device, uint32_t cx, + uint32_t cy, gs_color_space space) +{ + try { + const gs_color_format format = get_swap_format_from_space( + space, device->curSwapChain->initData.format); + + device->context->OMSetRenderTargets(0, NULL, NULL); + device->curSwapChain->Resize(cx, cy, format); + device->curSwapChain->space = space; + device->curFramebufferInvalidate = true; + } catch (const HRError &error) { + blog(LOG_ERROR, "device_resize_internal (D3D11): %s (%08lX)", + error.str, error.hr); + LogD3D11ErrorDetails(error, device); + } +} + void device_resize(gs_device_t *device, uint32_t cx, uint32_t cy) { if (!device->curSwapChain) { @@ -1347,14 +1438,26 @@ void device_resize(gs_device_t *device, uint32_t cx, uint32_t cy) return; } - try { - device->context->OMSetRenderTargets(0, NULL, NULL); - device->curSwapChain->Resize(cx, cy); - device->curFramebufferInvalidate = true; - } catch (const HRError &error) { - blog(LOG_ERROR, "device_resize (D3D11): %s (%08lX)", error.str, - error.hr); - LogD3D11ErrorDetails(error, device); + const enum gs_color_space next_space = + get_next_space(device, device->curSwapChain->hwnd); + device_resize_internal(device, cx, cy, next_space); +} + +enum gs_color_space device_get_color_space(gs_device_t *device) +{ + return device->curColorSpace; +} + +void device_update_color_space(gs_device_t *device) +{ + if (device->curSwapChain) { + const enum gs_color_space next_space = + get_next_space(device, device->curSwapChain->hwnd); + if (device->curSwapChain->space != next_space) + device_resize_internal(device, 0, 0, next_space); + } else { + blog(LOG_WARNING, + "device_update_color_space (D3D11): No active swap"); } } @@ -1874,8 +1977,10 @@ gs_zstencil_t *device_get_zstencil_target(const gs_device_t *device) return device->curZStencilBuffer; } -void device_set_render_target(gs_device_t *device, gs_texture_t *tex, - gs_zstencil_t *zstencil) +static void device_set_render_target_internal(gs_device_t *device, + gs_texture_t *tex, + gs_zstencil_t *zstencil, + enum gs_color_space space) { if (device->curSwapChain) { if (!tex) @@ -1885,12 +1990,13 @@ void device_set_render_target(gs_device_t *device, gs_texture_t *tex, } if (device->curRenderTarget == tex && - device->curZStencilBuffer == zstencil) - return; + device->curZStencilBuffer == zstencil) { + device->curColorSpace = space; + } if (tex && tex->type != GS_TEXTURE_2D) { blog(LOG_ERROR, - "device_set_render_target (D3D11): texture is not a 2D texture"); + "device_set_render_target_internal (D3D11): texture is not a 2D texture"); return; } @@ -1898,12 +2004,27 @@ void device_set_render_target(gs_device_t *device, gs_texture_t *tex, if (device->curRenderTarget != tex2d || device->curRenderSide != 0 || device->curZStencilBuffer != zstencil) { device->curRenderTarget = tex2d; - device->curRenderSide = 0; device->curZStencilBuffer = zstencil; + device->curRenderSide = 0; + device->curColorSpace = space; device->curFramebufferInvalidate = true; } } +void device_set_render_target(gs_device_t *device, gs_texture_t *tex, + gs_zstencil_t *zstencil) +{ + device_set_render_target_internal(device, tex, zstencil, GS_CS_SRGB); +} + +void device_set_render_target_with_color_space(gs_device_t *device, + gs_texture_t *tex, + gs_zstencil_t *zstencil, + enum gs_color_space space) +{ + device_set_render_target_internal(device, tex, zstencil, space); +} + void device_set_cube_render_target(gs_device_t *device, gs_texture_t *tex, int side, gs_zstencil_t *zstencil) { @@ -1931,8 +2052,9 @@ void device_set_cube_render_target(gs_device_t *device, gs_texture_t *tex, if (device->curRenderTarget != tex2d || device->curRenderSide != side || device->curZStencilBuffer != zstencil) { device->curRenderTarget = tex2d; - device->curRenderSide = side; device->curZStencilBuffer = zstencil; + device->curRenderSide = side; + device->curColorSpace = GS_CS_SRGB; device->curFramebufferInvalidate = true; } } @@ -2177,11 +2299,14 @@ void device_load_swapchain(gs_device_t *device, gs_swapchain_t *swapchain) device->curSwapChain = swapchain; - if (is_cube) + if (is_cube) { device_set_cube_render_target(device, target, device->curRenderSide, zs); - else - device_set_render_target(device, target, zs); + } else { + const enum gs_color_space space = swapchain ? swapchain->space + : GS_CS_SRGB; + device_set_render_target_internal(device, target, zs, space); + } } void device_clear(gs_device_t *device, uint32_t clear_flags, @@ -2922,6 +3047,12 @@ extern "C" EXPORT bool device_p010_available(gs_device_t *device) return device->p010Supported; } +extern "C" EXPORT bool device_is_monitor_hdr(gs_device_t *device, void *monitor) +{ + const HMONITOR hMonitor = static_cast(monitor); + return screen_supports_hdr(device, hMonitor); +} + extern "C" EXPORT void device_debug_marker_begin(gs_device_t *, const char *markername, const float color[4]) diff --git a/libobs-d3d11/d3d11-subsystem.hpp b/libobs-d3d11/d3d11-subsystem.hpp index f693e52f4..34297e00b 100644 --- a/libobs-d3d11/d3d11-subsystem.hpp +++ b/libobs-d3d11/d3d11-subsystem.hpp @@ -820,6 +820,7 @@ struct gs_swap_chain : gs_obj { HWND hwnd; gs_init_data initData; DXGI_SWAP_CHAIN_DESC swapDesc = {}; + gs_color_space space; UINT presentFlags = 0; gs_texture_2d target; @@ -829,7 +830,7 @@ struct gs_swap_chain : gs_obj { void InitTarget(uint32_t cx, uint32_t cy); void InitZStencilBuffer(uint32_t cx, uint32_t cy); - void Resize(uint32_t cx, uint32_t cy); + void Resize(uint32_t cx, uint32_t cy, gs_color_format format); void Init(); void Rebuild(ID3D11Device *dev); @@ -991,6 +992,7 @@ struct gs_device { gs_texture_2d *curRenderTarget = nullptr; gs_zstencil_buffer *curZStencilBuffer = nullptr; int curRenderSide = 0; + enum gs_color_space curColorSpace = GS_CS_SRGB; bool curFramebufferSrgb = false; bool curFramebufferInvalidate = false; gs_texture *curTextures[GS_MAX_TEXTURES]; diff --git a/libobs-opengl/gl-cocoa.m b/libobs-opengl/gl-cocoa.m index 83a245fe6..81f63ca99 100644 --- a/libobs-opengl/gl-cocoa.m +++ b/libobs-opengl/gl-cocoa.m @@ -310,6 +310,11 @@ void device_present(gs_device_t *device) [device->plat->context makeCurrentContext]; } +bool device_is_monitor_hdr(gs_device_t *device, void *monitor) +{ + return false; +} + void gl_getclientsize(const struct gs_swap_chain *swap, uint32_t *width, uint32_t *height) { diff --git a/libobs-opengl/gl-nix.c b/libobs-opengl/gl-nix.c index e387eaa4b..e87eb007e 100644 --- a/libobs-opengl/gl-nix.c +++ b/libobs-opengl/gl-nix.c @@ -124,6 +124,11 @@ extern void device_present(gs_device_t *device) gl_vtable->device_present(device); } +extern bool device_is_monitor_hdr(gs_device_t *device, void *monitor) +{ + return false; +} + extern struct gs_texture *device_texture_create_from_dmabuf( gs_device_t *device, unsigned int width, unsigned int height, uint32_t drm_format, enum gs_color_format color_format, diff --git a/libobs-opengl/gl-subsystem.c b/libobs-opengl/gl-subsystem.c index e49c89788..d8969b329 100644 --- a/libobs-opengl/gl-subsystem.c +++ b/libobs-opengl/gl-subsystem.c @@ -340,6 +340,17 @@ void device_resize(gs_device_t *device, uint32_t cx, uint32_t cy) gl_update(device); } +enum gs_color_space device_get_color_space(gs_device_t *device) +{ + return device->cur_color_space; +} + +void device_update_color_space(gs_device_t *device) +{ + if (!device->cur_swap) + blog(LOG_WARNING, "device_display_change (GL): No active swap"); +} + void device_get_size(const gs_device_t *device, uint32_t *cx, uint32_t *cy) { if (device->cur_swap) { @@ -819,9 +830,9 @@ static bool attach_zstencil(struct fbo_info *fbo, gs_zstencil_t *zs) } static bool set_target(gs_device_t *device, gs_texture_t *tex, int side, - gs_zstencil_t *zs) + gs_zstencil_t *zs, enum gs_color_space space) { - struct fbo_info *fbo; + device->cur_color_space = space; if (device->cur_render_target == tex && device->cur_zstencil_buffer == zs && @@ -835,7 +846,7 @@ static bool set_target(gs_device_t *device, gs_texture_t *tex, int side, if (!tex) return set_current_fbo(device, NULL); - fbo = get_fbo_by_tex(tex); + struct fbo_info *const fbo = get_fbo_by_tex(tex); if (!fbo) return false; @@ -864,7 +875,7 @@ void device_set_render_target(gs_device_t *device, gs_texture_t *tex, } } - if (!set_target(device, tex, 0, zstencil)) + if (!set_target(device, tex, 0, zstencil, GS_CS_SRGB)) goto fail; return; @@ -873,6 +884,33 @@ fail: blog(LOG_ERROR, "device_set_render_target (GL) failed"); } +void device_set_render_target_with_color_space(gs_device_t *device, + gs_texture_t *tex, + gs_zstencil_t *zstencil, + enum gs_color_space space) +{ + if (tex) { + if (tex->type != GS_TEXTURE_2D) { + blog(LOG_ERROR, "Texture is not a 2D texture"); + goto fail; + } + + if (!tex->is_render_target) { + blog(LOG_ERROR, "Texture is not a render target"); + goto fail; + } + } + + if (!set_target(device, tex, 0, zstencil, space)) + goto fail; + + return; + +fail: + blog(LOG_ERROR, + "device_set_render_target_with_color_space (GL) failed"); +} + void device_set_cube_render_target(gs_device_t *device, gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil) { @@ -888,7 +926,7 @@ void device_set_cube_render_target(gs_device_t *device, gs_texture_t *cubetex, } } - if (!set_target(device, cubetex, side, zstencil)) + if (!set_target(device, cubetex, side, zstencil, GS_CS_SRGB)) goto fail; return; diff --git a/libobs-opengl/gl-subsystem.h b/libobs-opengl/gl-subsystem.h index 513ba179a..daddffffc 100644 --- a/libobs-opengl/gl-subsystem.h +++ b/libobs-opengl/gl-subsystem.h @@ -653,6 +653,7 @@ struct gs_device { gs_shader_t *cur_pixel_shader; gs_swapchain_t *cur_swap; struct gs_program *cur_program; + enum gs_color_space cur_color_space; struct gs_program *first_program; diff --git a/libobs-opengl/gl-windows.c b/libobs-opengl/gl-windows.c index 61c80c83c..d9fcd28a2 100644 --- a/libobs-opengl/gl-windows.c +++ b/libobs-opengl/gl-windows.c @@ -598,6 +598,11 @@ extern void gl_getclientsize(const struct gs_swap_chain *swap, uint32_t *width, } } +EXPORT bool device_is_monitor_hdr(gs_device_t *device, void *monitor) +{ + return false; +} + EXPORT bool device_gdi_texture_available(void) { return false; diff --git a/libobs/data/default.effect b/libobs/data/default.effect index db4cfe2ae..d5785469a 100644 --- a/libobs/data/default.effect +++ b/libobs/data/default.effect @@ -1,5 +1,6 @@ uniform float4x4 ViewProj; uniform texture2d image; +uniform float multiplier; sampler_state def_sampler { Filter = Linear; @@ -69,6 +70,48 @@ float4 PSDrawSrgbDecompress(VertInOut vert_in) : TARGET return rgba; } +float4 PSDrawMultiply(VertInOut vert_in) : TARGET +{ + float4 rgba = image.Sample(def_sampler, vert_in.uv); + rgba.rgb *= multiplier; + return rgba; +} + +float3 rec709_to_rec2020(float3 v) +{ + float r = dot(v, float3(0.6274040f, 0.3292820f, 0.0433136f)); + float g = dot(v, float3(0.0690970f, 0.9195400f, 0.0113612f)); + float b = dot(v, float3(0.0163916f, 0.0880132f, 0.8955950f)); + return float3(r, g, b); +} + +float3 rec2020_to_rec709(float3 v) +{ + float r = dot(v, float3(1.6604910, -0.5876411, -0.0728499)); + float g = dot(v, float3(-0.1245505, 1.1328999, -0.0083494)); + float b = dot(v, float3(-0.0181508, -0.1005789, 1.1187297)); + return float3(r, g, b); +} + +float reinhard_channel(float x) +{ + return x / (x + 1.0); +} + +float3 reinhard(float3 rgb) +{ + return float3(reinhard_channel(rgb.r), reinhard_channel(rgb.g), reinhard_channel(rgb.b)); +} + +float4 PSDrawTonemap(VertInOut vert_in) : TARGET +{ + float4 rgba = image.Sample(def_sampler, vert_in.uv); + rgba.rgb = rec709_to_rec2020(rgba.rgb); + rgba.rgb = reinhard(rgba.rgb); + rgba.rgb = rec2020_to_rec709(rgba.rgb); + return rgba; +} + technique Draw { pass @@ -104,3 +147,21 @@ technique DrawSrgbDecompress pixel_shader = PSDrawSrgbDecompress(vert_in); } } + +technique DrawMultiply +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSDrawMultiply(vert_in); + } +} + +technique DrawTonemap +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSDrawTonemap(vert_in); + } +} diff --git a/libobs/graphics/device-exports.h b/libobs/graphics/device-exports.h index 55ebaf2be..290d33f94 100644 --- a/libobs/graphics/device-exports.h +++ b/libobs/graphics/device-exports.h @@ -37,6 +37,8 @@ EXPORT void *device_get_device_obj(gs_device_t *device); EXPORT gs_swapchain_t *device_swapchain_create(gs_device_t *device, const struct gs_init_data *data); EXPORT void device_resize(gs_device_t *device, uint32_t x, uint32_t y); +EXPORT enum gs_color_space device_get_color_space(gs_device_t *device); +EXPORT void device_update_color_space(gs_device_t *device); EXPORT void device_get_size(const gs_device_t *device, uint32_t *x, uint32_t *y); EXPORT uint32_t device_get_width(const gs_device_t *device); @@ -104,6 +106,9 @@ EXPORT gs_texture_t *device_get_render_target(const gs_device_t *device); EXPORT gs_zstencil_t *device_get_zstencil_target(const gs_device_t *device); EXPORT void device_set_render_target(gs_device_t *device, gs_texture_t *tex, gs_zstencil_t *zstencil); +EXPORT void device_set_render_target_with_color_space( + gs_device_t *device, gs_texture_t *tex, gs_zstencil_t *zstencil, + enum gs_color_space space); EXPORT void device_set_cube_render_target(gs_device_t *device, gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil); diff --git a/libobs/graphics/graphics-imports.c b/libobs/graphics/graphics-imports.c index 7b92b2994..599a34ba8 100644 --- a/libobs/graphics/graphics-imports.c +++ b/libobs/graphics/graphics-imports.c @@ -53,6 +53,8 @@ bool load_graphics_imports(struct gs_exports *exports, void *module, GRAPHICS_IMPORT(device_get_device_obj); GRAPHICS_IMPORT(device_swapchain_create); GRAPHICS_IMPORT(device_resize); + GRAPHICS_IMPORT(device_get_color_space); + GRAPHICS_IMPORT(device_update_color_space); GRAPHICS_IMPORT(device_get_size); GRAPHICS_IMPORT(device_get_width); GRAPHICS_IMPORT(device_get_height); @@ -81,6 +83,7 @@ bool load_graphics_imports(struct gs_exports *exports, void *module, GRAPHICS_IMPORT(device_get_render_target); GRAPHICS_IMPORT(device_get_zstencil_target); GRAPHICS_IMPORT(device_set_render_target); + GRAPHICS_IMPORT(device_set_render_target_with_color_space); GRAPHICS_IMPORT(device_set_cube_render_target); GRAPHICS_IMPORT(device_enable_framebuffer_srgb); GRAPHICS_IMPORT(device_framebuffer_srgb_enabled); @@ -192,6 +195,8 @@ bool load_graphics_imports(struct gs_exports *exports, void *module, GRAPHICS_IMPORT_OPTIONAL(device_nv12_available); GRAPHICS_IMPORT_OPTIONAL(device_p010_available); + GRAPHICS_IMPORT(device_is_monitor_hdr); + GRAPHICS_IMPORT(device_debug_marker_begin); GRAPHICS_IMPORT(device_debug_marker_end); diff --git a/libobs/graphics/graphics-internal.h b/libobs/graphics/graphics-internal.h index 706632478..8b64d2106 100644 --- a/libobs/graphics/graphics-internal.h +++ b/libobs/graphics/graphics-internal.h @@ -38,6 +38,8 @@ struct gs_exports { gs_swapchain_t *(*device_swapchain_create)( gs_device_t *device, const struct gs_init_data *data); void (*device_resize)(gs_device_t *device, uint32_t x, uint32_t y); + enum gs_color_space (*device_get_color_space)(gs_device_t *device); + void (*device_update_color_space)(gs_device_t *device); void (*device_get_size)(const gs_device_t *device, uint32_t *x, uint32_t *y); uint32_t (*device_get_width)(const gs_device_t *device); @@ -103,6 +105,9 @@ struct gs_exports { gs_zstencil_t *(*device_get_zstencil_target)(const gs_device_t *device); void (*device_set_render_target)(gs_device_t *device, gs_texture_t *tex, gs_zstencil_t *zstencil); + void (*device_set_render_target_with_color_space)( + gs_device_t *device, gs_texture_t *tex, gs_zstencil_t *zstencil, + enum gs_color_space space); void (*device_set_cube_render_target)(gs_device_t *device, gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil); @@ -268,6 +273,8 @@ struct gs_exports { bool (*device_nv12_available)(gs_device_t *device); bool (*device_p010_available)(gs_device_t *device); + bool (*device_is_monitor_hdr)(gs_device_t *device, void *monitor); + void (*device_debug_marker_begin)(gs_device_t *device, const char *markername, const float color[4]); diff --git a/libobs/graphics/graphics.c b/libobs/graphics/graphics.c index e931fe8d5..7fae78fc8 100644 --- a/libobs/graphics/graphics.c +++ b/libobs/graphics/graphics.c @@ -1308,6 +1308,16 @@ void gs_resize(uint32_t x, uint32_t y) graphics->exports.device_resize(graphics->device, x, y); } +void gs_update_color_space(void) +{ + graphics_t *graphics = thread_graphics; + + if (!gs_valid("gs_update_color_space")) + return; + + graphics->exports.device_update_color_space(graphics->device); +} + void gs_get_size(uint32_t *x, uint32_t *y) { graphics_t *graphics = thread_graphics; @@ -1715,6 +1725,16 @@ gs_shader_t *gs_get_pixel_shader(void) return graphics->exports.device_get_pixel_shader(graphics->device); } +enum gs_color_space gs_get_color_space(void) +{ + graphics_t *graphics = thread_graphics; + + if (!gs_valid("gs_get_color_space")) + return GS_CS_SRGB; + + return graphics->exports.device_get_color_space(graphics->device); +} + gs_texture_t *gs_get_render_target(void) { graphics_t *graphics = thread_graphics; @@ -1746,6 +1766,19 @@ void gs_set_render_target(gs_texture_t *tex, gs_zstencil_t *zstencil) zstencil); } +void gs_set_render_target_with_color_space(gs_texture_t *tex, + gs_zstencil_t *zstencil, + enum gs_color_space space) +{ + graphics_t *graphics = thread_graphics; + + if (!gs_valid("gs_set_render_target_with_color_space")) + return; + + graphics->exports.device_set_render_target_with_color_space( + graphics->device, tex, zstencil, space); +} + void gs_set_cube_render_target(gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil) { @@ -2807,6 +2840,15 @@ bool gs_p010_available(void) thread_graphics->device); } +bool gs_is_monitor_hdr(void *monitor) +{ + if (!gs_valid("gs_is_monitor_hdr")) + return false; + + return thread_graphics->exports.device_is_monitor_hdr( + thread_graphics->device, monitor); +} + void gs_debug_marker_begin(const float color[4], const char *markername) { if (!gs_valid("gs_debug_marker_begin")) diff --git a/libobs/graphics/graphics.h b/libobs/graphics/graphics.h index 35d2c8687..aa11dc874 100644 --- a/libobs/graphics/graphics.h +++ b/libobs/graphics/graphics.h @@ -79,6 +79,12 @@ enum gs_color_format { GS_RG16, }; +enum gs_color_space { + GS_CS_SRGB, /* SDR */ + GS_CS_709_EXTENDED, /* Canvas, Mac EDR (HDR) */ + GS_CS_709_SCRGB, /* 1.0 = 80 nits, Windows/Linux HDR */ +}; + enum gs_zstencil_format { GS_ZS_NONE, GS_Z16, @@ -461,6 +467,9 @@ EXPORT gs_texrender_t *gs_texrender_create(enum gs_color_format format, EXPORT void gs_texrender_destroy(gs_texrender_t *texrender); EXPORT bool gs_texrender_begin(gs_texrender_t *texrender, uint32_t cx, uint32_t cy); +EXPORT bool gs_texrender_begin_with_color_space(gs_texrender_t *texrender, + uint32_t cx, uint32_t cy, + enum gs_color_space space); EXPORT void gs_texrender_end(gs_texrender_t *texrender); EXPORT void gs_texrender_reset(gs_texrender_t *texrender); EXPORT gs_texture_t *gs_texrender_get_texture(const gs_texrender_t *texrender); @@ -634,6 +643,7 @@ EXPORT void gs_reset_blend_state(void); EXPORT gs_swapchain_t *gs_swapchain_create(const struct gs_init_data *data); EXPORT void gs_resize(uint32_t x, uint32_t y); +EXPORT void gs_update_color_space(void); EXPORT void gs_get_size(uint32_t *x, uint32_t *y); EXPORT uint32_t gs_get_width(void); EXPORT uint32_t gs_get_height(void); @@ -689,10 +699,14 @@ EXPORT void gs_load_default_samplerstate(bool b_3d, int unit); EXPORT gs_shader_t *gs_get_vertex_shader(void); EXPORT gs_shader_t *gs_get_pixel_shader(void); +EXPORT enum gs_color_space gs_get_color_space(void); EXPORT gs_texture_t *gs_get_render_target(void); EXPORT gs_zstencil_t *gs_get_zstencil_target(void); EXPORT void gs_set_render_target(gs_texture_t *tex, gs_zstencil_t *zstencil); +EXPORT void gs_set_render_target_with_color_space(gs_texture_t *tex, + gs_zstencil_t *zstencil, + enum gs_color_space space); EXPORT void gs_set_cube_render_target(gs_texture_t *cubetex, int side, gs_zstencil_t *zstencil); @@ -837,6 +851,8 @@ EXPORT bool gs_timer_range_get_data(gs_timer_range_t *range, bool *disjoint, EXPORT bool gs_nv12_available(void); EXPORT bool gs_p010_available(void); +EXPORT bool gs_is_monitor_hdr(void *monitor); + #define GS_USE_DEBUG_MARKERS 0 #if GS_USE_DEBUG_MARKERS static const float GS_DEBUG_COLOR_DEFAULT[] = {0.5f, 0.5f, 0.5f, 1.0f}; @@ -1039,6 +1055,20 @@ gs_generalize_format(enum gs_color_format format) } } +static inline enum gs_color_format +gs_get_format_from_space(enum gs_color_space space) +{ + switch (space) { + case GS_CS_SRGB: + break; + case GS_CS_709_EXTENDED: + case GS_CS_709_SCRGB: + return GS_RGBA16F; + } + + return GS_RGBA; +} + static inline uint32_t gs_get_total_levels(uint32_t width, uint32_t height, uint32_t depth) { diff --git a/libobs/graphics/texture-render.c b/libobs/graphics/texture-render.c index e06b21f24..476073178 100644 --- a/libobs/graphics/texture-render.c +++ b/libobs/graphics/texture-render.c @@ -26,6 +26,7 @@ struct gs_texture_render { gs_texture_t *target, *prev_target; gs_zstencil_t *zs, *prev_zs; + enum gs_color_space prev_space; uint32_t cx, cy; @@ -88,6 +89,13 @@ static bool texrender_resetbuffer(gs_texrender_t *texrender, uint32_t cx, } bool gs_texrender_begin(gs_texrender_t *texrender, uint32_t cx, uint32_t cy) +{ + return gs_texrender_begin_with_color_space(texrender, cx, cy, + GS_CS_SRGB); +} + +bool gs_texrender_begin_with_color_space(gs_texrender_t *texrender, uint32_t cx, + uint32_t cy, enum gs_color_space space) { if (!texrender || texrender->rendered) return false; @@ -109,7 +117,9 @@ bool gs_texrender_begin(gs_texrender_t *texrender, uint32_t cx, uint32_t cy) texrender->prev_target = gs_get_render_target(); texrender->prev_zs = gs_get_zstencil_target(); - gs_set_render_target(texrender->target, texrender->zs); + texrender->prev_space = gs_get_color_space(); + gs_set_render_target_with_color_space(texrender->target, texrender->zs, + space); gs_set_viewport(0, 0, texrender->cx, texrender->cy); @@ -121,7 +131,9 @@ void gs_texrender_end(gs_texrender_t *texrender) if (!texrender) return; - gs_set_render_target(texrender->prev_target, texrender->prev_zs); + gs_set_render_target_with_color_space(texrender->prev_target, + texrender->prev_zs, + texrender->prev_space); gs_matrix_pop(); gs_projection_pop(); diff --git a/libobs/obs-display.c b/libobs/obs-display.c index 0237f41e7..9f12a2b47 100644 --- a/libobs/obs-display.c +++ b/libobs/obs-display.c @@ -124,6 +124,18 @@ void obs_display_resize(obs_display_t *display, uint32_t cx, uint32_t cy) pthread_mutex_unlock(&display->draw_info_mutex); } +void obs_display_update_color_space(obs_display_t *display) +{ + if (!display) + return; + + pthread_mutex_lock(&display->draw_info_mutex); + + display->update_color_space = true; + + pthread_mutex_unlock(&display->draw_info_mutex); +} + void obs_display_add_draw_callback(obs_display_t *display, void (*draw)(void *param, uint32_t cx, uint32_t cy), @@ -155,7 +167,8 @@ void obs_display_remove_draw_callback(obs_display_t *display, } static inline void render_display_begin(struct obs_display *display, - uint32_t cx, uint32_t cy) + uint32_t cx, uint32_t cy, + bool update_color_space) { struct vec4 clear_color; @@ -165,11 +178,16 @@ static inline void render_display_begin(struct obs_display *display, gs_resize(cx, cy); display->cx = cx; display->cy = cy; + } else if (update_color_space) { + gs_update_color_space(); } gs_begin_scene(); - vec4_from_rgba(&clear_color, display->background_color); + if (gs_get_color_space() == GS_CS_SRGB) + vec4_from_rgba(&clear_color, display->background_color); + else + vec4_from_rgba_srgb(&clear_color, display->background_color); clear_color.w = 1.0f; gs_clear(GS_CLEAR_COLOR | GS_CLEAR_DEPTH | GS_CLEAR_STENCIL, @@ -191,6 +209,7 @@ static inline void render_display_end() void render_display(struct obs_display *display) { uint32_t cx, cy; + bool update_color_space; if (!display || !display->enabled) return; @@ -203,12 +222,15 @@ void render_display(struct obs_display *display) cx = display->next_cx; cy = display->next_cy; + update_color_space = display->update_color_space; + + display->update_color_space = false; pthread_mutex_unlock(&display->draw_info_mutex); /* -------------------------------------------- */ - render_display_begin(display, cx, cy); + render_display_begin(display, cx, cy, update_color_space); pthread_mutex_lock(&display->draw_callbacks_mutex); diff --git a/libobs/obs-internal.h b/libobs/obs-internal.h index f7f7e3468..356f69798 100644 --- a/libobs/obs-internal.h +++ b/libobs/obs-internal.h @@ -204,6 +204,7 @@ extern void obs_view_free(struct obs_view *view); /* displays */ struct obs_display { + bool update_color_space; bool enabled; uint32_t cx, cy; uint32_t next_cx, next_cy; @@ -255,6 +256,7 @@ struct obs_core_video { #endif gs_texture_t *render_texture; gs_texture_t *output_texture; + enum gs_color_space render_space; bool texture_rendered; bool textures_copied[NUM_TEXTURES]; bool texture_converted; @@ -321,6 +323,7 @@ struct obs_core_video { gs_effect_t *deinterlace_yadif_2x_effect; struct obs_video_info ovi; + uint32_t sdr_white_level; pthread_mutex_t task_mutex; struct circlebuf tasks; diff --git a/libobs/obs-video.c b/libobs/obs-video.c index a966ed0d5..3b5343d72 100644 --- a/libobs/obs-video.c +++ b/libobs/obs-video.c @@ -133,7 +133,8 @@ static inline void render_main_texture(struct obs_core_video *video) struct vec4 clear_color; vec4_set(&clear_color, 0.0f, 0.0f, 0.0f, 0.0f); - gs_set_render_target(video->render_texture, NULL); + gs_set_render_target_with_color_space(video->render_texture, NULL, + video->render_space); gs_clear(GS_CLEAR_COLOR, &clear_color, 1.0f, 0); set_render_size(video->base_width, video->base_height); diff --git a/libobs/obs.c b/libobs/obs.c index 7f5fd25bb..fd4efbd23 100644 --- a/libobs/obs.c +++ b/libobs/obs.c @@ -268,19 +268,24 @@ static bool obs_init_textures(struct obs_video_info *ovi) } } + enum gs_color_format format = GS_RGBA; + enum gs_color_space space = GS_CS_SRGB; + video->render_texture = gs_texture_create(ovi->base_width, - ovi->base_height, GS_RGBA, 1, + ovi->base_height, format, 1, NULL, GS_RENDER_TARGET); if (!video->render_texture) success = false; video->output_texture = gs_texture_create(ovi->output_width, - ovi->output_height, GS_RGBA, - 1, NULL, GS_RENDER_TARGET); + ovi->output_height, format, 1, + NULL, GS_RENDER_TARGET); if (!video->output_texture) success = false; - if (!success) { + if (success) { + video->render_space = space; + } else { for (size_t i = 0; i < NUM_TEXTURES; i++) { for (size_t c = 0; c < NUM_CHANNELS; c++) { if (video->copy_surfaces[i][c]) { @@ -1301,6 +1306,20 @@ bool obs_get_video_info(struct obs_video_info *ovi) return true; } +float obs_get_video_sdr_white_level(void) +{ + struct obs_core_video *video = &obs->video; + return video->graphics ? video->sdr_white_level : 300.f; +} + +void obs_set_video_sdr_white_level(float sdr_white_level) +{ + struct obs_core_video *video = &obs->video; + assert(video->graphics); + + video->sdr_white_level = (uint32_t)sdr_white_level; +} + bool obs_get_audio_info(struct obs_audio_info *oai) { struct obs_core_audio *audio = &obs->audio; @@ -1793,18 +1812,37 @@ static void obs_render_main_texture_internal(enum gs_blend_type src_c, if (!video->texture_rendered) return; + const enum gs_color_space source_space = video->render_space; + const enum gs_color_space current_space = gs_get_color_space(); + const char *tech_name = "Draw"; + float multiplier = 1.0f; + if ((current_space == GS_CS_SRGB) && + (source_space == GS_CS_709_EXTENDED)) { + tech_name = "DrawTonemap"; + } else if (current_space == GS_CS_709_SCRGB) { + tech_name = "DrawMultiply"; + multiplier = obs_get_video_sdr_white_level() / 80.0f; + } + + const bool previous = gs_framebuffer_srgb_enabled(); + gs_enable_framebuffer_srgb(true); + tex = video->render_texture; effect = obs_get_base_effect(OBS_EFFECT_DEFAULT); param = gs_effect_get_param_by_name(effect, "image"); - gs_effect_set_texture(param, tex); + gs_effect_set_texture_srgb(param, tex); + param = gs_effect_get_param_by_name(effect, "multiplier"); + gs_effect_set_float(param, multiplier); gs_blend_state_push(); gs_blend_function_separate(src_c, dest_c, src_a, dest_a); - while (gs_effect_loop(effect, "Draw")) + while (gs_effect_loop(effect, tech_name)) gs_draw_sprite(tex, 0, 0, 0); gs_blend_state_pop(); + + gs_enable_framebuffer_srgb(previous); } void obs_render_main_texture(void) diff --git a/libobs/obs.h b/libobs/obs.h index 763ede1d4..5d5d66177 100644 --- a/libobs/obs.h +++ b/libobs/obs.h @@ -410,6 +410,12 @@ 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 */ +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 current audio settings, returns false if no audio */ EXPORT bool obs_get_audio_info(struct obs_audio_info *oai); @@ -881,6 +887,9 @@ EXPORT void obs_display_destroy(obs_display_t *display); EXPORT void obs_display_resize(obs_display_t *display, uint32_t cx, uint32_t cy); +/** Updates the color space of this display */ +EXPORT void obs_display_update_color_space(obs_display_t *display); + /** * Adds a draw callback for this display context *