diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index 8f293a3000d8..d6a032e05257 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -1732,6 +1732,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 b37b830a44c9..87498d004468 100644
--- a/doc/classes/DisplayServer.xml
+++ b/doc/classes/DisplayServer.xml
@@ -2147,6 +2147,36 @@
Returns the current value of the given window's [param flag].
+
+
+
+
+ When [method window_is_hdr_output_enabled] returns [code]true[/code], this returns the current maximum luminance in nits (cd/m²) for HDR output by the window specified by [param window_id]. If the maximum luminance is being automatically adjusted based on the display's capabilities, this method will return that value. Otherwise, it will return the value set by [method window_set_hdr_output_max_luminance]. This maximum luminance value is used when calculating [method window_get_output_max_linear_value].
+ [b]Note:[/b] This maximum luminance may not match the physical behavior of the display, but will always be proportionally correct relative to [method window_get_hdr_output_current_reference_luminance].
+
+
+
+
+
+
+ When [method window_is_hdr_output_enabled] returns [code]true[/code], this returns the current reference white luminance in nits (cd/m²) for HDR output by the window specified by [param window_id]. If the reference luminance is being automatically adjusted to match the operating system brightness, this will return that value. Otherwise, it will return the value set by [method window_set_hdr_output_reference_luminance]. This reference luminance value is used when calculating [method window_get_output_max_linear_value].
+ [b]Note:[/b] This reference white luminance may not match the physical behavior of the display, but will always be proportionally correct relative to [method window_get_hdr_output_current_max_luminance].
+
+
+
+
+
+
+ Returns the maximum luminance in nits (cd/m²) set for HDR output by the window specified by [param window_id]. Negative values indicate that the value is being automatically adjusted based on the display's capabilities. See also [method window_get_hdr_output_current_max_luminance].
+
+
+
+
+
+
+ Returns the reference white luminance in nits (cd/m²) set for HDR output by the window specified by [param window_id]. Negative values indicate that the value is being automatically adjusted to match the operating system brightness. See also [method window_get_hdr_output_current_reference_luminance].
+
+
@@ -2177,6 +2207,13 @@
[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 for the window specified by [param window_id], regardless of SDR or HDR output. Returns [code]1.0[/code] if HDR is not enabled or not supported. When HDR output is enabled, this is calculated based on [method window_get_hdr_output_current_reference_luminance] and [method window_get_hdr_output_current_max_luminance]. This value is used by tonemapping and glow effects to ensure that bright colors are presented in the range that can be displayed by this window. Corresponds to [method Window.get_output_max_linear_value].
+
+
@@ -2242,6 +2279,27 @@
Returns [code]true[/code] if the window specified by [param window_id] is focused.
+
+
+
+
+ Returns [code]true[/code] if HDR output is currently enabled for the window specified by [param window_id]. The returned value may change dynamically based on system settings, display capabilities, and which display the window is currently on.
+
+
+
+
+
+
+ Returns [code]true[/code] if HDR output is requested for the window specified by [param window_id]. Corresponds to [member Window.hdr_output_requested].
+
+
+
+
+
+
+ Returns [code]true[/code] if 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.
+
+
@@ -2277,6 +2335,14 @@
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.
+
+
+
+
+
+ If [param enable] is [code]true[/code], HDR output is requested for the window specified by [param window_id]. The window will automatically switch between HDR and SDR if it is moved between displays, display capabilities change, or system settings are modified. This will internally force [member Viewport.use_hdr_2d] to be enabled on the main [Viewport]. All other [SubViewport] of the [Window] must have their [member Viewport.use_hdr_2d] property enabled to produce HDR output. Corresponds to [member Window.hdr_output_requested].
+
+
@@ -2324,6 +2390,22 @@
Enables or disables the given window's given [param flag].
+
+
+
+
+
+ Sets the maximum luminance in nits (cd/m²) for HDR output by the window specified by [param window_id]. If [param max_luminance] is negative, the window uses the display's maximum luminance that is reported by the operating system. By default, this luminance is set to [code]-1.0[/code] for every window. Typically this property should be left at this default value, but may optionally be exposed through in-game settings to allow the player to correct an inaccurate maximum luminance reported by the operating system. See also [method window_get_hdr_output_current_max_luminance] and [method window_get_hdr_output_max_luminance].
+
+
+
+
+
+
+
+ Sets the reference white luminance in nits (cd/m²) for HDR output by the window specified by [param window_id]. If [param reference_luminance] is negative, the window automatically adjusts to the brightness set by the operating system. By default, this luminance is set to [code]-1.0[/code] for every window. Typically this property should be left at this default value, but may optionally be exposed as an "HDR Brightness" in-game setting to allow the player to adjust the brightness of their game, independently of their device settings. See also [method window_get_hdr_output_current_reference_luminance] and [method window_get_hdr_output_reference_luminance].
+
+
@@ -2637,6 +2719,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[/b]
+
Unknown or custom role.
diff --git a/doc/classes/Environment.xml b/doc/classes/Environment.xml
index cb77390c8efb..517a3ecebe32 100644
--- a/doc/classes/Environment.xml
+++ b/doc/classes/Environment.xml
@@ -320,7 +320,7 @@
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_agx_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] When using the Mobile renderer with [member Viewport.use_hdr_2d] disabled, [member tonemap_agx_white] is ignored and a white value of [code]2.0[/code] will always be used instead.
+ [b]Note:[/b] When using the Mobile renderer with [member Viewport.use_hdr_2d] disabled, [member tonemap_agx_white] is ignored and a white value of [code]2.0[/code] will always be used instead. Otherwise, [member tonemap_agx_white] will be dynamically adjusted at runtime by multiplying it by the parent window's [method Window.get_output_max_linear_value] to ensure good behavior with HDR output.
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].
@@ -332,6 +332,7 @@
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.
+ [b]Note:[/b] [member tonemap_white] is ignored when using [constant TONE_MAPPER_LINEAR] and will be dynamically adjusted at runtime to never be less than the parent window's [method Window.get_output_max_linear_value] when using [constant TONE_MAPPER_REINHARDT] with [member Viewport.use_hdr_2d].
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.
@@ -432,10 +433,12 @@
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 because it produces output in the SDR range. It is recommended to use a different tonemapper 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 because it produces output in the SDR range. It is recommended to use a different tonemapper when rendering to an HDR display.
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.
@@ -444,10 +447,11 @@
Adds the glow effect to the scene.
- Adds the glow effect to the scene after modifying the glow influence based on the scene value; dark values will be highly influenced by glow and bright values will not be influenced by glow. This approach avoids bright values becoming overly bright from the glow effect. [member tonemap_white] is used to determine the maximum scene value where the glow should have no influence. When [member tonemap_mode] is set to [constant TONE_MAPPER_LINEAR], a value of [code]1.0[/code] will be used as the maximum scene value.
+ Adds the glow effect to the scene after modifying the glow influence based on the scene value; dark values will be highly influenced by glow and bright values will not be influenced by glow. This approach avoids bright values becoming overly bright from the glow effect. [member tonemap_white] is used to determine the maximum scene value where the glow should have no influence. When [member tonemap_mode] is set to [constant TONE_MAPPER_LINEAR], the parent window's [method Window.get_output_max_linear_value] will be used as the maximum scene value.
Adds the glow effect to the tonemapped image after modifying the glow influence based on the image value; dark values and bright values will not be influenced by glow and mid-range values will be highly influenced by glow. This approach avoids bright values becoming overly bright from the glow effect. The glow will have the largest influence on image values of [code]0.25[/code] and will have no influence when applied to image values greater than [code]1.0[/code].
+ [b]Note:[/b] This blend mode does not support HDR output because expects a maximum output value of [code]1.0[/code]. It is recommended to use a different blend mode when rendering to an HDR display.
Replaces all pixels' color by the glow effect. This can be used to simulate a full-screen blur effect by tweaking the glow parameters to match the original image's brightness or to preview glow configuration in the editor.
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index 2bd6df9015db..49f553a68e6e 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -973,6 +973,10 @@
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], HDR output is requested for the main window and the editor. The main window and editor will automatically switch between HDR and SDR if it is moved between displays, display capabilities change, or system settings are modified. This will internally force [member Viewport.use_hdr_2d] to be enabled on the main [Viewport]. All other [SubViewport] of the [Window] must have their [member Viewport.use_hdr_2d] property enabled to produce HDR output.
+ [b]Note:[/b] This property is only read when the project starts. To change this property at runtime, set [member Window.hdr_output_requested].
+
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 66dbd087bf44..3cfb3730d4a8 100644
--- a/doc/classes/RenderingDevice.xml
+++ b/doc/classes/RenderingDevice.xml
@@ -2538,6 +2538,9 @@
Support for 32-bit image atomic operations.
+
+ 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 50dbb3259c4e..52e4656aa34c 100644
--- a/doc/classes/RenderingServer.xml
+++ b/doc/classes/RenderingServer.xml
@@ -5512,10 +5512,12 @@
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 because it produces output in the SDR range. It is recommended to use a different tonemapper 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 because it produces output in the SDR range. It is recommended to use a different tonemapper when rendering to an HDR display.
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.
diff --git a/doc/classes/Window.xml b/doc/classes/Window.xml
index 26943d1917e4..ced700780352 100644
--- a/doc/classes/Window.xml
+++ b/doc/classes/Window.xml
@@ -120,6 +120,41 @@
Returns layout direction and text writing direction.
+
+
+
+ Returns the maximum value for linear color components that can be displayed in this window, regardless of SDR or HDR output. Returns [code]1.0[/code] if HDR is not enabled or not supported. This value is used by tonemapping and glow effects to ensure that bright colors are presented in the range that can be displayed by this window.
+ When using the Linear tonemapper or no tonemapping, use the returned value to scale content to maximize the display's brightness, such as for lasers or other bright effects. The following is an example that produces the brightest purple color that the display can produce:
+ [codeblocks]
+ [gdscript]
+ func _process(_delta: float):
+ # output_max_linear_value may change often, so do this every frame.
+ # max_linear_value is only valid when using no tonemapping or the
+ # Linear tonemapper.
+ var max_linear_value = get_window().get_output_max_linear_value()
+ # Replace this with your color:
+ var original_color = Color.PURPLE
+ # Normalize to max_linear_value to produce the brightest color possible,
+ # regardless of SDR or HDR output:
+ var bright_color = normalize_color(original_color, max_linear_value)
+
+
+ func normalize_color(srgb_color, max_linear_value = 1.0):
+ # Color must be linear-encoded to use math operations.
+ var linear_color = srgb_color.srgb_to_linear()
+ var max_rgb_value = maxf(linear_color.r, maxf(linear_color.g, linear_color.b))
+ var brightness_scale = max_linear_value / max_rgb_value
+ linear_color *= brightness_scale
+ # Undo changes to the alpha channel, which should not be modified.
+ linear_color.a = srgb_color.a
+ # Convert back to nonlinear sRGB encoding, which is required for Color in
+ # Godot unless stated otherwise.
+ return linear_color.linear_to_srgb()
+ [/gdscript]
+ [/codeblocks]
+ [b]Note:[/b] You will need to convert sRGB colors to linear before multiplying by this value to get correct results.
+
+
@@ -341,6 +376,12 @@
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.
+
+
@@ -630,6 +671,9 @@
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. This will internally force [member Viewport.use_hdr_2d] to be enabled on the main [Viewport]. All other [SubViewport] of this [Window] must have their [member Viewport.use_hdr_2d] property enabled to produce HDR output.
+
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 9d9ad8f5e6f2..ea542130d57d 100644
--- a/drivers/d3d12/rendering_context_driver_d3d12.cpp
+++ b/drivers/d3d12/rendering_context_driver_d3d12.cpp
@@ -152,16 +152,24 @@ Error RenderingContextDriverD3D12::_initialize_debug_layers() {
return OK;
}
-Error RenderingContextDriverD3D12::_initialize_devices() {
+Error RenderingContextDriverD3D12::_create_dxgi_factory() {
const UINT dxgi_factory_flags = use_validation_layers() ? DXGI_CREATE_FACTORY_DEBUG : 0;
typedef HRESULT(WINAPI * PFN_DXGI_CREATE_DXGI_FACTORY2)(UINT, REFIID, void **);
PFN_DXGI_CREATE_DXGI_FACTORY2 dxgi_CreateDXGIFactory2 = (PFN_DXGI_CREATE_DXGI_FACTORY2)(void *)GetProcAddress(lib_dxgi, "CreateDXGIFactory2");
ERR_FAIL_NULL_V(dxgi_CreateDXGIFactory2, ERR_CANT_CREATE);
+ dxgi_factory.Reset();
HRESULT res = dxgi_CreateDXGIFactory2(dxgi_factory_flags, IID_PPV_ARGS(&dxgi_factory));
ERR_FAIL_COND_V(!SUCCEEDED(res), ERR_CANT_CREATE);
+ return OK;
+}
+
+Error RenderingContextDriverD3D12::_initialize_devices() {
+ Error err = _create_dxgi_factory();
+ ERR_FAIL_COND_V(err != OK, ERR_CANT_CREATE);
+
// Enumerate all possible adapters.
LocalVector adapters;
IDXGIAdapter1 *adapter = nullptr;
@@ -203,7 +211,7 @@ Error RenderingContextDriverD3D12::_initialize_devices() {
if (factory_5 != nullptr) {
// The type is important as in general, sizeof(bool) != sizeof(BOOL).
BOOL feature_supported = FALSE;
- res = factory_5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &feature_supported, sizeof(feature_supported));
+ HRESULT res = factory_5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &feature_supported, sizeof(feature_supported));
if (SUCCEEDED(res)) {
tearing_supported = feature_supported;
} else {
@@ -280,6 +288,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;
@@ -337,6 +391,18 @@ ID3D12DeviceFactory *RenderingContextDriverD3D12::device_factory_get() const {
}
IDXGIFactory2 *RenderingContextDriverD3D12::dxgi_factory_get() const {
+ if (dxgi_factory == nullptr) {
+ return nullptr;
+ }
+
+ // Check if the factory is still current (not stale due to OS-level changes like Advanced Color toggle).
+ if (!dxgi_factory->IsCurrent()) {
+ Error err = const_cast(this)->_create_dxgi_factory();
+ if (err != OK) {
+ return nullptr;
+ }
+ }
+
return dxgi_factory.Get();
}
diff --git a/drivers/d3d12/rendering_context_driver_d3d12.h b/drivers/d3d12/rendering_context_driver_d3d12.h
index cd856e43f4cd..f07acad2b366 100644
--- a/drivers/d3d12/rendering_context_driver_d3d12.h
+++ b/drivers/d3d12/rendering_context_driver_d3d12.h
@@ -81,6 +81,7 @@ class RenderingContextDriverD3D12 : public RenderingContextDriver {
Error _init_device_factory();
Error _initialize_debug_layers();
Error _initialize_devices();
+ Error _create_dxgi_factory();
public:
virtual Error initialize() override;
@@ -93,6 +94,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;
@@ -112,6 +122,15 @@ class RenderingContextDriverD3D12 : public RenderingContextDriver {
uint32_t height = 0;
DisplayServer::VSyncMode vsync_mode = DisplayServer::VSYNC_ENABLED;
bool needs_resize = false;
+
+ bool hdr_output = false;
+ // BT.2408 recommendation of 203 nits for HDR Reference White, rounded to 200
+ // to be a more pleasant player-facing value. This value is used by Steam
+ // Deck and other Windows emulation that does not provide an SDRWhiteLevel.
+ float hdr_reference_luminance = 200.0f;
+ float hdr_max_luminance = 1000.0f;
+ float hdr_linear_luminance_scale = 80.0f;
+
#ifdef DCOMP_ENABLED
Microsoft::WRL::ComPtr composition_device;
Microsoft::WRL::ComPtr composition_target;
diff --git a/drivers/d3d12/rendering_device_driver_d3d12.cpp b/drivers/d3d12/rendering_device_driver_d3d12.cpp
index 6587bce26b53..b0ab82916f25 100644
--- a/drivers/d3d12/rendering_device_driver_d3d12.cpp
+++ b/drivers/d3d12/rendering_device_driver_d3d12.cpp
@@ -351,6 +351,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,
+};
+
static D3D12_CPU_DESCRIPTOR_HANDLE get_cpu_handle(D3D12_CPU_DESCRIPTOR_HANDLE p_handle, uint64_t p_index, uint32_t p_increment_size) {
p_handle.ptr += p_index * p_increment_size;
return p_handle;
@@ -2688,6 +2693,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();
}
@@ -2706,10 +2716,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;
@@ -2720,14 +2730,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);
}
@@ -2769,11 +2795,17 @@ 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;
+
#ifdef DCOMP_ENABLED
bool create_for_composition = OS::get_singleton()->is_layered_allowed();
#else
@@ -2789,6 +2821,10 @@ Error RenderingDeviceDriverD3D12::swap_chain_resize(CommandQueueID p_cmd_queue,
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;
@@ -2830,6 +2866,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 (create_for_composition) {
if (surface->composition_device.Get() == nullptr) {
@@ -2943,10 +2986,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);
}
@@ -5776,6 +5823,8 @@ bool RenderingDeviceDriverD3D12::has_feature(Features p_feature) {
return false;
case SUPPORTS_POINT_SIZE:
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 d88ebb7f41c7..40c0bea14a05 100644
--- a/drivers/d3d12/rendering_device_driver_d3d12.h
+++ b/drivers/d3d12/rendering_device_driver_d3d12.h
@@ -93,6 +93,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;
@@ -534,10 +535,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;
@@ -545,6 +549,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 04e07531dec3..493edfa48280 100644
--- a/drivers/gles3/rasterizer_scene_gles3.cpp
+++ b/drivers/gles3/rasterizer_scene_gles3.cpp
@@ -2251,7 +2251,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");
@@ -2387,7 +2387,7 @@ void RasterizerSceneGLES3::render_scene(const Ref &p_render_
tonemap_ubo.exposure = environment_get_exposure(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);
+ RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(render_data.environment, false, 1.0f);
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];
@@ -2856,7 +2856,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, false);
+ srgb_white = environment_get_white(p_render_data->environment, false, 1.0f);
}
if (glow_enabled) {
diff --git a/drivers/gles3/rasterizer_scene_gles3.h b/drivers/gles3/rasterizer_scene_gles3.h
index 3ca8fcf346bd..fdaf7f89f375 100644
--- a/drivers/gles3/rasterizer_scene_gles3.h
+++ b/drivers/gles3/rasterizer_scene_gles3.h
@@ -921,7 +921,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/metal/rendering_context_driver_metal.h b/drivers/metal/rendering_context_driver_metal.h
index 8e313d81f188..a2d47f2f96c6 100644
--- a/drivers/metal/rendering_context_driver_metal.h
+++ b/drivers/metal/rendering_context_driver_metal.h
@@ -84,6 +84,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;
@@ -109,6 +118,13 @@ 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;
+ // BT.2408 recommendation of 203 nits for HDR Reference White, rounded to 200
+ // to be a more pleasant player-facing value.
+ float hdr_reference_luminance = 200.0f;
+ float hdr_max_luminance = 1000.0f;
+ float hdr_linear_luminance_scale = 100.0f;
+
Surface(METAL_DEVICE p_device) :
device(p_device) {}
virtual ~Surface() = default;
diff --git a/drivers/metal/rendering_context_driver_metal.mm b/drivers/metal/rendering_context_driver_metal.mm
index 51b752f2a041..ea91bedb1d54 100644
--- a/drivers/metal/rendering_context_driver_metal.mm
+++ b/drivers/metal/rendering_context_driver_metal.mm
@@ -337,6 +337,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 c5ca6e9cb6e6..1380d599b38f 100644
--- a/drivers/metal/rendering_device_driver_metal.h
+++ b/drivers/metal/rendering_device_driver_metal.h
@@ -301,6 +301,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 2b5dff7434f1..4b54a6a9f41f 100644
--- a/drivers/metal/rendering_device_driver_metal.mm
+++ b/drivers/metal/rendering_device_driver_metal.mm
@@ -1058,6 +1058,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..9db37b330522 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,10 @@ 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);
+}
+
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..562c858e7fe4 100644
--- a/drivers/vulkan/rendering_context_driver_vulkan.h
+++ b/drivers/vulkan/rendering_context_driver_vulkan.h
@@ -138,12 +138,22 @@ 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;
// Vulkan-only methods.
struct Surface {
@@ -152,6 +162,13 @@ class RenderingContextDriverVulkan : public RenderingContextDriver {
uint32_t height = 0;
DisplayServer::VSyncMode vsync_mode = DisplayServer::VSYNC_ENABLED;
bool needs_resize = false;
+
+ bool hdr_output = false;
+ // BT.2408 recommendation of 203 nits for HDR Reference White, rounded to 200
+ // to be a more pleasant player-facing value.
+ float hdr_reference_luminance = 200.0f;
+ float hdr_max_luminance = 1000.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 c30e9c6b719a..dc972f35afda 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"
@@ -3219,6 +3220,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) {
@@ -3247,6 +3322,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);
}
@@ -3264,84 +3344,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);
}
@@ -3475,6 +3480,16 @@ 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;
+ }
+
VkSwapchainCreateInfoKHR swap_create_info = {};
swap_create_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
swap_create_info.surface = surface->vk_surface;
@@ -3573,10 +3588,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;
@@ -3700,12 +3753,33 @@ 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);
+ switch (swap_chain->color_space) {
+ case VK_COLOR_SPACE_SRGB_NONLINEAR_KHR:
+ return COLOR_SPACE_REC709_NONLINEAR_SRGB;
+ case VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT:
+ return COLOR_SPACE_REC709_LINEAR;
+ default:
+ DEV_ASSERT(false && "Unknown swap chain color space.");
+ return COLOR_SPACE_MAX;
+ }
+}
+
void RenderingDeviceDriverVulkan::swap_chain_set_max_fps(SwapChainID p_swap_chain, int p_max_fps) {
DEV_ASSERT(p_swap_chain.id != 0);
@@ -3728,10 +3802,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);
}
@@ -6510,6 +6580,15 @@ bool RenderingDeviceDriverVulkan::has_feature(Features p_feature) {
return framebuffer_depth_resolve;
case SUPPORTS_POINT_SIZE:
return true;
+ case SUPPORTS_HDR_OUTPUT:
+#if defined(WINDOWS_ENABLED)
+ // When using a Vulkan swapchain on Windows, some configurations
+ // involving integrated GPU hardware do not function correctly
+ // with HDR output.
+ return false;
+#else
+ return context_driver->is_colorspace_supported();
+#endif // defined(WINDOWS_ENABLED)
default:
return false;
}
diff --git a/drivers/vulkan/rendering_device_driver_vulkan.h b/drivers/vulkan/rendering_device_driver_vulkan.h
index c53ac31b5b6d..c03ca59fb321 100644
--- a/drivers/vulkan/rendering_device_driver_vulkan.h
+++ b/drivers/vulkan/rendering_device_driver_vulkan.h
@@ -407,6 +407,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:
@@ -416,6 +417,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 4b048b7d59be..0b59cfab1736 100644
--- a/editor/editor_node.cpp
+++ b/editor/editor_node.cpp
@@ -526,9 +526,17 @@ void EditorNode::_update_from_settings() {
scene_root->set_use_debanding(use_debanding);
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);
+ // Enable HDR if requested
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ DisplayServer::get_singleton()->window_request_hdr_output(hdr_requested);
+
+ const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d");
+ scene_root->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+ get_viewport()->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+
+ if (hdr_requested && !use_hdr_2d) {
+ WARN_PRINT_ED("HDR 2D was automatically enabled because HDR output was requested in project settings. 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/import/3d/scene_import_settings.cpp b/editor/import/3d/scene_import_settings.cpp
index 1e3f95300ee1..cd90d0e2f4f7 100644
--- a/editor/import/3d/scene_import_settings.cpp
+++ b/editor/import/3d/scene_import_settings.cpp
@@ -1993,6 +1993,15 @@ SceneImportSettingsDialog::SceneImportSettingsDialog() {
update_view_timer->set_one_shot(true);
update_view_timer->connect("timeout", callable_mp(this, &SceneImportSettingsDialog::_update_view_gizmos));
add_child(update_view_timer);
+
+ ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &SceneImportSettingsDialog::_project_settings_changed));
+ _project_settings_changed();
+}
+
+void SceneImportSettingsDialog::_project_settings_changed() {
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ const bool hdr_2d_enabled = GLOBAL_GET("rendering/viewport/hdr_2d");
+ base_viewport->set_use_hdr_2d(hdr_2d_enabled || hdr_requested);
}
SceneImportSettingsDialog::~SceneImportSettingsDialog() {
diff --git a/editor/import/3d/scene_import_settings.h b/editor/import/3d/scene_import_settings.h
index 6aae7a994012..70f2e0bab1a4 100644
--- a/editor/import/3d/scene_import_settings.h
+++ b/editor/import/3d/scene_import_settings.h
@@ -207,6 +207,7 @@ class SceneImportSettingsDialog : public ConfirmationDialog {
ResourceImporterScene *_resource_importer_scene = nullptr;
void _re_import();
+ void _project_settings_changed();
String base_path;
diff --git a/editor/scene/3d/camera_3d_editor_plugin.cpp b/editor/scene/3d/camera_3d_editor_plugin.cpp
index 4dc3cdfec285..6b7f70dcd815 100644
--- a/editor/scene/3d/camera_3d_editor_plugin.cpp
+++ b/editor/scene/3d/camera_3d_editor_plugin.cpp
@@ -93,8 +93,16 @@ Camera3DPreview::Camera3DPreview(Camera3D *p_camera) :
sub_viewport->connect("size_changed", callable_mp((CanvasItem *)display, &CanvasItem::queue_redraw));
sub_viewport->get_texture()->connect_changed(callable_mp((TexturePreview *)this, &Camera3DPreview::_update_texture_display_ratio));
- ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &Camera3DPreview::_update_sub_viewport_size));
+ ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &Camera3DPreview::_project_settings_changed));
+ _project_settings_changed();
+}
+
+void Camera3DPreview::_project_settings_changed() {
_update_sub_viewport_size();
+
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d");
+ sub_viewport->set_use_hdr_2d(use_hdr_2d || hdr_requested);
}
bool EditorInspectorPluginCamera3DPreview::can_handle(Object *p_object) {
diff --git a/editor/scene/3d/camera_3d_editor_plugin.h b/editor/scene/3d/camera_3d_editor_plugin.h
index a0eab38467f9..aac4ec9321a4 100644
--- a/editor/scene/3d/camera_3d_editor_plugin.h
+++ b/editor/scene/3d/camera_3d_editor_plugin.h
@@ -59,6 +59,7 @@ class Camera3DPreview : public TexturePreview {
SubViewport *sub_viewport = nullptr;
void _update_sub_viewport_size();
+ void _project_settings_changed();
public:
Camera3DPreview(Camera3D *p_camera);
diff --git a/editor/scene/3d/mesh_editor_plugin.cpp b/editor/scene/3d/mesh_editor_plugin.cpp
index 92b7f67d1f7f..4cfe6960333d 100644
--- a/editor/scene/3d/mesh_editor_plugin.cpp
+++ b/editor/scene/3d/mesh_editor_plugin.cpp
@@ -71,6 +71,12 @@ void MeshEditor::_update_rotation() {
rotation->set_transform(t);
}
+void MeshEditor::_project_settings_changed() {
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d");
+ viewport->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+}
+
void MeshEditor::edit(Ref p_mesh) {
mesh = p_mesh;
mesh_instance->set_mesh(mesh);
@@ -163,6 +169,9 @@ MeshEditor::MeshEditor() {
rot_x = 0;
rot_y = 0;
+
+ ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &MeshEditor::_project_settings_changed));
+ _project_settings_changed();
}
///////////////////////
diff --git a/editor/scene/3d/mesh_editor_plugin.h b/editor/scene/3d/mesh_editor_plugin.h
index 2091f74a881d..30ba10762a27 100644
--- a/editor/scene/3d/mesh_editor_plugin.h
+++ b/editor/scene/3d/mesh_editor_plugin.h
@@ -68,6 +68,7 @@ class MeshEditor : public SubViewportContainer {
void _on_light_1_switch_pressed();
void _on_light_2_switch_pressed();
void _update_rotation();
+ void _project_settings_changed();
protected:
virtual void _update_theme_item_cache() override;
diff --git a/editor/scene/3d/node_3d_editor_plugin.cpp b/editor/scene/3d/node_3d_editor_plugin.cpp
index 512c0f54dc4b..415116558eaa 100644
--- a/editor/scene/3d/node_3d_editor_plugin.cpp
+++ b/editor/scene/3d/node_3d_editor_plugin.cpp
@@ -3172,8 +3172,9 @@ void Node3DEditorViewport::_project_settings_changed() {
const bool transparent_background = GLOBAL_GET("rendering/viewport/transparent_background");
viewport->set_transparent_background(transparent_background);
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
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_requested);
const bool use_debanding = GLOBAL_GET("rendering/anti_aliasing/quality/use_debanding");
viewport->set_use_debanding(use_debanding);
@@ -9505,7 +9506,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/editor/scene/material_editor_plugin.cpp b/editor/scene/material_editor_plugin.cpp
index a5554ebe295a..8de2ab2d3995 100644
--- a/editor/scene/material_editor_plugin.cpp
+++ b/editor/scene/material_editor_plugin.cpp
@@ -152,6 +152,13 @@ void MaterialEditor::_update_rotation() {
rotation->set_transform(t);
}
+void MaterialEditor::_project_settings_changed() {
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d");
+ viewport->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+ viewport_2d->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+}
+
void MaterialEditor::edit(Ref p_material, const Ref &p_env) {
material = p_material;
camera->set_environment(p_env);
@@ -396,6 +403,9 @@ MaterialEditor::MaterialEditor() {
Vector2 stored_rot = EditorSettings::get_singleton()->get_project_metadata("inspector_options", "material_preview_rotation", Vector2());
_set_rotation(stored_rot.x, stored_rot.y);
+
+ ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &MaterialEditor::_project_settings_changed));
+ _project_settings_changed();
}
///////////////////////
diff --git a/editor/scene/material_editor_plugin.h b/editor/scene/material_editor_plugin.h
index f79f748c3f4e..649c86189523 100644
--- a/editor/scene/material_editor_plugin.h
+++ b/editor/scene/material_editor_plugin.h
@@ -102,6 +102,7 @@ class MaterialEditor : public Control {
void _set_rotation(real_t p_x_degrees, real_t p_y_degrees);
void _store_rotation_metadata();
void _update_rotation();
+ void _project_settings_changed();
protected:
virtual void _update_theme_item_cache() override;
diff --git a/platform/windows/display_server_windows.cpp b/platform/windows/display_server_windows.cpp
index 6d69e658be5e..99d69bb4d956 100644
--- a/platform/windows/display_server_windows.cpp
+++ b/platform/windows/display_server_windows.cpp
@@ -56,6 +56,7 @@
#endif
#if defined(D3D12_ENABLED)
#include "drivers/d3d12/rendering_context_driver_d3d12.h"
+#include
#endif
#if defined(GLES3_ENABLED)
#include "drivers/gles3/rasterizer_gles3.h"
@@ -149,6 +150,7 @@ bool DisplayServerWindows::has_feature(Feature p_feature) const {
case FEATURE_STATUS_INDICATOR:
case FEATURE_WINDOW_EMBEDDING:
case FEATURE_WINDOW_DRAG:
+ case FEATURE_HDR_OUTPUT:
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.
@@ -1191,6 +1193,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_
@@ -1280,6 +1300,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) {
@@ -1564,6 +1592,116 @@ Ref DisplayServerWindows::screen_get_image_rect(const Rect2i &p_rect) con
return img;
}
+#ifdef D3D12_ENABLED
+static bool _get_monitor_desc(HMONITOR p_monitor, RenderingContextDriver *rendering_context, DXGI_OUTPUT_DESC1 &r_Desc) {
+ r_Desc = {};
+
+ RenderingContextDriverD3D12 *d3d12_context = dynamic_cast(rendering_context);
+ if (!d3d12_context) {
+ return false;
+ }
+
+ Microsoft::WRL::ComPtr dxgi_factory = d3d12_context->dxgi_factory_get();
+ if (!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.
+ Microsoft::WRL::ComPtr dxgiAdapter;
+ UINT adapter_i = 0;
+ while (dxgi_factory->EnumAdapters1(adapter_i, &dxgiAdapter) == S_OK) {
+ Microsoft::WRL::ComPtr dxgiOutput;
+ DXGI_OUTPUT_DESC1 desc1;
+ UINT output_i = 0;
+ while (dxgiAdapter->EnumOutputs(output_i, &dxgiOutput) != DXGI_ERROR_NOT_FOUND) {
+ Microsoft::WRL::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
+
+// Store a list of displays that have failed to get their SDR white level so we don't spam the error log.
+static HashSet displays_with_white_level_error;
+
+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) {
+ const Vector3i key = Vector3i((int32_t)path.sourceInfo.adapterId.HighPart, (int32_t)path.sourceInfo.adapterId.LowPart, (int32_t)path.sourceInfo.id);
+
+ 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) {
+ if (!displays_with_white_level_error.has(key)) {
+ 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));
+ displays_with_white_level_error.insert(key);
+ }
+ 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) {
+ if (!displays_with_white_level_error.has(key)) {
+ 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));
+ displays_with_white_level_error.insert(key);
+ }
+ 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_
@@ -3195,6 +3333,104 @@ 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, rendering_context, 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);
+ }
+ }
+}
+
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_
@@ -3880,6 +4116,8 @@ void DisplayServerWindows::process_events() {
Input::get_singleton()->flush_buffered_events();
}
+ _update_hdr_output_for_tracked_windows();
+
LocalVector::Element *> to_remove;
for (List::Element *E = file_dialogs.front(); E; E = E->next()) {
FileDialogData *fd = E->get();
@@ -4339,6 +4577,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_EDMSG(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_linear_value(WindowID p_window) const {
+ _THREAD_SAFE_METHOD_
+
+#if defined(RD_ENABLED)
+ if (rendering_context) {
+ return rendering_context->window_get_output_max_linear_value(p_window);
+ }
+#endif
+
+ return 1.0f; // SDR
+}
+
void DisplayServerWindows::window_start_drag(WindowID p_window) {
_THREAD_SAFE_METHOD_
@@ -5817,6 +6206,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];
@@ -5912,6 +6305,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 11885e6bd05d..7ae341d1fb8b 100644
--- a/platform/windows/display_server_windows.h
+++ b/platform/windows/display_server_windows.h
@@ -379,6 +379,11 @@ class DisplayServerWindows : public DisplayServer {
bool is_popup = false;
Rect2i parent_safe_rect;
+ // HDR
+ bool hdr_output_requested = false;
+ float hdr_output_reference_luminance = -1.0f;
+ float hdr_output_max_luminance = -1.0f;
+
bool initialized = false;
HWND parent_hwnd = 0;
@@ -539,6 +544,18 @@ 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();
+
public:
LRESULT WndProcFileDialog(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
LRESULT WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
@@ -684,6 +701,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_linear_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/scene_tree.cpp b/scene/main/scene_tree.cpp
index 78e2181da18e..eaff13d80fd7 100644
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -2085,8 +2085,16 @@ SceneTree::SceneTree() {
const bool transparent_background = GLOBAL_DEF("rendering/viewport/transparent_background", false);
root->set_transparent_background(transparent_background);
+ // Enable HDR if requested
+ const bool hdr_requested = GLOBAL_GET("display/window/hdr/request_hdr_output");
+ DisplayServer::get_singleton()->window_request_hdr_output(hdr_requested);
+
const bool use_hdr_2d = GLOBAL_GET("rendering/viewport/hdr_2d");
- root->set_use_hdr_2d(use_hdr_2d);
+ root->set_use_hdr_2d(use_hdr_2d || hdr_requested);
+
+ if (hdr_requested && !use_hdr_2d) {
+ WARN_PRINT_ED("HDR 2D was automatically enabled because HDR output was requested in project settings. To avoid this warning, enable rendering/viewport/hdr_2d in the Project Settings.");
+ }
const int ssaa_mode = GLOBAL_DEF_BASIC(PropertyInfo(Variant::INT, "rendering/anti_aliasing/quality/screen_space_aa", PROPERTY_HINT_ENUM, "Disabled (Fastest),FXAA (Fast),SMAA (Average)"), 0);
root->set_screen_space_aa(Viewport::ScreenSpaceAA(ssaa_mode));
diff --git a/scene/main/window.cpp b/scene/main/window.cpp
index fb35ac507818..8e13ce7ce77d 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_linear_value() const {
+ ERR_READ_THREAD_GUARD_V(1.0f);
+
+ if (window_id != DisplayServer::INVALID_WINDOW_ID) {
+ return DisplayServer::get_singleton()->window_get_output_max_linear_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) {
@@ -672,6 +714,7 @@ void Window::_make_window() {
DisplayServer::get_singleton()->window_set_mouse_passthrough(mpath, window_id);
DisplayServer::get_singleton()->window_set_title(displayed_title, window_id);
DisplayServer::get_singleton()->window_attach_instance_id(get_instance_id(), window_id);
+ DisplayServer::get_singleton()->window_request_hdr_output(hdr_output_requested, window_id);
_update_window_size();
@@ -965,6 +1008,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;
@@ -1552,6 +1602,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();
@@ -1828,6 +1883,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;
@@ -3233,6 +3298,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_linear_value"), &Window::get_output_max_linear_value);
+
ClassDB::bind_method(D_METHOD("is_maximize_allowed"), &Window::is_maximize_allowed);
ClassDB::bind_method(D_METHOD("request_attention"), &Window::request_attention);
@@ -3431,6 +3501,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 1e01d372927c..5da5d187038a 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;
@@ -335,6 +339,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_linear_value() const;
+
bool is_maximize_allowed() const;
void request_attention();
diff --git a/servers/display/display_server.cpp b/servers/display/display_server.cpp
index cfafce58c6e3..0fa2f7c7be28 100644
--- a/servers/display/display_server.cpp
+++ b/servers/display/display_server.cpp
@@ -1267,6 +1267,52 @@ 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) {
+ if (p_enable) {
+ WARN_PRINT_ED("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_ED("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_ED("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_linear_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 +1515,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_linear_value", "window_id"), &DisplayServer::window_get_output_max_linear_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 +1721,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_OUTPUT);
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 493288067275..ea93dfdb2bd7 100644
--- a/servers/display/display_server.h
+++ b/servers/display/display_server.h
@@ -169,6 +169,7 @@ class DisplayServer : public Object {
FEATURE_NATIVE_COLOR_PICKER,
FEATURE_SELF_FITTING_WINDOWS,
FEATURE_ACCESSIBILITY_SCREEN_READER,
+ FEATURE_HDR_OUTPUT,
};
virtual bool has_feature(Feature p_feature) const = 0;
@@ -504,6 +505,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_linear_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 0f18f92600e2..d7def8912037 100644
--- a/servers/rendering/renderer_rd/effects/tone_mapper.cpp
+++ b/servers/rendering/renderer_rd/effects/tone_mapper.cpp
@@ -156,6 +156,7 @@ void ToneMapper::tonemapper(RID p_source_color, RID p_dst_framebuffer, const Ton
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;
@@ -240,6 +241,7 @@ void ToneMapper::tonemapper_mobile(RID p_source_color, RID p_dst_framebuffer, co
tonemap_mobile.push_constant.exposure = p_settings.exposure;
tonemap_mobile.push_constant.white = p_settings.white;
tonemap_mobile.push_constant.luminance_multiplier = p_settings.luminance_multiplier;
+ tonemap_mobile.push_constant.output_max_value = MAX(p_settings.max_value, 1.0f);
tonemap_mobile.push_constant.tonemapper_params[0] = p_settings.tonemapper_params[0];
tonemap_mobile.push_constant.tonemapper_params[1] = p_settings.tonemapper_params[1];
diff --git a/servers/rendering/renderer_rd/effects/tone_mapper.h b/servers/rendering/renderer_rd/effects/tone_mapper.h
index 9301be027aa2..d24a0f953478 100644
--- a/servers/rendering/renderer_rd/effects/tone_mapper.h
+++ b/servers/rendering/renderer_rd/effects/tone_mapper.h
@@ -109,7 +109,7 @@ class ToneMapper {
float pixel_size[2]; // 8 - 24
uint32_t tonemapper; // 4 - 28
- uint32_t pad; // 4 - 32
+ float output_max_value; // 4 - 32
uint32_t glow_texture_size[2]; // 8 - 40
float glow_intensity; // 4 - 44
@@ -139,6 +139,8 @@ class ToneMapper {
float white; // 4 - 48
float tonemapper_params[4]; // 16 - 64
+ float output_max_value; // 4 - 68
+ float pad[3]; // 12 - 80
};
/* tonemap actually writes to a framebuffer, which is
@@ -178,6 +180,7 @@ class ToneMapper {
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 517d83372980..e5323fabe601 100644
--- a/servers/rendering/renderer_rd/renderer_compositor_rd.cpp
+++ b/servers/rendering/renderer_rd/renderer_compositor_rd.cpp
@@ -43,9 +43,17 @@ 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 output_max_value = RD::get_singleton()->get_context_driver()->window_get_output_max_linear_value(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 +74,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 +104,11 @@ 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;
+ blit.push_constant.output_max_value = output_max_value;
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 +143,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 +184,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 +218,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,11 +248,25 @@ void RendererCompositorRD::set_boot_image_with_stretch(const Ref &p_image
screenrect.position /= window_size;
screenrect.size /= window_size;
- // p_color never needs to be converted to linear encoding because HDR 2D is always disabled for the boot image.
- // If HDR 2D can ever be enabled during the boot image, p_color must be converted to linear encoding for this case.
- RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin_for_screen(DisplayServer::MAIN_WINDOW_ID, p_color);
+ 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 output_max_value = RD::get_singleton()->get_context_driver()->window_get_output_max_linear_value(DisplayServer::MAIN_WINDOW_ID);
+ const float reference_multiplier = _compute_reference_multiplier(color_space, reference_luminance, linear_luminance_scale);
+
+ Color clear_color = p_color;
+ if (color_space != RD::COLOR_SPACE_REC709_NONLINEAR_SRGB) {
+ // draw_list_begin_for_screen requires linear-encoded Color when using an HDR buffer.
+ clear_color = p_color.srgb_to_linear();
+
+ clear_color.r *= reference_multiplier;
+ clear_color.g *= reference_multiplier;
+ clear_color.b *= reference_multiplier;
+ }
+
+ RD::DrawListID draw_list = RD::get_singleton()->draw_list_begin_for_screen(DisplayServer::MAIN_WINDOW_ID, clear_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);
@@ -246,8 +289,11 @@ 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;
+ blit.push_constant.output_max_value = output_max_value;
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..386e56fb8b6c 100644
--- a/servers/rendering/renderer_rd/renderer_compositor_rd.h
+++ b/servers/rendering/renderer_rd/renderer_compositor_rd.h
@@ -68,29 +68,37 @@ 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
+ float output_max_value; // 4 - 88
+ uint32_t pad[2]; // 8 - 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 +112,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 72e4b83453a5..fc3c52bd84ac 100644
--- a/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
+++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.cpp
@@ -728,14 +728,20 @@ void RendererSceneRenderRD::_render_buffers_post_process_and_tonemap(const Rende
// to a high white value.
bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32;
+ // 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.
+ float max_value = using_hdr ? p_render_data->window_output_max_value : 1.0;
+
tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment);
- RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white);
+ RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white, 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.white = environment_get_white(p_render_data->environment, limit_agx_white);
+ tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white, max_value);
tonemap.exposure = environment_get_exposure(p_render_data->environment);
+ tonemap.max_value = max_value;
}
tonemap.use_color_correction = false;
@@ -892,6 +898,8 @@ 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
@@ -899,14 +907,20 @@ void RendererSceneRenderRD::_post_process_subpass(RID p_source_texture, RID p_fr
// to a high white value.
bool limit_agx_white = rb->get_base_data_format() == RD::DATA_FORMAT_A2B10G10R10_UNORM_PACK32;
+ // 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.
+ float max_value = using_hdr ? p_render_data->window_output_max_value : 1.0;
+
tonemap.tonemap_mode = environment_get_tone_mapper(p_render_data->environment);
- RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white);
+ RendererEnvironmentStorage::TonemapParameters params = environment_get_tonemap_parameters(p_render_data->environment, limit_agx_white, 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.exposure = environment_get_exposure(p_render_data->environment);
- tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white);
+ tonemap.white = environment_get_white(p_render_data->environment, limit_agx_white, max_value);
+ tonemap.max_value = max_value;
}
// We don't support glow or auto exposure here, if they are needed, don't use subpasses!
@@ -920,8 +934,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);
@@ -1317,7 +1329,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();
@@ -1429,6 +1441,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 266323e2c04e..e84e2869fed7 100644
--- a/servers/rendering/renderer_rd/renderer_scene_render_rd.h
+++ b/servers/rendering/renderer_rd/renderer_scene_render_rd.h
@@ -245,7 +245,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..a04b4b84d1a9 100644
--- a/servers/rendering/renderer_rd/shaders/blit.glsl
+++ b/servers/rendering/renderer_rd/shaders/blit.glsl
@@ -4,23 +4,27 @@
#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;
+ float output_max_value;
+ uint pad[2];
}
data;
@@ -45,23 +49,27 @@ 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;
+ float output_max_value;
+ uint pad[2];
}
data;
@@ -75,6 +83,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 +149,41 @@ 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) {
+ 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));
+ // Negative values may be interpreted as scRGB colors,
+ // so clip them to the intended Rec. 709 colors.
+ // Additionally, it is important that the game developer can trust that
+ // Window.output_max_linear_value is truly the max output value, even if
+ // the max luminance has been misconfigured by the player. This ensures that
+ // the resulting image will always be as the game developer expects when they
+ // use Window.output_max_linear_value and tonemapping functions will behave
+ // as expected.
+ color.rgb = clamp(color.rgb, vec3(0.0), vec3(data.output_max_value));
+
+ // 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 and values above 1.0 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/smaa_blending.glsl b/servers/rendering/renderer_rd/shaders/effects/smaa_blending.glsl
index 9d9765f391a7..525f455acf29 100644
--- a/servers/rendering/renderer_rd/shaders/effects/smaa_blending.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/smaa_blending.glsl
@@ -75,8 +75,6 @@ params;
#define textureLinear(tex, uv) srgb_to_linear(textureLod(tex, uv, 0.0).rgb)
vec3 linear_to_srgb(vec3 color) {
- // If going to srgb, clamp from 0 to 1.
- color = clamp(color, vec3(0.0), vec3(1.0));
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)));
}
diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
index 85690090f161..0d6659251056 100644
--- a/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/tonemap.glsl
@@ -69,7 +69,7 @@ layout(push_constant, std430) uniform Params {
vec2 pixel_size;
uint tonemapper;
- uint pad;
+ float output_max_value;
uvec2 glow_texture_size;
float glow_intensity;
@@ -92,9 +92,8 @@ layout(location = 0) out vec4 frag_color;
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color) {
float white_squared = params.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);
+ // 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) {
@@ -148,14 +147,12 @@ vec3 tonemap_aces(vec3 color) {
// 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 an 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 params.tonemap_e.
- const float awp_shoulder_max = output_max_value - awp_crossover_point;
+ 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;
@@ -210,8 +207,6 @@ vec3 tonemap_agx(vec3 color) {
-0.855988495690215, 1.32639796461980, -0.238183969428088,
-0.108898916004672, -0.0270845997150571, 1.40253671195648);
- const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
-
// Apply inset matrix.
color = rec709_to_rec2020_agx_inset_matrix * color;
@@ -221,7 +216,7 @@ vec3 tonemap_agx(vec3 color) {
// 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);
+ color = min(vec3(params.output_max_value), color);
// Apply outset to make the result more chroma-laden and then go back to Rec. 709.
color = agx_outset_rec2020_to_rec709_matrix * color;
diff --git a/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl b/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl
index 369cd25de65f..b3f3d944d852 100644
--- a/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl
+++ b/servers/rendering/renderer_rd/shaders/effects/tonemap_mobile.glsl
@@ -92,6 +92,9 @@ layout(push_constant, std430) uniform Params {
float white;
vec4 tonemapper_params;
+
+ float output_max_value;
+ float pad[3];
}
params;
@@ -100,9 +103,8 @@ layout(location = 0) out vec4 frag_color;
// Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
vec3 tonemap_reinhard(vec3 color) {
float white_squared = params.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);
+ // 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) {
@@ -156,14 +158,12 @@ vec3 tonemap_aces(vec3 color) {
// 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 an 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 params.tonemap_e.
- const float awp_shoulder_max = output_max_value - awp_crossover_point;
+ 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;
@@ -218,8 +218,6 @@ vec3 tonemap_agx(vec3 color) {
-0.855988495690215, 1.32639796461980, -0.238183969428088,
-0.108898916004672, -0.0270845997150571, 1.40253671195648);
- const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0
-
// Apply inset matrix.
color = rec709_to_rec2020_agx_inset_matrix * color;
@@ -229,7 +227,7 @@ vec3 tonemap_agx(vec3 color) {
// 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);
+ color = min(vec3(params.output_max_value), color);
// Apply outset to make the result more chroma-laden and then go back to Rec. 709.
color = agx_outset_rec2020_to_rec709_matrix * color;
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 5fadfeb52500..7a82c15a6b58 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 8bcddc1b3a7c..6eb846d45b4e 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
}
diff --git a/servers/rendering/renderer_scene_cull.h b/servers/rendering/renderer_scene_cull.h
index 00104d9d4e7b..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();
@@ -1243,7 +1243,7 @@ class RendererSceneCull : public RenderingMethod {
PASS2(environment_set_tonemap_agx_contrast, RID, float)
PASS1RC(RS::EnvironmentToneMapper, environment_get_tone_mapper, RID)
PASS1RC(float, environment_get_exposure, RID)
- PASS2RC(float, environment_get_white, RID, bool)
+ 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 997824eab633..7909d07e9146 100644
--- a/servers/rendering/renderer_scene_render.cpp
+++ b/servers/rendering/renderer_scene_render.cpp
@@ -373,8 +373,8 @@ 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, bool p_limit_agx_white) const {
- return environment_storage.environment_get_white(p_env, p_limit_agx_white);
+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) {
@@ -385,8 +385,8 @@ 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) const {
- return environment_storage.environment_get_tonemap_parameters(p_env, p_limit_agx_white);
+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 34285f9c2cea..12376deef222 100644
--- a/servers/rendering/renderer_scene_render.h
+++ b/servers/rendering/renderer_scene_render.h
@@ -137,10 +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, bool p_limit_agx_white) 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) 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);
@@ -319,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 b965ec6181a1..563d41b59d8e 100644
--- a/servers/rendering/renderer_viewport.cpp
+++ b/servers/rendering/renderer_viewport.cpp
@@ -315,7 +315,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
@@ -357,6 +357,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_linear_value(parent_window);
+ }
+ }
}
bool can_draw_3d = RSG::scene->is_camera(p_viewport->camera) && !p_viewport->disable_3d;
@@ -652,7 +661,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);
}
@@ -695,7 +704,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);
}
@@ -709,7 +718,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);
}
@@ -733,6 +742,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) {
GodotProfileZoneGroupedFirst(_profile_zone, "prepare viewports");
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..16174bbc7acd 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_linear_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..f4e1708ac62f 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_linear_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 c3dc971f3430..0772faed0a6c 100644
--- a/servers/rendering/rendering_device.cpp
+++ b/servers/rendering/rendering_device.cpp
@@ -4493,6 +4493,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_
@@ -8325,6 +8336,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 13dbeaaf4c69..e9d196dbe1bf 100644
--- a/servers/rendering/rendering_device.h
+++ b/servers/rendering/rendering_device.h
@@ -1255,6 +1255,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 7948f85dc617..3b4966a68df9 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.
@@ -974,6 +980,7 @@ class RenderingDeviceCommons : public Object {
SUPPORTS_VULKAN_MEMORY_MODEL,
SUPPORTS_FRAMEBUFFER_DEPTH_RESOLVE,
SUPPORTS_POINT_SIZE,
+ SUPPORTS_HDR_OUTPUT,
};
enum SubgroupOperations {
diff --git a/servers/rendering/rendering_device_driver.h b/servers/rendering/rendering_device_driver.h
index 1e1ffc8a81a5..0359b5874a98 100644
--- a/servers/rendering/rendering_device_driver.h
+++ b/servers/rendering/rendering_device_driver.h
@@ -463,6 +463,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 6550faa091a1..ac11024a2b81 100644
--- a/servers/rendering/rendering_method.h
+++ b/servers/rendering/rendering_method.h
@@ -189,7 +189,7 @@ class RenderingMethod {
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, bool p_limit_agx_white) 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;
@@ -342,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/storage/environment_storage.cpp b/servers/rendering/storage/environment_storage.cpp
index 3efeca39e412..8fd0bc41158c 100644
--- a/servers/rendering/storage/environment_storage.cpp
+++ b/servers/rendering/storage/environment_storage.cpp
@@ -223,16 +223,14 @@ float RendererEnvironmentStorage::environment_get_exposure(RID p_env) const {
return env->exposure;
}
-float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_agx_white) 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);
- const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0.
-
// 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 output_max_value;
+ 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 output_max_value.
@@ -247,7 +245,7 @@ float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_
// Instead of constraining by matching the output_max_value, constrain
// by multiplying to ensure the desired non-uniform scaling behavior
// is maintained in the shoulder.
- return agx_white * output_max_value;
+ return agx_white * p_output_max_value;
}
} else { // Reinhard
// The Reinhard tonemapper is not designed to have a white parameter
@@ -255,7 +253,7 @@ float RendererEnvironmentStorage::environment_get_white(RID p_env, bool p_limit_
// 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(output_max_value, env->white);
+ return MAX(p_output_max_value, env->white);
}
}
@@ -271,19 +269,17 @@ float RendererEnvironmentStorage::environment_get_tonemap_agx_contrast(RID p_env
return env->tonemap_agx_contrast;
}
-RendererEnvironmentStorage::TonemapParameters RendererEnvironmentStorage::environment_get_tonemap_parameters(RID p_env, bool p_limit_agx_white) const {
+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());
- const float output_max_value = 1.0; // SDR always has an output_max_value of 1.0.
-
- float white = environment_get_white(p_env, p_limit_agx_white);
+ 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;
+ 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
@@ -319,7 +315,7 @@ RendererEnvironmentStorage::TonemapParameters RendererEnvironmentStorage::enviro
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_parameters.tonemap_e.
- const float awp_shoulder_max = output_max_value - awp_crossover_point;
+ const float awp_shoulder_max = p_output_max_value - awp_crossover_point;
float awp_high_clip = white;
diff --git a/servers/rendering/storage/environment_storage.h b/servers/rendering/storage/environment_storage.h
index d9d2a8d00d7b..9dd0d2b5b796 100644
--- a/servers/rendering/storage/environment_storage.h
+++ b/servers/rendering/storage/environment_storage.h
@@ -87,6 +87,7 @@ class RendererEnvironmentStorage {
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;
@@ -227,10 +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, bool p_limit_agx_white) 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) 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);