diff --git a/ReShade.vcxproj b/ReShade.vcxproj
index ac5adbd1c2..9eaac3648b 100644
--- a/ReShade.vcxproj
+++ b/ReShade.vcxproj
@@ -600,6 +600,7 @@
+
@@ -718,6 +719,7 @@
+
diff --git a/ReShade.vcxproj.filters b/ReShade.vcxproj.filters
index a66812e24d..395a56bd10 100644
--- a/ReShade.vcxproj.filters
+++ b/ReShade.vcxproj.filters
@@ -79,6 +79,9 @@
{9232c43b-559d-4435-b309-c8bbf4e6477b}
+
+ {e8eebdc2-4ab7-47d5-b053-e2dda568614e}
+
@@ -423,6 +426,9 @@
hooks\windows
+
+ api\dxgi
+
@@ -722,6 +728,9 @@
api\vulkan
+
+ api\dxgi
+
diff --git a/deps/sk_hdr_png/include/sk_hdr_png.hpp b/deps/sk_hdr_png/include/sk_hdr_png.hpp
index 3d21afdf74..eaad64790f 100644
--- a/deps/sk_hdr_png/include/sk_hdr_png.hpp
+++ b/deps/sk_hdr_png/include/sk_hdr_png.hpp
@@ -25,7 +25,8 @@
#pragma once
#include "reshade_api.hpp"
-//#include "reshade_api_display.hpp"
+#include "reshade_api_display.hpp"
+#include
#include
#include
@@ -310,8 +311,8 @@ namespace sk_hdr_png
};
};
- bool write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard);//, reshade::api::display* display);
- bool write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance_array, int quantization_bits);//, reshade::api::display* display);
+ bool write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard, reshade::api::display* display);
+ bool write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance_array, int quantization_bits, reshade::api::display* display);
cLLi_Payload calculate_content_light_info (const float* luminance, unsigned int width, unsigned int height);
bool copy_to_clipboard (const wchar_t* image_path);
bool remove_chunk (const char* chunk_name, void* data, size_t& size);
@@ -586,7 +587,7 @@ sk_hdr_png::remove_chunk (const char* chunk_name, void* data, size_t& size)
}
bool
-sk_hdr_png::write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance, int quantization_bits)//, reshade::api::display* display)
+sk_hdr_png::write_hdr_chunks (const wchar_t* image_path, unsigned int width, unsigned int height, const float* luminance, int quantization_bits, reshade::api::display* display)
{
if (image_path == nullptr || width == 0 || height == 0 || quantization_bits < 6)
{
@@ -706,7 +707,6 @@ sk_hdr_png::write_hdr_chunks (const wchar_t* image_path, unsigned int width, uns
sbit_chunk.write(fPNG);
chrm_chunk.write(fPNG);
-#if 0
///
/// Mastering metadata can be added, provided you are able to read this info
/// from the user's EDID.
@@ -744,7 +744,6 @@ sk_hdr_png::write_hdr_chunks (const wchar_t* image_path, unsigned int width, uns
Chunk mdcv_chunk = {sizeof(mdcv_data), {'m','D','C','v'}, &mdcv_data};
mdcv_chunk.write(fPNG);
}
-#endif
// Write the remainder of the original file
fwrite(insert_ptr, size - insert_pos, 1, fPNG);
@@ -814,8 +813,20 @@ sk_hdr_png::copy_to_clipboard (const wchar_t* image_path)
}
bool
-sk_hdr_png::write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard)//, reshade::api::display* display)
+sk_hdr_png::write_image_to_disk (const wchar_t* image_path, unsigned int width, unsigned int height, const void* pixels, int quantization_bits, format fmt, bool use_clipboard, reshade::api::display* display)
{
+ if (fmt == format::r16g16b16a16_float)
+ {
+ int cpu_info [4] = { };
+
+ CpuIdEx(cpu_info, 1, 0);
+ if (!std::bitset<32>(cpu_info[2])[29])
+ {
+ reshade::log::message(reshade::log::level::error, "CPU does not support AVX/F16C, required for scRGB screenshots!");
+ return false;
+ }
+ }
+
using namespace DirectX;
using namespace DirectX::PackedVector;
@@ -988,7 +999,7 @@ sk_hdr_png::write_image_to_disk (const wchar_t* image_path, unsigned int width,
if (SUCCEEDED(hr)) hr = encoder->Commit();
if (SUCCEEDED(hr))
{
- hr = write_hdr_chunks(image_path, width, height, luminance, quantization_bits/*, display*/) ? S_OK : E_FAIL;
+ hr = write_hdr_chunks(image_path, width, height, luminance, quantization_bits, display) ? S_OK : E_FAIL;
if (SUCCEEDED(hr))
{
diff --git a/include/reshade_api.hpp b/include/reshade_api.hpp
index 15f5ba7cb3..06d878d6cb 100644
--- a/include/reshade_api.hpp
+++ b/include/reshade_api.hpp
@@ -31,6 +31,8 @@ namespace reshade { namespace api
///
RESHADE_DEFINE_HANDLE(effect_uniform_variable);
+ struct display;
+
///
/// Input source for events triggered by user input.
///
@@ -806,5 +808,10 @@ namespace reshade { namespace api
///
/// File name of the effect file that should be reloaded.
virtual void reload_effect_next_frame(const char *effect_name) = 0;
+
+ ///
+ /// Gets the active display, which may change between frames or return nullptr if the swapchain is offscreen.
+ ///
+ virtual display* get_active_display() const = 0;
};
} }
diff --git a/include/reshade_api_device.hpp b/include/reshade_api_device.hpp
index 3deed53323..df9de88d46 100644
--- a/include/reshade_api_device.hpp
+++ b/include/reshade_api_device.hpp
@@ -250,6 +250,7 @@ namespace reshade { namespace api
/// For this will be a pointer to a 'ID3D11DeviceContext' (when recording), 'ID3D11CommandList' (when executing) or 'ID3D12GraphicsCommandList' object or a 'VkCommandBuffer' handle.
/// For this will be a pointer to a 'ID3D11DeviceContext' or 'ID3D12CommandQueue' object or a 'VkQueue' handle.
/// For this will be a pointer to a 'IDirect3DSwapChain9' or 'IDXGISwapChain' object or a 'HDC' or 'VkSwapchainKHR' handle.
+ /// For this will be a pointer to a 'IDXGIOutput'; DXGI is used even for runtimes that do not have a DXGI-based swapchain.
///
virtual uint64_t get_native() const = 0;
diff --git a/include/reshade_api_display.hpp b/include/reshade_api_display.hpp
new file mode 100644
index 0000000000..3f8ed4eb11
--- /dev/null
+++ b/include/reshade_api_display.hpp
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2024 Patrick Mours
+ * SPDX-License-Identifier: BSD-3-Clause OR MIT
+ */
+
+#pragma once
+
+#include "reshade_api_device.hpp"
+#include
+#include
+
+namespace reshade { namespace api
+{
+ ///
+ /// Describes quantities defined precisely as the ratio of two integers, such as refresh rates.
+ ///
+ struct rational
+ {
+ uint32_t numerator = 0;
+ uint32_t denomenator = 0;
+
+ constexpr float as_float() const { return denomenator != 0 ? static_cast (numerator)/static_cast(denomenator) : 0.0f; }
+ };
+
+ ///
+ /// Describes the colorimetry of a colorspace.
+ ///
+ struct colorimetry
+ {
+ float red [2] = { 0.0f, 0.0f };
+ float green [2] = { 0.0f, 0.0f };
+ float blue [2] = { 0.0f, 0.0f };
+ float white [2] = { 0.0f, 0.0f };
+ };
+
+ ///
+ /// Describes the dynamic range of a display or image.
+ ///
+ struct luminance_levels
+ {
+ float min_nits = 0.0f;
+ float max_nits = 0.0f;
+ float max_avg_nits = 0.0f;
+ };
+
+ ///
+ /// An output display.
+ /// Functionally equivalent to a 'IDXGIOutput'.
+ ///
+ struct __declspec(novtable) display : public api_object
+ {
+ ///
+ /// Indicates if cached properties are valid or potentially stale (i.e. refresh rate or resolution have changed).
+ ///
+ ///
+ /// If this returns false, wait one frame and call runtime::get_active_display() to get updated data.
+ ///
+ virtual bool is_current() const = 0;
+
+ using monitor = void*;
+
+ ///
+ /// Gets the handle of the monitor this display encapsulates.
+ ///
+ virtual monitor get_monitor() const = 0;
+
+ ///
+ /// Gets the (GDI) device name (i.e. \\DISPLAY1) of the the monitor; do not use this as a persistent display identifier!
+ ///
+ ///
+ /// This device name is not valid as a persistent display identifier for storage in configuration files, it may not refer to the same display device after a reboot.
+ ///
+ virtual const wchar_t* get_device_name() const = 0;
+
+ ///
+ /// Gets the device path (i.e. \\?\DISPLAY#DELA1E4#5&d93f871&0&UID33025#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}) of the the monitor.
+ ///
+ ///
+ /// The device path uniquely identifies a specific monitor (down to its serial number and the port it is attached to) and remains valid across system reboots.
+ /// This identifier is suitable for persistent user-defined display configuration, partial name matches can be used to identify instances of the same monitor when the port it attaches to is unimportant.
+ ///
+ virtual const wchar_t* get_device_path() const = 0;
+
+ ///
+ /// Gets the human readable name (i.e. Dell AW3423DW) of the the monitor.
+ ///
+ virtual const wchar_t* get_display_name() const = 0;
+
+ ///
+ /// Gets the size and location of the display on the desktop.
+ ///
+ virtual rect get_desktop_coords() const = 0;
+
+ ///
+ /// Gets the current color depth of the display.
+ ///
+ virtual uint32_t get_color_depth() const = 0;
+
+ ///
+ /// Gets the current refresh rate of the display.
+ ///
+ virtual rational get_refresh_rate() const = 0;
+
+ ///
+ /// Gets the current color space used for desktop composition.
+ ///
+ ///
+ /// This is independent from swap chain colorspace, it identifies a display as HDR capable even when not rendering in HDR.
+ ///
+ virtual color_space get_color_space() const = 0;
+
+ ///
+ /// Gets the display's reported native colorimetry (the data provided are often invalid or placeholders).
+ ///
+ ///
+ /// Users running Windows 11 or newer are encouraged to run "Windows HDR Calibration" to ensure the values reported to ReShade and games are accurate.
+ ///
+ virtual colorimetry get_colorimetry() const = 0;
+
+ ///
+ /// Gets the display's light output capabilities (the data provided are often invalid or placeholders).
+ ///
+ ///
+ /// Users running Windows 11 or newer are encouraged to run "Windows HDR Calibration" to ensure the values reported to ReShade and games are accurate.
+ ///
+ virtual luminance_levels get_luminance_caps() const = 0;
+
+ ///
+ /// Gets the desktop compositor's whitelevel for mapping SDR content to HDR.
+ ///
+ virtual float get_sdr_white_nits() const = 0;
+
+ ///
+ /// Checks if HDR is supported on the display, even if it is not currently enabled.
+ ///
+ virtual bool is_hdr_supported() const = 0;
+
+ ///
+ /// Checks if HDR is enabled on the display.
+ ///
+ virtual bool is_hdr_enabled() const = 0;
+
+ ///
+ /// Enables HDR on the display.
+ ///
+ ///
+ /// Be aware that this is a global display setting, and will not automatically revert to its original state when the application exits.
+ ///
+ virtual bool enable_hdr(bool enable) = 0;
+ };
+
+ using display_cache = std::unordered_map>;
+} }
diff --git a/include/reshade_events.hpp b/include/reshade_events.hpp
index e34777d1cb..964fe6575d 100644
--- a/include/reshade_events.hpp
+++ b/include/reshade_events.hpp
@@ -1705,8 +1705,14 @@ namespace reshade
///
reshade_overlay_technique,
+ ///
+ /// Called when the active display changes for a runtime.
+ /// Callback function signature: void (api::effect_runtime *runtime, api::display *display)
+ ///
+ display_change = 95,
+
#if RESHADE_ADDON
- max = 95 // Last value used internally by ReShade to determine number of events in this enum
+ max = 96 // Last value used internally by ReShade to determine number of events in this enum
#endif
};
@@ -1848,4 +1854,6 @@ namespace reshade
RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_overlay_uniform_variable, bool, api::effect_runtime *runtime, api::effect_uniform_variable variable);
RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::reshade_overlay_technique, bool, api::effect_runtime *runtime, api::effect_technique technique);
+
+ RESHADE_DEFINE_ADDON_EVENT_TRAITS(addon_event::display_change, void, api::effect_runtime *runtime, api::display *display);
}
diff --git a/source/dxgi/dxgi_impl_display.cpp b/source/dxgi/dxgi_impl_display.cpp
new file mode 100644
index 0000000000..9146b589df
--- /dev/null
+++ b/source/dxgi/dxgi_impl_display.cpp
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2024 Patrick Mours
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#include "dxgi_impl_display.hpp"
+
+reshade::dxgi::display_impl::display_impl(IDXGIOutput6 *output, DISPLAYCONFIG_PATH_TARGET_INFO *target) :
+ api_object_impl(output)
+{
+ assert(output != nullptr && target != nullptr);
+
+ if (output == nullptr || target == nullptr)
+ return;
+
+ _target_info = *target;
+
+ com_ptr adapter;
+ com_ptr factory;
+
+ if (SUCCEEDED(output->GetParent(IID_IDXGIAdapter,reinterpret_cast(&adapter))) &&
+ SUCCEEDED(adapter->GetParent(IID_IDXGIFactory,reinterpret_cast(&factory))))
+ {
+ factory->QueryInterface(&_parent);
+ }
+
+ _output = output;
+ output->GetDesc1(&_desc);
+
+ _refresh_rate = {
+ _target_info.refreshRate.Numerator,
+ _target_info.refreshRate.Denominator
+ };
+
+ DISPLAYCONFIG_TARGET_DEVICE_NAME
+ target_name = {};
+ target_name.header.adapterId = _target_info.adapterId;
+ target_name.header.id = _target_info.id;
+ target_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME;
+ target_name.header.size = sizeof (DISPLAYCONFIG_TARGET_DEVICE_NAME);
+
+ if (ERROR_SUCCESS == DisplayConfigGetDeviceInfo(&target_name.header))
+ {
+ _name = target_name.monitorFriendlyDeviceName;
+ _path = target_name.monitorDevicePath;
+ }
+
+ DISPLAYCONFIG_SDR_WHITE_LEVEL
+ sdr_white_level = {};
+ sdr_white_level.header.adapterId = _target_info.adapterId;
+ sdr_white_level.header.id = _target_info.id;
+ sdr_white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
+ sdr_white_level.header.size = sizeof (DISPLAYCONFIG_SDR_WHITE_LEVEL);
+
+ if (ERROR_SUCCESS == DisplayConfigGetDeviceInfo(&sdr_white_level.header))
+ {
+ _sdr_nits = (static_cast(sdr_white_level.SDRWhiteLevel) / 1000.0f) * 80.0f;
+ }
+
+#if (NTDDI_VERSION >= NTDDI_WIN11_GA)
+ // Prefer this API if the build environment and current OS support it, it can distinguish WCG from HDR
+ DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2
+ get_color_info2 = {};
+ get_color_info2.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2;
+ get_color_info2.header.size = sizeof (DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2);
+ get_color_info2.header.adapterId = _target_info.adapterId;
+ get_color_info2.header.id = _target_info.id;
+
+ if (ERROR_SUCCESS == DisplayConfigGetDeviceInfo(&get_color_info2.header))
+ {
+ _hdr_supported = get_color_info2.highDynamicRangeSupported && !get_color_info2.advancedColorLimitedByPolicy;
+ _hdr_enabled = get_color_info2.advancedColorActive && get_color_info2.highDynamicRangeUserEnabled && get_color_info2.activeColorMode == DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR;
+ }
+
+ else
+#endif
+ {
+ DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO
+ get_color_info = {};
+ get_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
+ get_color_info.header.size = sizeof (DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO);
+ get_color_info.header.adapterId = _target_info.adapterId;
+ get_color_info.header.id = _target_info.id;
+
+ if (ERROR_SUCCESS == DisplayConfigGetDeviceInfo(&get_color_info.header))
+ {
+ _hdr_supported = get_color_info.advancedColorSupported;
+ _hdr_enabled = get_color_info.advancedColorEnabled;
+ }
+ }
+};
+
+const wchar_t* reshade::dxgi::display_impl::get_device_name() const
+{
+ return _desc.DeviceName;
+};
+
+const wchar_t* reshade::dxgi::display_impl::get_device_path() const
+{
+ return _path.c_str();
+};
+
+const wchar_t* reshade::dxgi::display_impl::get_display_name() const
+{
+ return _name.c_str();
+};
+
+reshade::api::display::monitor reshade::dxgi::display_impl::get_monitor() const
+{
+ return _desc.Monitor;
+};
+
+reshade::api::rect reshade::dxgi::display_impl::get_desktop_coords() const
+{
+ return { _desc.DesktopCoordinates.left, _desc.DesktopCoordinates.top,
+ _desc.DesktopCoordinates.right, _desc.DesktopCoordinates.bottom };
+};
+
+uint32_t reshade::dxgi::display_impl::get_color_depth() const
+{
+ return _desc.BitsPerColor;
+};
+
+reshade::api::rational reshade::dxgi::display_impl::get_refresh_rate() const
+{
+ return _refresh_rate;
+};
+
+reshade::api::color_space reshade::dxgi::display_impl::get_color_space() const
+{
+ switch (_desc.ColorSpace)
+ {
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709:
+ return api::color_space::srgb_nonlinear;
+ case DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709:
+ return api::color_space::extended_srgb_linear;
+ case DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020:
+ return api::color_space::hdr10_st2084;
+ case DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P2020:
+ return api::color_space::hdr10_hlg;
+ default:
+ return api::color_space::unknown;
+ }
+};
+
+reshade::api::colorimetry reshade::dxgi::display_impl::get_colorimetry() const
+{
+ return api::colorimetry {
+ _desc.RedPrimary[0], _desc.RedPrimary[1],
+ _desc.GreenPrimary[0], _desc.GreenPrimary[1],
+ _desc.BluePrimary[0], _desc.BluePrimary[1],
+ _desc.WhitePoint[0], _desc.WhitePoint[1]
+ };
+};
+
+reshade::api::luminance_levels reshade::dxgi::display_impl::get_luminance_caps() const
+{
+ return api::luminance_levels {
+ _desc.MinLuminance,
+ _desc.MaxLuminance,
+ _desc.MaxFullFrameLuminance
+ };
+};
+
+float reshade::dxgi::display_impl::get_sdr_white_nits() const
+{
+ return _sdr_nits;
+}
+
+bool reshade::dxgi::display_impl::is_current() const
+{
+ return _parent->IsCurrent();
+}
+
+bool reshade::dxgi::display_impl::is_hdr_supported() const
+{
+ return _hdr_supported;
+}
+
+bool reshade::dxgi::display_impl::is_hdr_enabled() const
+{
+ return _hdr_enabled;
+}
+
+bool reshade::dxgi::display_impl::enable_hdr(bool enable)
+{
+ if (!_hdr_supported)
+ return false;
+
+#if (NTDDI_VERSION >= NTDDI_WIN11_GA)
+ // Prefer this API if the build environment and current OS support it, it can distinguish WCG from HDR
+ DISPLAYCONFIG_SET_HDR_STATE
+ set_hdr_state = {};
+ set_hdr_state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_HDR_STATE;
+ set_hdr_state.header.size = sizeof (DISPLAYCONFIG_SET_HDR_STATE);
+ set_hdr_state.header.adapterId = _target_info.adapterId;
+ set_hdr_state.header.id = _target_info.id;
+
+ set_hdr_state.enableHdr = enable;
+
+ if (ERROR_SUCCESS == DisplayConfigSetDeviceInfo(&set_hdr_state.header))
+ {
+ _hdr_enabled = set_hdr_state.enableHdr;
+ return true;
+ }
+
+ else
+#endif
+ {
+ DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE
+ set_advanced_color_state = {};
+ set_advanced_color_state.header.type = DISPLAYCONFIG_DEVICE_INFO_SET_ADVANCED_COLOR_STATE;
+ set_advanced_color_state.header.size = sizeof (DISPLAYCONFIG_SET_ADVANCED_COLOR_STATE);
+ set_advanced_color_state.header.adapterId = _target_info.adapterId;
+ set_advanced_color_state.header.id = _target_info.id;
+
+ set_advanced_color_state.enableAdvancedColor = enable;
+
+ if (ERROR_SUCCESS == DisplayConfigSetDeviceInfo(&set_advanced_color_state.header))
+ {
+ _hdr_enabled = set_advanced_color_state.enableAdvancedColor;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void reshade::dxgi::display_impl::flush_cache(reshade::api::display_cache& displays)
+{
+ // Detect spurious cache flushes to eliminate sending false display change events to add-ons.
+ bool flush_required = displays.empty();
+ if (!flush_required)
+ {
+ for (const auto &display : displays)
+ if (flush_required = !display.second->is_current())
+ break;
+
+ // If nothing has actually changed, then we are done here...
+ if (!flush_required)
+ return;
+ }
+
+ displays.clear();
+
+ std::vector paths;
+ std::vector modes;
+ UINT32 flags = QDC_ONLY_ACTIVE_PATHS | QDC_VIRTUAL_MODE_AWARE | QDC_VIRTUAL_REFRESH_RATE_AWARE;
+ LONG result = ERROR_SUCCESS;
+
+ do
+ {
+ UINT32 pathCount, modeCount;
+ if (GetDisplayConfigBufferSizes(flags, &pathCount, &modeCount) != ERROR_SUCCESS)
+ break;
+
+ paths.resize(pathCount);
+ modes.resize(modeCount);
+
+ result = QueryDisplayConfig(flags, &pathCount, paths.data(), &modeCount, modes.data(), nullptr);
+
+ paths.resize(pathCount);
+ modes.resize(modeCount);
+ } while (result == ERROR_INSUFFICIENT_BUFFER);
+
+ const auto CreateDXGIFactory1 = reinterpret_cast(
+ GetProcAddress(GetModuleHandleW(L"dxgi.dll"), "CreateDXGIFactory1"));
+
+ if (CreateDXGIFactory1 == nullptr)
+ return;
+
+ com_ptr factory;
+ CreateDXGIFactory1(IID_IDXGIFactory, reinterpret_cast(&factory));
+
+ if (factory != nullptr)
+ {
+ com_ptr adapter;
+ UINT adapter_idx = 0;
+ while (SUCCEEDED(factory->EnumAdapters(adapter_idx++, &adapter)))
+ {
+ com_ptr output;
+ UINT output_idx = 0;
+ while (SUCCEEDED(adapter->EnumOutputs(output_idx++, &output)))
+ {
+ com_ptr output6;
+ if (SUCCEEDED(output->QueryInterface(&output6)))
+ {
+ DXGI_OUTPUT_DESC1 desc = {};
+ output6->GetDesc1(&desc);
+
+ for (auto &path : paths)
+ {
+ DISPLAYCONFIG_SOURCE_DEVICE_NAME
+ source_name = {};
+ source_name.header.adapterId = path.sourceInfo.adapterId;
+ source_name.header.id = path.sourceInfo.id;
+ source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
+ source_name.header.size = sizeof (DISPLAYCONFIG_SOURCE_DEVICE_NAME);
+
+ if (DisplayConfigGetDeviceInfo(&source_name.header) != ERROR_SUCCESS)
+ continue;
+
+ if (_wcsicmp(desc.DeviceName, source_name.viewGdiDeviceName))
+ continue;
+
+ displays.emplace(desc.Monitor,
+ std::make_unique(output6.get(),&path.targetInfo));
+
+ break;
+ }
+ }
+ output.reset();
+ }
+ adapter.reset();
+ }
+ }
+}
diff --git a/source/dxgi/dxgi_impl_display.hpp b/source/dxgi/dxgi_impl_display.hpp
new file mode 100644
index 0000000000..a24f4b4e89
--- /dev/null
+++ b/source/dxgi/dxgi_impl_display.hpp
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 Patrick Mours
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+#pragma once
+
+#include
+#include
+#include "com_ptr.hpp"
+#include "reshade_api_object_impl.hpp"
+#include "reshade_api_display.hpp"
+
+namespace reshade::dxgi
+{
+ class display_impl : public api::api_object_impl
+ {
+ public:
+ explicit display_impl(IDXGIOutput6 *output, DISPLAYCONFIG_PATH_TARGET_INFO *target);
+
+ ///
+ /// Indicates if cached properties are valid or potentially stale (i.e. refresh rate or resolution have changed).
+ ///
+ ///
+ /// If this returns false, wait one frame and call runtime::get_active_display() to get updated data.
+ ///
+ virtual bool is_current() const final;
+
+ ///
+ /// Gets the handle of the monitor this display encapsulates.
+ ///
+ virtual monitor get_monitor() const final;
+
+ ///
+ /// Gets the (GDI) device name (i.e. \\DISPLAY1) of the the monitor; do not use this as a persistent display identifier!
+ ///
+ ///
+ /// This device name is not valid as a persistent display identifier for storage in configuration files, it may not refer to the same display device after a reboot.
+ ///
+ virtual const wchar_t* get_device_name() const final;
+
+ ///
+ /// Gets the device path (i.e. \\?\DISPLAY#DELA1E4#5&d93f871&0&UID33025#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}) of the the monitor.
+ ///
+ ///
+ /// The device path uniquely identifies a specific monitor (down to its serial number and the port it is attached to) and remains valid across system reboots.
+ /// This identifier is suitable for persistent user-defined display configuration, partial name matches can be used to identify instances of the same monitor when the port it attaches to is unimportant.
+ ///
+ virtual const wchar_t* get_device_path() const final;
+
+ ///
+ /// Gets the human readable name (i.e. Dell AW3423DW) of the the monitor.
+ ///
+ virtual const wchar_t* get_display_name() const final;
+
+ ///
+ /// Gets the size and location of the display on the desktop.
+ ///
+ virtual api::rect get_desktop_coords() const final;
+
+ ///
+ /// Gets the current color depth of the display.
+ ///
+ virtual uint32_t get_color_depth() const final;
+
+ ///
+ /// Gets the current refresh rate of the display.
+ ///
+ virtual api::rational get_refresh_rate() const final;
+
+ ///
+ /// Gets the current color space used for desktop composition.
+ ///
+ ///
+ /// This is independent from swap chain colorspace, it identifies a display as HDR capable even when not rendering in HDR.
+ ///
+ virtual api::color_space get_color_space() const final;
+
+ ///
+ /// Gets the display's reported native colorimetry (the data provided are often invalid or placeholders).
+ ///
+ ///
+ /// Users running Windows 11 or newer are encouraged to run "Windows HDR Calibration" to ensure the values reported to ReShade and games are accurate.
+ ///
+ virtual api::colorimetry get_colorimetry() const final;
+
+ ///
+ /// Gets the display's light output capabilities (the data provided are often invalid or placeholders).
+ ///
+ ///
+ /// Users running Windows 11 or newer are encouraged to run "Windows HDR Calibration" to ensure the values reported to ReShade and games are accurate.
+ ///
+ virtual api::luminance_levels get_luminance_caps() const final;
+
+ ///
+ /// Gets the desktop compositor's whitelevel for mapping SDR content to HDR.
+ ///
+ virtual float get_sdr_white_nits() const final;
+
+ ///
+ /// Checks if HDR is supported on the display, even if it is not currently enabled.
+ ///
+ virtual bool is_hdr_supported() const final;
+
+ ///
+ /// Checks if HDR is enabled on the display.
+ ///
+ virtual bool is_hdr_enabled() const final;
+
+ ///
+ /// Enables HDR on the display.
+ ///
+ ///
+ /// Be aware that this is a global display setting, and will not revert to its original state when the application exits.
+ ///
+ virtual bool enable_hdr(bool enable) final;
+
+ static void flush_cache(reshade::api::display_cache& displays);
+
+ protected:
+ DISPLAYCONFIG_PATH_TARGET_INFO _target_info = {};
+ com_ptr _parent;
+ com_ptr _output;
+ DXGI_OUTPUT_DESC1 _desc;
+ std::wstring _name = L"";
+ std::wstring _path = L"";
+ bool _hdr_supported = false;
+ bool _hdr_enabled = false;
+ float _sdr_nits = 0.0f;
+ api::rational _refresh_rate = {0};
+ };
+};
diff --git a/source/input.cpp b/source/input.cpp
index ef440c121c..bede8aadad 100644
--- a/source/input.cpp
+++ b/source/input.cpp
@@ -24,6 +24,8 @@ static std::chrono::high_resolution_clock::time_point s_last_cursor_warp = {};
static std::atomic s_block_mouse = false;
static std::atomic s_block_keyboard = false;
static std::atomic s_block_cursor_warping = false;
+static std::atomic_uint32_t s_last_desktop_change = 1; // All runtimes are initialized to 0, and will establish a desktop cache on the first frame drawn
+bool is_desktop_current(std::atomic_uint32_t& timestamp) { return std::atomic_exchange(×tamp,s_last_desktop_change) >= s_last_desktop_change; }
reshade::input::input(window_handle window)
: _window(window)
@@ -83,6 +85,13 @@ bool reshade::input::handle_window_message(const void *message_data)
MSG details = *static_cast(message_data);
+ // The desktop display topology has changed, desktop runtimes should invalidate cached display properties
+ if (details.message == WM_DISPLAYCHANGE)
+ {
+ s_last_desktop_change = std::max(s_last_desktop_change.load(), static_cast(details.time));
+ return false;
+ }
+
bool is_mouse_message = details.message >= WM_MOUSEFIRST && details.message <= WM_MOUSELAST;
bool is_keyboard_message = details.message >= WM_KEYFIRST && details.message <= WM_KEYLAST;
diff --git a/source/runtime.cpp b/source/runtime.cpp
index 7803d67b52..c6d7ed56ab 100644
--- a/source/runtime.cpp
+++ b/source/runtime.cpp
@@ -34,6 +34,7 @@
#include
#include
#include
+#include
#include
bool resolve_path(std::filesystem::path &path, std::error_code &ec)
@@ -474,6 +475,9 @@ bool reshade::runtime::on_init()
invoke_addon_event(this);
#endif
+ if (_containing_output == nullptr)
+ on_display_change();
+
log::message(log::level::info, "Recreated runtime environment on runtime %p ('%s').", this, _config_path.u8string().c_str());
return true;
@@ -943,6 +947,82 @@ void reshade::runtime::on_present(api::command_queue *present_queue)
if (std::numeric_limits::max() != g_network_traffic)
g_network_traffic = 0;
#endif
+
+ HWND window = static_cast(_swapchain->get_hwnd());
+ if (window != 0)
+ {
+ extern bool is_desktop_current(std::atomic_uint32_t& timestamp);
+ if (!is_desktop_current(_last_desktop_change) || (_containing_output == nullptr || !_containing_output->is_current()))
+ {
+ // Flush the entire desktop display cache
+ on_display_change();
+ }
+
+ RECT window_rect = {};
+
+ if ((GetWindowRect(window,&window_rect) &&
+ ((std::exchange(_last_desktop_x,window_rect.left) != window_rect.left) |
+ (std::exchange(_last_desktop_y,window_rect.top) != window_rect.top)))
+ || _containing_output == nullptr)
+ {
+ const auto monitor = MonitorFromWindow(window,MONITOR_DEFAULTTONEAREST);
+
+ if (monitor != nullptr && _displays.count(monitor))
+ {
+ if (_containing_output == nullptr || _containing_output->get_monitor() != monitor)
+ {
+ // The monitor the window is attached to has changed
+ on_display_change();
+ }
+ }
+ }
+ }
+}
+
+void reshade::runtime::on_display_change()
+{
+ const bool flush = _displays.empty() || (_containing_output != nullptr && !_containing_output->is_current());
+
+ auto original_output = std::exchange(_containing_output, nullptr);
+
+ if (flush)
+ {
+ reshade::dxgi::display_impl::flush_cache(_displays);
+ }
+
+ if (HWND window = reinterpret_cast(get_hwnd()))
+ {
+ api::display::monitor monitor {MonitorFromWindow(window,MONITOR_DEFAULTTONEAREST)};
+ if (_displays.count(monitor))
+ {
+ RECT window_rect = {};
+ if (GetWindowRect(window,&window_rect))
+ {
+ _last_desktop_x = window_rect.left;
+ _last_desktop_y = window_rect.top;
+ }
+
+ if (std::exchange(_containing_output,_displays[monitor].get()) != original_output)
+ {
+#if RESHADE_ADDON
+ invoke_addon_event(this,_containing_output);
+#endif
+
+#if RESHADE_VERBOSE_LOG
+ auto& luminance_caps = _containing_output->get_luminance_caps();
+
+ log::message(log::level::info, "Swap chain is displaying at %5.2f Hz on '%ws' (%ws) - MinNits = %3.1f, MaxNits = %3.1f, MaxAvgNits = %3.1f.",
+ _containing_output->get_refresh_rate().as_float(), _containing_output->get_display_name(), _containing_output->get_device_name(),
+ luminance_caps.min_nits, luminance_caps.max_nits, luminance_caps.max_avg_nits);
+#endif
+ }
+ }
+ }
+
+ else
+ {
+ log::message(log::level::warning, "Unable to map runtime %p's swap chain to a containing display output!", this);
+ }
}
void reshade::runtime::load_config()
@@ -4877,7 +4957,7 @@ void reshade::runtime::save_screenshot(const std::string_view postfix)
// Implicit HDR PNG when running in HDR
case 3:
- save_success = sk_hdr_png::write_image_to_disk(screenshot_path.c_str (), _width, _height, pixels.data(), _screenshot_hdr_bits, _back_buffer_format, _screenshot_clipboard_copy);
+ save_success = sk_hdr_png::write_image_to_disk(screenshot_path.c_str (), _width, _height, pixels.data(), _screenshot_hdr_bits, _back_buffer_format, _screenshot_clipboard_copy, _containing_output);
break;
}
diff --git a/source/runtime.hpp b/source/runtime.hpp
index 49a492b876..83f0435b70 100644
--- a/source/runtime.hpp
+++ b/source/runtime.hpp
@@ -6,6 +6,7 @@
#pragma once
#include "reshade_api.hpp"
+#include "reshade_api_display.hpp"
#include "state_block.hpp"
#include "imgui_code_editor.hpp"
#include
@@ -36,6 +37,7 @@ namespace reshade
bool on_init();
void on_reset();
void on_present(api::command_queue *present_queue);
+ void on_display_change();
uint64_t get_native() const final { return _swapchain->get_native(); }
@@ -170,6 +172,8 @@ namespace reshade
void reload_effect_next_frame(const char *effect_name) final;
+ api::display* get_active_display() const final { return _containing_output; }
+
private:
static void check_for_update();
@@ -252,6 +256,11 @@ namespace reshade
uint16_t _back_buffer_samples = 1;
api::format _back_buffer_format = api::format::unknown;
api::color_space _back_buffer_color_space = api::color_space::unknown;
+ api::display* _containing_output = nullptr; // The display that currently owns the swapchain
+ api::display_cache _displays; // Cached mapping of all monitors attached to the desktop and their display properties
+ std::atomic_uint32_t _last_desktop_change = 0; // Invalidate the display cache when necessary
+ int _last_desktop_x = 0;
+ int _last_desktop_y = 0;
bool _is_vr = false;
#if RESHADE_ADDON