Browse Source

libobs/graphics: Add color space support

jpark37 3 years ago
parent
commit
abddfead2f

+ 32 - 1
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,10 +648,20 @@ 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
 
 ---------------------
 

+ 158 - 33
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<IDXGIAdapter> adapter;
+	ComPtr<IDXGIOutput> output;
+	ComPtr<IDXGIOutput6> 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<IDXGISwapChain3> 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,

+ 3 - 1
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];

+ 43 - 5
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;

+ 1 - 0
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;
 

+ 5 - 0
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);

+ 3 - 0
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);

+ 5 - 0
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);

+ 33 - 0
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)
 {

+ 28 - 0
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);
 
@@ -1039,6 +1053,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)
 {

+ 14 - 2
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();