diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp index cd30a323d2a9..ff2e428547a2 100644 --- a/core/config/project_settings.cpp +++ b/core/config/project_settings.cpp @@ -1643,6 +1643,8 @@ ProjectSettings::ProjectSettings() { GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_width_override", PROPERTY_HINT_RANGE, "0,7680,1,or_greater"), 0); // 8K resolution GLOBAL_DEF(PropertyInfo(Variant::INT, "display/window/size/window_height_override", PROPERTY_HINT_RANGE, "0,4320,1,or_greater"), 0); // 8K resolution + GLOBAL_DEF("display/window/hdr/request_hdr_output", false); + GLOBAL_DEF("display/window/energy_saving/keep_screen_on", true); GLOBAL_DEF("animation/warnings/check_invalid_track_paths", true); GLOBAL_DEF("animation/warnings/check_angle_interpolation_type_conflicting", true); diff --git a/doc/classes/DisplayServer.xml b/doc/classes/DisplayServer.xml index a7cc06698162..4835232d28df 100644 --- a/doc/classes/DisplayServer.xml +++ b/doc/classes/DisplayServer.xml @@ -2131,6 +2131,44 @@ Returns the current value of the given window's [param flag]. + + + + + Returns the current maximum luminance in nits (cd/m²) for HDR content for the window specified by [param window_id]. + If max luminance is being auto adjusted based on the display's capabilities, this will return that value. + Otherwise, it will return the value set by [method window_set_hdr_output_max_luminance]. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + + + + + + Returns the current reference luminance in nits (cd/m²) for HDR content for the window specified by [param window_id]. + If reference luminance is being auto adjusted based on the display's capabilities, this will return that value. + Otherwise, it will return the value set by [method window_set_hdr_output_reference_luminance]. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + + + + + + Returns the maximum luminance in nits (cd/m²) set for HDR content for the window specified by [param window_id]. + Negative values indicate that the value is being auto adjusted based on the display's capabilities. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + + + + + + Returns the SDR reference luminance in nits (cd/m²) set for HDR content for the window specified by [param window_id]. + Negative values indicate that the value is being auto adjusted based on the display's capabilities. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + @@ -2161,6 +2199,16 @@ [b]Note:[/b] This method is implemented on Android, Linux (X11/Wayland), macOS, and Windows. + + + + + Returns the maximum value for linear color components that can be displayed in HDR mode for the window specified by [param window_id]. + Use this to scale HDR content to max out the display's brightness, for example lasers or other bright effects. + Will return 1.0 if HDR is not enabled or not supported. + [b]Note:[/b] You will need to convert sRGB colors to linear before multiplying by this value to get accurate results. + + @@ -2226,6 +2274,32 @@ Returns [code]true[/code] if the window specified by [param window_id] is focused. + + + + + Returns whether HDR output is currently enabled for the window specified by [param window_id]. + This value may change dynamically based on system settings, display capabilities, and which display the window is currently on. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + + + + + + Returns whether HDR output is requested for the window specified by [param window_id]. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + + + + + + Returns whether the window specified by [param window_id] supports HDR output. + This depends on the platform, display capabilities, system settings, and the display the window is currently on. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + + @@ -2261,6 +2335,18 @@ Makes the window specified by [param window_id] request attention, which is materialized by the window title and taskbar entry blinking until the window is focused. This usually has no visible effect if the window is currently focused. The exact behavior varies depending on the operating system. + + + + + + Sets whether HDR output should be requested for the window specified by [param window_id], falling back to SDR if not supported, and automatically switching between HDR and SDR as the window moves between displays, display capabilities change, or system settings are modified. + Only available on platforms that support HDR output, have HDR enabled in the system settings, and have a compatible display connected. + [b]Note:[/b] Some integrated GPUs have poor support for HDR output, even when they claim to support it. If you experience issues, try disabling this setting. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + [b]Note:[/b] Requires support by the rendering device. + + @@ -2308,6 +2394,34 @@ Enables or disables the given window's given [param flag]. + + + + + + Sets the maximum luminance in nits (cd/m²) for HDR content for the window specified by [param window_id]. + Set to a negative value to automatically use the display's maximum capability. + By default, this is set to [code]-1[/code]. + This controls the maximum brightness of bright elements in HDR content, typically by scaling down highlights within the scene. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + [b]Note:[/b] Requires support by the rendering device. + [b]Note:[/b] On some platforms, setting a custom max luminance is not supported and will always be auto adjusted based on the display's capabilities. + + + + + + + + Sets the SDR reference luminance in nits (cd/m²) for HDR content for the window specified by [param window_id]. + Set to a negative value to automatically adjust to the reference level set by the OS or window manager. + By default, this is set to [code]-1[/code]. + This controls the brightness of SDR content (such as UI) when HDR is enabled. + [b]Note:[/b] Requires support for [constant FEATURE_HDR]. + [b]Note:[/b] Requires support by the rendering device. + [b]Note:[/b] On some platforms, setting a custom reference luminance is not supported and will always be auto adjusted based on the display's capabilities. + + @@ -2620,6 +2734,9 @@ Display server supports interaction with screen reader or Braille display. [b]Linux (X11/Wayland), macOS, Windows[/b] + + Display server supports HDR output. [b]Windows, Linux (Wayland)[/b] + Unknown or custom role. diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml index a36f24403b06..7fb3013f244a 100644 --- a/doc/classes/Environment.xml +++ b/doc/classes/Environment.xml @@ -315,6 +315,13 @@ The maximum number of steps for screen-space reflections. Higher values are slower. + + Increasing [member tonemap_agx_contrast] will make dark values darker and bright values brighter. Produces a higher quality result than [member adjustment_contrast] without any additional performance cost, but is only available when using the [constant TONE_MAPPER_AGX] tonemapper. + + + The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, it is recommended to set [member tonemap_white] to at least [code]6.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. [member tonemap_agx_white] is the same as [member tonemap_white], but is only effective with the [constant TONE_MAPPER_AGX] tonemapper. See also [member tonemap_exposure]. + [b]Note:[/b] A value of [code]2.0[/code] will always be used when using the Mobile renderer with [member Viewport.use_hdr_2d] disabled and [member tonemap_agx_white] will be ignored. + Adjusts the brightness of values before they are provided to the tonemapper. Higher [member tonemap_exposure] values result in a brighter image. See also [member tonemap_white]. [b]Note:[/b] Values provided to the tonemapper will also be multiplied by [code]2.0[/code] and [code]1.8[/code] for [constant TONE_MAPPER_FILMIC] and [constant TONE_MAPPER_ACES] respectively to produce a similar apparent brightness as [constant TONE_MAPPER_LINEAR]. @@ -323,8 +330,8 @@ The tonemapping mode to use. Tonemapping is the process that "converts" HDR values to be suitable for rendering on an LDR display. (Godot doesn't support rendering on HDR displays yet.) - The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, recommended values are between [code]6.0[/code] and [code]8.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. See also [member tonemap_exposure]. - [b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR] or [constant TONE_MAPPER_AGX]. + The white reference value for tonemapping, which indicates where bright white is located in the scale of values provided to the tonemapper. For photorealistic lighting, it is recommended to set [member tonemap_white] to at least [code]6.0[/code]. Higher values result in less blown out highlights, but may make the scene appear lower contrast. [member tonemap_agx_white] will be used instead when using the [constant TONE_MAPPER_AGX] tonemapper. See also [member tonemap_exposure]. + [b]Note:[/b] [member tonemap_white] must be set to [code]2.0[/code] or lower on the Mobile renderer to produce bright images. The [Color] of the volumetric fog when interacting with lights. Mist and fog have an albedo close to [code]Color(1, 1, 1, 1)[/code] while smoke has a darker albedo. @@ -425,14 +432,15 @@ Uses a film-like tonemapping curve to prevent clipping of bright values and provide better contrast than [constant TONE_MAPPER_REINHARDT]. Slightly slower than [constant TONE_MAPPER_REINHARDT]. + [b]Note:[/b] This tonemapper does not support HDR output and clips output to the [code]0.0-1.0[/code] range. A different tonemapper should be used when rendering to an HDR display. Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant TONE_MAPPER_FILMIC]. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x. + [b]Note:[/b] This tonemapper does not support HDR output and clips output to the [code]0.0-1.0[/code] range. A different tonemapper should be used when rendering to an HDR display. - Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. - [b]Note:[/b] [member tonemap_white] is fixed at a value of [code]16.29[/code], which makes [constant TONE_MAPPER_AGX] unsuitable for use with the Mobile rendering method. + Uses an adjustable film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. Adds the glow effect to the scene. diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index a5a9f6aa60e1..57a8afc2336d 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -952,6 +952,11 @@ The default screen orientation to use on mobile devices. See [enum DisplayServer.ScreenOrientation] for possible values. [b]Note:[/b] When set to a portrait orientation, this project setting does not flip the project resolution's width and height automatically. Instead, you have to set [member display/window/size/viewport_width] and [member display/window/size/viewport_height] accordingly. + + If [code]true[/code], requests HDR output for the main window and editor, falling back to SDR if not supported, and automatically switching between HDR and SDR as the window moves between displays, display capabilities change, or system settings are modified. + Only available on platforms that support HDR output, have HDR enabled in the system settings, and have a compatible display connected. + [b]Note:[/b] Some integrated GPUs have poor support for HDR output, even when they claim to support it. If you experience issues, try disabling this setting. + If [code]true[/code], iOS devices that support high refresh rate/"ProMotion" will be allowed to render at up to 120 frames per second. diff --git a/doc/classes/RenderingDevice.xml b/doc/classes/RenderingDevice.xml index 7b4fb65ac93e..b11bc335a310 100644 --- a/doc/classes/RenderingDevice.xml +++ b/doc/classes/RenderingDevice.xml @@ -2504,6 +2504,9 @@ Support for 32-bit image atomic operations. + + Features support for high dynamic range (HDR) output. + Maximum number of uniform sets that can be bound at a given time. diff --git a/doc/classes/RenderingServer.xml b/doc/classes/RenderingServer.xml index b3f36e6e00e7..4aaf0931a75c 100644 --- a/doc/classes/RenderingServer.xml +++ b/doc/classes/RenderingServer.xml @@ -1523,6 +1523,14 @@ Sets the variables to be used with the "tonemap" post-process effect. See [Environment] for more details. + + + + + + See [member Environment.tonemap_agx_contrast] for more details. + + @@ -5494,14 +5502,15 @@ Uses a film-like tonemapping curve to prevent clipping of bright values and provide better contrast than [constant ENV_TONE_MAPPER_REINHARD]. Slightly slower than [constant ENV_TONE_MAPPER_REINHARD]. + [b]Note:[/b] This tonemapper does not support HDR output and clips output to the [code]0.0-1.0[/code] range. A different tonemapper should be used when rendering to an HDR display. Uses a high-contrast film-like tonemapping curve and desaturates bright values for a more realistic appearance. Slightly slower than [constant ENV_TONE_MAPPER_FILMIC]. [b]Note:[/b] This tonemapping operator is called "ACES Fitted" in Godot 3.x. + [b]Note:[/b] This tonemapper does not support HDR output and clips output to the [code]0.0-1.0[/code] range. A different tonemapper should be used when rendering to an HDR display. - Uses a film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. - [b]Note:[/b] [member Environment.tonemap_white] is fixed at a value of [code]16.29[/code], which makes [constant ENV_TONE_MAPPER_AGX] unsuitable for use with the Mobile rendering method. + Uses an adjustable film-like tonemapping curve and desaturates bright values for a more realistic appearance. Better than other tonemappers at maintaining the hue of colors as they become brighter. The slowest tonemapping option. Lowest quality of roughness filter for screen-space reflections. Rough materials will not have blurrier screen-space reflections compared to smooth (non-rough) materials. This is the fastest option. diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml index 85b502544ec4..c9e02a10d3d3 100644 --- a/doc/classes/Window.xml +++ b/doc/classes/Window.xml @@ -120,6 +120,15 @@ Returns layout direction and text writing direction. + + + + Returns the maximum value for linear color components that can be displayed in HDR mode for this window. + Use this to scale HDR content to max out the display's brightness, for example lasers or other bright effects. + Will return 1.0 if HDR is not enabled or not supported. + [b]Note:[/b] You will need to convert sRGB colors to linear before multiplying by this value to get accurate results. + + @@ -341,6 +350,13 @@ Returns [code]true[/code] if the window is currently embedded in another window. + + + + Returns [code]true[/code] if the window supports HDR output. + This depends on the platform, display capabilities, system settings, and the display the window is currently on. + + @@ -628,6 +644,11 @@ If [code]true[/code], native window will be used regardless of parent viewport and project settings. + + If [code]true[/code], requests HDR output for the window, falling back to SDR if not supported, and automatically switching between HDR and SDR as the window moves between displays, display capabilities change, or system settings are modified. + Only available on platforms that support HDR output, have HDR enabled in the system settings, and have a compatible display connected. + [b]Note:[/b] Some integrated GPUs have poor support for HDR output, even when they claim to support it. If you experience issues, try disabling this setting. + Specifies the initial type of position for the [Window]. diff --git a/drivers/d3d12/rendering_context_driver_d3d12.cpp b/drivers/d3d12/rendering_context_driver_d3d12.cpp index eabfbe71d68d..60fecb2fdfed 100644 --- a/drivers/d3d12/rendering_context_driver_d3d12.cpp +++ b/drivers/d3d12/rendering_context_driver_d3d12.cpp @@ -282,6 +282,52 @@ DisplayServer::VSyncMode RenderingContextDriverD3D12::surface_get_vsync_mode(Sur return surface->vsync_mode; } +void RenderingContextDriverD3D12::surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_output = p_enabled; + surface->needs_resize = true; +} + +bool RenderingContextDriverD3D12::surface_get_hdr_output_enabled(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_output; +} + +void RenderingContextDriverD3D12::surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_reference_luminance = p_reference_luminance; +} + +float RenderingContextDriverD3D12::surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_reference_luminance; +} + +void RenderingContextDriverD3D12::surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_max_luminance = p_max_luminance; +} + +float RenderingContextDriverD3D12::surface_get_hdr_output_max_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_max_luminance; +} + +void RenderingContextDriverD3D12::surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_linear_luminance_scale = p_linear_luminance_scale; +} + +float RenderingContextDriverD3D12::surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_linear_luminance_scale; +} + +float RenderingContextDriverD3D12::surface_get_hdr_output_max_value(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return MAX(surface->hdr_max_luminance / MAX(surface->hdr_reference_luminance, 1.0f), 1.0f); +} + uint32_t RenderingContextDriverD3D12::surface_get_width(SurfaceID p_surface) const { Surface *surface = (Surface *)(p_surface); return surface->width; diff --git a/drivers/d3d12/rendering_context_driver_d3d12.h b/drivers/d3d12/rendering_context_driver_d3d12.h index b8c2d6d2788b..6548c7c98b19 100644 --- a/drivers/d3d12/rendering_context_driver_d3d12.h +++ b/drivers/d3d12/rendering_context_driver_d3d12.h @@ -91,6 +91,15 @@ class RenderingContextDriverD3D12 : public RenderingContextDriver { virtual void surface_set_size(SurfaceID p_surface, uint32_t p_width, uint32_t p_height) override; virtual void surface_set_vsync_mode(SurfaceID p_surface, DisplayServer::VSyncMode p_vsync_mode) override; virtual DisplayServer::VSyncMode surface_get_vsync_mode(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) override; + virtual bool surface_get_hdr_output_enabled(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) override; + virtual float surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) override; + virtual float surface_get_hdr_output_max_luminance(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) override; + virtual float surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const override; + virtual float surface_get_hdr_output_max_value(SurfaceID p_surface) const override; virtual uint32_t surface_get_width(SurfaceID p_surface) const override; virtual uint32_t surface_get_height(SurfaceID p_surface) const override; virtual void surface_set_needs_resize(SurfaceID p_surface, bool p_needs_resize) override; @@ -110,6 +119,13 @@ class RenderingContextDriverD3D12 : public RenderingContextDriver { uint32_t height = 0; DisplayServer::VSyncMode vsync_mode = DisplayServer::VSYNC_ENABLED; bool needs_resize = false; + + bool hdr_output = false; + bool hdr_prefer_high_precision = false; + float hdr_reference_luminance = 0.0f; + float hdr_max_luminance = 0.0f; + float hdr_linear_luminance_scale = 80.0f; + #ifdef DCOMP_ENABLED ComPtr composition_device; ComPtr composition_target; diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp index 6865ae0bc975..b5178caafa99 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.cpp +++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp @@ -343,6 +343,11 @@ const RenderingDeviceDriverD3D12::D3D12Format RenderingDeviceDriverD3D12::RD_TO_ /* DATA_FORMAT_ASTC_12x12_SFLOAT_BLOCK */ {}, }; +const DXGI_COLOR_SPACE_TYPE RenderingDeviceDriverD3D12::RD_TO_DXGI_COLOR_SPACE_TYPE[RDD::COLOR_SPACE_MAX]{ + /* COLOR_SPACE_REC709_LINEAR */ DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709, + /* COLOR_SPACE_REC709_NONLINEAR_SRGB */ DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709, +}; + Error RenderingDeviceDriverD3D12::CPUDescriptorsHeapPools::allocate(ID3D12Device *p_device, const D3D12_DESCRIPTOR_HEAP_DESC &p_desc, CPUDescriptorsHeapHandle &r_result) { ERR_FAIL_COND_V(p_desc.NodeMask != 0 || p_desc.Flags != 0 || p_desc.Type >= D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES, ERR_INVALID_PARAMETER); @@ -2796,6 +2801,11 @@ void RenderingDeviceDriverD3D12::command_buffer_execute_secondary(CommandBufferI void RenderingDeviceDriverD3D12::_swap_chain_release(SwapChain *p_swap_chain) { _swap_chain_release_buffers(p_swap_chain); + if (p_swap_chain->render_pass.id != 0) { + render_pass_free(p_swap_chain->render_pass); + p_swap_chain->render_pass = RenderPassID(); + } + p_swap_chain->d3d_swap_chain.Reset(); } @@ -2814,10 +2824,10 @@ void RenderingDeviceDriverD3D12::_swap_chain_release_buffers(SwapChain *p_swap_c p_swap_chain->framebuffers.clear(); } -RDD::SwapChainID RenderingDeviceDriverD3D12::swap_chain_create(RenderingContextDriver::SurfaceID p_surface) { +RDD::RenderPassID RenderingDeviceDriverD3D12::_swap_chain_create_render_pass(RDD::DataFormat p_format) { // Create the render pass that will be used to draw to the swap chain's framebuffers. RDD::Attachment attachment; - attachment.format = DATA_FORMAT_R8G8B8A8_UNORM; + attachment.format = p_format; attachment.samples = RDD::TEXTURE_SAMPLES_1; attachment.load_op = RDD::ATTACHMENT_LOAD_OP_CLEAR; attachment.store_op = RDD::ATTACHMENT_STORE_OP_STORE; @@ -2828,14 +2838,30 @@ RDD::SwapChainID RenderingDeviceDriverD3D12::swap_chain_create(RenderingContextD color_ref.aspect.set_flag(RDD::TEXTURE_ASPECT_COLOR_BIT); subpass.color_references.push_back(color_ref); - RenderPassID render_pass = render_pass_create(attachment, subpass, {}, 1, AttachmentReference()); - ERR_FAIL_COND_V(!render_pass, SwapChainID()); + return render_pass_create(attachment, subpass, {}, 1, AttachmentReference()); +} + +void RenderingDeviceDriverD3D12::_determine_swap_chain_format(SwapChain *p_swap_chain, DataFormat &r_format, ColorSpace &r_color_space) { + DEV_ASSERT(p_swap_chain); + DEV_ASSERT(p_swap_chain->surface != 0); + + // Direct3D Hardware level 10 mandates support for all these formats. + // Godot requires at least Hardware level 11, so these formats are guaranteed to be supported. + if (context_driver->surface_get_hdr_output_enabled(p_swap_chain->surface)) { + r_format = DATA_FORMAT_R16G16B16A16_SFLOAT; + r_color_space = COLOR_SPACE_REC709_LINEAR; + } else { + r_format = DATA_FORMAT_R8G8B8A8_UNORM; + r_color_space = COLOR_SPACE_REC709_NONLINEAR_SRGB; + } +} + +RDD::SwapChainID RenderingDeviceDriverD3D12::swap_chain_create(RenderingContextDriver::SurfaceID p_surface) { + DEV_ASSERT(p_surface != 0); - // Create the empty swap chain until it is resized. + // Create an empty swap chain until it is resized. SwapChain *swap_chain = memnew(SwapChain); swap_chain->surface = p_surface; - swap_chain->data_format = attachment.format; - swap_chain->render_pass = render_pass; return SwapChainID(swap_chain); } @@ -2877,17 +2903,27 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue, break; } - if (swap_chain->d3d_swap_chain != nullptr && creation_flags != swap_chain->creation_flags) { - // The swap chain must be recreated if the creation flags are different. + RDD::DataFormat new_data_format; + RDD::ColorSpace new_color_space; + _determine_swap_chain_format(swap_chain, new_data_format, new_color_space); + + if (swap_chain->d3d_swap_chain != nullptr && (creation_flags != swap_chain->creation_flags || new_data_format != swap_chain->data_format)) { + // The swap chain must be recreated if the creation flags or data format are different. _swap_chain_release(swap_chain); } + swap_chain->data_format = new_data_format; + DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = {}; if (swap_chain->d3d_swap_chain != nullptr) { _swap_chain_release_buffers(swap_chain); res = swap_chain->d3d_swap_chain->ResizeBuffers(p_desired_framebuffer_count, surface->width, surface->height, DXGI_FORMAT_UNKNOWN, creation_flags); ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_UNAVAILABLE); } else { + DEV_ASSERT(swap_chain->render_pass.id == 0); + swap_chain->render_pass = _swap_chain_create_render_pass(new_data_format); + ERR_FAIL_COND_V(!swap_chain->render_pass, ERR_CANT_CREATE); + swap_chain_desc.BufferCount = p_desired_framebuffer_count; swap_chain_desc.Format = RD_TO_D3D12_FORMAT[swap_chain->data_format].general_format; swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; @@ -2925,6 +2961,13 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue, ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_CANT_CREATE); } + if (swap_chain->color_space != new_color_space) { + res = swap_chain->d3d_swap_chain->SetColorSpace1(RD_TO_DXGI_COLOR_SPACE_TYPE[new_color_space]); + ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_CANT_CREATE); + + swap_chain->color_space = new_color_space; + } + #ifdef DCOMP_ENABLED if (surface->composition_device.Get() == nullptr) { using PFN_DCompositionCreateDevice = HRESULT(WINAPI *)(IDXGIDevice *, REFIID, void **); @@ -3036,10 +3079,14 @@ RDD::DataFormat RenderingDeviceDriverD3D12::swap_chain_get_format(SwapChainID p_ return swap_chain->data_format; } +RDD::ColorSpace RenderingDeviceDriverD3D12::swap_chain_get_color_space(SwapChainID p_swap_chain) { + const SwapChain *swap_chain = (const SwapChain *)(p_swap_chain.id); + return swap_chain->color_space; +} + void RenderingDeviceDriverD3D12::swap_chain_free(SwapChainID p_swap_chain) { SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); _swap_chain_release(swap_chain); - render_pass_free(swap_chain->render_pass); memdelete(swap_chain); } @@ -5891,6 +5938,8 @@ bool RenderingDeviceDriverD3D12::has_feature(Features p_feature) { return true; case SUPPORTS_VULKAN_MEMORY_MODEL: return false; + case SUPPORTS_HDR_OUTPUT: + return true; default: return false; } diff --git a/drivers/d3d12/rendering_device_driver_d3d12.h b/drivers/d3d12/rendering_device_driver_d3d12.h index 5d77ceb1019d..23c8418cbba5 100644 --- a/drivers/d3d12/rendering_device_driver_d3d12.h +++ b/drivers/d3d12/rendering_device_driver_d3d12.h @@ -88,6 +88,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { }; static const D3D12Format RD_TO_D3D12_FORMAT[RDD::DATA_FORMAT_MAX]; + static const DXGI_COLOR_SPACE_TYPE RD_TO_DXGI_COLOR_SPACE_TYPE[RDD::COLOR_SPACE_MAX]; struct DeviceLimits { uint64_t max_srvs_per_shader_stage = 0; @@ -559,10 +560,13 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { TightLocalVector render_targets_info; TightLocalVector framebuffers; RDD::DataFormat data_format = DATA_FORMAT_MAX; + RDD::ColorSpace color_space = COLOR_SPACE_MAX; }; void _swap_chain_release(SwapChain *p_swap_chain); void _swap_chain_release_buffers(SwapChain *p_swap_chain); + RenderPassID _swap_chain_create_render_pass(RDD::DataFormat p_format); + void _determine_swap_chain_format(SwapChain *p_swap_chain, DataFormat &r_format, ColorSpace &r_color_space); public: virtual SwapChainID swap_chain_create(RenderingContextDriver::SurfaceID p_surface) override; @@ -570,6 +574,7 @@ class RenderingDeviceDriverD3D12 : public RenderingDeviceDriver { virtual FramebufferID swap_chain_acquire_framebuffer(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, bool &r_resize_required) override; virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override; virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override; + virtual ColorSpace swap_chain_get_color_space(SwapChainID p_swap_chain) override; virtual void swap_chain_free(SwapChainID p_swap_chain) override; /*********************/ diff --git a/drivers/gles3/rasterizer_scene_gles3.cpp b/drivers/gles3/rasterizer_scene_gles3.cpp index 293647dc9aad..ef1a9b4c4747 100644 --- a/drivers/gles3/rasterizer_scene_gles3.cpp +++ b/drivers/gles3/rasterizer_scene_gles3.cpp @@ -2244,7 +2244,7 @@ void RasterizerSceneGLES3::_render_shadow_pass(RID p_light, RID p_shadow_atlas, glBindFramebuffer(GL_FRAMEBUFFER, GLES3::TextureStorage::system_fbo); } -void RasterizerSceneGLES3::render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data, RenderingMethod::RenderInfo *r_render_info) { +void RasterizerSceneGLES3::render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data, RenderingMethod::RenderInfo *r_render_info) { GLES3::TextureStorage *texture_storage = GLES3::TextureStorage::get_singleton(); GLES3::Config *config = GLES3::Config::get_singleton(); RENDER_TIMESTAMP("Setup 3D Scene"); @@ -2370,9 +2370,12 @@ void RasterizerSceneGLES3::render_scene(const Ref &p_render_ } tonemap_ubo.exposure = environment_get_exposure(render_data.environment); - tonemap_ubo.white = environment_get_white(render_data.environment); tonemap_ubo.tonemapper = int32_t(environment_get_tone_mapper(render_data.environment)); - + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(render_data.environment, false, 1.0); + tonemap_ubo.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap_ubo.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap_ubo.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap_ubo.tonemapper_params[3] = params.tonemapper_params[3]; tonemap_ubo.brightness = environment_get_adjustments_brightness(render_data.environment); tonemap_ubo.contrast = environment_get_adjustments_contrast(render_data.environment); tonemap_ubo.saturation = environment_get_adjustments_saturation(render_data.environment); @@ -2825,7 +2828,7 @@ void RasterizerSceneGLES3::_render_post_processing(const RenderDataGLES3 *p_rend glow_hdr_bleed_threshold = environment_get_glow_hdr_bleed_threshold(p_render_data->environment); glow_hdr_bleed_scale = environment_get_glow_hdr_bleed_scale(p_render_data->environment); glow_hdr_luminance_cap = environment_get_glow_hdr_luminance_cap(p_render_data->environment); - srgb_white = environment_get_white(p_render_data->environment); + srgb_white = environment_get_white(p_render_data->environment, false, 1.0); } if (glow_enabled) { diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h index e46a87885629..00d3248915f7 100644 --- a/drivers/gles3/rasterizer_scene_gles3.h +++ b/drivers/gles3/rasterizer_scene_gles3.h @@ -449,14 +449,14 @@ class RasterizerSceneGLES3 : public RendererSceneRender { struct TonemapUBO { float exposure = 1.0; - float white = 1.0; int32_t tonemapper = 0; int32_t pad = 0; - int32_t pad2 = 0; + float tonemapper_params[4] = { 0.0, 0.0, 0.0, 0.0 }; float brightness = 1.0; float contrast = 1.0; float saturation = 1.0; + int32_t pad3 = 0; }; static_assert(sizeof(TonemapUBO) % 16 == 0, "Tonemap UBO size must be a multiple of 16 bytes"); @@ -913,7 +913,7 @@ class RasterizerSceneGLES3 : public RendererSceneRender { void voxel_gi_set_quality(RS::VoxelGIQuality) override; - void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) override; + void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) override; void render_material(const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, const PagedArray &p_instances, RID p_framebuffer, const Rect2i &p_region) override; void render_particle_collider_heightfield(RID p_collider, const Transform3D &p_transform, const PagedArray &p_instances) override; diff --git a/drivers/gles3/shaders/effects/post.glsl b/drivers/gles3/shaders/effects/post.glsl index fd79d1a61411..3e6440bb51cd 100644 --- a/drivers/gles3/shaders/effects/post.glsl +++ b/drivers/gles3/shaders/effects/post.glsl @@ -131,7 +131,7 @@ void main() { color.rgb = srgb_to_linear(color.rgb); - color.rgb = apply_tonemapping(color.rgb, white); + color.rgb = apply_tonemapping(color.rgb); #ifdef USE_BCS // Apply brightness: diff --git a/drivers/gles3/shaders/scene.glsl b/drivers/gles3/shaders/scene.glsl index 974721f89138..d46da35d0260 100644 --- a/drivers/gles3/shaders/scene.glsl +++ b/drivers/gles3/shaders/scene.glsl @@ -2534,7 +2534,7 @@ void main() { // Tonemap before writing as we are writing to an sRGB framebuffer frag_color.rgb *= exposure; #ifdef APPLY_TONEMAPPING - frag_color.rgb = apply_tonemapping(frag_color.rgb, white); + frag_color.rgb = apply_tonemapping(frag_color.rgb); #endif frag_color.rgb = linear_to_srgb(frag_color.rgb); @@ -2806,7 +2806,7 @@ void main() { // Tonemap before writing as we are writing to an sRGB framebuffer additive_light_color *= exposure; #ifdef APPLY_TONEMAPPING - additive_light_color = apply_tonemapping(additive_light_color, white); + additive_light_color = apply_tonemapping(additive_light_color); #endif additive_light_color = linear_to_srgb(additive_light_color); diff --git a/drivers/gles3/shaders/sky.glsl b/drivers/gles3/shaders/sky.glsl index a65f87e8f6e6..62250eae081a 100644 --- a/drivers/gles3/shaders/sky.glsl +++ b/drivers/gles3/shaders/sky.glsl @@ -265,7 +265,7 @@ void main() { color *= exposure; #ifdef APPLY_TONEMAPPING - color = apply_tonemapping(color, white); + color = apply_tonemapping(color); #endif color = linear_to_srgb(color); diff --git a/drivers/gles3/shaders/tonemap_inc.glsl b/drivers/gles3/shaders/tonemap_inc.glsl index dd7df09c38a3..7e2546d47571 100644 --- a/drivers/gles3/shaders/tonemap_inc.glsl +++ b/drivers/gles3/shaders/tonemap_inc.glsl @@ -1,13 +1,13 @@ layout(std140) uniform TonemapData { //ubo:0 float exposure; - float white; int tonemapper; int pad; - int pad2; + vec4 tonemapper_params; float brightness; float contrast; float saturation; + int pad3; }; // This expects 0-1 range input. @@ -28,17 +28,18 @@ vec3 srgb_to_linear(vec3 color) { #ifdef APPLY_TONEMAPPING // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt -vec3 tonemap_reinhard(vec3 color, float p_white) { - float white_squared = p_white * p_white; +vec3 tonemap_reinhard(vec3 color) { + float white_squared = tonemapper_params.x; vec3 white_squared_color = white_squared * color; // Equivalent to color * (1 + color / white_squared) / (1 + color) return (white_squared_color + color * color) / (white_squared_color + white_squared); } -vec3 tonemap_filmic(vec3 color, float p_white) { - // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers - // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) - // has no effect on the curve's general shape or visual properties +vec3 tonemap_filmic(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. + // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers. + // Also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. const float exposure_bias = 2.0f; const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float B = 0.30f * exposure_bias; @@ -48,14 +49,14 @@ vec3 tonemap_filmic(vec3 color, float p_white) { const float F = 0.30f; vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; - float p_white_tonemapped = ((p_white * (A * p_white + C * B) + D * E) / (p_white * (A * p_white + B) + D * F)) - E / F; - return color_tonemapped / p_white_tonemapped; + return color_tonemapped / tonemapper_params.x; } // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // (MIT License). -vec3 tonemap_aces(vec3 color, float p_white) { +vec3 tonemap_aces(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. const float exposure_bias = 1.8f; const float A = 0.0245786f; const float B = 0.000090537f; @@ -78,46 +79,46 @@ vec3 tonemap_aces(vec3 color, float p_white) { vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E); color_tonemapped *= odt_to_rgb; - p_white *= exposure_bias; - float p_white_tonemapped = (p_white * (p_white + A) - B) / (p_white * (C * p_white + D) + E); - - return color_tonemapped / p_white_tonemapped; + return color_tonemapped / tonemapper_params.x; } -// Polynomial approximation of EaryChow's AgX sigmoid curve. -// x must be within the range [0.0, 1.0] -vec3 agx_contrast_approx(vec3 x) { - // Generated with Excel trendline - // Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps - // Additional padding values were added to give correct intersections at 0.0 and 1.0 - // 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0 - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; +// allenwp tonemapping curve; developed for use in the Godot game engine. +// Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/ +// Input must be a non-negative linear scene value. +vec3 allenwp_curve(vec3 x) { + const float output_max_value = 1.0; // SDR always has a output_max_value of 1.0 + + // These constants must match the those in the C++ code that calculates the parameters. + // 18% "middle gray" is perceptually 50% of the brightness of reference white. + const float awp_crossover_point = 0.18; + // When output_max_value and/or awp_crossover_point are no longer constant, + // awp_shoulder_max can be calculated on the CPU and passed in as tonemap_e. + const float awp_shoulder_max = output_max_value - awp_crossover_point; + + float awp_contrast = tonemapper_params.x; + float awp_toe_a = tonemapper_params.y; + float awp_slope = tonemapper_params.z; + float awp_w = tonemapper_params.w; + + // Reinhard-like shoulder: + vec3 s = x - awp_crossover_point; + vec3 slope_s = awp_slope * s; + s = slope_s * (1.0 + s / awp_w) / (1.0 + (slope_s / awp_shoulder_max)); + s += awp_crossover_point; + + // Sigmoid power function toe: + vec3 t = pow(x, vec3(awp_contrast)); + t = t / (t + awp_toe_a); + + return mix(s, t, lessThan(x, vec3(awp_crossover_point))); } // This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender. // This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses. // Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py +// Colorspace transformation source: https://www.colour-science.org:8010/apps/rgb_colourspace_transformation_matrix vec3 tonemap_agx(vec3 color) { - // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: - const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( - 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603, - 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023, - 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528); - - // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. - const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( - 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409, - -0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117, - -0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889); - - // LOG2_MIN = -10.0 - // LOG2_MAX = +6.5 - // MIDDLE_GRAY = 0.18 - const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) - const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) - + // Input color should be non-negative! // Large negative values in one channel and large positive values in other // channels can result in a colour that appears darker and more saturated than // desired after passing it through the inset matrix. For this reason, it is @@ -125,28 +126,40 @@ vec3 tonemap_agx(vec3 color) { // This is done before the Rec. 2020 transform to allow the Rec. 2020 // transform to be combined with the AgX inset matrix. This results in a loss // of color information that could be correctly interpreted within the - // Rec. 2020 color space as positive RGB values, but it is less common for Godot - // to provide this function with negative sRGB values and therefore not worth + // Rec. 2020 color space as positive RGB values, but is often not worth // the performance cost of an additional matrix multiplication. - // A value of 2e-10 intentionally introduces insignificant error to prevent - // log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after - // the matrix transform. - color = max(color, 2e-10); + // + // Additionally, this AgX configuration was created subjectively based on + // output appearance in the sRGB color space, so it is possible that these + // matrices will not perform well with non-sRGB output (more testing with + // future wide-gamut displays is be needed). + // See this comment from the author on the decisions made to create the matrices: + // https://github.com/godotengine/godot-proposals/issues/12317#issuecomment-2835824250 - // Do AGX in rec2020 to match Blender and then apply inset matrix. - color = srgb_to_rec2020_agx_inset_matrix * color; + // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: + const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( + 0.544814746488245, 0.140416948464053, 0.0888104196149096, + 0.373787398372697, 0.754137554567394, 0.178871756420858, + 0.0813978551390581, 0.105445496968552, 0.732317823964232); - // Log2 space encoding. - // Must be clamped because agx_contrast_approx may not work - // well with values outside of the range [0.0, 1.0] - color = clamp(log2(color), min_ev, max_ev); - color = (color - min_ev) / (max_ev - min_ev); + // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. + const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( + 1.96488741169489, -0.299313364904742, -0.164352742528393, + -0.855988495690215, 1.32639796461980, -0.238183969428088, + -0.108898916004672, -0.0270845997150571, 1.40253671195648); + + const float output_max_value = 1.0; // SDR always has a output_max_value of 1.0 - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); + // Apply inset matrix. + color = srgb_to_rec2020_agx_inset_matrix * color; + + // Use the allenwp tonemapping curve to match the Blender AgX curve while + // providing stability across all variable dyanimc range (SDR, HDR, EDR). + color = allenwp_curve(color); - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Clipping to output_max_value is required to address a cyan colour that occurs + // with very bright inputs. + color = min(vec3(output_max_value), color); // Apply outset to make the result more chroma-laden and then go back to linear sRGB. color = agx_outset_rec2020_to_srgb_matrix * color; @@ -163,17 +176,21 @@ vec3 tonemap_agx(vec3 color) { #define TONEMAPPER_ACES 3 #define TONEMAPPER_AGX 4 -vec3 apply_tonemapping(vec3 color, float p_white) { // inputs are LINEAR - // Ensure color values passed to tonemappers are positive. - // They can be negative in the case of negative lights, which leads to undesired behavior. +vec3 apply_tonemapping(vec3 color) { // inputs are LINEAR if (tonemapper == TONEMAPPER_LINEAR) { return color; - } else if (tonemapper == TONEMAPPER_REINHARD) { - return tonemap_reinhard(max(vec3(0.0f), color), p_white); + } + + // Ensure color values passed to tonemappers are positive. + // They can be negative in the case of negative lights, which leads to undesired behavior. + color = max(vec3(0.0), color); + + if (tonemapper == TONEMAPPER_REINHARD) { + return tonemap_reinhard(color); } else if (tonemapper == TONEMAPPER_FILMIC) { - return tonemap_filmic(max(vec3(0.0f), color), p_white); + return tonemap_filmic(color); } else if (tonemapper == TONEMAPPER_ACES) { - return tonemap_aces(max(vec3(0.0f), color), p_white); + return tonemap_aces(color); } else { // TONEMAPPER_AGX return tonemap_agx(color); } diff --git a/drivers/metal/rendering_context_driver_metal.h b/drivers/metal/rendering_context_driver_metal.h index d2dd4958a991..53a931e2de0a 100644 --- a/drivers/metal/rendering_context_driver_metal.h +++ b/drivers/metal/rendering_context_driver_metal.h @@ -77,6 +77,15 @@ class API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) RenderingContextDriverMe void surface_set_size(SurfaceID p_surface, uint32_t p_width, uint32_t p_height) final override; void surface_set_vsync_mode(SurfaceID p_surface, DisplayServer::VSyncMode p_vsync_mode) final override; DisplayServer::VSyncMode surface_get_vsync_mode(SurfaceID p_surface) const final override; + virtual void surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) final override; + virtual bool surface_get_hdr_output_enabled(SurfaceID p_surface) const final override; + virtual void surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) final override; + virtual float surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const final override; + virtual void surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) final override; + virtual float surface_get_hdr_output_max_luminance(SurfaceID p_surface) const final override; + virtual void surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) final override; + virtual float surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const final override; + virtual float surface_get_hdr_output_max_value(SurfaceID p_surface) const final override; uint32_t surface_get_width(SurfaceID p_surface) const final override; uint32_t surface_get_height(SurfaceID p_surface) const final override; void surface_set_needs_resize(SurfaceID p_surface, bool p_needs_resize) final override; @@ -110,6 +119,12 @@ class API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) RenderingContextDriverMe bool needs_resize = false; double present_minimum_duration = 0.0; + bool hdr_output = false; + bool hdr_prefer_high_precision = false; + float hdr_reference_luminance = 0.0f; + float hdr_max_luminance = 0.0f; + float hdr_linear_luminance_scale = 100.0f; + Surface( #ifdef __OBJC__ id p_device diff --git a/drivers/metal/rendering_context_driver_metal.mm b/drivers/metal/rendering_context_driver_metal.mm index 6871b97385fb..236178ba84fa 100644 --- a/drivers/metal/rendering_context_driver_metal.mm +++ b/drivers/metal/rendering_context_driver_metal.mm @@ -215,6 +215,52 @@ void present(MDCommandBuffer *p_cmd_buffer) override final { return surface->vsync_mode; } +void RenderingContextDriverMetal::surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_output = p_enabled; + surface->needs_resize = true; +} + +bool RenderingContextDriverMetal::surface_get_hdr_output_enabled(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_output; +} + +void RenderingContextDriverMetal::surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_reference_luminance = p_reference_luminance; +} + +float RenderingContextDriverMetal::surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_reference_luminance; +} + +void RenderingContextDriverMetal::surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_max_luminance = p_max_luminance; +} + +float RenderingContextDriverMetal::surface_get_hdr_output_max_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_max_luminance; +} + +void RenderingContextDriverMetal::surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_linear_luminance_scale = p_linear_luminance_scale; +} + +float RenderingContextDriverMetal::surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_linear_luminance_scale; +} + +float RenderingContextDriverMetal::surface_get_hdr_output_max_value(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return MAX(surface->hdr_max_luminance / MAX(surface->hdr_reference_luminance, 1.0f), 1.0f); +} + uint32_t RenderingContextDriverMetal::surface_get_width(SurfaceID p_surface) const { Surface *surface = (Surface *)(p_surface); return surface->width; diff --git a/drivers/metal/rendering_device_driver_metal.h b/drivers/metal/rendering_device_driver_metal.h index fcefae591ef4..cf189688ba4a 100644 --- a/drivers/metal/rendering_device_driver_metal.h +++ b/drivers/metal/rendering_device_driver_metal.h @@ -223,6 +223,7 @@ class API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) RenderingDeviceDriverMet virtual FramebufferID swap_chain_acquire_framebuffer(CommandQueueID p_cmd_queue, SwapChainID p_swap_chain, bool &r_resize_required) override final; virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override final; virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final; + virtual ColorSpace swap_chain_get_color_space(SwapChainID p_swap_chain) override final; virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final; virtual void swap_chain_free(SwapChainID p_swap_chain) override final; diff --git a/drivers/metal/rendering_device_driver_metal.mm b/drivers/metal/rendering_device_driver_metal.mm index 3502259e579f..d1708bb2e75d 100644 --- a/drivers/metal/rendering_device_driver_metal.mm +++ b/drivers/metal/rendering_device_driver_metal.mm @@ -1031,6 +1031,10 @@ static const API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0)) MTLSamplerBorderC return swap_chain->data_format; } +RDD::ColorSpace RenderingDeviceDriverMetal::swap_chain_get_color_space(SwapChainID p_swap_chain) { + return RDD::COLOR_SPACE_REC709_NONLINEAR_SRGB; +} + void RenderingDeviceDriverMetal::swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) { SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); RenderingContextDriverMetal::Surface *metal_surface = (RenderingContextDriverMetal::Surface *)(swap_chain->surface); diff --git a/drivers/vulkan/rendering_context_driver_vulkan.cpp b/drivers/vulkan/rendering_context_driver_vulkan.cpp index f702cf233d7c..0dbcd3298179 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_context_driver_vulkan.cpp @@ -436,6 +436,9 @@ Error RenderingContextDriverVulkan::_initialize_instance_extensions() { // This extension allows us to use the properties2 features to query additional device capabilities. _register_requested_instance_extension(VK_KHR_GET_PHYSICAL_DEVICE_PROPERTIES_2_EXTENSION_NAME, false); + // This extension allows us to use colorspaces other than SRGB. + _register_requested_instance_extension(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME, false); + #if defined(USE_VOLK) && (defined(MACOS_ENABLED) || defined(IOS_ENABLED)) _register_requested_instance_extension(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, true); #endif @@ -991,6 +994,52 @@ DisplayServer::VSyncMode RenderingContextDriverVulkan::surface_get_vsync_mode(Su return surface->vsync_mode; } +void RenderingContextDriverVulkan::surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_output = p_enabled; + surface->needs_resize = true; +} + +bool RenderingContextDriverVulkan::surface_get_hdr_output_enabled(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_output; +} + +void RenderingContextDriverVulkan::surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_reference_luminance = p_reference_luminance; +} + +float RenderingContextDriverVulkan::surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_reference_luminance; +} + +void RenderingContextDriverVulkan::surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_max_luminance = p_max_luminance; +} + +float RenderingContextDriverVulkan::surface_get_hdr_output_max_luminance(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_max_luminance; +} + +void RenderingContextDriverVulkan::surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) { + Surface *surface = (Surface *)(p_surface); + surface->hdr_linear_luminance_scale = p_linear_luminance_scale; +} + +float RenderingContextDriverVulkan::surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return surface->hdr_linear_luminance_scale; +} + +float RenderingContextDriverVulkan::surface_get_hdr_output_max_value(SurfaceID p_surface) const { + Surface *surface = (Surface *)(p_surface); + return MAX(surface->hdr_max_luminance / MAX(surface->hdr_reference_luminance, 1.0f), 1.0f); +} + uint32_t RenderingContextDriverVulkan::surface_get_width(SurfaceID p_surface) const { Surface *surface = (Surface *)(p_surface); return surface->width; @@ -1021,6 +1070,14 @@ bool RenderingContextDriverVulkan::is_debug_utils_enabled() const { return enabled_instance_extension_names.has(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); } +bool RenderingContextDriverVulkan::is_colorspace_supported() const { + return enabled_instance_extension_names.has(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME); +} + +bool RenderingContextDriverVulkan::is_colorspace_externally_managed() const { + return false; +} + VkInstance RenderingContextDriverVulkan::instance_get() const { return instance; } diff --git a/drivers/vulkan/rendering_context_driver_vulkan.h b/drivers/vulkan/rendering_context_driver_vulkan.h index 2881a17cb7bb..894f06fe3e56 100644 --- a/drivers/vulkan/rendering_context_driver_vulkan.h +++ b/drivers/vulkan/rendering_context_driver_vulkan.h @@ -138,12 +138,23 @@ class RenderingContextDriverVulkan : public RenderingContextDriver { virtual void surface_set_size(SurfaceID p_surface, uint32_t p_width, uint32_t p_height) override; virtual void surface_set_vsync_mode(SurfaceID p_surface, DisplayServer::VSyncMode p_vsync_mode) override; virtual DisplayServer::VSyncMode surface_get_vsync_mode(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) override; + virtual bool surface_get_hdr_output_enabled(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) override; + virtual float surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) override; + virtual float surface_get_hdr_output_max_luminance(SurfaceID p_surface) const override; + virtual void surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) override; + virtual float surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const override; + virtual float surface_get_hdr_output_max_value(SurfaceID p_surface) const override; virtual uint32_t surface_get_width(SurfaceID p_surface) const override; virtual uint32_t surface_get_height(SurfaceID p_surface) const override; virtual void surface_set_needs_resize(SurfaceID p_surface, bool p_needs_resize) override; virtual bool surface_get_needs_resize(SurfaceID p_surface) const override; virtual void surface_destroy(SurfaceID p_surface) override; virtual bool is_debug_utils_enabled() const override; + bool is_colorspace_supported() const; + virtual bool is_colorspace_externally_managed() const; // Vulkan-only methods. struct Surface { @@ -152,6 +163,12 @@ class RenderingContextDriverVulkan : public RenderingContextDriver { uint32_t height = 0; DisplayServer::VSyncMode vsync_mode = DisplayServer::VSYNC_ENABLED; bool needs_resize = false; + + bool hdr_output = false; + bool hdr_prefer_high_precision = false; + float hdr_reference_luminance = 0.0f; + float hdr_max_luminance = 0.0f; + float hdr_linear_luminance_scale = 100.0f; }; VkInstance instance_get() const; diff --git a/drivers/vulkan/rendering_device_driver_vulkan.cpp b/drivers/vulkan/rendering_device_driver_vulkan.cpp index 90291bd94479..01ea0478de4a 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.cpp +++ b/drivers/vulkan/rendering_device_driver_vulkan.cpp @@ -32,6 +32,7 @@ #include "core/config/project_settings.h" #include "core/io/marshalls.h" +#include "core/templates/fixed_vector.h" #include "vulkan_hooks.h" #include "thirdparty/misc/smolv.h" @@ -3069,6 +3070,80 @@ void RenderingDeviceDriverVulkan::command_buffer_execute_secondary(CommandBuffer /**** SWAP CHAIN ****/ /********************/ +struct FormatCandidate { + VkFormat format; + VkColorSpaceKHR colorspace; +}; + +bool RenderingDeviceDriverVulkan::_determine_swap_chain_format(RenderingContextDriver::SurfaceID p_surface, VkFormat &r_format, VkColorSpaceKHR &r_color_space) { + DEV_ASSERT(p_surface != 0); + + RenderingContextDriverVulkan::Surface *surface = (RenderingContextDriverVulkan::Surface *)(p_surface); + const RenderingContextDriverVulkan::Functions &functions = context_driver->functions_get(); + + // Retrieve the formats supported by the surface. + uint32_t format_count = 0; + VkResult err = functions.GetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface->vk_surface, &format_count, nullptr); + ERR_FAIL_COND_V(err != VK_SUCCESS, false); + + TightLocalVector formats; + formats.resize(format_count); + err = functions.GetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface->vk_surface, &format_count, formats.ptr()); + ERR_FAIL_COND_V(err != VK_SUCCESS, false); + + // If the format list includes just one entry of VK_FORMAT_UNDEFINED, the surface has no preferred format. + if (format_count == 1 && formats[0].format == VK_FORMAT_UNDEFINED) { + r_format = VK_FORMAT_B8G8R8A8_UNORM; + r_color_space = formats[0].colorSpace; + return true; + } + + bool colorspace_supported = context_driver->is_colorspace_supported(); + bool hdr_output_requested = context_driver->surface_get_hdr_output_enabled(p_surface); + + // Determine which formats to prefer based on the requested capabilities. + FixedVector preferred_formats; + + // If the surface requests HDR output, try to get an HDR format. + if (hdr_output_requested && colorspace_supported) { + // This format is preferred for HDR output + preferred_formats.push_back({ VK_FORMAT_R16G16B16A16_SFLOAT, VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT }); + } + + // These formats are always considered for SDR. + preferred_formats.push_back({ VK_FORMAT_B8G8R8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }); + preferred_formats.push_back({ VK_FORMAT_R8G8B8A8_UNORM, VK_COLOR_SPACE_SRGB_NONLINEAR_KHR }); + + bool found = false; + for (const FormatCandidate &candidate : preferred_formats) { + for (uint32_t i = 0; i < format_count; i++) { + if (formats[i].format == candidate.format && formats[i].colorSpace == candidate.colorspace) { + r_format = formats[i].format; + r_color_space = formats[i].colorSpace; + found = true; + break; + } + } + + if (found) { + break; + } + } + + // Warnings for when HDR capabilities are requested but not found. + if (hdr_output_requested) { + if (!colorspace_supported) { + WARN_PRINT("HDR output requested but the vulkan driver does not support VK_EXT_swapchain_colorspace, falling back to SDR."); + } + + if (r_color_space == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + WARN_PRINT("HDR output requested but no HDR compatible format was found, falling back to SDR."); + } + } + + return found; +} + void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) { // Destroy views and framebuffers associated to the swapchain's images. for (FramebufferID framebuffer : swap_chain->framebuffers) { @@ -3097,6 +3172,11 @@ void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) { swap_chain->vk_swapchain = VK_NULL_HANDLE; } + if (swap_chain->render_pass.id != 0) { + render_pass_free(swap_chain->render_pass); + swap_chain->render_pass = RenderPassID(); + } + for (uint32_t i = 0; i < swap_chain->command_queues_acquired.size(); i++) { _recreate_image_semaphore(swap_chain->command_queues_acquired[i], swap_chain->command_queues_acquired_semaphores[i], false); } @@ -3114,84 +3194,9 @@ void RenderingDeviceDriverVulkan::_swap_chain_release(SwapChain *swap_chain) { RenderingDeviceDriver::SwapChainID RenderingDeviceDriverVulkan::swap_chain_create(RenderingContextDriver::SurfaceID p_surface) { DEV_ASSERT(p_surface != 0); - RenderingContextDriverVulkan::Surface *surface = (RenderingContextDriverVulkan::Surface *)(p_surface); - const RenderingContextDriverVulkan::Functions &functions = context_driver->functions_get(); - - // Retrieve the formats supported by the surface. - uint32_t format_count = 0; - VkResult err = functions.GetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface->vk_surface, &format_count, nullptr); - ERR_FAIL_COND_V(err != VK_SUCCESS, SwapChainID()); - - TightLocalVector formats; - formats.resize(format_count); - err = functions.GetPhysicalDeviceSurfaceFormatsKHR(physical_device, surface->vk_surface, &format_count, formats.ptr()); - ERR_FAIL_COND_V(err != VK_SUCCESS, SwapChainID()); - - VkFormat format = VK_FORMAT_UNDEFINED; - VkColorSpaceKHR color_space = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; - if (format_count == 1 && formats[0].format == VK_FORMAT_UNDEFINED) { - // If the format list includes just one entry of VK_FORMAT_UNDEFINED, the surface has no preferred format. - format = VK_FORMAT_B8G8R8A8_UNORM; - color_space = formats[0].colorSpace; - } else if (format_count > 0) { - // Use one of the supported formats, prefer B8G8R8A8_UNORM. - const VkFormat preferred_format = VK_FORMAT_B8G8R8A8_UNORM; - const VkFormat second_format = VK_FORMAT_R8G8B8A8_UNORM; - for (uint32_t i = 0; i < format_count; i++) { - if (formats[i].format == preferred_format || formats[i].format == second_format) { - format = formats[i].format; - if (formats[i].format == preferred_format) { - // This is the preferred format, stop searching. - break; - } - } - } - } - - // No formats are supported. - ERR_FAIL_COND_V_MSG(format == VK_FORMAT_UNDEFINED, SwapChainID(), "Surface did not return any valid formats."); - - // Create the render pass for the chosen format. - VkAttachmentDescription2KHR attachment = {}; - attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2_KHR; - attachment.format = format; - attachment.samples = VK_SAMPLE_COUNT_1_BIT; - attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; - attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; - attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; - attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; - attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; - attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; - - VkAttachmentReference2KHR color_reference = {}; - color_reference.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2_KHR; - color_reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; - - VkSubpassDescription2KHR subpass = {}; - subpass.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2_KHR; - subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; - subpass.colorAttachmentCount = 1; - subpass.pColorAttachments = &color_reference; - - VkRenderPassCreateInfo2KHR pass_info = {}; - pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2_KHR; - pass_info.attachmentCount = 1; - pass_info.pAttachments = &attachment; - pass_info.subpassCount = 1; - pass_info.pSubpasses = &subpass; - - VkRenderPass vk_render_pass = VK_NULL_HANDLE; - err = _create_render_pass(vk_device, &pass_info, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_RENDER_PASS), &vk_render_pass); - ERR_FAIL_COND_V(err != VK_SUCCESS, SwapChainID()); - - RenderPassInfo *render_pass_info = VersatileResource::allocate(resources_allocator); - render_pass_info->vk_render_pass = vk_render_pass; - + // Create an empty swap chain until it is resized. SwapChain *swap_chain = memnew(SwapChain); swap_chain->surface = p_surface; - swap_chain->format = format; - swap_chain->color_space = color_space; - swap_chain->render_pass = RenderPassID(render_pass_info); return SwapChainID(swap_chain); } @@ -3325,6 +3330,34 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue, has_comp_alpha[(uint64_t)p_cmd_queue.id] = (composite_alpha != VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR); } + // Determine the format and color space for the swap chain. + VkFormat format = VK_FORMAT_UNDEFINED; + VkColorSpaceKHR color_space = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + if (!_determine_swap_chain_format(swap_chain->surface, format, color_space)) { + ERR_FAIL_V_MSG(ERR_CANT_CREATE, "Surface did not return any valid formats."); + } else { + swap_chain->format = format; + swap_chain->color_space = color_space; + } + + switch (swap_chain->color_space) { + case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR: + swap_chain->rdd_color_space = COLOR_SPACE_REC709_NONLINEAR_SRGB; + break; + break; + case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT: + swap_chain->rdd_color_space = COLOR_SPACE_REC709_LINEAR; + break; + default: + DEV_ASSERT(false && "Unknown swap chain color space."); + swap_chain->rdd_color_space = COLOR_SPACE_MAX; + } + + if (context_driver->is_colorspace_externally_managed()) { + // On Wayland the display server will use wp-color-management to manage the window's colorspace + swap_chain->color_space = VK_COLOR_SPACE_PASS_THROUGH_EXT; + } + VkSwapchainCreateInfoKHR swap_create_info = {}; swap_create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; swap_create_info.surface = surface->vk_surface; @@ -3423,10 +3456,48 @@ Error RenderingDeviceDriverVulkan::swap_chain_resize(CommandQueueID p_cmd_queue, swap_chain->framebuffers.reserve(image_count); - const RenderPassInfo *render_pass = (const RenderPassInfo *)(swap_chain->render_pass.id); + // Create the render pass for the chosen format. + VkAttachmentDescription2KHR attachment = {}; + attachment.sType = VK_STRUCTURE_TYPE_ATTACHMENT_DESCRIPTION_2_KHR; + attachment.format = format; + attachment.samples = VK_SAMPLE_COUNT_1_BIT; + attachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; + attachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + attachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + attachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + attachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + attachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + + VkAttachmentReference2KHR color_reference = {}; + color_reference.sType = VK_STRUCTURE_TYPE_ATTACHMENT_REFERENCE_2_KHR; + color_reference.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + + VkSubpassDescription2KHR subpass = {}; + subpass.sType = VK_STRUCTURE_TYPE_SUBPASS_DESCRIPTION_2_KHR; + subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; + subpass.colorAttachmentCount = 1; + subpass.pColorAttachments = &color_reference; + + VkRenderPassCreateInfo2KHR pass_info = {}; + pass_info.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO_2_KHR; + pass_info.attachmentCount = 1; + pass_info.pAttachments = &attachment; + pass_info.subpassCount = 1; + pass_info.pSubpasses = &subpass; + + VkRenderPass vk_render_pass = VK_NULL_HANDLE; + err = _create_render_pass(vk_device, &pass_info, VKC::get_allocation_callbacks(VK_OBJECT_TYPE_RENDER_PASS), &vk_render_pass); + ERR_FAIL_COND_V(err != VK_SUCCESS, ERR_CANT_CREATE); + + RenderPassInfo *render_pass_info = VersatileResource::allocate(resources_allocator); + render_pass_info->vk_render_pass = vk_render_pass; + + DEV_ASSERT(swap_chain->render_pass.id == 0); + swap_chain->render_pass = RenderPassID(render_pass_info); + VkFramebufferCreateInfo fb_create_info = {}; fb_create_info.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; - fb_create_info.renderPass = render_pass->vk_render_pass; + fb_create_info.renderPass = vk_render_pass; fb_create_info.attachmentCount = 1; fb_create_info.width = surface->width; fb_create_info.height = surface->height; @@ -3550,12 +3621,25 @@ RDD::DataFormat RenderingDeviceDriverVulkan::swap_chain_get_format(SwapChainID p return DATA_FORMAT_B8G8R8A8_UNORM; case VK_FORMAT_R8G8B8A8_UNORM: return DATA_FORMAT_R8G8B8A8_UNORM; + case VK_FORMAT_A2B10G10R10_UNORM_PACK32: + return DATA_FORMAT_A2B10G10R10_UNORM_PACK32; + case VK_FORMAT_A2R10G10B10_UNORM_PACK32: + return DATA_FORMAT_A2R10G10B10_UNORM_PACK32; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return DATA_FORMAT_R16G16B16A16_SFLOAT; default: DEV_ASSERT(false && "Unknown swap chain format."); return DATA_FORMAT_MAX; } } +RDD::ColorSpace RenderingDeviceDriverVulkan::swap_chain_get_color_space(SwapChainID p_swap_chain) { + DEV_ASSERT(p_swap_chain.id != 0); + + SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); + return swap_chain->rdd_color_space; +} + void RenderingDeviceDriverVulkan::swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) { DEV_ASSERT(p_swap_chain.id != 0); @@ -3578,10 +3662,6 @@ void RenderingDeviceDriverVulkan::swap_chain_free(SwapChainID p_swap_chain) { SwapChain *swap_chain = (SwapChain *)(p_swap_chain.id); _swap_chain_release(swap_chain); - if (swap_chain->render_pass) { - render_pass_free(swap_chain->render_pass); - } - memdelete(swap_chain); } @@ -5995,6 +6075,8 @@ bool RenderingDeviceDriverVulkan::has_feature(Features p_feature) { #endif case SUPPORTS_VULKAN_MEMORY_MODEL: return vulkan_memory_model_support && vulkan_memory_model_device_scope_support; + case SUPPORTS_HDR_OUTPUT: + return context_driver->is_colorspace_supported(); default: return false; } diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h index ff3fe7963fec..dc8ffed64aef 100644 --- a/drivers/vulkan/rendering_device_driver_vulkan.h +++ b/drivers/vulkan/rendering_device_driver_vulkan.h @@ -362,6 +362,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { RenderingContextDriver::SurfaceID surface = RenderingContextDriver::SurfaceID(); VkFormat format = VK_FORMAT_UNDEFINED; VkColorSpaceKHR color_space = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + RDD::ColorSpace rdd_color_space = RDD::COLOR_SPACE_REC709_NONLINEAR_SRGB; TightLocalVector images; TightLocalVector image_views; TightLocalVector present_semaphores; @@ -376,6 +377,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { #endif }; + bool _determine_swap_chain_format(RenderingContextDriver::SurfaceID p_surface, VkFormat &r_format, VkColorSpaceKHR &r_color_space); void _swap_chain_release(SwapChain *p_swap_chain); public: @@ -385,6 +387,7 @@ class RenderingDeviceDriverVulkan : public RenderingDeviceDriver { virtual RenderPassID swap_chain_get_render_pass(SwapChainID p_swap_chain) override final; virtual int swap_chain_get_pre_rotation_degrees(SwapChainID p_swap_chain) override final; virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) override final; + virtual ColorSpace swap_chain_get_color_space(SwapChainID p_swap_chain) override final; virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) override final; virtual void swap_chain_free(SwapChainID p_swap_chain) override final; diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index abc50032c933..388a421a25ec 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -462,6 +462,14 @@ void EditorNode::_update_from_settings() { scene_root->propagate_notification(Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED); } + // Enable HDR if requested and available. + bool hdr_output_needs_hdr_2d = false; + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HDR) && RD::get_singleton() && RD::get_singleton()->has_feature(RD::Features::SUPPORTS_HDR_OUTPUT)) { + bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output"); + DisplayServer::get_singleton()->window_request_hdr_output(hdr_requested); + hdr_output_needs_hdr_2d = hdr_requested; + } + RS::DOFBokehShape dof_shape = RS::DOFBokehShape(int(GLOBAL_GET("rendering/camera/depth_of_field/depth_of_field_bokeh_shape"))); RS::get_singleton()->camera_attributes_set_dof_blur_bokeh_shape(dof_shape); RS::DOFBlurQuality dof_quality = RS::DOFBlurQuality(int(GLOBAL_GET("rendering/camera/depth_of_field/depth_of_field_bokeh_quality"))); @@ -521,8 +529,11 @@ void EditorNode::_update_from_settings() { get_viewport()->set_use_debanding(use_debanding); bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d"); - scene_root->set_use_hdr_2d(use_hdr_2d); - get_viewport()->set_use_hdr_2d(use_hdr_2d); + scene_root->set_use_hdr_2d(use_hdr_2d || hdr_output_needs_hdr_2d); + get_viewport()->set_use_hdr_2d(use_hdr_2d || hdr_output_needs_hdr_2d); + if (use_hdr_2d != hdr_output_needs_hdr_2d) { + WARN_PRINT("Forcing HDR 2D on because HDR output is enabled. To avoid this warning, enable rendering/viewport/hdr_2d in the Project Settings."); + } float mesh_lod_threshold = GLOBAL_GET("rendering/mesh_lod/lod_change/threshold_pixels"); scene_root->set_mesh_lod_threshold(mesh_lod_threshold); diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp index aac34bd69124..e871db5a71a7 100644 --- a/editor/scene/3d/node_3d_editor_plugin.cpp +++ b/editor/scene/3d/node_3d_editor_plugin.cpp @@ -3083,6 +3083,12 @@ void Node3DEditorViewport::_project_settings_changed() { _update_shrink(); + // Check if HDR is supported and enabled and force 2D HDR if so. + bool hdr_output_needs_hdr_2d = false; + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HDR) && RD::get_singleton() && RD::get_singleton()->has_feature(RD::Features::SUPPORTS_HDR_OUTPUT)) { + hdr_output_needs_hdr_2d = GLOBAL_GET("display/window/hdr/request_hdr_output"); + } + // Update MSAA, screen-space AA and debanding if changed const int msaa_mode = GLOBAL_GET("rendering/anti_aliasing/quality/msaa_3d"); @@ -3096,7 +3102,7 @@ void Node3DEditorViewport::_project_settings_changed() { viewport->set_transparent_background(transparent_background); const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d"); - viewport->set_use_hdr_2d(use_hdr_2d); + viewport->set_use_hdr_2d(use_hdr_2d || hdr_output_needs_hdr_2d); const bool use_debanding = GLOBAL_GET("rendering/anti_aliasing/quality/use_debanding"); viewport->set_use_debanding(use_debanding); @@ -9353,7 +9359,7 @@ void Node3DEditor::_load_default_preview_settings() { if (OS::get_singleton()->get_current_rendering_method() != "gl_compatibility" && OS::get_singleton()->get_current_rendering_method() != "dummy") { environ_glow_button->set_pressed_no_signal(true); } - environ_tonemap_button->set_pressed_no_signal(true); + environ_tonemap_button->set_pressed_no_signal(false); environ_ao_button->set_pressed_no_signal(false); environ_gi_button->set_pressed_no_signal(false); sun_shadow_max_distance->set_value_no_signal(100); diff --git a/main/main.cpp b/main/main.cpp index 8df3d00c8525..5c96587a5bce 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -3389,6 +3389,12 @@ Error Main::setup2(bool p_show_boot_logo) { } } + // Enable HDR if requested and available. + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HDR) && RD::get_singleton() && RD::get_singleton()->has_feature(RD::Features::SUPPORTS_HDR_OUTPUT)) { + bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output"); + DisplayServer::get_singleton()->window_request_hdr_output(hdr_requested); + } + Color clear = GLOBAL_DEF_BASIC("rendering/environment/defaults/default_clear_color", Color(0.3, 0.3, 0.3)); RenderingServer::get_singleton()->set_default_clear_color(clear); diff --git a/platform/linuxbsd/wayland/SCsub b/platform/linuxbsd/wayland/SCsub index 2fe1c36ffaba..f20debc127c9 100644 --- a/platform/linuxbsd/wayland/SCsub +++ b/platform/linuxbsd/wayland/SCsub @@ -58,6 +58,9 @@ generated_sources = [ generate_from_xml("viewporter", "#thirdparty/wayland-protocols/stable/viewporter/viewporter.xml"), generate_from_xml("xdg_shell", "#thirdparty/wayland-protocols/stable/xdg-shell/xdg-shell.xml"), # Staging protocols + generate_from_xml( + "color_management", "#thirdparty/wayland-protocols/staging/color-management/color-management-v1.xml" + ), generate_from_xml("cursor_shape", "#thirdparty/wayland-protocols/staging/cursor-shape/cursor-shape-v1.xml"), generate_from_xml( "fractional_scale", "#thirdparty/wayland-protocols/staging/fractional-scale/fractional-scale-v1.xml" diff --git a/platform/linuxbsd/wayland/display_server_wayland.cpp b/platform/linuxbsd/wayland/display_server_wayland.cpp index 62b6271d7a57..dd1c57562656 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.cpp +++ b/platform/linuxbsd/wayland/display_server_wayland.cpp @@ -39,6 +39,7 @@ #define DEBUG_LOG_WAYLAND(...) #endif +#include "core/config/project_settings.h" #include "core/os/main_loop.h" #include "servers/rendering/dummy/rasterizer_dummy.h" @@ -195,7 +196,8 @@ bool DisplayServerWayland::has_feature(Feature p_feature) const { case FEATURE_WINDOW_DRAG: case FEATURE_CLIPBOARD_PRIMARY: case FEATURE_SUBWINDOWS: - case FEATURE_SELF_FITTING_WINDOWS: { + case FEATURE_SELF_FITTING_WINDOWS: + case FEATURE_HDR: { return true; } break; @@ -739,6 +741,9 @@ DisplayServer::WindowID DisplayServerWayland::create_sub_window(WindowMode p_mod // can only know once we show it. wd.rect = p_rect; + wd.color_profile.target_max_luminance = GLOBAL_GET("display/window/hdr/max_luminance"); + wd.color_profile.reference_luminance = GLOBAL_GET("display/window/hdr/reference_luminance"); + wd.title = "Godot"; wd.parent_id = p_transient_parent; return id; @@ -1437,6 +1442,126 @@ void DisplayServerWayland::window_start_resize(WindowResizeEdge p_edge, WindowID wayland_thread.window_start_resize(p_edge, p_window); } +void DisplayServerWayland::_window_update_hdr_state(WindowID p_window_id) { + ERR_FAIL_COND(!windows.has(p_window_id)); + WindowData &wd = windows[p_window_id]; + +#if defined(RD_ENABLED) + if (rendering_context) { + // The `display/window/hdr/enabled` project setting makes all windows request hdr. + // On Windows this means enable hdr for all windows on an hdr screen. + // Since on Wayland all screens support hdr we use whether the window "prefers" hdr or not instead. + bool hdr_preferred = wd.color_profile.target_max_luminance > wd.color_profile.reference_luminance; + bool hdr_desired = wayland_thread.supports_hdr() && hdr_preferred && wd.hdr_requested; + + if (rendering_context->window_get_hdr_output_enabled(p_window_id) != hdr_desired) { + rendering_context->window_set_hdr_output_enabled(p_window_id, hdr_desired); + } + + if (hdr_desired) { + rendering_context->window_set_hdr_output_max_luminance(p_window_id, wd.color_profile.target_max_luminance); + rendering_context->window_set_hdr_output_reference_luminance(p_window_id, wd.color_profile.reference_luminance); + rendering_context->window_set_hdr_output_linear_luminance_scale(p_window_id, wd.color_profile.target_max_luminance); + + wd.color_profile.named_primary = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB; + wd.color_profile.named_transfer_function = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR; + } else { + wd.color_profile.named_primary = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB; + wd.color_profile.named_transfer_function = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22; + } + + MutexLock mutex_lock(wayland_thread.mutex); + wayland_thread.window_set_color_profile(p_window_id, wd.color_profile); + } +#endif +} + +bool DisplayServerWayland::window_is_hdr_output_supported(WindowID p_window_id) const { + ERR_FAIL_COND_V(!windows.has(p_window_id), false); + const WindowData &wd = windows[p_window_id]; + + return wd.color_profile.target_max_luminance > wd.color_profile.reference_luminance; +} + +void DisplayServerWayland::window_request_hdr_output(const bool p_enabled, WindowID p_window_id) { +#if defined(RD_ENABLED) + ERR_FAIL_COND_MSG(!(rendering_device && rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)), "HDR output is not supported by the rendering device."); +#endif + + ERR_FAIL_COND(!windows.has(p_window_id)); + WindowData &wd = windows[p_window_id]; + wd.hdr_requested = p_enabled; + + _window_update_hdr_state(p_window_id); +} + +bool DisplayServerWayland::window_is_hdr_output_requested(WindowID p_window_id) const { + ERR_FAIL_COND_V(!windows.has(p_window_id), false); + const WindowData &wd = windows[p_window_id]; + return wd.hdr_requested; +} + +bool DisplayServerWayland::window_is_hdr_output_enabled(WindowID p_window_id) const { +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_enabled(p_window_id); + } +#endif + return false; +} + +void DisplayServerWayland::window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window_id) { + ERR_FAIL_COND(!windows.has(p_window_id)); + WindowData &wd = windows[p_window_id]; + wd.color_profile.requested_reference_luminance = p_reference_luminance; +} + +float DisplayServerWayland::window_get_hdr_output_reference_luminance(WindowID p_window_id) const { + ERR_FAIL_COND_V(!windows.has(p_window_id), 0.0f); + const WindowData &wd = windows[p_window_id]; + return wd.color_profile.requested_reference_luminance; +} + +float DisplayServerWayland::window_get_hdr_output_current_reference_luminance(WindowID p_window_id) const { +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_reference_luminance(p_window_id); + } +#endif + return 0.0f; +} + +void DisplayServerWayland::window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window_id) { + ERR_FAIL_COND(!windows.has(p_window_id)); + WindowData &wd = windows[p_window_id]; + wd.color_profile.requested_max_luminance = p_max_luminance; +} + +float DisplayServerWayland::window_get_hdr_output_max_luminance(WindowID p_window_id) const { + ERR_FAIL_COND_V(!windows.has(p_window_id), 0.0f); + const WindowData &wd = windows[p_window_id]; + return wd.color_profile.requested_max_luminance; +} + +float DisplayServerWayland::window_get_hdr_output_current_max_luminance(WindowID p_window_id) const { +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_max_luminance(p_window_id); + } +#endif + return 0.0f; +} + +float DisplayServerWayland::window_get_output_max_value(WindowID p_window_id) const { +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_max_luminance(p_window_id); + } +#endif + + return 0.0f; +} + void DisplayServerWayland::cursor_set_shape(CursorShape p_shape) { ERR_FAIL_INDEX(p_shape, CURSOR_MAX); @@ -1734,6 +1859,14 @@ void DisplayServerWayland::process_events() { } continue; } + + Ref color_profile_msg = msg; + if (color_profile_msg.is_valid()) { + WindowData &wd = windows[color_profile_msg->id]; + wd.color_profile = color_profile_msg->color_profile; + + _window_update_hdr_state(color_profile_msg->id); + } } wayland_thread.keyboard_echo_keys(); diff --git a/platform/linuxbsd/wayland/display_server_wayland.h b/platform/linuxbsd/wayland/display_server_wayland.h index daa436008e81..a2703421590c 100644 --- a/platform/linuxbsd/wayland/display_server_wayland.h +++ b/platform/linuxbsd/wayland/display_server_wayland.h @@ -101,6 +101,9 @@ class DisplayServerWayland : public DisplayServer { DisplayServer::WindowMode mode = WINDOW_MODE_WINDOWED; + bool hdr_requested; + WaylandThread::ColorProfile color_profile; + Callable rect_changed_callback; Callable window_event_callback; Callable input_event_callback; @@ -183,6 +186,8 @@ class DisplayServerWayland : public DisplayServer { void _update_window_rect(const Rect2i &p_rect, WindowID p_window_id = MAIN_WINDOW_ID); + void _window_update_hdr_state(WindowID p_window); + void try_suspend(); void initialize_tts() const; @@ -322,6 +327,22 @@ class DisplayServerWayland : public DisplayServer { virtual void window_start_drag(WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_start_resize(WindowResizeEdge p_edge, WindowID p_window) override; + virtual bool window_is_hdr_output_supported(WindowID p_window_id = MAIN_WINDOW_ID) const override; + + virtual void window_request_hdr_output(const bool p_enabled, WindowID p_window_id = MAIN_WINDOW_ID) override; + virtual bool window_is_hdr_output_requested(WindowID p_window_id = MAIN_WINDOW_ID) const override; + virtual bool window_is_hdr_output_enabled(WindowID p_window_id = MAIN_WINDOW_ID) const override; + + virtual void window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window_id = MAIN_WINDOW_ID) override; + virtual float window_get_hdr_output_reference_luminance(WindowID p_window_id = MAIN_WINDOW_ID) const override; + virtual float window_get_hdr_output_current_reference_luminance(WindowID p_window_id = MAIN_WINDOW_ID) const override; + + virtual void window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window_id = MAIN_WINDOW_ID) override; + virtual float window_get_hdr_output_max_luminance(WindowID p_window_id = MAIN_WINDOW_ID) const override; + virtual float window_get_hdr_output_current_max_luminance(WindowID p_window_id = MAIN_WINDOW_ID) const override; + + virtual float window_get_output_max_value(WindowID p_window_id = MAIN_WINDOW_ID) const override; + virtual void cursor_set_shape(CursorShape p_shape) override; virtual CursorShape cursor_get_shape() const override; virtual void cursor_set_custom_image(const Ref &p_cursor, CursorShape p_shape, const Vector2 &p_hotspot) override; diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp index 8abcc464baa2..27bc0e291b01 100644 --- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp +++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.cpp @@ -55,6 +55,10 @@ RenderingContextDriver::SurfaceID RenderingContextDriverVulkanWayland::surface_c return SurfaceID(surface); } +bool RenderingContextDriverVulkanWayland::is_colorspace_externally_managed() const { + return true; +} + RenderingContextDriverVulkanWayland::RenderingContextDriverVulkanWayland() { // Does nothing. } diff --git a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.h b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.h index 0515471eb92e..3a7f8f326e68 100644 --- a/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.h +++ b/platform/linuxbsd/wayland/rendering_context_driver_vulkan_wayland.h @@ -40,6 +40,7 @@ class RenderingContextDriverVulkanWayland : public RenderingContextDriverVulkan protected: SurfaceID surface_create(const void *p_platform_data) override final; + virtual bool is_colorspace_externally_managed() const override final; public: struct WindowPlatformData { diff --git a/platform/linuxbsd/wayland/wayland_thread.cpp b/platform/linuxbsd/wayland/wayland_thread.cpp index a845f40c0d6a..ab49904a3d03 100644 --- a/platform/linuxbsd/wayland/wayland_thread.cpp +++ b/platform/linuxbsd/wayland/wayland_thread.cpp @@ -549,6 +549,16 @@ void WaylandThread::_wl_registry_on_global(void *data, struct wl_registry *wl_re registry->wp_viewporter_name = name; } + if (strcmp(interface, wp_color_manager_v1_interface.name) == 0) { + registry->wp_color_manager = (struct wp_color_manager_v1 *)wl_registry_bind(wl_registry, name, &wp_color_manager_v1_interface, 1); + registry->wp_color_manager_name = name; + wl_proxy_tag_godot((struct wl_proxy *)registry->wp_color_manager); + + WaylandThread::ColorManagementState *color_state = memnew(WaylandThread::ColorManagementState); + wp_color_manager_v1_add_listener(registry->wp_color_manager, &wp_color_manager_listener, color_state); + return; + } + if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { registry->wp_cursor_shape_manager = (struct wp_cursor_shape_manager_v1 *)wl_registry_bind(wl_registry, name, &wp_cursor_shape_manager_v1_interface, 1); registry->wp_cursor_shape_manager_name = name; @@ -767,6 +777,32 @@ void WaylandThread::_wl_registry_on_global_remove(void *data, struct wl_registry return; } + if (name == registry->wp_color_manager_name) { + for (KeyValue &pair : registry->wayland_thread->windows) { + WindowState ws = pair.value; + + if (ws.wp_color_management_surface) { + wp_color_management_surface_v1_destroy(ws.wp_color_management_surface); + ws.wp_color_management_surface = nullptr; + } + + if (ws.wp_color_management_surface_feedback) { + wp_color_management_surface_feedback_v1_destroy(ws.wp_color_management_surface_feedback); + ws.wp_color_management_surface_feedback = nullptr; + } + } + + if (registry->wp_color_manager) { + ColorManagementState *color_state = wp_color_manager_get_state(registry->wp_color_manager); + memdelete(color_state); + + wp_color_manager_v1_destroy(registry->wp_color_manager); + registry->wp_color_manager = nullptr; + } + + registry->wp_color_manager_name = 0; + } + if (name == registry->wp_cursor_shape_manager_name) { if (registry->wp_cursor_shape_manager) { wp_cursor_shape_manager_v1_destroy(registry->wp_cursor_shape_manager); @@ -2360,6 +2396,117 @@ void WaylandThread::_wl_data_source_on_dnd_finished(void *data, struct wl_data_s void WaylandThread::_wl_data_source_on_action(void *data, struct wl_data_source *wl_data_source, uint32_t dnd_action) { } +void WaylandThread::_wp_color_manager_on_supported_intent(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t render_intent) { + ColorManagementState *cms = (ColorManagementState *)data; + ERR_FAIL_NULL(cms); + + cms->supported_render_intents |= 1 << render_intent; +} + +void WaylandThread::_wp_color_manager_on_supported_feature(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t feature) { + ColorManagementState *cms = (ColorManagementState *)data; + ERR_FAIL_NULL(cms); + + cms->supported_render_feature |= 1 << feature; +} + +void WaylandThread::_wp_color_manager_on_supported_tf_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t tf) { + ColorManagementState *cms = (ColorManagementState *)data; + ERR_FAIL_NULL(cms); + + cms->supported_transfer_function |= 1 << tf; +} + +void WaylandThread::_wp_color_manager_on_supported_primaries_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t primaries) { + ColorManagementState *cms = (ColorManagementState *)data; + ERR_FAIL_NULL(cms); + + cms->supported_primaries |= 1 << primaries; +} + +void WaylandThread::_wp_color_manager_on_done(void *data, struct wp_color_manager_v1 *wp_color_manager_v1) { +} + +void WaylandThread::_wp_color_management_surface_feedback_on_preferred_changed(void *data, struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback_v1, uint32_t identity) { + struct wp_image_description_v1 *image_description = wp_color_management_surface_feedback_v1_get_preferred_parametric(wp_color_management_surface_feedback_v1); + + wp_image_description_v1_add_listener(image_description, &wp_image_description_listener, data); +} + +void WaylandThread::_wp_image_description_on_failed(void *data, struct wp_image_description_v1 *image_descrptor, uint32_t cause, const char *msg) { + WARN_PRINT(msg); +} + +void WaylandThread::_wp_image_description_on_ready(void *data, struct wp_image_description_v1 *image_descriptor, uint32_t identity) { + WindowState *ws = (WindowState *)data; + ERR_FAIL_NULL(ws); + + struct wp_image_description_info_v1 *image_info = wp_image_description_v1_get_information(image_descriptor); + if (image_info != nullptr) { + ColorProfileMessage *msg = memnew(ColorProfileMessage); + msg->id = ws->id; + msg->wayland_thread = ws->wayland_thread; + + wp_image_description_info_v1_add_listener(image_info, &wp_image_description_info_listener, msg); + wp_image_description_v1_destroy(image_descriptor); + } +} + +void WaylandThread::_wp_image_description_info_on_done(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1) { + ColorProfileMessage *msg = (ColorProfileMessage *)data; + ERR_FAIL_NULL(msg); + + msg->wayland_thread->push_message(msg); +} + +void WaylandThread::_wp_image_description_info_on_icc_file(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t icc, uint32_t icc_size) { + ::close(icc); +} + +void WaylandThread::_wp_image_description_info_on_primaries(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { +} + +void WaylandThread::_wp_image_description_info_on_primaries_named(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t primaries) { + ColorProfileMessage *msg = (ColorProfileMessage *)data; + ERR_FAIL_NULL(msg); + + msg->color_profile.named_primary = primaries; +} + +void WaylandThread::_wp_image_description_info_on_tf_power(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t eexp) { +} + +void WaylandThread::_wp_image_description_info_on_tf_named(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t tf) { + ColorProfileMessage *msg = (ColorProfileMessage *)data; + ERR_FAIL_NULL(msg); + + msg->color_profile.named_transfer_function = tf; +} + +void WaylandThread::_wp_image_description_info_on_luminances(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum) { + ColorProfileMessage *msg = (ColorProfileMessage *)data; + ERR_FAIL_NULL(msg); + + msg->color_profile.reference_luminance = reference_lum; +} + +void WaylandThread::_wp_image_description_info_on_target_primaries(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y) { +} + +void WaylandThread::_wp_image_description_info_on_target_luminance(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t min_lum, uint32_t max_lum) { + ColorProfileMessage *msg = (ColorProfileMessage *)data; + ERR_FAIL_NULL(msg); + + msg->color_profile.target_min_luminance = static_cast(min_lum) / 10000; // The uint32 is multiplied by 10000 for precision + msg->color_profile.target_max_luminance = max_lum; +} + +void WaylandThread::_wp_image_description_info_on_target_max_cll(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t max_cll) { +} + +void WaylandThread::_wp_image_description_info_on_target_max_fall(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t max_fall) { +} + void WaylandThread::_wp_fractional_scale_on_preferred_scale(void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale) { WindowState *ws = (WindowState *)data; ERR_FAIL_NULL(ws); @@ -3183,6 +3330,14 @@ WaylandThread::OfferState *WaylandThread::wp_primary_selection_offer_get_offer_s return nullptr; } +WaylandThread::ColorManagementState *WaylandThread::wp_color_manager_get_state(wp_color_manager_v1 *p_color_manager) { + if (p_color_manager && wl_proxy_is_godot((wl_proxy *)p_color_manager)) { + return (ColorManagementState *)wp_color_manager_v1_get_user_data(p_color_manager); + } + + return nullptr; +} + // This is implemented as a method because this is the simplest way of // accounting for dynamic output scale changes. int WaylandThread::window_state_get_preferred_buffer_scale(WindowState *p_ws) { @@ -3561,6 +3716,14 @@ void WaylandThread::window_create(DisplayServer::WindowID p_window_id, int p_wid } } + if (supports_hdr()) { + ws.wp_color_management_surface_feedback = wp_color_manager_v1_get_surface_feedback(registry.wp_color_manager, ws.wl_surface); + wp_color_management_surface_feedback_v1_add_listener(ws.wp_color_management_surface_feedback, &wp_color_management_surface_feedback_listener, &ws); + + //NOTE: requires vulkan to use the VK_COLOR_SPACE_PASSTHROUGH_EXT colorspace to not raise protocol errors + ws.wp_color_management_surface = wp_color_manager_v1_get_surface(registry.wp_color_manager, ws.wl_surface); + } + bool decorated = false; #ifdef LIBDECOR_ENABLED @@ -3727,6 +3890,14 @@ void WaylandThread::window_destroy(DisplayServer::WindowID p_window_id) { wp_fractional_scale_v1_destroy(ws.wp_fractional_scale); } + if (ws.wp_color_management_surface) { + wp_color_management_surface_v1_destroy(ws.wp_color_management_surface); + } + + if (ws.wp_color_management_surface_feedback) { + wp_color_management_surface_feedback_v1_destroy(ws.wp_color_management_surface_feedback); + } + if (ws.wp_viewport) { wp_viewport_destroy(ws.wp_viewport); } @@ -4269,6 +4440,34 @@ bool WaylandThread::window_get_idle_inhibition(DisplayServer::WindowID p_window_ return ws.wp_idle_inhibitor != nullptr; } +void WaylandThread::window_set_color_profile(DisplayServer::WindowID p_window_id, ColorProfile p_profile) { + ERR_FAIL_COND(!windows.has(p_window_id)); + const WindowState &ws = windows[p_window_id]; + + if (!ws.wp_color_management_surface) { + return; + } + + ColorManagementState *cms = wp_color_manager_get_state(registry.wp_color_manager); + + struct wp_image_description_creator_params_v1 *builder = wp_color_manager_v1_create_parametric_creator(registry.wp_color_manager); + wp_image_description_creator_params_v1_set_primaries_named(builder, p_profile.named_primary); + wp_image_description_creator_params_v1_set_tf_named(builder, p_profile.named_transfer_function); + + if ((cms->supported_render_feature & WP_COLOR_MANAGER_V1_FEATURE_SET_LUMINANCES) > 0) { + if (p_profile.named_transfer_function == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22) { + wp_image_description_creator_params_v1_set_luminances(builder, 2000, 80, 80); + } else { + uint32_t min_luminance = static_cast(p_profile.target_min_luminance * 10000); + wp_image_description_creator_params_v1_set_luminances(builder, min_luminance, p_profile.target_max_luminance, p_profile.reference_luminance); + } + } + + struct wp_image_description_v1 *image_desc = wp_image_description_creator_params_v1_create(builder); + wp_color_management_surface_v1_set_image_description(ws.wp_color_management_surface, image_desc, WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); + wp_image_description_v1_destroy(image_desc); +} + WaylandThread::ScreenData WaylandThread::screen_get_data(int p_screen) const { ERR_FAIL_INDEX_V(p_screen, registry.wl_outputs.size(), ScreenData()); @@ -4856,6 +5055,22 @@ void WaylandThread::primary_set_text(const String &p_text) { wl_display_roundtrip(wl_display); } +bool WaylandThread::supports_hdr() const { + ColorManagementState *color_state = wp_color_manager_get_state(registry.wp_color_manager); + if (!color_state) { + return false; + } + + // We require parametric profiles until we can support reading ICC files. + if ((color_state->supported_render_feature & (1 << WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC)) == 0) { + return false; + } + + // We require the compositor to support extended linear sRGB. + // sRGB primaries support is assumed. + return color_state->supported_transfer_function & (1 << WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); +} + void WaylandThread::commit_surfaces() { for (KeyValue &pair : windows) { wl_surface_commit(pair.value.wl_surface); @@ -5165,6 +5380,10 @@ void WaylandThread::destroy() { zxdg_decoration_manager_v1_destroy(registry.xdg_decoration_manager); } + if (registry.wp_color_manager) { + wp_color_manager_v1_destroy(registry.wp_color_manager); + } + if (registry.wp_cursor_shape_manager) { wp_cursor_shape_manager_v1_destroy(registry.wp_cursor_shape_manager); } diff --git a/platform/linuxbsd/wayland/wayland_thread.h b/platform/linuxbsd/wayland/wayland_thread.h index 37ae4a27e785..0d5361269691 100644 --- a/platform/linuxbsd/wayland/wayland_thread.h +++ b/platform/linuxbsd/wayland/wayland_thread.h @@ -60,6 +60,7 @@ #include "wayland/protocol/pointer_gestures.gen.h" #include "wayland/protocol/relative_pointer.gen.h" #undef pointer +#include "wayland/protocol/color_management.gen.h" #include "wayland/protocol/fractional_scale.gen.h" #include "wayland/protocol/tablet.gen.h" #include "wayland/protocol/text_input.gen.h" @@ -88,6 +89,21 @@ class WaylandThread { public: + struct ColorProfile { + uint32_t named_primary = WP_COLOR_MANAGER_V1_PRIMARIES_SRGB; + uint32_t named_transfer_function = WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_GAMMA22; + + // The luminances the compositor recommends. + float target_min_luminance = 0; + float target_max_luminance = 0; + float reference_luminance = 0; + + // The luminances the developer requested. + // We never pass these to compositor and are just for convenience. + float requested_max_luminance = 0; + float requested_reference_luminance = 0; + }; + // Messages used for exchanging information between Godot's and Wayland's thread. class Message : public RefCounted { GDSOFTCLASS(Message, RefCounted); @@ -150,6 +166,14 @@ class WaylandThread { String text; }; + class ColorProfileMessage : public WindowMessage { + GDSOFTCLASS(ColorProfileMessage, WindowMessage); + + public: + WaylandThread *wayland_thread; + ColorProfile color_profile; + }; + struct RegistryState { WaylandThread *wayland_thread; @@ -186,6 +210,9 @@ class WaylandThread { struct wp_viewporter *wp_viewporter = nullptr; uint32_t wp_viewporter_name = 0; + struct wp_color_manager_v1 *wp_color_manager = nullptr; + uint32_t wp_color_manager_name = 0; + struct wp_fractional_scale_manager_v1 *wp_fractional_scale_manager = nullptr; uint32_t wp_fractional_scale_manager_name = 0; @@ -294,6 +321,9 @@ class WaylandThread { // What the compositor is recommending us. double preferred_fractional_scale = 0; + struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback = nullptr; + struct wp_color_management_surface_v1 *wp_color_management_surface = nullptr; + struct zxdg_toplevel_decoration_v1 *xdg_toplevel_decoration = nullptr; struct zwp_idle_inhibitor_v1 *wp_idle_inhibitor = nullptr; @@ -529,6 +559,14 @@ class WaylandThread { Point2i hotspot; }; + struct ColorManagementState { + // Compositor features + uint32_t supported_render_intents = 0; + uint32_t supported_render_feature = 0; + uint32_t supported_transfer_function = 0; + uint32_t supported_primaries = 0; + }; + private: struct ThreadData { SafeFlag thread_done; @@ -686,6 +724,29 @@ class WaylandThread { static void _xdg_popup_on_repositioned(void *data, struct xdg_popup *xdg_popup, uint32_t token); // wayland-protocols event handlers. + static void _wp_color_manager_on_supported_intent(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t render_intent); + static void _wp_color_manager_on_supported_feature(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t feature); + static void _wp_color_manager_on_supported_tf_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t tf); + static void _wp_color_manager_on_supported_primaries_named(void *data, struct wp_color_manager_v1 *wp_color_manager_v1, uint32_t primaries); + static void _wp_color_manager_on_done(void *data, struct wp_color_manager_v1 *wp_color_manager_v1); + + static void _wp_color_management_surface_feedback_on_preferred_changed(void *data, struct wp_color_management_surface_feedback_v1 *wp_color_management_surface_feedback_v1, uint32_t identity); + + static void _wp_image_description_on_failed(void *data, struct wp_image_description_v1 *wp_image_description_v1, uint32_t cause, const char *msg); + static void _wp_image_description_on_ready(void *data, struct wp_image_description_v1 *wp_image_description_v1, uint32_t identity); + + static void _wp_image_description_info_on_done(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1); + static void _wp_image_description_info_on_icc_file(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t icc, uint32_t icc_size); + static void _wp_image_description_info_on_primaries(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y); + static void _wp_image_description_info_on_primaries_named(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t primaries); + static void _wp_image_description_info_on_tf_power(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t eexp); + static void _wp_image_description_info_on_tf_named(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t tf); + static void _wp_image_description_info_on_luminances(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t min_lum, uint32_t max_lum, uint32_t reference_lum); + static void _wp_image_description_info_on_target_primaries(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, int32_t r_x, int32_t r_y, int32_t g_x, int32_t g_y, int32_t b_x, int32_t b_y, int32_t w_x, int32_t w_y); + static void _wp_image_description_info_on_target_luminance(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t min_lum, uint32_t max_lum); + static void _wp_image_description_info_on_target_max_cll(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t max_cll); + static void _wp_image_description_info_on_target_max_fall(void *data, struct wp_image_description_info_v1 *wp_image_description_info_v1, uint32_t max_fall); + static void _wp_fractional_scale_on_preferred_scale(void *data, struct wp_fractional_scale_v1 *wp_fractional_scale_v1, uint32_t scale); static void _wp_relative_pointer_on_relative_motion(void *data, struct zwp_relative_pointer_v1 *wp_relative_pointer_v1, uint32_t uptime_hi, uint32_t uptime_lo, wl_fixed_t dx, wl_fixed_t dy, wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel); @@ -847,6 +908,37 @@ class WaylandThread { }; // wayland-protocols event listeners. + static constexpr struct wp_color_manager_v1_listener wp_color_manager_listener = { + .supported_intent = _wp_color_manager_on_supported_intent, + .supported_feature = _wp_color_manager_on_supported_feature, + .supported_tf_named = _wp_color_manager_on_supported_tf_named, + .supported_primaries_named = _wp_color_manager_on_supported_primaries_named, + .done = _wp_color_manager_on_done, + }; + + static constexpr struct wp_color_management_surface_feedback_v1_listener wp_color_management_surface_feedback_listener = { + .preferred_changed = _wp_color_management_surface_feedback_on_preferred_changed, + }; + + static constexpr struct wp_image_description_v1_listener wp_image_description_listener = { + .failed = _wp_image_description_on_failed, + .ready = _wp_image_description_on_ready, + }; + + static constexpr struct wp_image_description_info_v1_listener wp_image_description_info_listener = { + .done = _wp_image_description_info_on_done, + .icc_file = _wp_image_description_info_on_icc_file, + .primaries = _wp_image_description_info_on_primaries, + .primaries_named = _wp_image_description_info_on_primaries_named, + .tf_power = _wp_image_description_info_on_tf_power, + .tf_named = _wp_image_description_info_on_tf_named, + .luminances = _wp_image_description_info_on_luminances, + .target_primaries = _wp_image_description_info_on_target_primaries, + .target_luminance = _wp_image_description_info_on_target_luminance, + .target_max_cll = _wp_image_description_info_on_target_max_cll, + .target_max_fall = _wp_image_description_info_on_target_max_fall, + }; + static constexpr struct wp_fractional_scale_v1_listener wp_fractional_scale_listener = { .preferred_scale = _wp_fractional_scale_on_preferred_scale, }; @@ -1008,6 +1100,7 @@ class WaylandThread { static OfferState *wl_data_offer_get_offer_state(struct wl_data_offer *p_offer); static OfferState *wp_primary_selection_offer_get_offer_state(struct zwp_primary_selection_offer_v1 *p_offer); + static ColorManagementState *wp_color_manager_get_state(wp_color_manager_v1 *p_color_manager); void seat_state_unlock_pointer(SeatState *p_ss); void seat_state_lock_pointer(SeatState *p_ss); @@ -1065,6 +1158,9 @@ class WaylandThread { void window_set_idle_inhibition(DisplayServer::WindowID p_window_id, bool p_enable); bool window_get_idle_inhibition(DisplayServer::WindowID p_window_id) const; + // Optional - require wp_color_management_v1 + void window_set_color_profile(DisplayServer::WindowID p_window_id, ColorProfile p_profile); + ScreenData screen_get_data(int p_screen) const; int get_screen_count() const; @@ -1105,6 +1201,8 @@ class WaylandThread { void primary_set_text(const String &p_text); + bool supports_hdr() const; + void commit_surfaces(); void set_frame(); diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp index 1cce42d57f2e..331f70c787e9 100644 --- a/platform/windows/display_server_windows.cpp +++ b/platform/windows/display_server_windows.cpp @@ -151,6 +151,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const { case FEATURE_STATUS_INDICATOR: case FEATURE_WINDOW_EMBEDDING: case FEATURE_WINDOW_DRAG: + case FEATURE_HDR: return true; case FEATURE_SCREEN_EXCLUDE_FROM_CAPTURE: return (os_ver.dwBuildNumber >= 19041); // Fully supported on Windows 10 Vibranium R1 (2004)+ only, captured as black rect on older versions. @@ -1149,6 +1150,24 @@ static BOOL CALLBACK _MonitorEnumProcCount(HMONITOR hMonitor, HDC hdcMonitor, LP return TRUE; } +static BOOL CALLBACK _MonitorEnumProcMonitor(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { + EnumScreenData *data = (EnumScreenData *)dwData; + + if (data->screen == data->count) { + data->monitor = hMonitor; + return FALSE; + } + + data->count++; + return TRUE; +} + +static HMONITOR _get_hmonitor_of_screen(int p_screen) { + EnumScreenData data = { 0, p_screen, nullptr }; + EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcMonitor, (LPARAM)&data); + return data.monitor; +} + int DisplayServerWindows::get_screen_count() const { _THREAD_SAFE_METHOD_ @@ -1238,6 +1257,14 @@ typedef struct { float rate; } EnumRefreshRateData; +typedef struct { + Vector paths; + Vector modes; + int count; + int screen; + float sdrWhiteLevelInNits; +} EnumSdrWhiteLevelData; + static BOOL CALLBACK _MonitorEnumProcSize(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { EnumSizeData *data = (EnumSizeData *)dwData; if (data->count == data->screen) { @@ -1520,6 +1547,100 @@ Ref DisplayServerWindows::screen_get_image_rect(const Rect2i &p_rect) con return img; } +#ifdef D3D12_ENABLED +static bool _get_monitor_desc(HMONITOR p_monitor, DXGI_OUTPUT_DESC1 &r_Desc) { + ComPtr dxgi_factory; + r_Desc = {}; + + if (FAILED(CreateDXGIFactory2(0, IID_PPV_ARGS(&dxgi_factory)))) { + return false; + } + + // Note: As of August, 2025 Microsoft's sample code only checks the default + // adapter, but sometimes p_monitor may belong to another adapter. + ComPtr dxgiAdapter; + UINT adapter_i = 0; + while (dxgi_factory->EnumAdapters1(adapter_i, &dxgiAdapter) == S_OK) { + ComPtr dxgiOutput; + DXGI_OUTPUT_DESC1 desc1; + UINT output_i = 0; + while (dxgiAdapter->EnumOutputs(output_i, &dxgiOutput) != DXGI_ERROR_NOT_FOUND) { + ComPtr output6; + if (FAILED(dxgiOutput.As(&output6))) { + continue; + } + + if (FAILED(output6->GetDesc1(&desc1))) { + continue; + } + + if (desc1.Monitor == p_monitor) { + r_Desc = desc1; + return true; + } + + output_i++; + } + adapter_i++; + } + + return false; +} +#endif // D3D12_ENABLED + +static BOOL CALLBACK _MonitorEnumProcSdrWhiteLevel(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) { + EnumSdrWhiteLevelData *data = (EnumSdrWhiteLevelData *)dwData; + + // Only return TRUE to go to the next screen. Everything past this point should return FALSE. + if (data->count != data->screen) { + data->count++; + return TRUE; + } + + MONITORINFOEXW minfo; + memset(&minfo, 0, sizeof(minfo)); + minfo.cbSize = sizeof(minfo); + if (!GetMonitorInfoW(hMonitor, &minfo)) { + ERR_FAIL_V_MSG(FALSE, vformat("Failed to get monitor info for screen: %d", data->screen)); + } + + // Find this screen's path. + for (const DISPLAYCONFIG_PATH_INFO &path : data->paths) { + DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name; + memset(&source_name, 0, sizeof(source_name)); + source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source_name.header.size = sizeof(source_name); + source_name.header.adapterId = path.sourceInfo.adapterId; + source_name.header.id = path.sourceInfo.id; + + if (DisplayConfigGetDeviceInfo(&source_name.header) != ERROR_SUCCESS) { + ERR_PRINT(vformat("Failed to get source name for screen: %d, adapterId: 0x%08X%08X, id: %d", data->screen, (uint32_t)path.sourceInfo.adapterId.HighPart, (uint32_t)path.sourceInfo.adapterId.LowPart, path.sourceInfo.id)); + continue; + } + + if (wcscmp(minfo.szDevice, source_name.viewGdiDeviceName) != 0) { + continue; + } + + // Query the SDR white level. + DISPLAYCONFIG_SDR_WHITE_LEVEL sdr_white_level; + memset(&sdr_white_level, 0, sizeof(sdr_white_level)); + sdr_white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL; + sdr_white_level.header.size = sizeof(sdr_white_level); + sdr_white_level.header.adapterId = path.targetInfo.adapterId; + sdr_white_level.header.id = path.targetInfo.id; + + if (DisplayConfigGetDeviceInfo(&sdr_white_level.header) != ERROR_SUCCESS) { + ERR_PRINT(vformat("Failed to get SDR white level for screen: %d, adapterId: 0x%08X%08X, id: %d", data->screen, (uint32_t)path.targetInfo.adapterId.HighPart, (uint32_t)path.targetInfo.adapterId.LowPart, path.targetInfo.id)); + continue; + } + + data->sdrWhiteLevelInNits = (float)sdr_white_level.SDRWhiteLevel / 1000.0f * 80.0f; + } + + return FALSE; +} + float DisplayServerWindows::screen_get_refresh_rate(int p_screen) const { _THREAD_SAFE_METHOD_ @@ -3137,6 +3258,148 @@ HWND DisplayServerWindows::_find_window_from_process_id(OS::ProcessID p_pid, HWN return NULL; } +float DisplayServerWindows::_screen_get_reference_luminance(int p_screen) const { + const float default_return = 0.0f; + p_screen = _get_screen_index(p_screen); + EnumSdrWhiteLevelData data = { Vector(), Vector(), 0, p_screen, default_return }; + + uint32_t path_count = 0; + uint32_t mode_count = 0; + + if (GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) { + ERR_FAIL_V_MSG(0.0f, "Failed to get display config buffer sizes."); + } + + data.paths.resize(path_count); + data.modes.resize(mode_count); + + if (QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, &path_count, data.paths.ptrw(), &mode_count, data.modes.ptrw(), nullptr) != ERROR_SUCCESS) { + ERR_FAIL_V_MSG(0.0f, "Failed to query display config."); + } + + EnumDisplayMonitors(nullptr, nullptr, _MonitorEnumProcSdrWhiteLevel, (LPARAM)&data); + return data.sdrWhiteLevelInNits; +} + +// Get screen HDR capabilities for internal use only. +// Do not report values from this method to the user. +DisplayServerWindows::ScreenHdrData DisplayServerWindows::_get_screen_hdr_data(int p_screen) const { + ScreenHdrData data; +#ifdef D3D12_ENABLED + HMONITOR monitor = _get_hmonitor_of_screen(p_screen); + DXGI_OUTPUT_DESC1 desc; + if (_get_monitor_desc(monitor, desc)) { + data.hdr_supported = desc.ColorSpace == DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020; + data.min_luminance = desc.MinLuminance; + data.max_luminance = desc.MaxLuminance; + data.max_average_luminance = desc.MaxFullFrameLuminance; + } +#else + // If we don't have D3D12, assume HDR is not supported. + // This is to avoid a poor user experience due to missing display information. + data.hdr_supported = false; + data.min_luminance = 0.0f; + data.max_luminance = 0.0f; + data.max_average_luminance = 0.0f; +#endif // D3D12_ENABLED + + data.sdr_white_level = _screen_get_reference_luminance(p_screen); + return data; +} + +void DisplayServerWindows::_update_hdr_output_for_window(WindowID p_window, const WindowData &p_window_data, ScreenHdrData p_screen_data) { +#ifdef RD_ENABLED + if (rendering_context) { + bool current_hdr_enabled = rendering_context->window_get_hdr_output_enabled(p_window); + bool desired_hdr_enabled = p_window_data.hdr_output_requested && p_screen_data.hdr_supported; + + if (current_hdr_enabled != desired_hdr_enabled) { + rendering_context->window_set_hdr_output_enabled(p_window, desired_hdr_enabled); + rendering_context->window_set_hdr_output_linear_luminance_scale(p_window, 80.0f); + } + + // If auto reference luminance is enabled, update it based on the current SDR white level. + if (p_window_data.hdr_output_reference_luminance < 0.0f) { + if (p_screen_data.sdr_white_level > 0.0f) { + rendering_context->window_set_hdr_output_reference_luminance(p_window, p_screen_data.sdr_white_level); + } + // If we cannot get the SDR white level, leave the previous value unchanged. + } + + // If auto max luminance is enabled, update it based on the screen's max luminance. + if (p_window_data.hdr_output_max_luminance < 0.0f) { + if (p_screen_data.max_luminance > 0.0f) { + rendering_context->window_set_hdr_output_max_luminance(p_window, p_screen_data.max_luminance); + } + // If we cannot get the screen's max luminance, leave the previous value unchanged. + } + } +#endif // RD_ENABLED +} + +void DisplayServerWindows::_update_hdr_output_for_tracked_windows() { + HashMap outputs; + for (const KeyValue &E : windows) { + if (E.value.hdr_output_requested) { + int screen = window_get_current_screen(E.key); + + ScreenHdrData data; + if (!outputs.has(screen)) { + data = _get_screen_hdr_data(screen); + outputs.insert(screen, data); + } else { + data = outputs[screen]; + } + + _update_hdr_output_for_window(E.key, E.value, data); + } + } +} + +void DisplayServerWindows::_check_for_sdr_white_level_changes() { + // Only check every 500ms to avoid excessive polling. + uint64_t current_time = OS::get_singleton()->get_ticks_msec(); + if (current_time - last_sdr_white_level_check_time < 500) { + return; + } + last_sdr_white_level_check_time = current_time; + + // Check if any tracked windows have HDR output enabled. + bool has_hdr_windows = false; + for (const KeyValue &E : windows) { + if (E.value.hdr_output_requested) { + has_hdr_windows = true; + break; + } + } + + // No need to continue if no windows are using HDR output. + if (!has_hdr_windows) { + return; + } + + // Check each screen for SDR white level changes. + bool sdr_white_level_changed = false; + int screen_count = get_screen_count(); + for (int i = 0; i < screen_count; i++) { + float current_sdr_white_level = _screen_get_reference_luminance(i); + + // Check if this is the first time we're checking this screen or if the value changed. + if (!last_known_sdr_white_levels.has(i)) { + last_known_sdr_white_levels[i] = current_sdr_white_level; + } else if (!Math::is_equal_approx(last_known_sdr_white_levels[i], current_sdr_white_level)) { + // SDR white level changed. + last_known_sdr_white_levels[i] = current_sdr_white_level; + sdr_white_level_changed = true; + } + } + + // If any screen's SDR white level changed, update HDR output for all tracked windows. + if (sdr_white_level_changed) { + _update_hdr_output_for_tracked_windows(); + } +} + Error DisplayServerWindows::embed_process(WindowID p_window, OS::ProcessID p_pid, const Rect2i &p_rect, bool p_visible, bool p_grab_focus) { _THREAD_SAFE_METHOD_ @@ -3822,6 +4085,9 @@ void DisplayServerWindows::process_events() { Input::get_singleton()->flush_buffered_events(); } + // Check for SDR white level changes to update HDR output if needed. + _check_for_sdr_white_level_changes(); + LocalVector::Element *> to_remove; for (List::Element *E = file_dialogs.front(); E; E = E->next()) { FileDialogData *fd = E->get(); @@ -4261,6 +4527,157 @@ DisplayServer::VSyncMode DisplayServerWindows::window_get_vsync_mode(WindowID p_ return DisplayServer::VSYNC_ENABLED; } +bool DisplayServerWindows::window_is_hdr_output_supported(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + if (rendering_device && !rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)) { + return false; // HDR output is not supported by the rendering device. + } +#endif + + // The window supports HDR if the screen it is on supports HDR. + int screen = window_get_current_screen(p_window); + DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen); + return data.hdr_supported; +} + +void DisplayServerWindows::window_request_hdr_output(const bool p_enable, WindowID p_window) { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + ERR_FAIL_COND_MSG(p_enable && (rendering_device && rendering_device->has_feature(RenderingDevice::Features::SUPPORTS_HDR_OUTPUT)) == false, "HDR output is not supported by the rendering device."); +#endif + + WindowData &wd = windows[p_window]; + wd.hdr_output_requested = p_enable; + + int screen = window_get_current_screen(p_window); + DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen); + _update_hdr_output_for_window(p_window, wd, data); +} + +bool DisplayServerWindows::window_is_hdr_output_requested(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + + const WindowData &wd = windows[p_window]; + return wd.hdr_output_requested; +} + +bool DisplayServerWindows::window_is_hdr_output_enabled(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_enabled(p_window); + } +#endif + + return false; +} + +void DisplayServerWindows::window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + WindowData &wd = windows[p_window]; + + if (Math::is_equal_approx(wd.hdr_output_reference_luminance, p_reference_luminance)) { + return; + } + + wd.hdr_output_reference_luminance = p_reference_luminance; + + // Negative luminance means auto-adjust + if (wd.hdr_output_reference_luminance < 0.0f) { + int screen = window_get_current_screen(p_window); + DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen); + _update_hdr_output_for_window(p_window, wd, data); + } else { + // Otherwise, apply the requested luminance +#if defined(RD_ENABLED) + if (rendering_context) { + rendering_context->window_set_hdr_output_reference_luminance(p_window, p_reference_luminance); + } +#endif + } +} + +float DisplayServerWindows::window_get_hdr_output_reference_luminance(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + + const WindowData &wd = windows[p_window]; + return wd.hdr_output_reference_luminance; +} + +float DisplayServerWindows::window_get_hdr_output_current_reference_luminance(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_reference_luminance(p_window); + } +#endif + + return 0.0f; +} + +void DisplayServerWindows::window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window) { + _THREAD_SAFE_METHOD_ + + WindowData &wd = windows[p_window]; + + if (Math::is_equal_approx(wd.hdr_output_max_luminance, p_max_luminance)) { + return; + } + + wd.hdr_output_max_luminance = p_max_luminance; + + // Negative luminance means auto-adjust + if (wd.hdr_output_max_luminance < 0.0f) { + int screen = window_get_current_screen(p_window); + DisplayServerWindows::ScreenHdrData data = _get_screen_hdr_data(screen); + _update_hdr_output_for_window(p_window, wd, data); + } else { + // Otherwise, apply the requested luminance +#if defined(RD_ENABLED) + if (rendering_context) { + rendering_context->window_set_hdr_output_max_luminance(p_window, p_max_luminance); + } +#endif + } +} + +float DisplayServerWindows::window_get_hdr_output_max_luminance(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + + const WindowData &wd = windows[p_window]; + return wd.hdr_output_max_luminance; +} + +float DisplayServerWindows::window_get_hdr_output_current_max_luminance(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_hdr_output_max_luminance(p_window); + } +#endif + + return 0.0f; +} + +float DisplayServerWindows::window_get_output_max_value(WindowID p_window) const { + _THREAD_SAFE_METHOD_ + +#if defined(RD_ENABLED) + if (rendering_context) { + return rendering_context->window_get_output_max_value(p_window); + } +#endif + + return 1.0f; // SDR +} + void DisplayServerWindows::window_start_drag(WindowID p_window) { _THREAD_SAFE_METHOD_ @@ -5736,6 +6153,10 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA } } break; + case WM_DISPLAYCHANGE: { + _update_hdr_output_for_tracked_windows(); + } break; + case WM_WINDOWPOSCHANGED: { WindowData &window = windows[window_id]; @@ -5829,6 +6250,8 @@ LRESULT DisplayServerWindows::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARA window.rect_changed_callback.call(Rect2i(window.last_pos.x, window.last_pos.y, window.width, window.height)); } + _update_hdr_output_for_tracked_windows(); + // Update cursor clip region after window rect has changed. if (mouse_mode == MOUSE_MODE_CAPTURED || mouse_mode == MOUSE_MODE_CONFINED || mouse_mode == MOUSE_MODE_CONFINED_HIDDEN) { RECT crect; diff --git a/platform/windows/display_server_windows.h b/platform/windows/display_server_windows.h index 6c2d0c5f6423..975945f632fc 100644 --- a/platform/windows/display_server_windows.h +++ b/platform/windows/display_server_windows.h @@ -371,6 +371,12 @@ class DisplayServerWindows : public DisplayServer { bool is_popup = false; Rect2i parent_safe_rect; + // HDR + bool hdr_output_requested = false; + bool hdr_output_enabled = false; + float hdr_output_reference_luminance = -1.0f; + float hdr_output_max_luminance = -1.0f; + bool initialized = false; HWND parent_hwnd = 0; @@ -515,6 +521,23 @@ class DisplayServerWindows : public DisplayServer { void initialize_tts() const; + struct ScreenHdrData { + bool hdr_supported = false; + float min_luminance = 0.0f; + float max_luminance = 0.0f; + float max_average_luminance = 0.0f; + float sdr_white_level = 0.0f; + }; + float _screen_get_reference_luminance(int p_screen) const; + ScreenHdrData _get_screen_hdr_data(int p_screen) const; + void _update_hdr_output_for_window(WindowID p_window, const WindowData &p_window_data, ScreenHdrData p_screen_data); + void _update_hdr_output_for_tracked_windows(); + + // Track last known SDR white levels for change detection. + HashMap last_known_sdr_white_levels; + uint64_t last_sdr_white_level_check_time = 0; + void _check_for_sdr_white_level_changes(); + public: LRESULT WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); @@ -660,6 +683,22 @@ class DisplayServerWindows : public DisplayServer { virtual void window_set_vsync_mode(DisplayServer::VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID) override; virtual DisplayServer::VSyncMode window_get_vsync_mode(WindowID p_vsync_mode) const override; + virtual bool window_is_hdr_output_supported(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_request_hdr_output(const bool p_enable, WindowID p_window = MAIN_WINDOW_ID) override; + virtual bool window_is_hdr_output_requested(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual bool window_is_hdr_output_enabled(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window = MAIN_WINDOW_ID) override; + virtual float window_get_hdr_output_reference_luminance(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual float window_get_hdr_output_current_reference_luminance(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual void window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window = MAIN_WINDOW_ID) override; + virtual float window_get_hdr_output_max_luminance(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual float window_get_hdr_output_current_max_luminance(WindowID p_window = MAIN_WINDOW_ID) const override; + + virtual float window_get_output_max_value(WindowID p_window = MAIN_WINDOW_ID) const override; + virtual void window_start_drag(WindowID p_window = MAIN_WINDOW_ID) override; virtual void window_start_resize(WindowResizeEdge p_edge, WindowID p_window = MAIN_WINDOW_ID) override; diff --git a/scene/main/window.cpp b/scene/main/window.cpp index 48894759f336..b4ed1a18f3f0 100644 --- a/scene/main/window.cpp +++ b/scene/main/window.cpp @@ -573,6 +573,48 @@ bool Window::is_popup() const { return get_flag(Window::FLAG_POPUP) || get_flag(Window::FLAG_NO_FOCUS); } +bool Window::is_hdr_output_supported() const { + ERR_READ_THREAD_GUARD_V(false); + + if (window_id != DisplayServer::INVALID_WINDOW_ID) { + return DisplayServer::get_singleton()->window_is_hdr_output_supported(window_id); + } + + return false; +} + +void Window::set_hdr_output_requested(bool p_requested) { + ERR_MAIN_THREAD_GUARD; + + hdr_output_requested = p_requested; + + if (window_id != DisplayServer::INVALID_WINDOW_ID) { + DisplayServer::get_singleton()->window_request_hdr_output(hdr_output_requested, window_id); + } + + _update_viewport_for_hdr_output(); +} + +bool Window::is_hdr_output_requested() const { + ERR_READ_THREAD_GUARD_V(false); + + if (window_id != DisplayServer::INVALID_WINDOW_ID) { + hdr_output_requested = DisplayServer::get_singleton()->window_is_hdr_output_requested(window_id); + } + + return hdr_output_requested; +} + +float Window::get_output_max_value() const { + ERR_READ_THREAD_GUARD_V(1.0f); + + if (window_id != DisplayServer::INVALID_WINDOW_ID) { + return DisplayServer::get_singleton()->window_get_output_max_value(window_id); + } + + return 1.0f; +} + bool Window::is_maximize_allowed() const { ERR_READ_THREAD_GUARD_V(false); if (window_id != DisplayServer::INVALID_WINDOW_ID) { @@ -673,6 +715,11 @@ void Window::_make_window() { DisplayServer::get_singleton()->window_set_title(displayed_title, window_id); DisplayServer::get_singleton()->window_attach_instance_id(get_instance_id(), window_id); + // Set HDR output settings. + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_HDR) && RD::get_singleton() && RD::get_singleton()->has_feature(RD::Features::SUPPORTS_HDR_OUTPUT)) { + DisplayServer::get_singleton()->window_request_hdr_output(hdr_output_requested, window_id); + } + _update_window_size(); if (transient_parent) { @@ -945,6 +992,13 @@ void Window::set_visible(bool p_visible) { } embedder->_sub_window_register(this); RS::get_singleton()->viewport_set_update_mode(get_viewport_rid(), RS::VIEWPORT_UPDATE_WHEN_PARENT_VISIBLE); + + // Make sure the sub-window shares the HDR output settings of the embedder. + Window *containing_window = embedder->get_window(); + if (containing_window) { + hdr_output_requested = containing_window->is_hdr_output_requested(); + _update_viewport_for_hdr_output(); + } } else { embedder->_sub_window_remove(this); embedder = nullptr; @@ -1532,6 +1586,11 @@ void Window::_notification(int p_what) { size = DisplayServer::get_singleton()->window_get_size(window_id); focused = DisplayServer::get_singleton()->window_is_focused(window_id); } + // Update HDR settings to reflect the current state of the window. + { + hdr_output_requested = DisplayServer::get_singleton()->window_is_hdr_output_requested(window_id); + _update_viewport_for_hdr_output(); + } _update_window_size(); // Inform DisplayServer of minimum and maximum size. _update_viewport_size(); // Then feed back to the viewport. _update_window_callbacks(); @@ -1799,6 +1858,16 @@ void Window::child_controls_changed() { callable_mp(this, &Window::_update_child_controls).call_deferred(); } +void Window::_update_viewport_for_hdr_output() { + // If HDR output is enabled, we need to enable HDR 2D rendering as well. + // This is required to get the correct dynamic range for the final output. + // We only need to do this if the viewport is not already set up for HDR 2D rendering. + + if (!is_using_hdr_2d()) { + RS::get_singleton()->viewport_set_use_hdr_2d(viewport, hdr_output_requested); + } +} + void Window::_update_child_controls() { if (!updating_child_controls) { return; @@ -3168,6 +3237,11 @@ void Window::_bind_methods() { ClassDB::bind_method(D_METHOD("set_flag", "flag", "enabled"), &Window::set_flag); ClassDB::bind_method(D_METHOD("get_flag", "flag"), &Window::get_flag); + ClassDB::bind_method(D_METHOD("is_hdr_output_supported"), &Window::is_hdr_output_supported); + ClassDB::bind_method(D_METHOD("set_hdr_output_requested", "requested"), &Window::set_hdr_output_requested); + ClassDB::bind_method(D_METHOD("is_hdr_output_requested"), &Window::is_hdr_output_requested); + ClassDB::bind_method(D_METHOD("get_output_max_value"), &Window::get_output_max_value); + ClassDB::bind_method(D_METHOD("is_maximize_allowed"), &Window::is_maximize_allowed); ClassDB::bind_method(D_METHOD("request_attention"), &Window::request_attention); @@ -3362,6 +3436,9 @@ void Window::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "content_scale_stretch", PROPERTY_HINT_ENUM, "Fractional,Integer"), "set_content_scale_stretch", "get_content_scale_stretch"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "content_scale_factor", PROPERTY_HINT_RANGE, "0.5,8.0,0.01"), "set_content_scale_factor", "get_content_scale_factor"); + ADD_GROUP("HDR Output", "hdr_output_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hdr_output_requested"), "set_hdr_output_requested", "is_hdr_output_requested"); + #ifndef DISABLE_DEPRECATED ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_translate", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_auto_translate", "is_auto_translating"); #endif diff --git a/scene/main/window.h b/scene/main/window.h index 25ce600e140e..f9952a987c0f 100644 --- a/scene/main/window.h +++ b/scene/main/window.h @@ -134,6 +134,10 @@ class Window : public Viewport { WindowInitialPosition initial_position = WINDOW_INITIAL_POSITION_ABSOLUTE; bool force_native = false; + mutable bool hdr_output_requested = false; + + void _update_viewport_for_hdr_output(); + bool transient = false; bool transient_to_focused = false; bool exclusive = false; @@ -334,6 +338,11 @@ class Window : public Viewport { bool is_popup() const; + bool is_hdr_output_supported() const; + void set_hdr_output_requested(bool p_enabled); + bool is_hdr_output_requested() const; + float get_output_max_value() const; + bool is_maximize_allowed() const; void request_attention(); diff --git a/scene/resources/environment.cpp b/scene/resources/environment.cpp index 32dc79aa4e58..eb97e6473e3f 100644 --- a/scene/resources/environment.cpp +++ b/scene/resources/environment.cpp @@ -227,12 +227,30 @@ float Environment::get_tonemap_white() const { return tonemap_white; } +void Environment::set_tonemap_agx_white(float p_white) { + tonemap_agx_white = p_white; + _update_tonemap(); +} + +float Environment::get_tonemap_agx_white() const { + return tonemap_agx_white; +} + +void Environment::set_tonemap_agx_contrast(float p_agx_contrast) { + tonemap_agx_contrast = p_agx_contrast; + RS::get_singleton()->environment_set_tonemap_agx_contrast(environment, p_agx_contrast); +} + +float Environment::get_tonemap_agx_contrast() const { + return tonemap_agx_contrast; +} + void Environment::_update_tonemap() { RS::get_singleton()->environment_set_tonemap( environment, RS::EnvironmentToneMapper(tone_mapper), tonemap_exposure, - tonemap_white); + tone_mapper == TONE_MAPPER_AGX ? tonemap_agx_white : tonemap_white); } // SSR @@ -1116,7 +1134,14 @@ void Environment::_validate_property(PropertyInfo &p_property) const { } if (p_property.name == "tonemap_white" && (tone_mapper == TONE_MAPPER_LINEAR || tone_mapper == TONE_MAPPER_AGX)) { - // Whitepoint adjustment is not available with AgX or linear as it's hardcoded there. + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + + if (p_property.name == "tonemap_agx_white" && tone_mapper != TONE_MAPPER_AGX) { + p_property.usage = PROPERTY_USAGE_NO_EDITOR; + } + + if (p_property.name == "tonemap_agx_contrast" && tone_mapper != TONE_MAPPER_AGX) { p_property.usage = PROPERTY_USAGE_NO_EDITOR; } @@ -1243,11 +1268,17 @@ void Environment::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tonemap_exposure"), &Environment::get_tonemap_exposure); ClassDB::bind_method(D_METHOD("set_tonemap_white", "white"), &Environment::set_tonemap_white); ClassDB::bind_method(D_METHOD("get_tonemap_white"), &Environment::get_tonemap_white); + ClassDB::bind_method(D_METHOD("set_tonemap_agx_white", "white"), &Environment::set_tonemap_agx_white); + ClassDB::bind_method(D_METHOD("get_tonemap_agx_white"), &Environment::get_tonemap_agx_white); + ClassDB::bind_method(D_METHOD("set_tonemap_agx_contrast", "contrast"), &Environment::set_tonemap_agx_contrast); + ClassDB::bind_method(D_METHOD("get_tonemap_agx_contrast"), &Environment::get_tonemap_agx_contrast); ADD_GROUP("Tonemap", "tonemap_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tonemap_mode", PROPERTY_HINT_ENUM, "Linear,Reinhard,Filmic,ACES,AgX"), "set_tonemapper", "get_tonemapper"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_exposure", PROPERTY_HINT_RANGE, "0,4,0.01,or_greater"), "set_tonemap_exposure", "get_tonemap_exposure"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_white", PROPERTY_HINT_RANGE, "1,16,0.01,or_greater"), "set_tonemap_white", "get_tonemap_white"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_agx_white", PROPERTY_HINT_RANGE, "2,16.5,0.01,or_greater"), "set_tonemap_agx_white", "get_tonemap_agx_white"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "tonemap_agx_contrast", PROPERTY_HINT_RANGE, "1.0,2.0,0.01,or_greater"), "set_tonemap_agx_contrast", "get_tonemap_agx_contrast"); // SSR diff --git a/scene/resources/environment.h b/scene/resources/environment.h index cb6fa57775c6..cfc1757f2455 100644 --- a/scene/resources/environment.h +++ b/scene/resources/environment.h @@ -115,6 +115,8 @@ class Environment : public Resource { ToneMapper tone_mapper = TONE_MAPPER_LINEAR; float tonemap_exposure = 1.0; float tonemap_white = 1.0; + float tonemap_agx_white = 16.29; // Default to Blender's AgX white. + float tonemap_agx_contrast = 1.25; // Default to approximately Blender's AgX contrast. void _update_tonemap(); // SSR @@ -271,6 +273,10 @@ class Environment : public Resource { float get_tonemap_exposure() const; void set_tonemap_white(float p_white); float get_tonemap_white() const; + void set_tonemap_agx_white(float p_white); + float get_tonemap_agx_white() const; + void set_tonemap_agx_contrast(float p_agx_contrast); + float get_tonemap_agx_contrast() const; // SSR void set_ssr_enabled(bool p_enabled); diff --git a/servers/display/display_server.cpp b/servers/display/display_server.cpp index 66048a2e1719..e3e9dac93add 100644 --- a/servers/display/display_server.cpp +++ b/servers/display/display_server.cpp @@ -1267,6 +1267,50 @@ DisplayServer::VSyncMode DisplayServer::window_get_vsync_mode(WindowID p_window) return VSyncMode::VSYNC_ENABLED; } +bool DisplayServer::window_is_hdr_output_supported(WindowID p_window) const { + return false; +} + +void DisplayServer::window_request_hdr_output(const bool p_enable, WindowID p_window) { + WARN_PRINT("HDR output is not supported by this display server."); +} + +bool DisplayServer::window_is_hdr_output_requested(WindowID p_window) const { + return false; +} + +bool DisplayServer::window_is_hdr_output_enabled(WindowID p_window) const { + return false; +} + +void DisplayServer::window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window) { + WARN_PRINT("HDR output is not supported by this display server."); +} + +float DisplayServer::window_get_hdr_output_reference_luminance(WindowID p_window) const { + return -1.0f; +} + +float DisplayServer::window_get_hdr_output_current_reference_luminance(WindowID p_window) const { + return 0.0f; +} + +void DisplayServer::window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window) { + WARN_PRINT("HDR output is not supported by this display server."); +} + +float DisplayServer::window_get_hdr_output_max_luminance(WindowID p_window) const { + return -1.0f; +} + +float DisplayServer::window_get_hdr_output_current_max_luminance(WindowID p_window) const { + return 0.0f; +} + +float DisplayServer::window_get_output_max_value(WindowID p_window) const { + return 1.0f; +} + DisplayServer::WindowID DisplayServer::get_focused_window() const { return MAIN_WINDOW_ID; // Proper value for single windows. } @@ -1469,6 +1513,22 @@ void DisplayServer::_bind_methods() { ClassDB::bind_method(D_METHOD("window_set_vsync_mode", "vsync_mode", "window_id"), &DisplayServer::window_set_vsync_mode, DEFVAL(MAIN_WINDOW_ID)); ClassDB::bind_method(D_METHOD("window_get_vsync_mode", "window_id"), &DisplayServer::window_get_vsync_mode, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_is_hdr_output_supported", "window_id"), &DisplayServer::window_is_hdr_output_supported, DEFVAL(MAIN_WINDOW_ID)); + + ClassDB::bind_method(D_METHOD("window_request_hdr_output", "enable", "window_id"), &DisplayServer::window_request_hdr_output, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_is_hdr_output_requested", "window_id"), &DisplayServer::window_is_hdr_output_requested, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_is_hdr_output_enabled", "window_id"), &DisplayServer::window_is_hdr_output_enabled, DEFVAL(MAIN_WINDOW_ID)); + + ClassDB::bind_method(D_METHOD("window_set_hdr_output_reference_luminance", "reference_luminance", "window_id"), &DisplayServer::window_set_hdr_output_reference_luminance, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_get_hdr_output_reference_luminance", "window_id"), &DisplayServer::window_get_hdr_output_reference_luminance, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_get_hdr_output_current_reference_luminance", "window_id"), &DisplayServer::window_get_hdr_output_current_reference_luminance, DEFVAL(MAIN_WINDOW_ID)); + + ClassDB::bind_method(D_METHOD("window_set_hdr_output_max_luminance", "max_luminance", "window_id"), &DisplayServer::window_set_hdr_output_max_luminance, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_get_hdr_output_max_luminance", "window_id"), &DisplayServer::window_get_hdr_output_max_luminance, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_get_hdr_output_current_max_luminance", "window_id"), &DisplayServer::window_get_hdr_output_current_max_luminance, DEFVAL(MAIN_WINDOW_ID)); + + ClassDB::bind_method(D_METHOD("window_get_output_max_value", "window_id"), &DisplayServer::window_get_output_max_value, DEFVAL(MAIN_WINDOW_ID)); + ClassDB::bind_method(D_METHOD("window_is_maximize_allowed", "window_id"), &DisplayServer::window_is_maximize_allowed, DEFVAL(MAIN_WINDOW_ID)); ClassDB::bind_method(D_METHOD("window_maximize_on_title_dbl_click"), &DisplayServer::window_maximize_on_title_dbl_click); ClassDB::bind_method(D_METHOD("window_minimize_on_title_dbl_click"), &DisplayServer::window_minimize_on_title_dbl_click); @@ -1659,6 +1719,7 @@ void DisplayServer::_bind_methods() { BIND_ENUM_CONSTANT(FEATURE_NATIVE_COLOR_PICKER); BIND_ENUM_CONSTANT(FEATURE_SELF_FITTING_WINDOWS); BIND_ENUM_CONSTANT(FEATURE_ACCESSIBILITY_SCREEN_READER); + BIND_ENUM_CONSTANT(FEATURE_HDR); BIND_ENUM_CONSTANT(ROLE_UNKNOWN); BIND_ENUM_CONSTANT(ROLE_DEFAULT_BUTTON); diff --git a/servers/display/display_server.h b/servers/display/display_server.h index 920b1c4458bf..fa341f341214 100644 --- a/servers/display/display_server.h +++ b/servers/display/display_server.h @@ -168,6 +168,7 @@ class DisplayServer : public Object { FEATURE_NATIVE_COLOR_PICKER, FEATURE_SELF_FITTING_WINDOWS, FEATURE_ACCESSIBILITY_SCREEN_READER, + FEATURE_HDR, }; virtual bool has_feature(Feature p_feature) const = 0; @@ -498,6 +499,22 @@ class DisplayServer : public Object { virtual void window_set_vsync_mode(VSyncMode p_vsync_mode, WindowID p_window = MAIN_WINDOW_ID); virtual VSyncMode window_get_vsync_mode(WindowID p_window) const; + virtual bool window_is_hdr_output_supported(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_request_hdr_output(const bool p_enable, WindowID p_window = MAIN_WINDOW_ID); + virtual bool window_is_hdr_output_requested(WindowID p_window = MAIN_WINDOW_ID) const; + virtual bool window_is_hdr_output_enabled(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_hdr_output_reference_luminance(const float p_reference_luminance, WindowID p_window = MAIN_WINDOW_ID); + virtual float window_get_hdr_output_reference_luminance(WindowID p_window = MAIN_WINDOW_ID) const; + virtual float window_get_hdr_output_current_reference_luminance(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual void window_set_hdr_output_max_luminance(const float p_max_luminance, WindowID p_window = MAIN_WINDOW_ID); + virtual float window_get_hdr_output_max_luminance(WindowID p_window = MAIN_WINDOW_ID) const; + virtual float window_get_hdr_output_current_max_luminance(WindowID p_window = MAIN_WINDOW_ID) const; + + virtual float window_get_output_max_value(WindowID p_window = MAIN_WINDOW_ID) const; + virtual bool window_is_maximize_allowed(WindowID p_window = MAIN_WINDOW_ID) const = 0; virtual void window_set_flag(WindowFlags p_flag, bool p_enabled, WindowID p_window = MAIN_WINDOW_ID) = 0; diff --git a/servers/rendering/dummy/rasterizer_scene_dummy.h b/servers/rendering/dummy/rasterizer_scene_dummy.h index ad255124f8f6..356623312e8a 100644 --- a/servers/rendering/dummy/rasterizer_scene_dummy.h +++ b/servers/rendering/dummy/rasterizer_scene_dummy.h @@ -152,7 +152,7 @@ class RasterizerSceneDummy : public RendererSceneRender { void voxel_gi_set_quality(RS::VoxelGIQuality) override {} - void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_info = nullptr) override {} + void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_info = nullptr) override {} void render_material(const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, const PagedArray &p_instances, RID p_framebuffer, const Rect2i &p_region) override {} void render_particle_collider_heightfield(RID p_collider, const Transform3D &p_transform, const PagedArray &p_instances) override {} diff --git a/servers/rendering/renderer_rd/effects/tone_mapper.cpp b/servers/rendering/renderer_rd/effects/tone_mapper.cpp index 385e7e6cb9cb..74571645f329 100644 --- a/servers/rendering/renderer_rd/effects/tone_mapper.cpp +++ b/servers/rendering/renderer_rd/effects/tone_mapper.cpp @@ -114,11 +114,16 @@ void ToneMapper::tonemapper(RID p_source_color, RID p_dst_framebuffer, const Ton } tonemap.push_constant.tonemapper = p_settings.tonemap_mode; + tonemap.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0]; + tonemap.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1]; + tonemap.push_constant.tonemapper_params[2] = p_settings.tonemapper_params[2]; + tonemap.push_constant.tonemapper_params[3] = p_settings.tonemapper_params[3]; tonemap.push_constant.flags |= p_settings.use_auto_exposure ? TONEMAP_FLAG_USE_AUTO_EXPOSURE : 0; tonemap.push_constant.exposure = p_settings.exposure; tonemap.push_constant.white = p_settings.white; tonemap.push_constant.auto_exposure_scale = p_settings.auto_exposure_scale; tonemap.push_constant.luminance_multiplier = p_settings.luminance_multiplier; + tonemap.push_constant.output_max_value = MAX(p_settings.max_value, 1.0f); tonemap.push_constant.flags |= p_settings.use_color_correction ? TONEMAP_FLAG_USE_COLOR_CORRECTION : 0; @@ -205,10 +210,15 @@ void ToneMapper::tonemapper(RD::DrawListID p_subpass_draw_list, RID p_source_col } tonemap.push_constant.tonemapper = p_settings.tonemap_mode; + tonemap.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0]; + tonemap.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1]; + tonemap.push_constant.tonemapper_params[2] = p_settings.tonemapper_params[2]; + tonemap.push_constant.tonemapper_params[3] = p_settings.tonemapper_params[3]; tonemap.push_constant.flags |= p_settings.use_auto_exposure ? TONEMAP_FLAG_USE_AUTO_EXPOSURE : 0; tonemap.push_constant.exposure = p_settings.exposure; tonemap.push_constant.white = p_settings.white; tonemap.push_constant.auto_exposure_scale = p_settings.auto_exposure_scale; + tonemap.push_constant.output_max_value = MAX(p_settings.max_value, 1.0f); tonemap.push_constant.flags |= p_settings.use_color_correction ? TONEMAP_FLAG_USE_COLOR_CORRECTION : 0; if (p_settings.debanding_mode == TonemapSettings::DEBANDING_MODE_8_BIT) { diff --git a/servers/rendering/renderer_rd/effects/tone_mapper.h b/servers/rendering/renderer_rd/effects/tone_mapper.h index f14455d9a5f6..0441a43c8301 100644 --- a/servers/rendering/renderer_rd/effects/tone_mapper.h +++ b/servers/rendering/renderer_rd/effects/tone_mapper.h @@ -74,19 +74,23 @@ class ToneMapper { float pixel_size[2]; // 8 - 24 uint32_t tonemapper; // 4 - 28 - uint32_t pad; // 4 - 32 + uint32_t pad1; // 4 - 32 + float tonemapper_params[4]; // 16 - 48 - uint32_t glow_texture_size[2]; // 8 - 40 - float glow_intensity; // 4 - 44 - float glow_map_strength; // 4 - 48 + uint32_t glow_texture_size[2]; // 8 - 56 + float glow_intensity; // 4 - 60 + float glow_map_strength; // 4 - 64 - uint32_t glow_mode; // 4 - 52 - float glow_levels[7]; // 28 - 80 + uint32_t glow_mode; // 4 - 68 + float glow_levels[7]; // 28 - 96 - float exposure; // 4 - 84 - float white; // 4 - 88 - float auto_exposure_scale; // 4 - 92 - float luminance_multiplier; // 4 - 96 + float exposure; // 4 - 100 + float white; // 4 - 104 + float auto_exposure_scale; // 4 - 108 + float luminance_multiplier; // 4 - 112 + + float output_max_value; // 4 - 100 + uint32_t pad2[3]; // 12 - 112 }; /* tonemap actually writes to a framebuffer, which is @@ -124,8 +128,10 @@ class ToneMapper { RID glow_map; RS::EnvironmentToneMapper tonemap_mode = RS::ENV_TONE_MAPPER_LINEAR; + float tonemapper_params[4] = { 0.0, 0.0, 0.0, 0.0 }; float exposure = 1.0; float white = 1.0; + float max_value = 1.0; bool use_auto_exposure = false; float auto_exposure_scale = 0.5; diff --git a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp index 00627b14c70d..047e45cccb81 100644 --- a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp @@ -43,9 +43,16 @@ void RendererCompositorRD::blit_render_targets_to_screen(DisplayServer::WindowID return; } + BlitPipelines blit_pipelines = _get_blit_pipelines_for_format(RD::get_singleton()->screen_get_framebuffer_format(p_screen)); + RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin_for_screen(p_screen); ERR_FAIL_COND(draw_list == RD::INVALID_ID); + const RD::ColorSpace color_space = RD::get_singleton()->screen_get_color_space(p_screen); + const float reference_luminance = RD::get_singleton()->get_context_driver()->window_get_hdr_output_reference_luminance(p_screen); + const float linear_luminance_scale = RD::get_singleton()->get_context_driver()->window_get_hdr_output_linear_luminance_scale(p_screen); + const float reference_multiplier = _compute_reference_multiplier(color_space, reference_luminance, linear_luminance_scale); + for (int i = 0; i < p_amount; i++) { RID rd_texture = texture_storage->render_target_get_rd_texture(p_render_targets[i].render_target); ERR_CONTINUE(rd_texture.is_null()); @@ -66,7 +73,8 @@ void RendererCompositorRD::blit_render_targets_to_screen(DisplayServer::WindowID Size2 screen_size(RD::get_singleton()->screen_get_width(p_screen), RD::get_singleton()->screen_get_height(p_screen)); BlitMode mode = p_render_targets[i].lens_distortion.apply ? BLIT_MODE_LENS : (p_render_targets[i].multi_view.use_layer ? BLIT_MODE_USE_LAYER : BLIT_MODE_NORMAL); - RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blit.pipelines[mode]); + + RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blit_pipelines.pipelines[mode]); RD::get_singleton()->draw_list_bind_index_array(draw_list, blit.array); RD::get_singleton()->draw_list_bind_uniform_set(draw_list, it->value, 0); @@ -95,8 +103,10 @@ void RendererCompositorRD::blit_render_targets_to_screen(DisplayServer::WindowID blit.push_constant.k2 = p_render_targets[i].lens_distortion.k2; blit.push_constant.upscale = p_render_targets[i].lens_distortion.upscale; blit.push_constant.aspect_ratio = p_render_targets[i].lens_distortion.aspect_ratio; - blit.push_constant.convert_to_srgb = texture_storage->render_target_is_using_hdr(p_render_targets[i].render_target); + blit.push_constant.source_is_srgb = !texture_storage->render_target_is_using_hdr(p_render_targets[i].render_target); blit.push_constant.use_debanding = texture_storage->render_target_is_using_debanding(p_render_targets[i].render_target); + blit.push_constant.target_color_space = color_space; + blit.push_constant.reference_multiplier = reference_multiplier; RD::get_singleton()->draw_list_set_push_constant(draw_list, &blit.push_constant, sizeof(BlitPushConstant)); RD::get_singleton()->draw_list_draw(draw_list, true); @@ -131,16 +141,8 @@ void RendererCompositorRD::initialize() { blit_modes.push_back("\n"); blit.shader.initialize(blit_modes); - blit.shader_version = blit.shader.version_create(); - for (int i = 0; i < BLIT_MODE_MAX; i++) { - blit.pipelines[i] = RD::get_singleton()->render_pipeline_create(blit.shader.version_get_shader(blit.shader_version, i), RD::get_singleton()->screen_get_framebuffer_format(DisplayServer::MAIN_WINDOW_ID), RD::INVALID_ID, RD::RENDER_PRIMITIVE_TRIANGLES, RD::PipelineRasterizationState(), RD::PipelineMultisampleState(), RD::PipelineDepthStencilState(), i == BLIT_MODE_NORMAL_ALPHA ? RenderingDevice::PipelineColorBlendState::create_blend() : RenderingDevice::PipelineColorBlendState::create_disabled(), 0); - - // Unload shader modules to save memory. - RD::get_singleton()->shader_destroy_modules(blit.shader.version_get_shader(blit.shader_version, i)); - } - //create index array for copy shader Vector pv; pv.resize(6 * 2); @@ -180,6 +182,29 @@ void RendererCompositorRD::finalize() { RD::get_singleton()->free_rid(blit.sampler); } +RendererCompositorRD::BlitPipelines RendererCompositorRD::_get_blit_pipelines_for_format(RenderingDevice::FramebufferFormatID format) { + HashMap::Iterator it = blit.pipelinesByFormat.find(format); + if (it != blit.pipelinesByFormat.end()) { + return it->value; + } + + BlitPipelines pipelines; + for (int i = 0; i < BLIT_MODE_MAX; i++) { + pipelines.pipelines[i] = RD::get_singleton()->render_pipeline_create(blit.shader.version_get_shader(blit.shader_version, i), format, RD::INVALID_ID, RD::RENDER_PRIMITIVE_TRIANGLES, RD::PipelineRasterizationState(), RD::PipelineMultisampleState(), RD::PipelineDepthStencilState(), i == BLIT_MODE_NORMAL_ALPHA ? RenderingDevice::PipelineColorBlendState::create_blend() : RenderingDevice::PipelineColorBlendState::create_disabled(), 0); + } + blit.pipelinesByFormat.insert(format, pipelines); + return pipelines; +} + +float RendererCompositorRD::_compute_reference_multiplier(RD::ColorSpace p_color_space, const float p_reference_luminance, const float p_linear_luminance_scale) { + switch (p_color_space) { + case RD::COLOR_SPACE_REC709_LINEAR: + return p_reference_luminance / p_linear_luminance_scale; + default: + return 1.0f; + } +} + void RendererCompositorRD::set_boot_image_with_stretch(const Ref &p_image, const Color &p_color, RenderingServer::SplashStretchMode p_stretch_mode, bool p_use_filter) { if (p_image.is_null() || p_image->is_empty()) { return; @@ -191,6 +216,8 @@ void RendererCompositorRD::set_boot_image_with_stretch(const Ref &p_image return; } + BlitPipelines blit_pipelines = _get_blit_pipelines_for_format(RD::get_singleton()->screen_get_framebuffer_format(DisplayServer::MAIN_WINDOW_ID)); + RID texture = texture_storage->texture_allocate(); texture_storage->texture_2d_initialize(texture, p_image); RID rd_texture = texture_storage->texture_get_rd_texture(texture, false); @@ -219,9 +246,14 @@ void RendererCompositorRD::set_boot_image_with_stretch(const Ref &p_image screenrect.position /= window_size; screenrect.size /= window_size; + const RD::ColorSpace color_space = RD::get_singleton()->screen_get_color_space(DisplayServer::MAIN_WINDOW_ID); + const float reference_luminance = RD::get_singleton()->get_context_driver()->window_get_hdr_output_reference_luminance(DisplayServer::MAIN_WINDOW_ID); + const float linear_luminance_scale = RD::get_singleton()->get_context_driver()->window_get_hdr_output_linear_luminance_scale(DisplayServer::MAIN_WINDOW_ID); + const float reference_multiplier = _compute_reference_multiplier(color_space, reference_luminance, linear_luminance_scale); + RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin_for_screen(DisplayServer::MAIN_WINDOW_ID, p_color); - RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blit.pipelines[BLIT_MODE_NORMAL_ALPHA]); + RD::get_singleton()->draw_list_bind_render_pipeline(draw_list, blit_pipelines.pipelines[BLIT_MODE_NORMAL_ALPHA]); RD::get_singleton()->draw_list_bind_index_array(draw_list, blit.array); RD::get_singleton()->draw_list_bind_uniform_set(draw_list, uset, 0); @@ -244,8 +276,10 @@ void RendererCompositorRD::set_boot_image_with_stretch(const Ref &p_image blit.push_constant.k2 = 0; blit.push_constant.upscale = 1.0; blit.push_constant.aspect_ratio = 1.0; - blit.push_constant.convert_to_srgb = false; + blit.push_constant.source_is_srgb = true; blit.push_constant.use_debanding = false; + blit.push_constant.target_color_space = color_space; + blit.push_constant.reference_multiplier = reference_multiplier; RD::get_singleton()->draw_list_set_push_constant(draw_list, &blit.push_constant, sizeof(BlitPushConstant)); RD::get_singleton()->draw_list_draw(draw_list, true); diff --git a/servers/rendering/renderer_rd/renderer_compositor_rd.h b/servers/rendering/renderer_rd/renderer_compositor_rd.h index 6218869a5d06..90678cf590db 100644 --- a/servers/rendering/renderer_rd/renderer_compositor_rd.h +++ b/servers/rendering/renderer_rd/renderer_compositor_rd.h @@ -68,29 +68,36 @@ class RendererCompositorRD : public RendererCompositor { }; struct BlitPushConstant { - float src_rect[4]; - float dst_rect[4]; - - float rotation_sin; - float rotation_cos; - - float eye_center[2]; - float k1; - float k2; - - float upscale; - float aspect_ratio; - uint32_t layer; - uint32_t convert_to_srgb; - uint32_t use_debanding; - float pad; + float src_rect[4]; // 16 - 16 + float dst_rect[4]; // 16 - 32 + + float rotation_sin; // 4 - 36 + float rotation_cos; // 4 - 40 + float eye_center[2]; // 8 - 48 + + float k1; // 4 - 52 + float k2; // 4 - 56 + float upscale; // 4 - 60 + float aspect_ratio; // 4 - 64 + + uint32_t layer; // 4 - 68 + uint32_t source_is_srgb; // 4 - 72 + uint32_t use_debanding; // 4 - 76 + uint32_t target_color_space; // 4 - 80 + + float reference_multiplier; // 4 - 84 + uint32_t pad[3]; // 12 - 96 (padding to reach 16-byte boundary) + }; + + struct BlitPipelines { + RID pipelines[BLIT_MODE_MAX]; }; struct Blit { BlitPushConstant push_constant; BlitShaderRD shader; RID shader_version; - RID pipelines[BLIT_MODE_MAX]; + HashMap pipelinesByFormat; RID index_buffer; RID array; RID sampler; @@ -104,6 +111,9 @@ class RendererCompositorRD : public RendererCompositor { static uint64_t frame; static RendererCompositorRD *singleton; + BlitPipelines _get_blit_pipelines_for_format(RenderingDevice::FramebufferFormatID format); + float _compute_reference_multiplier(RD::ColorSpace p_color_space, const float p_reference_luminance, const float p_linear_luminance_scale); + public: RendererUtilities *get_utilities() { return utilities; } RendererLightStorage *get_light_storage() { return light_storage; } diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp index b8a9561537cc..5ecca139f392 100644 --- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp +++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp @@ -671,9 +671,24 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende tonemap.texture_size = Vector2i(color_size.x, color_size.y); if (p_render_data->environment.is_valid()) { + // When we are using RGB10A2 render buffer format, our scene + // is limited to a maximum of 2.0. In this case we should limit + // the max white of tonemappers, specifically AgX which defaults + // to a high white value. + bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32; + tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment); - tonemap.white = environment_get_white(p_render_data->environment); tonemap.exposure = environment_get_exposure(p_render_data->environment); + // When using HDR 2D, we use the parent window's output max value. + // Otherwise, we're tonemapping to an SDR low bit depth buffer, so + // we need to use SDR range with a max value of 1.0. + tonemap.max_value = using_hdr ? p_render_data->window_output_max_value : 1.0; + tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white, tonemap.max_value); + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white, tonemap.max_value); + tonemap.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap.tonemapper_params[3] = params.tonemapper_params[3]; } tonemap.use_color_correction = false; @@ -891,10 +906,27 @@ void RendererSceneRenderRD::_post_process_subpass(RID p_source_texture, RID p_fr RendererRD::ToneMapper::TonemapSettings tonemap; + bool using_hdr = texture_storage->render_target_is_using_hdr(rb->get_render_target()); + if (p_render_data->environment.is_valid()) { + // When we are using RGB10A2 render buffer format, our scene + // is limited to a maximum of 2.0. In this case we should limit + // the max white of tonemappers, specifically AgX which defaults + // to a high white value. + bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32; + tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment); tonemap.exposure = environment_get_exposure(p_render_data->environment); - tonemap.white = environment_get_white(p_render_data->environment); + // When using HDR 2D, we use the parent window's output max value. + // Otherwise, we're tonemapping to an SDR low bit depth buffer, so + // we need to use SDR range with a max value of 1.0. + tonemap.max_value = using_hdr ? p_render_data->window_output_max_value : 1.0; + tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white, tonemap.max_value); + RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white, tonemap.max_value); + tonemap.tonemapper_params[0] = params.tonemapper_params[0]; + tonemap.tonemapper_params[1] = params.tonemapper_params[1]; + tonemap.tonemapper_params[2] = params.tonemapper_params[2]; + tonemap.tonemapper_params[3] = params.tonemapper_params[3]; } // We don't support glow or auto exposure here, if they are needed, don't use subpasses! @@ -908,8 +940,6 @@ void RendererSceneRenderRD::_post_process_subpass(RID p_source_texture, RID p_fr ERR_FAIL_MSG("Auto Exposure is not supported when using subpasses."); } - bool using_hdr = texture_storage->render_target_is_using_hdr(rb->get_render_target()); - tonemap.use_glow = false; tonemap.glow_texture = texture_storage->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_BLACK); tonemap.glow_map = texture_storage->texture_rd_get_default(RendererRD::TextureStorage::DEFAULT_RD_TEXTURE_WHITE); @@ -1314,7 +1344,7 @@ void RendererSceneRenderRD::_post_prepass_render(RenderDataRD *p_render_data, bo } } -void RendererSceneRenderRD::render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data, RenderingMethod::RenderInfo *r_render_info) { +void RendererSceneRenderRD::render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data, RenderingMethod::RenderInfo *r_render_info) { RendererRD::LightStorage *light_storage = RendererRD::LightStorage::get_singleton(); RendererRD::TextureStorage *texture_storage = RendererRD::TextureStorage::get_singleton(); @@ -1410,6 +1440,7 @@ void RendererSceneRenderRD::render_scene(const Ref &p_render render_data.render_sdfgi_regions = p_render_sdfgi_regions; render_data.render_sdfgi_region_count = p_render_sdfgi_region_count; render_data.sdfgi_update_data = p_sdfgi_update_data; + render_data.window_output_max_value = p_window_output_max_value; render_data.render_info = r_render_info; diff --git a/servers/rendering/renderer_rd/renderer_scene_render_rd.h b/servers/rendering/renderer_rd/renderer_scene_render_rd.h index cc55002b6074..4ecd801feca2 100644 --- a/servers/rendering/renderer_rd/renderer_scene_render_rd.h +++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.h @@ -253,7 +253,7 @@ class RendererSceneRenderRD : public RendererSceneRender, public RenderingShader virtual void base_uniforms_changed() = 0; - virtual void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) override; + virtual void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) override; virtual void render_material(const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, const PagedArray &p_instances, RID p_framebuffer, const Rect2i &p_region) override; diff --git a/servers/rendering/renderer_rd/shaders/blit.glsl b/servers/rendering/renderer_rd/shaders/blit.glsl index d8ba4493e29f..b9b54d8e0b2d 100644 --- a/servers/rendering/renderer_rd/shaders/blit.glsl +++ b/servers/rendering/renderer_rd/shaders/blit.glsl @@ -4,23 +4,26 @@ #VERSION_DEFINES -layout(push_constant, std140) uniform Pos { +layout(push_constant, std430) uniform Pos { vec4 src_rect; vec4 dst_rect; float rotation_sin; float rotation_cos; - vec2 eye_center; + float k1; float k2; - float upscale; float aspect_ratio; + uint layer; - bool convert_to_srgb; + bool source_is_srgb; bool use_debanding; - float pad; + uint target_color_space; + + float reference_multiplier; + uint pad[3]; } data; @@ -45,23 +48,26 @@ void main() { #VERSION_DEFINES -layout(push_constant, std140) uniform Pos { +layout(push_constant, std430) uniform Pos { vec4 src_rect; vec4 dst_rect; float rotation_sin; float rotation_cos; - vec2 eye_center; + float k1; float k2; - float upscale; float aspect_ratio; + uint layer; - bool convert_to_srgb; + bool source_is_srgb; bool use_debanding; - float pad; + uint target_color_space; + + float reference_multiplier; + uint pad[3]; } data; @@ -75,6 +81,14 @@ layout(binding = 0) uniform sampler2DArray src_rt; layout(binding = 0) uniform sampler2D src_rt; #endif +// Keep in sync with RenderingDeviceCommons::ColorSpace +#define COLOR_SPACE_REC709_LINEAR 0 +#define COLOR_SPACE_REC709_NONLINEAR_SRGB 1 + +vec3 srgb_to_linear(vec3 color) { + return mix(pow((color.rgb + vec3(0.055)) * (1.0 / (1.0 + 0.055)), vec3(2.4)), color.rgb * (1.0 / 12.92), lessThan(color.rgb, vec3(0.04045))); +} + vec3 linear_to_srgb(vec3 color) { const vec3 a = vec3(0.055f); return mix((vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a, 12.92f * color.rgb, lessThan(color.rgb, vec3(0.0031308f))); @@ -133,19 +147,34 @@ void main() { color = texture(src_rt, uv); #endif - if (data.convert_to_srgb) { - color.rgb = linear_to_srgb(color.rgb); // Regular linear -> SRGB conversion. - - // Even if debanding was applied earlier in the rendering process, it must - // be reapplied after the linear_to_srgb floating point operations. - // When the linear_to_srgb operation was not performed, the source is - // already an 8-bit format and debanding cannot be effective. In this - // case, GPU driver rounding error can add noise so debanding should be - // skipped entirely. - if (data.use_debanding) { - color.rgb += screen_space_dither(gl_FragCoord.xy); + // Colorspace conversion for final blit + if (data.target_color_space == COLOR_SPACE_REC709_LINEAR) { + // Negative values may be interpreted as scRGB colors, + // so clip them to the intended sRGB colors. + color.rgb = max(vec3(0.0), color.rgb); + if (data.source_is_srgb == true) { + // sRGB -> linear conversion + color.rgb = srgb_to_linear(color.rgb); } - color.rgb = clamp(color.rgb, vec3(0.0), vec3(1.0)); + // Adjust brightness of SDR content to reference luminance + color.rgb *= data.reference_multiplier; + } else if (data.target_color_space == COLOR_SPACE_REC709_NONLINEAR_SRGB) { + // Negative values will be clipped by the target, so no need to + // clip them here. + if (data.source_is_srgb == false) { + // linear -> sRGB conversion + color.rgb = linear_to_srgb(color.rgb); + + // Even if debanding was applied earlier in the rendering process, it must + // be reapplied after the linear_to_srgb floating point operations. + // When the linear_to_srgb operation was not performed, the source is + // already an 8-bit format and debanding cannot be effective. In this + // case, GPU driver rounding error can add noise so debanding should be + // skipped entirely. + if (data.use_debanding) { + color.rgb += screen_space_dither(gl_FragCoord.xy); + } + } } } diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl index 8e73c38866ae..52e0f7979c84 100644 --- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl +++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl @@ -75,7 +75,8 @@ layout(push_constant, std430) uniform Params { vec2 pixel_size; uint tonemapper; - uint pad; + uint pad1; + vec4 tonemapper_params; uvec2 glow_texture_size; float glow_intensity; @@ -88,6 +89,9 @@ layout(push_constant, std430) uniform Params { float white; float auto_exposure_scale; float luminance_multiplier; + + float output_max_value; + uint pad2[3]; } params; @@ -199,17 +203,18 @@ vec4 texture2D_bicubic(sampler2D tex, vec2 uv, int p_lod) { #endif // !USE_GLOW_FILTER_BICUBIC // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt -vec3 tonemap_reinhard(vec3 color, float white) { - float white_squared = white * white; - vec3 white_squared_color = white_squared * color; - // Equivalent to color * (1 + color / white_squared) / (1 + color) - return (white_squared_color + color * color) / (white_squared_color + white_squared); +vec3 tonemap_reinhard(vec3 color) { + float white_squared = params.tonemapper_params.x; + + // Updated version of the Reinhard tonemapper supporting HDR rendering. + return color * (1.0f + color / white_squared) / (1.0f + color / params.output_max_value); } -vec3 tonemap_filmic(vec3 color, float white) { - // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers - // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values) - // has no effect on the curve's general shape or visual properties +vec3 tonemap_filmic(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. + // exposure_bias: Input scale (color *= bias, env->white *= bias) to make the brightness consistent with other tonemappers. + // Also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. const float exposure_bias = 2.0f; const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance const float B = 0.30f * exposure_bias; @@ -219,14 +224,14 @@ vec3 tonemap_filmic(vec3 color, float white) { const float F = 0.30f; vec3 color_tonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F; - float white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F; - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl // (MIT License). -vec3 tonemap_aces(vec3 color, float white) { +vec3 tonemap_aces(vec3 color) { + // These constants must match the those in the C++ code that calculates the parameters. const float exposure_bias = 1.8f; const float A = 0.0245786f; const float B = 0.000090537f; @@ -249,46 +254,44 @@ vec3 tonemap_aces(vec3 color, float white) { vec3 color_tonemapped = (color * (color + A) - B) / (color * (C * color + D) + E); color_tonemapped *= odt_to_rgb; - white *= exposure_bias; - float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E); - - return color_tonemapped / white_tonemapped; + return color_tonemapped / params.tonemapper_params.x; } -// Polynomial approximation of EaryChow's AgX sigmoid curve. -// x must be within the range [0.0, 1.0] -vec3 agx_contrast_approx(vec3 x) { - // Generated with Excel trendline - // Input data: Generated using python sigmoid with EaryChow's configuration and 57 steps - // Additional padding values were added to give correct intersections at 0.0 and 1.0 - // 6th order, intercept of 0.0 to remove an operation and ensure intersection at 0.0 - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2; +// allenwp tonemapping curve; developed for use in the Godot game engine. +// Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/ +// Input must be a non-negative linear scene value. +vec3 allenwp_curve(vec3 x) { + // These constants must match the those in the C++ code that calculates the parameters. + // 18% "middle gray" is perceptually 50% of the brightness of reference white. + const float awp_crossover_point = 0.18; + // When output_max_value and/or awp_crossover_point are no longer constant, + // awp_shoulder_max can be calculated on the CPU and passed in as params.tonemap_e. + const float awp_shoulder_max = params.output_max_value - awp_crossover_point; + + float awp_contrast = params.tonemapper_params.x; + float awp_toe_a = params.tonemapper_params.y; + float awp_slope = params.tonemapper_params.z; + float awp_w = params.tonemapper_params.w; + + // Reinhard-like shoulder: + vec3 s = x - awp_crossover_point; + vec3 slope_s = awp_slope * s; + s = slope_s * (1.0 + s / awp_w) / (1.0 + (slope_s / awp_shoulder_max)); + s += awp_crossover_point; + + // Sigmoid power function toe: + vec3 t = pow(x, vec3(awp_contrast)); + t = t / (t + awp_toe_a); + + return mix(s, t, lessThan(x, vec3(awp_crossover_point))); } // This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender. // This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses. // Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py +// Colorspace transformation source: https://www.colour-science.org:8010/apps/rgb_colourspace_transformation_matrix vec3 tonemap_agx(vec3 color) { - // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: - const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( - 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603, - 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023, - 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528); - - // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. - const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( - 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409, - -0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117, - -0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889); - - // LOG2_MIN = -10.0 - // LOG2_MAX = +6.5 - // MIDDLE_GRAY = 0.18 - const float min_ev = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY) - const float max_ev = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY) - + // Input color should be non-negative! // Large negative values in one channel and large positive values in other // channels can result in a colour that appears darker and more saturated than // desired after passing it through the inset matrix. For this reason, it is @@ -296,28 +299,38 @@ vec3 tonemap_agx(vec3 color) { // This is done before the Rec. 2020 transform to allow the Rec. 2020 // transform to be combined with the AgX inset matrix. This results in a loss // of color information that could be correctly interpreted within the - // Rec. 2020 color space as positive RGB values, but it is less common for Godot - // to provide this function with negative sRGB values and therefore not worth + // Rec. 2020 color space as positive RGB values, but is often not worth // the performance cost of an additional matrix multiplication. - // A value of 2e-10 intentionally introduces insignificant error to prevent - // log2(0.0) after the inset matrix is applied; color will be >= 1e-10 after - // the matrix transform. - color = max(color, 2e-10); + // + // Additionally, this AgX configuration was created subjectively based on + // output appearance in the sRGB color space, so it is possible that these + // matrices will not perform well with non-sRGB output (more testing with + // future wide-gamut displays is be needed). + // See this comment from the author on the decisions made to create the matrices: + // https://github.com/godotengine/godot-proposals/issues/12317#issuecomment-2835824250 - // Do AGX in rec2020 to match Blender and then apply inset matrix. - color = srgb_to_rec2020_agx_inset_matrix * color; + // Combined linear sRGB to linear Rec 2020 and Blender AgX inset matrices: + const mat3 srgb_to_rec2020_agx_inset_matrix = mat3( + 0.544814746488245, 0.140416948464053, 0.0888104196149096, + 0.373787398372697, 0.754137554567394, 0.178871756420858, + 0.0813978551390581, 0.105445496968552, 0.732317823964232); + + // Combined inverse AgX outset matrix and linear Rec 2020 to linear sRGB matrices. + const mat3 agx_outset_rec2020_to_srgb_matrix = mat3( + 1.96488741169489, -0.299313364904742, -0.164352742528393, + -0.855988495690215, 1.32639796461980, -0.238183969428088, + -0.108898916004672, -0.0270845997150571, 1.40253671195648); - // Log2 space encoding. - // Must be clamped because agx_contrast_approx may not work - // well with values outside of the range [0.0, 1.0] - color = clamp(log2(color), min_ev, max_ev); - color = (color - min_ev) / (max_ev - min_ev); + // Apply inset matrix. + color = srgb_to_rec2020_agx_inset_matrix * color; - // Apply sigmoid function approximation. - color = agx_contrast_approx(color); + // Use the allenwp tonemapping curve to match the Blender AgX curve while + // providing stability across all variable dyanimc range (SDR, HDR, EDR). + color = allenwp_curve(color); - // Convert back to linear before applying outset matrix. - color = pow(color, vec3(2.4)); + // Clipping to output_max_value is required to address a cyan colour that occurs + // with very bright inputs. + color = min(vec3(params.output_max_value), color); // Apply outset to make the result more chroma-laden and then go back to linear sRGB. color = agx_outset_rec2020_to_srgb_matrix * color; @@ -344,17 +357,21 @@ vec3 srgb_to_linear(vec3 color) { #define TONEMAPPER_ACES 3 #define TONEMAPPER_AGX 4 -vec3 apply_tonemapping(vec3 color, float white) { // inputs are LINEAR - // Ensure color values passed to tonemappers are positive. - // They can be negative in the case of negative lights, which leads to undesired behavior. +vec3 apply_tonemapping(vec3 color) { // inputs are LINEAR if (params.tonemapper == TONEMAPPER_LINEAR) { return color; - } else if (params.tonemapper == TONEMAPPER_REINHARD) { - return tonemap_reinhard(max(vec3(0.0f), color), white); + } + + // Ensure color values passed to tonemappers are positive. + // They can be negative in the case of negative lights, which leads to undesired behavior. + color = max(vec3(0.0), color); + + if (params.tonemapper == TONEMAPPER_REINHARD) { + return tonemap_reinhard(color); } else if (params.tonemapper == TONEMAPPER_FILMIC) { - return tonemap_filmic(max(vec3(0.0f), color), white); + return tonemap_filmic(color); } else if (params.tonemapper == TONEMAPPER_ACES) { - return tonemap_aces(max(vec3(0.0f), color), white); + return tonemap_aces(color); } else { // TONEMAPPER_AGX return tonemap_agx(color); } @@ -904,7 +921,7 @@ void main() { // Tonemap to lower dynamic range. - color.rgb = apply_tonemapping(color.rgb, params.white); + color.rgb = apply_tonemapping(color.rgb); // Additional effects. @@ -918,7 +935,7 @@ void main() { if (params.glow_map_strength > 0.001) { glow = mix(glow, texture(glow_map, uv_interp).rgb * glow, params.glow_map_strength); } - glow = apply_tonemapping(glow, params.white); + glow = apply_tonemapping(glow); color.rgb = apply_glow(color.rgb, glow, params.white); } #endif diff --git a/servers/rendering/renderer_rd/storage_rd/render_data_rd.h b/servers/rendering/renderer_rd/storage_rd/render_data_rd.h index 5f33f5888e59..945d37220c2c 100644 --- a/servers/rendering/renderer_rd/storage_rd/render_data_rd.h +++ b/servers/rendering/renderer_rd/storage_rd/render_data_rd.h @@ -74,6 +74,8 @@ class RenderDataRD : public RenderData { bool lightmap_bicubic_filter = false; + float window_output_max_value = 1.0; + RenderingMethod::RenderInfo *render_info = nullptr; /* Viewport data */ diff --git a/servers/rendering/renderer_scene_cull.cpp b/servers/rendering/renderer_scene_cull.cpp index c73fb6ffda50..06bc0079d86b 100644 --- a/servers/rendering/renderer_scene_cull.cpp +++ b/servers/rendering/renderer_scene_cull.cpp @@ -2589,7 +2589,7 @@ bool RendererSceneCull::_light_instance_update_shadow(Instance *p_instance, cons return animated_material_found; } -void RendererSceneCull::render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, RenderInfo *r_render_info) { +void RendererSceneCull::render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderInfo *r_render_info) { #ifndef _3D_DISABLED Camera *camera = camera_owner.get_or_null(p_camera); @@ -2704,7 +2704,7 @@ void RendererSceneCull::render_camera(const Ref &p_render_bu // For now just cull on the first camera RendererSceneOcclusionCull::get_singleton()->buffer_update(p_viewport, camera_data.main_transform, camera_data.main_projection, camera_data.is_orthogonal); - _render_scene(&camera_data, p_render_buffers, environment, camera->attributes, compositor, camera->visible_layers, p_scenario, p_viewport, p_shadow_atlas, RID(), -1, p_screen_mesh_lod_threshold, true, r_render_info); + _render_scene(&camera_data, p_render_buffers, environment, camera->attributes, compositor, camera->visible_layers, p_scenario, p_viewport, p_shadow_atlas, RID(), -1, p_screen_mesh_lod_threshold, p_window_output_max_value, true, r_render_info); #endif } @@ -3098,7 +3098,7 @@ void RendererSceneCull::_scene_particles_set_view_axis(RID p_particles, const Ve RSG::particles_storage->particles_set_view_axis(p_particles, p_axis, p_up_axis); } -void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref &p_render_buffers, RID p_environment, RID p_force_camera_attributes, RID p_compositor, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, bool p_using_shadows, RenderingMethod::RenderInfo *r_render_info) { +void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref &p_render_buffers, RID p_environment, RID p_force_camera_attributes, RID p_compositor, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, float p_window_output_max_value, bool p_using_shadows, RenderingMethod::RenderInfo *r_render_info) { Instance *render_reflection_probe = instance_owner.get_or_null(p_reflection_probe); //if null, not rendering to it // Prepare the light - camera volume culling system. @@ -3484,7 +3484,7 @@ void RendererSceneCull::_render_scene(const RendererSceneRender::CameraData *p_c } RENDER_TIMESTAMP("Render 3D Scene"); - scene_render->render_scene(p_render_buffers, p_camera_data, prev_camera_data, scene_cull_result.geometry_instances, scene_cull_result.light_instances, scene_cull_result.reflections, scene_cull_result.voxel_gi_instances, scene_cull_result.decals, scene_cull_result.lightmaps, scene_cull_result.fog_volumes, p_environment, camera_attributes, p_compositor, p_shadow_atlas, occluders_tex, p_reflection_probe.is_valid() ? RID() : scenario->reflection_atlas, p_reflection_probe, p_reflection_probe_pass, p_screen_mesh_lod_threshold, render_shadow_data, max_shadows_used, render_sdfgi_data, cull.sdfgi.region_count, &sdfgi_update_data, r_render_info); + scene_render->render_scene(p_render_buffers, p_camera_data, prev_camera_data, scene_cull_result.geometry_instances, scene_cull_result.light_instances, scene_cull_result.reflections, scene_cull_result.voxel_gi_instances, scene_cull_result.decals, scene_cull_result.lightmaps, scene_cull_result.fog_volumes, p_environment, camera_attributes, p_compositor, p_shadow_atlas, occluders_tex, p_reflection_probe.is_valid() ? RID() : scenario->reflection_atlas, p_reflection_probe, p_reflection_probe_pass, p_screen_mesh_lod_threshold, render_shadow_data, max_shadows_used, render_sdfgi_data, cull.sdfgi.region_count, p_window_output_max_value, &sdfgi_update_data, r_render_info); if (p_viewport.is_valid()) { RSG::viewport->viewport_set_prev_camera_data(p_viewport, p_camera_data); @@ -3535,7 +3535,7 @@ RID RendererSceneCull::_render_get_compositor(RID p_camera, RID p_scenario) { return RID(); } -void RendererSceneCull::render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas) { +void RendererSceneCull::render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas, float p_window_output_max_value) { #ifndef _3D_DISABLED Scenario *scenario = scenario_owner.get_or_null(p_scenario); @@ -3551,7 +3551,7 @@ void RendererSceneCull::render_empty_scene(const Ref &p_rend RendererSceneRender::CameraData camera_data; camera_data.set_camera(Transform3D(), Projection(), true, false, false); - scene_render->render_scene(p_render_buffers, &camera_data, &camera_data, PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), environment, RID(), compositor, p_shadow_atlas, RID(), scenario->reflection_atlas, RID(), 0, 0, nullptr, 0, nullptr, 0, nullptr); + scene_render->render_scene(p_render_buffers, &camera_data, &camera_data, PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), PagedArray(), environment, RID(), compositor, p_shadow_atlas, RID(), scenario->reflection_atlas, RID(), 0, 0, nullptr, 0, nullptr, 0, p_window_output_max_value, nullptr); #endif } @@ -3630,7 +3630,7 @@ bool RendererSceneCull::_render_reflection_probe_step(Instance *p_instance, int camera_data.set_camera(xform, cm, false, false, false); Ref render_buffers = RSG::light_storage->reflection_probe_atlas_get_render_buffers(scenario->reflection_atlas); - _render_scene(&camera_data, render_buffers, environment, RID(), RID(), RSG::light_storage->reflection_probe_get_cull_mask(p_instance->base), p_instance->scenario->self, RID(), shadow_atlas, reflection_probe->instance, p_step, mesh_lod_threshold, use_shadows); + _render_scene(&camera_data, render_buffers, environment, RID(), RID(), RSG::light_storage->reflection_probe_get_cull_mask(p_instance->base), p_instance->scenario->self, RID(), shadow_atlas, reflection_probe->instance, p_step, mesh_lod_threshold, 1.0, use_shadows); } else { //do roughness postprocess step until it believes it's done diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h index 74f3b42eb6ed..6c63f98cbcb8 100644 --- a/servers/rendering/renderer_scene_cull.h +++ b/servers/rendering/renderer_scene_cull.h @@ -1153,10 +1153,10 @@ class RendererSceneCull : public RenderingMethod { bool _render_reflection_probe_step(Instance *p_instance, int p_step); - void _render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref &p_render_buffers, RID p_environment, RID p_force_camera_attributes, RID p_compositor, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, bool p_using_shadows = true, RenderInfo *r_render_info = nullptr); - void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas); + void _render_scene(const RendererSceneRender::CameraData *p_camera_data, const Ref &p_render_buffers, RID p_environment, RID p_force_camera_attributes, RID p_compositor, uint32_t p_visible_layers, RID p_scenario, RID p_viewport, RID p_shadow_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, float p_window_output_max_value, bool p_using_shadows = true, RenderInfo *r_render_info = nullptr); + void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas, float p_window_output_max_value); - void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, RenderingMethod::RenderInfo *r_render_info = nullptr); + void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_screen_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderingMethod::RenderInfo *r_render_info = nullptr); void update_dirty_instances() const; void render_particle_colliders(); @@ -1240,9 +1240,10 @@ class RendererSceneCull : public RenderingMethod { // Tonemap PASS4(environment_set_tonemap, RID, RS::EnvironmentToneMapper, float, float) + PASS2(environment_set_tonemap_agx_contrast, RID, float) PASS1RC(RS::EnvironmentToneMapper, environment_get_tone_mapper, RID) PASS1RC(float, environment_get_exposure, RID) - PASS1RC(float, environment_get_white, RID) + PASS3RC(float, environment_get_white, RID, bool, float) // Fog PASS11(environment_set_fog, RID, bool, const Color &, float, float, float, float, float, float, float, RS::EnvironmentFogMode) diff --git a/servers/rendering/renderer_scene_render.cpp b/servers/rendering/renderer_scene_render.cpp index 3300a55a32bd..7909d07e9146 100644 --- a/servers/rendering/renderer_scene_render.cpp +++ b/servers/rendering/renderer_scene_render.cpp @@ -373,8 +373,20 @@ float RendererSceneRender::environment_get_exposure(RID p_env) const { return environment_storage.environment_get_exposure(p_env); } -float RendererSceneRender::environment_get_white(RID p_env) const { - return environment_storage.environment_get_white(p_env); +float RendererSceneRender::environment_get_white(RID p_env, bool p_limit_agx_white, float p_output_max_value) const { + return environment_storage.environment_get_white(p_env, p_limit_agx_white, p_output_max_value); +} + +void RendererSceneRender::environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) { + environment_storage.environment_set_tonemap_agx_contrast(p_env, p_agx_contrast); +} + +float RendererSceneRender::environment_get_tonemap_agx_contrast(RID p_env) const { + return environment_storage.environment_get_tonemap_agx_contrast(p_env); +} + +RendererEnvironmentStorage::TonemapParameters RendererSceneRender::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white, float p_output_max_value) const { + return environment_storage.environment_get_tonemap_parameters(p_env, p_limit_agx_white, p_output_max_value); } // Fog diff --git a/servers/rendering/renderer_scene_render.h b/servers/rendering/renderer_scene_render.h index 44b8764ef07a..12376deef222 100644 --- a/servers/rendering/renderer_scene_render.h +++ b/servers/rendering/renderer_scene_render.h @@ -137,7 +137,10 @@ class RendererSceneRender { void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white); RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const; float environment_get_exposure(RID p_env) const; - float environment_get_white(RID p_env) const; + float environment_get_white(RID p_env, bool p_limit_agx_white, float p_output_max_value) const; + void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast); + float environment_get_tonemap_agx_contrast(RID p_env) const; + RendererEnvironmentStorage::TonemapParameters environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white, float p_output_max_value) const; // Fog void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode); @@ -316,7 +319,7 @@ class RendererSceneRender { void set_multiview_camera(uint32_t p_view_count, const Transform3D *p_transforms, const Projection *p_projections, bool p_is_orthogonal, bool p_is_frustum, bool p_vaspect, uint32_t p_visible_layers = 0xFFFFFFFF); }; - virtual void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) = 0; + virtual void render_scene(const Ref &p_render_buffers, const CameraData *p_camera_data, const CameraData *p_prev_camera_data, const PagedArray &p_instances, const PagedArray &p_lights, const PagedArray &p_reflection_probes, const PagedArray &p_voxel_gi_instances, const PagedArray &p_decals, const PagedArray &p_lightmaps, const PagedArray &p_fog_volumes, RID p_environment, RID p_camera_attributes, RID p_compositor, RID p_shadow_atlas, RID p_occluder_debug_tex, RID p_reflection_atlas, RID p_reflection_probe, int p_reflection_probe_pass, float p_screen_mesh_lod_threshold, const RenderShadowData *p_render_shadows, int p_render_shadow_count, const RenderSDFGIData *p_render_sdfgi_regions, int p_render_sdfgi_region_count, float p_window_output_max_value, const RenderSDFGIUpdateData *p_sdfgi_update_data = nullptr, RenderingMethod::RenderInfo *r_render_info = nullptr) = 0; virtual void render_material(const Transform3D &p_cam_transform, const Projection &p_cam_projection, bool p_cam_orthogonal, const PagedArray &p_instances, RID p_framebuffer, const Rect2i &p_region) = 0; virtual void render_particle_collider_heightfield(RID p_collider, const Transform3D &p_transform, const PagedArray &p_instances) = 0; diff --git a/servers/rendering/renderer_viewport.cpp b/servers/rendering/renderer_viewport.cpp index e5104bba6403..6b8825bbe072 100644 --- a/servers/rendering/renderer_viewport.cpp +++ b/servers/rendering/renderer_viewport.cpp @@ -314,7 +314,7 @@ void RendererViewport::_draw_3d(Viewport *p_viewport) { } float screen_mesh_lod_threshold = p_viewport->mesh_lod_threshold / float(p_viewport->size.width); - RSG::scene->render_camera(p_viewport->render_buffers, p_viewport->camera, p_viewport->scenario, p_viewport->self, p_viewport->internal_size, p_viewport->jitter_phase_count, screen_mesh_lod_threshold, p_viewport->shadow_atlas, xr_interface, &p_viewport->render_info); + RSG::scene->render_camera(p_viewport->render_buffers, p_viewport->camera, p_viewport->scenario, p_viewport->self, p_viewport->internal_size, p_viewport->jitter_phase_count, screen_mesh_lod_threshold, p_viewport->shadow_atlas, xr_interface, p_viewport->window_output_max_value, &p_viewport->render_info); RENDER_TIMESTAMP("< Render 3D Scene"); #endif // _3D_DISABLED @@ -356,6 +356,15 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { force_clear_render_target = true; } } + + p_viewport->window_output_max_value = 1.0; + DisplayServer::WindowID parent_window = _get_containing_window(p_viewport); + if (RD::get_singleton() && parent_window != DisplayServer::INVALID_WINDOW_ID) { + RenderingContextDriver *context_driver = RD::get_singleton()->get_context_driver(); + if (context_driver->window_get_hdr_output_enabled(parent_window)) { + p_viewport->window_output_max_value = context_driver->window_get_output_max_value(parent_window); + } + } } bool can_draw_3d = RSG::scene->is_camera(p_viewport->camera) && !p_viewport->disable_3d; @@ -651,7 +660,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { // Clear now otherwise we copy over garbage from the render target. RSG::texture_storage->render_target_do_clear_request(p_viewport->render_target); if (!can_draw_3d) { - RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas); + RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas, p_viewport->window_output_max_value); } else { _draw_3d(p_viewport); } @@ -694,7 +703,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { // Clear now otherwise we copy over garbage from the render target. RSG::texture_storage->render_target_do_clear_request(p_viewport->render_target); if (!can_draw_3d) { - RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas); + RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas, p_viewport->window_output_max_value); } else { _draw_3d(p_viewport); } @@ -708,7 +717,7 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { // Clear now otherwise we copy over garbage from the render target. RSG::texture_storage->render_target_do_clear_request(p_viewport->render_target); if (!can_draw_3d) { - RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas); + RSG::scene->render_empty_scene(p_viewport->render_buffers, p_viewport->scenario, p_viewport->shadow_atlas, p_viewport->window_output_max_value); } else { _draw_3d(p_viewport); } @@ -732,6 +741,21 @@ void RendererViewport::_draw_viewport(Viewport *p_viewport) { } } +DisplayServer::WindowID RendererViewport::_get_containing_window(Viewport *p_viewport) { + if (p_viewport->viewport_to_screen != DisplayServer::INVALID_WINDOW_ID) { + return p_viewport->viewport_to_screen; + } + + if (p_viewport->parent.is_valid()) { + Viewport *parent = viewport_owner.get_or_null(p_viewport->parent); + if (parent) { + return _get_containing_window(parent); + } + } + + return DisplayServer::INVALID_WINDOW_ID; +} + void RendererViewport::draw_viewports(bool p_swap_buffers) { timestamp_vp_map.clear(); diff --git a/servers/rendering/renderer_viewport.h b/servers/rendering/renderer_viewport.h index fa506be98cd8..2ba1d1e4fbb8 100644 --- a/servers/rendering/renderer_viewport.h +++ b/servers/rendering/renderer_viewport.h @@ -79,9 +79,9 @@ class RendererViewport { bool use_occlusion_culling = false; bool occlusion_buffer_dirty = false; - DisplayServer::WindowID viewport_to_screen; + DisplayServer::WindowID viewport_to_screen = DisplayServer::INVALID_WINDOW_ID; Rect2 viewport_to_screen_rect; - bool viewport_render_direct_to_screen; + bool viewport_render_direct_to_screen = false; bool disable_2d = false; RS::ViewportEnvironmentMode disable_environment = RS::VIEWPORT_ENVIRONMENT_INHERIT; @@ -116,6 +116,7 @@ class RendererViewport { bool transparent_bg = false; bool use_hdr_2d = false; + float window_output_max_value = 1.0; uint32_t canvas_cull_mask = 0xffffffff; @@ -158,6 +159,7 @@ class RendererViewport { clear_mode = RS::VIEWPORT_CLEAR_ALWAYS; transparent_bg = false; use_hdr_2d = false; + window_output_max_value = 1.0; viewport_to_screen = DisplayServer::INVALID_WINDOW_ID; shadow_atlas_size = 0; @@ -207,6 +209,7 @@ class RendererViewport { void _configure_3d_render_buffers(Viewport *p_viewport); void _draw_3d(Viewport *p_viewport); void _draw_viewport(Viewport *p_viewport); + DisplayServer::WindowID _get_containing_window(Viewport *p_viewport); int occlusion_rays_per_thread = 512; diff --git a/servers/rendering/rendering_context_driver.cpp b/servers/rendering/rendering_context_driver.cpp index b623be40980f..6bac0870f860 100644 --- a/servers/rendering/rendering_context_driver.cpp +++ b/servers/rendering/rendering_context_driver.cpp @@ -75,6 +75,80 @@ DisplayServer::VSyncMode RenderingContextDriver::window_get_vsync_mode(DisplaySe } } +void RenderingContextDriver::window_set_hdr_output_enabled(DisplayServer::WindowID p_window, bool p_enabled) { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + surface_set_hdr_output_enabled(surface, p_enabled); + } +} + +bool RenderingContextDriver::window_get_hdr_output_enabled(DisplayServer::WindowID p_window) const { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + return surface_get_hdr_output_enabled(surface); + } else { + return false; + } +} + +void RenderingContextDriver::window_set_hdr_output_reference_luminance(DisplayServer::WindowID p_window, float p_reference_luminance) { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + surface_set_hdr_output_reference_luminance(surface, p_reference_luminance); + } +} + +float RenderingContextDriver::window_get_hdr_output_reference_luminance(DisplayServer::WindowID p_window) const { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + return surface_get_hdr_output_reference_luminance(surface); + } else { + return 0.0f; + } +} + +void RenderingContextDriver::window_set_hdr_output_max_luminance(DisplayServer::WindowID p_window, float p_max_luminance) { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + surface_set_hdr_output_max_luminance(surface, p_max_luminance); + } +} + +float RenderingContextDriver::window_get_hdr_output_max_luminance(DisplayServer::WindowID p_window) const { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + return surface_get_hdr_output_max_luminance(surface); + } else { + return 0.0f; + } +} + +void RenderingContextDriver::window_set_hdr_output_linear_luminance_scale(DisplayServer::WindowID p_window, float p_linear_luminance_scale) { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + surface_set_hdr_output_linear_luminance_scale(surface, p_linear_luminance_scale); + } +} + +float RenderingContextDriver::window_get_hdr_output_linear_luminance_scale(DisplayServer::WindowID p_window) const { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + return surface_get_hdr_output_linear_luminance_scale(surface); + } else { + return 0.0f; + } +} + +float RenderingContextDriver::window_get_output_max_value(DisplayServer::WindowID p_window) const { + SurfaceID surface = surface_get_from_window(p_window); + if (surface) { + if (surface_get_hdr_output_enabled(surface)) { + return surface_get_hdr_output_max_value(surface); + } + } + return 1.0f; // SDR +} + void RenderingContextDriver::window_destroy(DisplayServer::WindowID p_window) { SurfaceID surface = surface_get_from_window(p_window); if (surface) { diff --git a/servers/rendering/rendering_context_driver.h b/servers/rendering/rendering_context_driver.h index f30880b0402d..6cc94961ea2b 100644 --- a/servers/rendering/rendering_context_driver.h +++ b/servers/rendering/rendering_context_driver.h @@ -47,6 +47,15 @@ class RenderingContextDriver { void window_set_size(DisplayServer::WindowID p_window, uint32_t p_width, uint32_t p_height); void window_set_vsync_mode(DisplayServer::WindowID p_window, DisplayServer::VSyncMode p_vsync_mode); DisplayServer::VSyncMode window_get_vsync_mode(DisplayServer::WindowID p_window) const; + void window_set_hdr_output_enabled(DisplayServer::WindowID p_window, bool p_enabled); + bool window_get_hdr_output_enabled(DisplayServer::WindowID p_window) const; + void window_set_hdr_output_reference_luminance(DisplayServer::WindowID p_window, float p_reference_luminance); + float window_get_hdr_output_reference_luminance(DisplayServer::WindowID p_window) const; + void window_set_hdr_output_max_luminance(DisplayServer::WindowID p_window, float p_max_luminance); + float window_get_hdr_output_max_luminance(DisplayServer::WindowID p_window) const; + void window_set_hdr_output_linear_luminance_scale(DisplayServer::WindowID p_window, float p_linear_luminance_scale); + float window_get_hdr_output_linear_luminance_scale(DisplayServer::WindowID p_window) const; + float window_get_output_max_value(DisplayServer::WindowID p_window) const; void window_destroy(DisplayServer::WindowID p_window); public: @@ -97,6 +106,15 @@ class RenderingContextDriver { virtual void surface_set_size(SurfaceID p_surface, uint32_t p_width, uint32_t p_height) = 0; virtual void surface_set_vsync_mode(SurfaceID p_surface, DisplayServer::VSyncMode p_vsync_mode) = 0; virtual DisplayServer::VSyncMode surface_get_vsync_mode(SurfaceID p_surface) const = 0; + virtual void surface_set_hdr_output_enabled(SurfaceID p_surface, bool p_enabled) = 0; + virtual bool surface_get_hdr_output_enabled(SurfaceID p_surface) const = 0; + virtual void surface_set_hdr_output_reference_luminance(SurfaceID p_surface, float p_reference_luminance) = 0; + virtual float surface_get_hdr_output_reference_luminance(SurfaceID p_surface) const = 0; + virtual void surface_set_hdr_output_max_luminance(SurfaceID p_surface, float p_max_luminance) = 0; + virtual float surface_get_hdr_output_max_luminance(SurfaceID p_surface) const = 0; + virtual void surface_set_hdr_output_linear_luminance_scale(SurfaceID p_surface, float p_linear_luminance_scale) = 0; + virtual float surface_get_hdr_output_linear_luminance_scale(SurfaceID p_surface) const = 0; + virtual float surface_get_hdr_output_max_value(SurfaceID p_surface) const = 0; virtual uint32_t surface_get_width(SurfaceID p_surface) const = 0; virtual uint32_t surface_get_height(SurfaceID p_surface) const = 0; virtual void surface_set_needs_resize(SurfaceID p_surface, bool p_needs_resize) = 0; diff --git a/servers/rendering/rendering_device.cpp b/servers/rendering/rendering_device.cpp index 3d2caeeaa318..59b89f3a3c3b 100644 --- a/servers/rendering/rendering_device.cpp +++ b/servers/rendering/rendering_device.cpp @@ -4274,6 +4274,17 @@ RenderingDevice::FramebufferFormatID RenderingDevice::screen_get_framebuffer_for return const_cast(this)->framebuffer_format_create(screen_attachment); } +RenderingDevice::ColorSpace RenderingDevice::screen_get_color_space(DisplayServer::WindowID p_screen) const { + _THREAD_SAFE_METHOD_ + + HashMap::ConstIterator it = screen_swap_chains.find(p_screen); + ERR_FAIL_COND_V_MSG(it == screen_swap_chains.end(), COLOR_SPACE_MAX, "Screen was never prepared."); + + ColorSpace color_space = driver->swap_chain_get_color_space(it->value); + ERR_FAIL_COND_V_MSG(color_space == COLOR_SPACE_MAX, COLOR_SPACE_MAX, "Unknown color space."); + return color_space; +} + Error RenderingDevice::screen_free(DisplayServer::WindowID p_screen) { _THREAD_SAFE_METHOD_ @@ -7934,6 +7945,7 @@ void RenderingDevice::_bind_methods() { BIND_ENUM_CONSTANT(SUPPORTS_METALFX_TEMPORAL); BIND_ENUM_CONSTANT(SUPPORTS_BUFFER_DEVICE_ADDRESS); BIND_ENUM_CONSTANT(SUPPORTS_IMAGE_ATOMIC_32_BIT); + BIND_ENUM_CONSTANT(SUPPORTS_HDR_OUTPUT); BIND_ENUM_CONSTANT(LIMIT_MAX_BOUND_UNIFORM_SETS); BIND_ENUM_CONSTANT(LIMIT_MAX_FRAMEBUFFER_COLOR_ATTACHMENTS); diff --git a/servers/rendering/rendering_device.h b/servers/rendering/rendering_device.h index 312dcfcc130a..ae344b76188c 100644 --- a/servers/rendering/rendering_device.h +++ b/servers/rendering/rendering_device.h @@ -1199,6 +1199,7 @@ class RenderingDevice : public RenderingDeviceCommons { int screen_get_height(DisplayServer::WindowID p_screen = DisplayServer::MAIN_WINDOW_ID) const; int screen_get_pre_rotation_degrees(DisplayServer::WindowID p_screen = DisplayServer::MAIN_WINDOW_ID) const; FramebufferFormatID screen_get_framebuffer_format(DisplayServer::WindowID p_screen = DisplayServer::MAIN_WINDOW_ID) const; + ColorSpace screen_get_color_space(DisplayServer::WindowID p_screen = DisplayServer::MAIN_WINDOW_ID) const; Error screen_free(DisplayServer::WindowID p_screen = DisplayServer::MAIN_WINDOW_ID); /*************************/ diff --git a/servers/rendering/rendering_device_commons.h b/servers/rendering/rendering_device_commons.h index 738f3c8680d4..d172d58bffac 100644 --- a/servers/rendering/rendering_device_commons.h +++ b/servers/rendering/rendering_device_commons.h @@ -317,6 +317,12 @@ class RenderingDeviceCommons : public Object { DATA_FORMAT_MAX, }; + enum ColorSpace { + COLOR_SPACE_REC709_LINEAR, + COLOR_SPACE_REC709_NONLINEAR_SRGB, + COLOR_SPACE_MAX, + }; + // Breadcrumb markers are useful for debugging GPU crashes (i.e. DEVICE_LOST). Internally // they're just an uint32_t to "tag" a GPU command. These are only used for debugging and do not // (or at least shouldn't) alter the execution behavior in any way. @@ -954,6 +960,7 @@ class RenderingDeviceCommons : public Object { SUPPORTS_BUFFER_DEVICE_ADDRESS, SUPPORTS_IMAGE_ATOMIC_32_BIT, SUPPORTS_VULKAN_MEMORY_MODEL, + SUPPORTS_HDR_OUTPUT, }; enum SubgroupOperations { diff --git a/servers/rendering/rendering_device_driver.h b/servers/rendering/rendering_device_driver.h index 2d208a5c8fd2..353eea53339c 100644 --- a/servers/rendering/rendering_device_driver.h +++ b/servers/rendering/rendering_device_driver.h @@ -454,6 +454,9 @@ class RenderingDeviceDriver : public RenderingDeviceCommons { // Retrieve the format used by the swap chain's framebuffers. virtual DataFormat swap_chain_get_format(SwapChainID p_swap_chain) = 0; + // Retrieve the color space used by the swap chain's framebuffers. + virtual ColorSpace swap_chain_get_color_space(SwapChainID p_swap_chain) = 0; + // Tells the swapchain the max_fps so it can use the proper frame pacing. // Android uses this with Swappy library. Some implementations or platforms may ignore this hint. virtual void swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) {} diff --git a/servers/rendering/rendering_method.h b/servers/rendering/rendering_method.h index 5de00d97e4f1..b30d12d99364 100644 --- a/servers/rendering/rendering_method.h +++ b/servers/rendering/rendering_method.h @@ -185,10 +185,11 @@ class RenderingMethod { // Tonemap virtual void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white) = 0; + virtual void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) = 0; virtual RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const = 0; virtual float environment_get_exposure(RID p_env) const = 0; - virtual float environment_get_white(RID p_env) const = 0; + virtual float environment_get_white(RID p_env, bool p_limit_agx_white, float p_output_max_value) const = 0; // Fog virtual void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode = RS::EnvironmentFogMode::ENV_FOG_MODE_EXPONENTIAL) = 0; @@ -341,13 +342,13 @@ class RenderingMethod { virtual void sdfgi_set_debug_probe_select(const Vector3 &p_position, const Vector3 &p_dir) = 0; - virtual void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas) = 0; + virtual void render_empty_scene(const Ref &p_render_buffers, RID p_scenario, RID p_shadow_atlas, float p_window_output_max_value) = 0; struct RenderInfo { int info[RS::VIEWPORT_RENDER_INFO_TYPE_MAX][RS::VIEWPORT_RENDER_INFO_MAX] = {}; }; - virtual void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, RenderInfo *r_render_info = nullptr) = 0; + virtual void render_camera(const Ref &p_render_buffers, RID p_camera, RID p_scenario, RID p_viewport, Size2 p_viewport_size, uint32_t p_jitter_phase_count, float p_mesh_lod_threshold, RID p_shadow_atlas, Ref &p_xr_interface, float p_window_output_max_value, RenderInfo *r_render_info = nullptr) = 0; virtual void update() = 0; virtual void render_probes() = 0; diff --git a/servers/rendering/rendering_server.cpp b/servers/rendering/rendering_server.cpp index 8c02698ed5c7..ef9116448d60 100644 --- a/servers/rendering/rendering_server.cpp +++ b/servers/rendering/rendering_server.cpp @@ -3101,6 +3101,7 @@ void RenderingServer::_bind_methods() { ClassDB::bind_method(D_METHOD("environment_set_ambient_light", "env", "color", "ambient", "energy", "sky_contribution", "reflection_source"), &RenderingServer::environment_set_ambient_light, DEFVAL(RS::ENV_AMBIENT_SOURCE_BG), DEFVAL(1.0), DEFVAL(0.0), DEFVAL(RS::ENV_REFLECTION_SOURCE_BG)); ClassDB::bind_method(D_METHOD("environment_set_glow", "env", "enable", "levels", "intensity", "strength", "mix", "bloom_threshold", "blend_mode", "hdr_bleed_threshold", "hdr_bleed_scale", "hdr_luminance_cap", "glow_map_strength", "glow_map"), &RenderingServer::environment_set_glow); ClassDB::bind_method(D_METHOD("environment_set_tonemap", "env", "tone_mapper", "exposure", "white"), &RenderingServer::environment_set_tonemap); + ClassDB::bind_method(D_METHOD("environment_set_tonemap_agx_contrast", "env", "agx_contrast"), &RenderingServer::environment_set_tonemap_agx_contrast); ClassDB::bind_method(D_METHOD("environment_set_adjustment", "env", "enable", "brightness", "contrast", "saturation", "use_1d_color_correction", "color_correction"), &RenderingServer::environment_set_adjustment); ClassDB::bind_method(D_METHOD("environment_set_ssr", "env", "enable", "max_steps", "fade_in", "fade_out", "depth_tolerance"), &RenderingServer::environment_set_ssr); ClassDB::bind_method(D_METHOD("environment_set_ssao", "env", "enable", "radius", "intensity", "power", "detail", "horizon", "sharpness", "light_affect", "ao_channel_affect"), &RenderingServer::environment_set_ssao); diff --git a/servers/rendering/rendering_server.h b/servers/rendering/rendering_server.h index 7fe0d3223c80..cb6c65355746 100644 --- a/servers/rendering/rendering_server.h +++ b/servers/rendering/rendering_server.h @@ -1283,6 +1283,7 @@ class RenderingServer : public Object { }; virtual void environment_set_tonemap(RID p_env, EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white) = 0; + virtual void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) = 0; virtual void environment_set_adjustment(RID p_env, bool p_enable, float p_brightness, float p_contrast, float p_saturation, bool p_use_1d_color_correction, RID p_color_correction) = 0; virtual void environment_set_ssr(RID p_env, bool p_enable, int p_max_steps, float p_fade_in, float p_fade_out, float p_depth_tolerance) = 0; diff --git a/servers/rendering/rendering_server_default.h b/servers/rendering/rendering_server_default.h index 62a155a534c8..87e8e7e772f7 100644 --- a/servers/rendering/rendering_server_default.h +++ b/servers/rendering/rendering_server_default.h @@ -830,6 +830,7 @@ class RenderingServerDefault : public RenderingServer { FUNC1(environment_glow_set_use_bicubic_upscale, bool) FUNC4(environment_set_tonemap, RID, EnvironmentToneMapper, float, float) + FUNC2(environment_set_tonemap_agx_contrast, RID, float) FUNC7(environment_set_adjustment, RID, bool, float, float, float, bool, RID) diff --git a/servers/rendering/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp index 34a36848620a..bdd815506062 100644 --- a/servers/rendering/storage/environment_storage.cpp +++ b/servers/rendering/storage/environment_storage.cpp @@ -208,13 +208,7 @@ void RendererEnvironmentStorage::environment_set_tonemap(RID p_env, RS::Environm ERR_FAIL_NULL(env); env->exposure = p_exposure; env->tone_mapper = p_tone_mapper; - if (p_tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { - env->white = 1.0; // With HDR output, this should be the output max value instead. - } else if (p_tone_mapper == RS::ENV_TONE_MAPPER_AGX) { - env->white = 16.29; - } else { - env->white = MAX(1.0, p_white); // Glow with screen blend mode does not work when white < 1.0. - } + env->white = p_white; } RS::EnvironmentToneMapper RendererEnvironmentStorage::environment_get_tone_mapper(RID p_env) const { @@ -229,10 +223,120 @@ float RendererEnvironmentStorage::environment_get_exposure(RID p_env) const { return env->exposure; } -float RendererEnvironmentStorage::environment_get_white(RID p_env) const { +float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_agx_white, float p_output_max_value) const { Environment *env = environment_owner.get_or_null(p_env); ERR_FAIL_NULL_V(env, 1.0); - return env->white; + + // Glow with screen blend mode does not work when white < 1.0, so make sure + // it is at least 1.0 for all tonemappers: + if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { + return p_output_max_value; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC || env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) { + // Filmic and ACES only support SDR; their white is stable regardless + // of p_output_max_value. + return MAX(1.0, env->white); + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) { + // AgX works best with a high white. 2.0 is the minimum required for + // good behavior with Mobile rendering method. + if (p_limit_agx_white) { + return 2.0; + } else { + float agx_white = MAX(2.0, env->white); + // Instead of constraining by matching the p_output_max_value, constrain + // by multiplying to ensure the desired non-uniform scaling behavior + // is maintained in the shoulder. + return agx_white * p_output_max_value; + } + } else { // Reinhard + // The Reinhard tonemapper is not designed to have a white parameter + // that is less than the output max value. This is especially important + // in the variable Extended Dynamic Range (EDR) paradigm where the + // output max value may change to be greater or less than the white + // parameter, depending on the available dynamic range. + return MAX(p_output_max_value, env->white); + } +} + +void RendererEnvironmentStorage::environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast) { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL(env); + env->tonemap_agx_contrast = p_agx_contrast; +} + +float RendererEnvironmentStorage::environment_get_tonemap_agx_contrast(RID p_env) const { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL_V(env, 1.0); + return env->tonemap_agx_contrast; +} + +RendererEnvironmentStorage::TonemapParameters RendererEnvironmentStorage::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white, float p_output_max_value) const { + Environment *env = environment_owner.get_or_null(p_env); + ERR_FAIL_NULL_V(env, TonemapParameters()); + + float white = environment_get_white(p_env, p_limit_agx_white, p_output_max_value); + TonemapParameters tonemap_parameters = TonemapParameters(); + + if (env->tone_mapper == RS::ENV_TONE_MAPPER_LINEAR) { + // Linear has no tonemapping parameters + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_REINHARD) { + tonemap_parameters.white_squared = (white * white) / p_output_max_value; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_FILMIC) { + // These constants must match those in the shader code. + // exposure_bias: Input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers + // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values). + // Has no effect on the curve's general shape or visual properties. + const float exposure_bias = 2.0f; + const float A = 0.22f * exposure_bias * exposure_bias; // bias baked into constants for performance + const float B = 0.30f * exposure_bias; + const float C = 0.10f; + const float D = 0.20f; + const float E = 0.01f; + const float F = 0.30f; + + tonemap_parameters.white_tonemapped = ((white * (A * white + C * B) + D * E) / (white * (A * white + B) + D * F)) - E / F; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_ACES) { + // These constants must match those in the shader code. + const float exposure_bias = 1.8f; + const float A = 0.0245786f; + const float B = 0.000090537f; + const float C = 0.983729f; + const float D = 0.432951f; + const float E = 0.238081f; + + white *= exposure_bias; + float white_tonemapped = (white * (white + A) - B) / (white * (C * white + D) + E); + tonemap_parameters.white_tonemapped = white_tonemapped; + } else if (env->tone_mapper == RS::ENV_TONE_MAPPER_AGX) { + // Calculate allenwp tonemapping curve parameters on the CPU to improve shader performance. + // Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/ + + // These constants must match the those in the shader code. + // 18% "middle gray" is perceptually 50% of the brightness of reference white. + const float awp_crossover_point = 0.18; + // When p_output_max_value and/or awp_crossover_point are no longer constant, awp_shoulder_max can + // be calculated on the CPU and passed in as tonemap_parameters.tonemap_e. + const float awp_shoulder_max = p_output_max_value - awp_crossover_point; + + float awp_high_clip = white; + + // awp_toe_a is a solution generated by Mathematica that ensures intersection at awp_crossover_point. + float awp_toe_a = ((1.0 / awp_crossover_point) - 1.0) * pow(awp_crossover_point, env->tonemap_agx_contrast); + // Slope formula is simply the derivative of the toe function with an input of awp_crossover_point. + float awp_slope_denom = pow(awp_crossover_point, env->tonemap_agx_contrast) + awp_toe_a; + float awp_slope = (env->tonemap_agx_contrast * pow(awp_crossover_point, env->tonemap_agx_contrast - 1.0) * awp_toe_a) / (awp_slope_denom * awp_slope_denom); + + float awp_w = awp_high_clip - awp_crossover_point; + awp_w = awp_w * awp_w; + awp_w = awp_w / awp_shoulder_max; + awp_w = awp_w * awp_slope; + + tonemap_parameters.awp_contrast = env->tonemap_agx_contrast; + tonemap_parameters.awp_toe_a = awp_toe_a; + tonemap_parameters.awp_slope = awp_slope; + tonemap_parameters.awp_w = awp_w; + } + + return tonemap_parameters; } // Fog diff --git a/servers/rendering/storage/environment_storage.h b/servers/rendering/storage/environment_storage.h index 62852cf37f30..9dd0d2b5b796 100644 --- a/servers/rendering/storage/environment_storage.h +++ b/servers/rendering/storage/environment_storage.h @@ -34,6 +34,30 @@ #include "servers/rendering/rendering_server.h" class RendererEnvironmentStorage { +public: + union TonemapParameters { + // Shader vec4: + float tonemapper_params[4]; + + // Reinhard: + struct { + float white_squared; + }; + + // Filmic and ACES: + struct { + float white_tonemapped; + }; + + // AgX: + struct { + float awp_contrast; + float awp_toe_a; + float awp_slope; + float awp_w; + }; + }; + private: static RendererEnvironmentStorage *singleton; @@ -62,6 +86,8 @@ class RendererEnvironmentStorage { RS::EnvironmentToneMapper tone_mapper; float exposure = 1.0; float white = 1.0; + float tonemap_agx_contrast = 1.25; // Default to approximately Blender's AgX contrast + float max_value = 1.0; // Fog bool fog_enabled = false; @@ -202,7 +228,10 @@ class RendererEnvironmentStorage { void environment_set_tonemap(RID p_env, RS::EnvironmentToneMapper p_tone_mapper, float p_exposure, float p_white); RS::EnvironmentToneMapper environment_get_tone_mapper(RID p_env) const; float environment_get_exposure(RID p_env) const; - float environment_get_white(RID p_env) const; + float environment_get_white(RID p_env, bool p_limit_agx_white, float p_output_max_value) const; + void environment_set_tonemap_agx_contrast(RID p_env, float p_agx_contrast); + float environment_get_tonemap_agx_contrast(RID p_env) const; + TonemapParameters environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white, float p_output_max_value) const; // Fog void environment_set_fog(RID p_env, bool p_enable, const Color &p_light_color, float p_light_energy, float p_sun_scatter, float p_density, float p_height, float p_height_density, float p_aerial_perspective, float p_sky_affect, RS::EnvironmentFogMode p_mode); diff --git a/thirdparty/wayland-protocols/staging/color-management/README b/thirdparty/wayland-protocols/staging/color-management/README new file mode 100644 index 000000000000..e14f2a8cb0d4 --- /dev/null +++ b/thirdparty/wayland-protocols/staging/color-management/README @@ -0,0 +1,11 @@ +Color management and HDR protocol + +Maintainers: +Sebastian Wick +Pekka Paalanen + + +Historical credits not mentioned in the first commit: + +- Niels Ole Salscheider, for an early protocol version + https://lists.freedesktop.org/archives/wayland-devel/2014-October/017755.html diff --git a/thirdparty/wayland-protocols/staging/color-management/color-management-v1.xml b/thirdparty/wayland-protocols/staging/color-management/color-management-v1.xml new file mode 100644 index 000000000000..7f8da78f1238 --- /dev/null +++ b/thirdparty/wayland-protocols/staging/color-management/color-management-v1.xml @@ -0,0 +1,1631 @@ + + + + Copyright 2019 Sebastian Wick + Copyright 2019 Erwin Burema + Copyright 2020 AMD + Copyright 2020-2024 Collabora, Ltd. + Copyright 2024 Xaver Hugl + Copyright 2022-2025 Red Hat, Inc. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + The aim of the color management extension is to allow clients to know + the color properties of outputs, and to tell the compositor about the color + properties of their content on surfaces. Doing this enables a compositor + to perform automatic color management of content for different outputs + according to how content is intended to look like. + + The color properties are represented as an image description object which + is immutable after it has been created. A wl_output always has an + associated image description that clients can observe. A wl_surface + always has an associated preferred image description as a hint chosen by + the compositor that clients can also observe. Clients can set an image + description on a wl_surface to denote the color characteristics of the + surface contents. + + An image description includes SDR and HDR colorimetry and encoding, HDR + metadata, and viewing environment parameters. An image description does + not include the properties set through color-representation extension. + It is expected that the color-representation extension is used in + conjunction with the color management extension when necessary, + particularly with the YUV family of pixel formats. + + Recommendation ITU-T H.273 + "Coding-independent code points for video signal type identification" + shall be referred to as simply H.273 here. + + The color-and-hdr repository + (https://gitlab.freedesktop.org/pq/color-and-hdr) contains + background information on the protocol design and legacy color management. + It also contains a glossary, learning resources for digital color, tools, + samples and more. + + The terminology used in this protocol is based on common color science and + color encoding terminology where possible. The glossary in the color-and-hdr + repository shall be the authority on the definition of terms in this + protocol. + + Warning! The protocol described in this file is currently in the testing + phase. Backward compatible changes may be added together with the + corresponding interface version bump. Backward incompatible changes can + only be done by creating a new major version of the extension. + + + + + A singleton global interface used for getting color management extensions + for wl_surface and wl_output objects, and for creating client defined + image description objects. The extension interfaces allow + getting the image description of outputs and setting the image + description of surfaces. + + Compositors should never remove this global. + + + + + Destroy the wp_color_manager_v1 object. This does not affect any other + objects in any way. + + + + + + + + + + + See the ICC.1:2022 specification from the International Color Consortium + for more details about rendering intents. + + The principles of ICC defined rendering intents apply with all types of + image descriptions, not only those with ICC file profiles. + + Compositors must support the perceptual rendering intent. Other + rendering intents are optional. + + + + + + + + + + + + + + + + + + + + The compositor supports set_mastering_display_primaries request with a + target color volume fully contained inside the primary color volume. + + + + + The compositor additionally supports target color volumes that + extend outside of the primary color volume. + + This can only be advertised if feature set_mastering_display_primaries + is supported as well. + + + + + + + + Named color primaries used to encode well-known sets of primaries. H.273 + is the authority, when it comes to the exact values of primaries and + authoritative specifications, where an equivalent code point exists. + + A value of 0 is invalid and will never be present in the list of enums. + + Descriptions do list the specifications for convenience. + + + + + Color primaries as defined by + - Rec. ITU-R BT.709-6 + - Rec. ITU-R BT.1361-0 conventional colour gamut system and extended + colour gamut system (historical) + - IEC 61966-2-1 sRGB or sYCC + - IEC 61966-2-4 + - Society of Motion Picture and Television Engineers (SMPTE) RP 177 + (1993) Annex B + Equivalent to H.273 ColourPrimaries code point 1. + + + + + Color primaries as defined by + - Rec. ITU-R BT.470-6 System M (historical) + - United States National Television System Committee 1953 + Recommendation for transmission standards for color television + - United States Federal Communications Commission (2003) Title 47 Code + of Federal Regulations 73.682 (a)(20) + Equivalent to H.273 ColourPrimaries code point 4. + + + + + Color primaries as defined by + - Rec. ITU-R BT.470-6 System B, G (historical) + - Rec. ITU-R BT.601-7 625 + - Rec. ITU-R BT.1358-0 625 (historical) + - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + Equivalent to H.273 ColourPrimaries code point 5. + + + + + Color primaries as defined by + - Rec. ITU-R BT.601-7 525 + - Rec. ITU-R BT.1358-1 525 or 625 (historical) + - Rec. ITU-R BT.1700-0 NTSC + - SMPTE 170M (2004) + - SMPTE 240M (1999) (historical) + Equivalent to H.273 ColourPrimaries code point 6 and 7. + + + + + Color primaries as defined by H.273 for generic film. + Equivalent to H.273 ColourPrimaries code point 8. + + + + + Color primaries as defined by + - Rec. ITU-R BT.2020-2 + - Rec. ITU-R BT.2100-0 + Equivalent to H.273 ColourPrimaries code point 9. + + + + + Color primaries as defined as the maximum of the CIE 1931 XYZ color + space by + - SMPTE ST 428-1 + - (CIE 1931 XYZ as in ISO 11664-1) + Equivalent to H.273 ColourPrimaries code point 10. + + + + + Color primaries as defined by Digital Cinema System and published in + SMPTE RP 431-2 (2011). Equivalent to H.273 ColourPrimaries code point + 11. + + + + + Color primaries as defined by Digital Cinema System and published in + SMPTE EG 432-1 (2010). + Equivalent to H.273 ColourPrimaries code point 12. + + + + + Color primaries as defined by Adobe as "Adobe RGB" and later published + by ISO 12640-4 (2011). + + + + + + + Named transfer functions used to represent well-known transfer + characteristics. H.273 is the authority, when it comes to the exact + formulas and authoritative specifications, where an equivalent code + point exists. + + A value of 0 is invalid and will never be present in the list of enums. + + Descriptions do list the specifications for convenience. + + + + + Rec. ITU-R BT.1886 is the display transfer characteristic assumed by + - Rec. ITU-R BT.601-7 525 and 625 + - Rec. ITU-R BT.709-6 + - Rec. ITU-R BT.2020-2 + These recommendations are referred to by H.273 TransferCharacteristics + code points 1, 6, 14, and 15, which are all equivalent. + + This TF implies these default luminances from Rec. ITU-R BT.2035: + - primary color volume minimum: 0.01 cd/m² + - primary color volume maximum: 100 cd/m² + - reference white: 100 cd/m² + + + + + Transfer characteristics as defined by + - Rec. ITU-R BT.470-6 System M (historical) + - United States National Television System Committee 1953 + Recommendation for transmission standards for color television + - United States Federal Communications Commission (2003) Title 47 Code + of Federal Regulations 73.682 (a) (20) + - Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM + Equivalent to H.273 TransferCharacteristics code point 4. + + + + + Transfer characteristics as defined by + - Rec. ITU-R BT.470-6 System B, G (historical) + Equivalent to H.273 TransferCharacteristics code point 5. + + + + + Transfer characteristics as defined by + - SMPTE ST 240 (1999) + Equivalent to H.273 TransferCharacteristics code point 7. + + + + + Linear transfer function defined over all real numbers. + Normalised electrical values are equal the normalised optical values. + + The differences to H.273 TransferCharacteristics code point 8 are + the definition over all real numbers. + + + + + Logarithmic transfer characteristic (100:1 range). + Equivalent to H.273 TransferCharacteristics code point 9. + + + + + Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range). + Equivalent to H.273 TransferCharacteristics code point 10. + + + + + Transfer characteristics as defined by + - IEC 61966-2-4 + Equivalent to H.273 TransferCharacteristics code point 11. + + + + + Transfer characteristics as defined by + - IEC 61966-2-1 sRGB + Equivalent to H.273 TransferCharacteristics code point 13 with + MatrixCoefficients set to 0. + + + + + Transfer characteristics as defined by + - IEC 61966-2-1 sYCC + Equivalent to H.273 TransferCharacteristics code point 13 with + MatrixCoefficients set to anything but 0. + + + + + Transfer characteristics as defined by + - SMPTE ST 2084 (2014) for 10-, 12-, 14- and 16-bit systems + - Rec. ITU-R BT.2100-2 perceptual quantization (PQ) system + Equivalent to H.273 TransferCharacteristics code point 16. + + This TF implies these default luminances + - primary color volume minimum: 0.005 cd/m² + - primary color volume maximum: 10000 cd/m² + - reference white: 203 cd/m² + + The difference between the primary color volume minimum and maximum + must be approximately 10000 cd/m² as that is the swing of the EOTF + defined by ST 2084 and BT.2100. The default value for the + reference white is a protocol addition: it is suggested by + Report ITU-R BT.2408-7 and is not part of ST 2084 or BT.2100. + + + + + Transfer characteristics as defined by + - SMPTE ST 428-1 (2019) + Equivalent to H.273 TransferCharacteristics code point 17. + + + + + Transfer characteristics as defined by + - ARIB STD-B67 (2015) + - Rec. ITU-R BT.2100-2 hybrid log-gamma (HLG) system + Equivalent to H.273 TransferCharacteristics code point 18. + + This TF implies these default luminances + - primary color volume minimum: 0.005 cd/m² + - primary color volume maximum: 1000 cd/m² + - reference white: 203 cd/m² + + HLG is a relative display-referred signal with a specified + non-linear mapping to the display peak luminance (the HLG OOTF). + All absolute luminance values used here for HLG assume a 1000 cd/m² + peak display. + + The default value for the reference white is a protocol addition: + it is suggested by Report ITU-R BT.2408-7 and is not part of + ARIB STD-B67 or BT.2100. + + + + + + + This creates a new wp_color_management_output_v1 object for the + given wl_output. + + See the wp_color_management_output_v1 interface for more details. + + + + + + + + + If a wp_color_management_surface_v1 object already exists for the given + wl_surface, the protocol error surface_exists is raised. + + This creates a new color wp_color_management_surface_v1 object for the + given wl_surface. + + See the wp_color_management_surface_v1 interface for more details. + + + + + + + + + This creates a new color wp_color_management_surface_feedback_v1 object + for the given wl_surface. + + See the wp_color_management_surface_feedback_v1 interface for more + details. + + + + + + + + + Makes a new ICC-based image description creator object with all + properties initially unset. The client can then use the object's + interface to define all the required properties for an image description + and finally create a wp_image_description_v1 object. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.icc_v2_v4. + Otherwise this request raises the protocol error unsupported_feature. + + + + + + + + Makes a new parametric image description creator object with all + properties initially unset. The client can then use the object's + interface to define all the required properties for an image description + and finally create a wp_image_description_v1 object. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.parametric. + Otherwise this request raises the protocol error unsupported_feature. + + + + + + + + This creates a pre-defined image description for the so-called + Windows-scRGB stimulus encoding. This comes from the Windows 10 handling + of its own definition of an scRGB color space for an HDR screen + driven in BT.2100/PQ signalling mode. + + Windows-scRGB uses sRGB (BT.709) color primaries and white point. + The transfer characteristic is extended linear. + + The nominal color channel value range is extended, meaning it includes + negative and greater than 1.0 values. Negative values are used to + escape the sRGB color gamut boundaries. To make use of the extended + range, the client needs to use a pixel format that can represent those + values, e.g. floating-point 16 bits per channel. + + Nominal color value R=G=B=0.0 corresponds to BT.2100/PQ system + 0 cd/m², and R=G=B=1.0 corresponds to BT.2100/PQ system 80 cd/m². + The maximum is R=G=B=125.0 corresponding to 10k cd/m². + + Windows-scRGB is displayed by Windows 10 by converting it to + BT.2100/PQ, maintaining the CIE 1931 chromaticity and mapping the + luminance as above. No adjustment is made to the signal to account + for the viewing conditions. + + The reference white level of Windows-scRGB is unknown. If a + reference white level must be assumed for compositor processing, it + should be R=G=B=2.5375 corresponding to 203 cd/m² of Report ITU-R + BT.2408-7. + + The target color volume of Windows-scRGB is unknown. The color gamut + may be anything between sRGB and BT.2100. + + Note: EGL_EXT_gl_colorspace_scrgb_linear definition differs from + Windows-scRGB by using R=G=B=1.0 as the reference white level, while + Windows-scRGB reference white level is unknown or varies. However, + it seems probable that Windows implements both + EGL_EXT_gl_colorspace_scrgb_linear and Vulkan + VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT as Windows-scRGB. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.windows_scrgb. + Otherwise this request raises the protocol error unsupported_feature. + + The resulting image description object does not allow get_information + request. The wp_image_description_v1.ready event shall be sent. + + + + + + + + When this object is created, it shall immediately send this event once + for each rendering intent the compositor supports. + + + + + + + + When this object is created, it shall immediately send this event once + for each compositor supported feature listed in the enumeration. + + + + + + + + When this object is created, it shall immediately send this event once + for each named transfer function the compositor supports with the + parametric image description creator. + + + + + + + + When this object is created, it shall immediately send this event once + for each named set of primaries the compositor supports with the + parametric image description creator. + + + + + + + + This event is sent when all supported rendering intents, features, + transfer functions and named primaries have been sent. + + + + + + + A wp_color_management_output_v1 describes the color properties of an + output. + + The wp_color_management_output_v1 is associated with the wl_output global + underlying the wl_output object. Therefore the client destroying the + wl_output object has no impact, but the compositor removing the output + global makes the wp_color_management_output_v1 object inert. + + + + + Destroy the color wp_color_management_output_v1 object. This does not + affect any remaining protocol objects. + + + + + + This event is sent whenever the image description of the output changed, + followed by one wl_output.done event common to output events across all + extensions. + + If the client wants to use the updated image description, it needs to do + get_image_description again, because image description objects are + immutable. + + + + + + This creates a new wp_image_description_v1 object for the current image + description of the output. There always is exactly one image description + active for an output so the client should destroy the image description + created by earlier invocations of this request. This request is usually + sent as a reaction to the image_description_changed event or when + creating a wp_color_management_output_v1 object. + + The image description of an output represents the color encoding the + output expects. There might be performance and power advantages, as well + as improved color reproduction, if a content update matches the image + description of the output it is being shown on. If a content update is + shown on any other output than the one it matches the image description + of, then the color reproduction on those outputs might be considerably + worse. + + The created wp_image_description_v1 object preserves the image + description of the output from the time the object was created. + + The resulting image description object allows get_information request. + + If this protocol object is inert, the resulting image description object + shall immediately deliver the wp_image_description_v1.failed event with + the no_output cause. + + If the interface version is inadequate for the output's image + description, meaning that the client does not support all the events + needed to deliver the crucial information, the resulting image + description object shall immediately deliver the + wp_image_description_v1.failed event with the low_version cause. + + Otherwise the object shall immediately deliver the ready event. + + + + + + + + + A wp_color_management_surface_v1 allows the client to set the color + space and HDR properties of a surface. + + If the wl_surface associated with the wp_color_management_surface_v1 is + destroyed, the wp_color_management_surface_v1 object becomes inert. + + + + + Destroy the wp_color_management_surface_v1 object and do the same as + unset_image_description. + + + + + + + + + + + + + If this protocol object is inert, the protocol error inert is raised. + + Set the image description of the underlying surface. The image + description and rendering intent are double-buffered state, see + wl_surface.commit. + + It is the client's responsibility to understand the image description + it sets on a surface, and to provide content that matches that image + description. Compositors might convert images to match their own or any + other image descriptions. + + Image descriptions which are not ready (see wp_image_description_v1) + are forbidden in this request, and in such case the protocol error + image_description is raised. + + All image descriptions which are ready (see wp_image_description_v1) + are allowed and must always be accepted by the compositor. + + A rendering intent provides the client's preference on how content + colors should be mapped to each output. The render_intent value must + be one advertised by the compositor with + wp_color_manager_v1.render_intent event, otherwise the protocol error + render_intent is raised. + + When an image description is set on a surface, the Transfer + Characteristics of the image description defines the valid range of + the nominal (real-valued) color channel values. The processing of + out-of-range color channel values is undefined, but compositors are + recommended to clamp the values to the valid range when possible. + + By default, a surface does not have an associated image description + nor a rendering intent. The handling of color on such surfaces is + compositor implementation defined. Compositors should handle such + surfaces as sRGB, but may handle them differently if they have specific + requirements. + + Setting the image description has copy semantics; after this request, + the image description can be immediately destroyed without affecting + the pending state of the surface. + + + + + + + + + If this protocol object is inert, the protocol error inert is raised. + + This request removes any image description from the surface. See + set_image_description for how a compositor handles a surface without + an image description. This is double-buffered state, see + wl_surface.commit. + + + + + + + A wp_color_management_surface_feedback_v1 allows the client to get the + preferred image description of a surface. + + If the wl_surface associated with this object is destroyed, the + wp_color_management_surface_feedback_v1 object becomes inert. + + + + + Destroy the wp_color_management_surface_feedback_v1 object. + + + + + + + + + + + + The preferred image description is the one which likely has the most + performance and/or quality benefits for the compositor if used by the + client for its wl_surface contents. This event is sent whenever the + compositor changes the wl_surface's preferred image description. + + This event sends the identity of the new preferred state as the argument, + so clients who are aware of the image description already can reuse it. + Otherwise, if the client client wants to know what the preferred image + description is, it shall use the get_preferred request. + + The preferred image description is not automatically used for anything. + It is only a hint, and clients may set any valid image description with + set_image_description, but there might be performance and color accuracy + improvements by providing the wl_surface contents in the preferred + image description. Therefore clients that can, should render according + to the preferred image description + + + + + + + + If this protocol object is inert, the protocol error inert is raised. + + The preferred image description represents the compositor's preferred + color encoding for this wl_surface at the current time. There might be + performance and power advantages, as well as improved color + reproduction, if the image description of a content update matches the + preferred image description. + + This creates a new wp_image_description_v1 object for the currently + preferred image description for the wl_surface. The client should + stop using and destroy the image descriptions created by earlier + invocations of this request for the associated wl_surface. + This request is usually sent as a reaction to the preferred_changed + event or when creating a wp_color_management_surface_feedback_v1 object + if the client is capable of adapting to image descriptions. + + The created wp_image_description_v1 object preserves the preferred image + description of the wl_surface from the time the object was created. + + The resulting image description object allows get_information request. + + If the image description is parametric, the client should set it on its + wl_surface only if the image description is an exact match with the + client content. Particularly if everything else matches, but the target + color volume is greater than what the client needs, the client should + create its own parameric image description with its exact parameters. + + If the interface version is inadequate for the preferred image + description, meaning that the client does not support all the + events needed to deliver the crucial information, the resulting image + description object shall immediately deliver the + wp_image_description_v1.failed event with the low_version cause, + otherwise the object shall immediately deliver the ready event. + + + + + + + + The same description as for get_preferred applies, except the returned + image description is guaranteed to be parametric. This is meant for + clients that can only deal with parametric image descriptions. + + If the compositor doesn't support parametric image descriptions, the + unsupported_feature error is emitted. + + + + + + + + + This type of object is used for collecting all the information required + to create a wp_image_description_v1 object from an ICC file. A complete + set of required parameters consists of these properties: + - ICC file + + Each required property must be set exactly once if the client is to create + an image description. The set requests verify that a property was not + already set. The create request verifies that all required properties are + set. There may be several alternative requests for setting each property, + and in that case the client must choose one of them. + + Once all properties have been set, the create request must be used to + create the image description object, destroying the creator in the + process. + + + + + + + + + + + + + + + Create an image description object based on the ICC information + previously set on this object. A compositor must parse the ICC data in + some undefined but finite amount of time. + + The completeness of the parameter set is verified. If the set is not + complete, the protocol error incomplete_set is raised. For the + definition of a complete set, see the description of this interface. + + If the particular combination of the information is not supported + by the compositor, the resulting image description object shall + immediately deliver the wp_image_description_v1.failed event with the + 'unsupported' cause. If a valid image description was created from the + information, the wp_image_description_v1.ready event will eventually + be sent instead. + + This request destroys the wp_image_description_creator_icc_v1 object. + + The resulting image description object does not allow get_information + request. + + + + + + + + Sets the ICC profile file to be used as the basis of the image + description. + + The data shall be found through the given fd at the given offset, having + the given length. The fd must be seekable and readable. Violating these + requirements raises the bad_fd protocol error. + + If reading the data fails due to an error independent of the client, the + compositor shall send the wp_image_description_v1.failed event on the + created wp_image_description_v1 with the 'operating_system' cause. + + The maximum size of the ICC profile is 32 MB. If length is greater than + that or zero, the protocol error bad_size is raised. If offset + length + exceeds the file size, the protocol error out_of_file is raised. + + A compositor may read the file at any time starting from this request + and only until whichever happens first: + - If create request was issued, the wp_image_description_v1 object + delivers either failed or ready event; or + - if create request was not issued, this + wp_image_description_creator_icc_v1 object is destroyed. + + A compositor shall not modify the contents of the file, and the fd may + be sealed for writes and size changes. The client must ensure to its + best ability that the data does not change while the compositor is + reading it. + + The data must represent a valid ICC profile. The ICC profile version + must be 2 or 4, it must be a 3 channel profile and the class must be + Display or ColorSpace. Violating these requirements will not result in a + protocol error, but will eventually send the + wp_image_description_v1.failed event on the created + wp_image_description_v1 with the 'unsupported' cause. + + See the International Color Consortium specification ICC.1:2022 for more + details about ICC profiles. + + If ICC file has already been set on this object, the protocol error + already_set is raised. + + + + + + + + + + + This type of object is used for collecting all the parameters required + to create a wp_image_description_v1 object. A complete set of required + parameters consists of these properties: + - transfer characteristic function (tf) + - chromaticities of primaries and white point (primary color volume) + + The following properties are optional and have a well-defined default + if not explicitly set: + - primary color volume luminance range + - reference white luminance level + - mastering display primaries and white point (target color volume) + - mastering luminance range + + The following properties are optional and will be ignored + if not explicitly set: + - maximum content light level + - maximum frame-average light level + + Each required property must be set exactly once if the client is to create + an image description. The set requests verify that a property was not + already set. The create request verifies that all required properties are + set. There may be several alternative requests for setting each property, + and in that case the client must choose one of them. + + Once all properties have been set, the create request must be used to + create the image description object, destroying the creator in the + process. + + + + + + + + + + + + + + + + Create an image description object based on the parameters previously + set on this object. + + The completeness of the parameter set is verified. If the set is not + complete, the protocol error incomplete_set is raised. For the + definition of a complete set, see the description of this interface. + + The protocol error invalid_luminance is raised if any of the following + requirements is not met: + - When max_cll is set, it must be greater than min L and less or equal + to max L of the mastering luminance range. + - When max_fall is set, it must be greater than min L and less or equal + to max L of the mastering luminance range. + - When both max_cll and max_fall are set, max_fall must be less or equal + to max_cll. + + If the particular combination of the parameter set is not supported + by the compositor, the resulting image description object shall + immediately deliver the wp_image_description_v1.failed event with the + 'unsupported' cause. If a valid image description was created from the + parameter set, the wp_image_description_v1.ready event will eventually + be sent instead. + + This request destroys the wp_image_description_creator_params_v1 + object. + + The resulting image description object does not allow get_information + request. + + + + + + + + Sets the transfer characteristic using explicitly enumerated named + functions. + + When the resulting image description is attached to an image, the + content should be encoded and decoded according to the industry standard + practices for the transfer characteristic. + + Only names advertised with wp_color_manager_v1 event supported_tf_named + are allowed. Other values shall raise the protocol error invalid_tf. + + If transfer characteristic has already been set on this object, the + protocol error already_set is raised. + + + + + + + + Sets the color component transfer characteristic to a power curve with + the given exponent. Negative values are handled by mirroring the + positive half of the curve through the origin. The valid domain and + range of the curve are all finite real numbers. This curve represents + the conversion from electrical to optical color channel values. + + When the resulting image description is attached to an image, the + content should be encoded with the inverse of the power curve. + + The curve exponent shall be multiplied by 10000 to get the argument eexp + value to carry the precision of 4 decimals. + + The curve exponent must be at least 1.0 and at most 10.0. Otherwise the + protocol error invalid_tf is raised. + + If transfer characteristic has already been set on this object, the + protocol error already_set is raised. + + This request can be used when the compositor advertises + wp_color_manager_v1.feature.set_tf_power. Otherwise this request raises + the protocol error unsupported_feature. + + + + + + + + Sets the color primaries and white point using explicitly named sets. + This describes the primary color volume which is the basis for color + value encoding. + + Only names advertised with wp_color_manager_v1 event + supported_primaries_named are allowed. Other values shall raise the + protocol error invalid_primaries_named. + + If primaries have already been set on this object, the protocol error + already_set is raised. + + + + + + + + Sets the color primaries and white point using CIE 1931 xy chromaticity + coordinates. This describes the primary color volume which is the basis + for color value encoding. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + If primaries have already been set on this object, the protocol error + already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_primaries. Otherwise this request raises + the protocol error unsupported_feature. + + + + + + + + + + + + + + + Sets the primary color volume luminance range and the reference white + luminance level. These values include the minimum display emission + and ambient flare luminances, assumed to be optically additive and have + the chromaticity of the primary color volume white point. + + The default luminances from + https://www.color.org/chardata/rgb/srgb.xalter are + - primary color volume minimum: 0.2 cd/m² + - primary color volume maximum: 80 cd/m² + - reference white: 80 cd/m² + + Setting a named transfer characteristic can imply other default + luminances. + + The default luminances get overwritten when this request is used. + With transfer_function.st2084_pq the given 'max_lum' value is ignored, + and 'max_lum' is taken as 'min_lum' + 10000 cd/m². + + 'min_lum' and 'max_lum' specify the minimum and maximum luminances of + the primary color volume as reproduced by the targeted display. + + 'reference_lum' specifies the luminance of the reference white as + reproduced by the targeted display, and reflects the targeted viewing + environment. + + Compositors should make sure that all content is anchored, meaning that + an input signal level of 'reference_lum' on one image description and + another input signal level of 'reference_lum' on another image + description should produce the same output level, even though the + 'reference_lum' on both image representations can be different. + + 'reference_lum' may be higher than 'max_lum'. In that case reaching + the reference white output level in image content requires the + 'extended_target_volume' feature support. + + If 'max_lum' or 'reference_lum' are less than or equal to 'min_lum', + the protocol error invalid_luminance is raised. + + The minimum luminance is multiplied by 10000 to get the argument + 'min_lum' value and carries precision of 4 decimals. The maximum + luminance and reference white luminance values are unscaled. + + If the primary color volume luminance range and the reference white + luminance level have already been set on this object, the protocol error + already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_luminances. Otherwise this request + raises the protocol error unsupported_feature. + + + + + + + + + + Provides the color primaries and white point of the mastering display + using CIE 1931 xy chromaticity coordinates. This is compatible with the + SMPTE ST 2086 definition of HDR static metadata. + + The mastering display primaries and mastering display luminances define + the target color volume. + + If mastering display primaries are not explicitly set, the target color + volume is assumed to have the same primaries as the primary color volume. + + The target color volume is defined by all tristimulus values between 0.0 + and 1.0 (inclusive) of the color space defined by the given mastering + display primaries and white point. The colorimetry is identical between + the container color space and the mastering display color space, + including that no chromatic adaptation is applied even if the white + points differ. + + The target color volume can exceed the primary color volume to allow for + a greater color volume with an existing color space definition (for + example scRGB). It can be smaller than the primary color volume to + minimize gamut and tone mapping distances for big color spaces (HDR + metadata). + + To make use of the entire target color volume a suitable pixel format + has to be chosen (e.g. floating point to exceed the primary color + volume, or abusing limited quantization range as with xvYCC). + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + If mastering display primaries have already been set on this object, the + protocol error already_set is raised. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise + this request raises the protocol error unsupported_feature. The + advertisement implies support only for target color volumes fully + contained within the primary color volume. + + If a compositor additionally supports target color volume exceeding the + primary color volume, it must advertise + wp_color_manager_v1.feature.extended_target_volume. If a client uses + target color volume exceeding the primary color volume and the + compositor does not support it, the result is implementation defined. + Compositors are recommended to detect this case and fail the image + description gracefully, but it may as well result in color artifacts. + + + + + + + + + + + + + + + Sets the luminance range that was used during the content mastering + process as the minimum and maximum absolute luminance L. These values + include the minimum display emission and ambient flare luminances, + assumed to be optically additive and have the chromaticity of the + primary color volume white point. This should be + compatible with the SMPTE ST 2086 definition of HDR static metadata. + + The mastering display primaries and mastering display luminances define + the target color volume. + + If mastering luminances are not explicitly set, the target color volume + is assumed to have the same min and max luminances as the primary color + volume. + + If max L is less than or equal to min L, the protocol error + invalid_luminance is raised. + + Min L value is multiplied by 10000 to get the argument min_lum value + and carry precision of 4 decimals. Max L value is unscaled for max_lum. + + This request can be used if the compositor advertises + wp_color_manager_v1.feature.set_mastering_display_primaries. Otherwise + this request raises the protocol error unsupported_feature. The + advertisement implies support only for target color volumes fully + contained within the primary color volume. + + If a compositor additionally supports target color volume exceeding the + primary color volume, it must advertise + wp_color_manager_v1.feature.extended_target_volume. If a client uses + target color volume exceeding the primary color volume and the + compositor does not support it, the result is implementation defined. + Compositors are recommended to detect this case and fail the image + description gracefully, but it may as well result in color artifacts. + + + + + + + + + Sets the maximum content light level (max_cll) as defined by CTA-861-H. + + max_cll is undefined by default. + + + + + + + + Sets the maximum frame-average light level (max_fall) as defined by + CTA-861-H. + + max_fall is undefined by default. + + + + + + + + + An image description carries information about the color encoding used on + a surface when attached to a wl_surface via + wp_color_management_surface_v1.set_image_description. A compositor can use + this information to decode pixel values into colorimetrically meaningful + quantities. + + Note, that the wp_image_description_v1 object is not ready to be used + immediately after creation. The object eventually delivers either the + 'ready' or the 'failed' event, specified in all requests creating it. The + object is deemed "ready" after receiving the 'ready' event. + + An object which is not ready is illegal to use, it can only be destroyed. + Any other request in this interface shall result in the 'not_ready' + protocol error. Attempts to use an object which is not ready through other + interfaces shall raise protocol errors defined there. + + Once created and regardless of how it was created, a + wp_image_description_v1 object always refers to one fixed image + description. It cannot change after creation. + + + + + Destroy this object. It is safe to destroy an object which is not ready. + + Destroying a wp_image_description_v1 object has no side-effects, not + even if a wp_color_management_surface_v1.set_image_description has not + yet been followed by a wl_surface.commit. + + + + + + + + + + + + + + + + + + + + + + If creating a wp_image_description_v1 object fails for a reason that is + not defined as a protocol error, this event is sent. + + The requests that create image description objects define whether and + when this can occur. Only such creation requests can trigger this event. + This event cannot be triggered after the image description was + successfully formed. + + Once this event has been sent, the wp_image_description_v1 object will + never become ready and it can only be destroyed. + + + + + + + + + Once this event has been sent, the wp_image_description_v1 object is + deemed "ready". Ready objects can be used to send requests and can be + used through other interfaces. + + Every ready wp_image_description_v1 protocol object refers to an + underlying image description record in the compositor. Multiple protocol + objects may end up referring to the same record. Clients may identify + these "copies" by comparing their id numbers: if the numbers from two + protocol objects are identical, the protocol objects refer to the same + image description record. Two different image description records + cannot have the same id number simultaneously. The id number does not + change during the lifetime of the image description record. + + The id number is valid only as long as the protocol object is alive. If + all protocol objects referring to the same image description record are + destroyed, the id number may be recycled for a different image + description record. + + Image description id number is not a protocol object id. Zero is + reserved as an invalid id number. It shall not be possible for a client + to refer to an image description by its id number in protocol. The id + numbers might not be portable between Wayland connections. A compositor + shall not send an invalid id number. + + This identity allows clients to de-duplicate image description records + and avoid get_information request if they already have the image + description information. + + + + + + + + Creates a wp_image_description_info_v1 object which delivers the + information that makes up the image description. + + Not all image description protocol objects allow get_information + request. Whether it is allowed or not is defined by the request that + created the object. If get_information is not allowed, the protocol + error no_information is raised. + + + + + + + + + Sends all matching events describing an image description object exactly + once and finally sends the 'done' event. + + This means + - if the image description is parametric, it must send + - primaries + - named_primaries, if applicable + - at least one of tf_power and tf_named, as applicable + - luminances + - target_primaries + - target_luminance + - if the image description is parametric, it may send, if applicable, + - target_max_cll + - target_max_fall + - if the image description contains an ICC profile, it must send the + icc_file event + + Once a wp_image_description_info_v1 object has delivered a 'done' event it + is automatically destroyed. + + Every wp_image_description_info_v1 created from the same + wp_image_description_v1 shall always return the exact same data. + + + + + Signals the end of information events and destroys the object. + + + + + + The icc argument provides a file descriptor to the client which may be + memory-mapped to provide the ICC profile matching the image description. + The fd is read-only, and if mapped then it must be mapped with + MAP_PRIVATE by the client. + + The ICC profile version and other details are determined by the + compositor. There is no provision for a client to ask for a specific + kind of a profile. + + + + + + + + + + Delivers the primary color volume primaries and white point using CIE + 1931 xy chromaticity coordinates. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + + + + + + + + + + + + + + Delivers the primary color volume primaries and white point using an + explicitly enumerated named set. + + + + + + + + The color component transfer characteristic of this image description is + a pure power curve. This event provides the exponent of the power + function. This curve represents the conversion from electrical to + optical pixel or color values. + + The curve exponent has been multiplied by 10000 to get the argument eexp + value to carry the precision of 4 decimals. + + + + + + + + Delivers the transfer characteristic using an explicitly enumerated + named function. + + + + + + + + Delivers the primary color volume luminance range and the reference + white luminance level. These values include the minimum display emission + and ambient flare luminances, assumed to be optically additive and have + the chromaticity of the primary color volume white point. + + The minimum luminance is multiplied by 10000 to get the argument + 'min_lum' value and carries precision of 4 decimals. The maximum + luminance and reference white luminance values are unscaled. + + + + + + + + + + Provides the color primaries and white point of the target color volume + using CIE 1931 xy chromaticity coordinates. This is compatible with the + SMPTE ST 2086 definition of HDR static metadata for mastering displays. + + While primary color volume is about how color is encoded, the target + color volume is the actually displayable color volume. If target color + volume is equal to the primary color volume, then this event is not + sent. + + Each coordinate value is multiplied by 1 million to get the argument + value to carry precision of 6 decimals. + + + + + + + + + + + + + + + Provides the luminance range that the image description is targeting as + the minimum and maximum absolute luminance L. These values include the + minimum display emission and ambient flare luminances, assumed to be + optically additive and have the chromaticity of the primary color + volume white point. This should be compatible with the SMPTE ST 2086 + definition of HDR static metadata. + + This luminance range is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + + Min L value is multiplied by 10000 to get the argument min_lum value and + carry precision of 4 decimals. Max L value is unscaled for max_lum. + + + + + + + + + Provides the targeted max_cll of the image description. max_cll is + defined by CTA-861-H. + + This luminance is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + + + + + + + + Provides the targeted max_fall of the image description. max_fall is + defined by CTA-861-H. + + This luminance is only theoretical and may not correspond to the + luminance of light emitted on an actual display. + + + + + +