From 820ea2fd3359608f9a72f9c6889e1d631db2ca32 Mon Sep 17 00:00:00 2001 From: UshioA Date: Sun, 19 Jul 2026 21:48:17 +0800 Subject: [PATCH 1/2] Fix ImGui rendering on D3D12 HDR swap chains --- mhw-cs-plugin-loader/D3DModule.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mhw-cs-plugin-loader/D3DModule.cpp b/mhw-cs-plugin-loader/D3DModule.cpp index d5a643c..0fc5397 100644 --- a/mhw-cs-plugin-loader/D3DModule.cpp +++ b/mhw-cs-plugin-loader/D3DModule.cpp @@ -558,6 +558,8 @@ void D3DModule::d3d12_initialize_imgui(IDXGISwapChain* swap_chain) { const auto rtv_descriptor_size = m_d3d12_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV); D3D12_CPU_DESCRIPTOR_HANDLE rtv_handle = m_d3d12_back_buffers->GetCPUDescriptorHandleForHeapStart(); + DXGI_FORMAT back_buffer_format = DXGI_FORMAT_UNKNOWN; + for (auto i = 0u; i < desc.BufferCount; ++i) { ComPtr back_buffer; if (FAILED(swap_chain->GetBuffer(i, IID_PPV_ARGS(back_buffer.GetAddressOf())))) { @@ -566,7 +568,17 @@ void D3DModule::d3d12_initialize_imgui(IDXGISwapChain* swap_chain) { } const auto buffer_desc = back_buffer->GetDesc(); - dlog::debug("Creating RTV for back buffer {}, with size {}x{}", i, buffer_desc.Width, buffer_desc.Height); + if (i == 0) { + back_buffer_format = buffer_desc.Format; + } + + dlog::debug( + "Creating RTV for back buffer {}, with size {}x{} and format {}", + i, + buffer_desc.Width, + buffer_desc.Height, + static_cast(buffer_desc.Format) + ); m_d3d12_device->CreateRenderTargetView(back_buffer.Get(), nullptr, rtv_handle); m_d3d12_frame_contexts[i].RenderTargetDescriptor = rtv_handle; @@ -583,7 +595,7 @@ void D3DModule::d3d12_initialize_imgui(IDXGISwapChain* swap_chain) { ImGui_ImplWin32_EnableDpiAwareness(); if (!ImGui_ImplDX12_Init(m_d3d12_device, desc.BufferCount, - DXGI_FORMAT_R8G8B8A8_UNORM, m_d3d12_srv_heap.Get(), + back_buffer_format, m_d3d12_srv_heap.Get(), m_d3d12_srv_heap->GetCPUDescriptorHandleForHeapStart(), m_d3d12_srv_heap->GetGPUDescriptorHandleForHeapStart())) { dlog::error("Failed to initialize ImGui D3D12"); From 7d3e4ca95a9a64bf99d1232bdabd289d06f95f6c Mon Sep 17 00:00:00 2001 From: UshioA Date: Sun, 19 Jul 2026 22:32:28 +0800 Subject: [PATCH 2/2] Color manage ImGui output for HDR swap chains --- dependencies/imgui/imgui_impl_dx11.cpp | 26 ++---- dependencies/imgui/imgui_impl_dx11.h | 3 +- dependencies/imgui/imgui_impl_dx12.cpp | 27 +++--- dependencies/imgui/imgui_impl_dx12.h | 4 +- dependencies/imgui/imgui_impl_dxgi_hdr.h | 89 +++++++++++++++++++ mhw-cs-plugin-loader/D3DModule.cpp | 39 +++++++- .../mhw-cs-plugin-loader.vcxproj | 3 +- .../mhw-cs-plugin-loader.vcxproj.filters | 5 +- 8 files changed, 156 insertions(+), 40 deletions(-) create mode 100644 dependencies/imgui/imgui_impl_dxgi_hdr.h diff --git a/dependencies/imgui/imgui_impl_dx11.cpp b/dependencies/imgui/imgui_impl_dx11.cpp index 0ad1364..dbf2ed0 100644 --- a/dependencies/imgui/imgui_impl_dx11.cpp +++ b/dependencies/imgui/imgui_impl_dx11.cpp @@ -63,6 +63,7 @@ struct ImGui_ImplDX11_Data ID3D11RasterizerState* pRasterizerState; ID3D11BlendState* pBlendState; ID3D11DepthStencilState* pDepthStencilState; + ImGui_ImplDXGI_ColorSpace ColorSpace; int VertexBufferSize; int IndexBufferSize; @@ -456,24 +457,14 @@ bool ImGui_ImplDX11_CreateDeviceObjects() // Create the pixel shader { - static const char* pixelShader = - "struct PS_INPUT\ - {\ - float4 pos : SV_POSITION;\ - float4 col : COLOR0;\ - float2 uv : TEXCOORD0;\ - };\ - sampler sampler0;\ - Texture2D texture0;\ - \ - float4 main(PS_INPUT input) : SV_Target\ - {\ - float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \ - return out_col; \ - }"; + const D3D_SHADER_MACRO shaderDefines[] = + { + { "IMGUI_COLOR_SPACE", ImGui_ImplDXGI_GetColorSpaceShaderDefine(bd->ColorSpace) }, + { nullptr, nullptr }, + }; ID3DBlob* pixelShaderBlob; - if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr))) + if (FAILED(D3DCompile(ImGui_ImplDXGI_PixelShader, strlen(ImGui_ImplDXGI_PixelShader), nullptr, shaderDefines, nullptr, "main", "ps_4_0", 0, 0, &pixelShaderBlob, nullptr))) return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob! if (bd->pd3dDevice->CreatePixelShader(pixelShaderBlob->GetBufferPointer(), pixelShaderBlob->GetBufferSize(), nullptr, &bd->pPixelShader) != S_OK) { @@ -548,13 +539,14 @@ void ImGui_ImplDX11_InvalidateDeviceObjects() if (bd->pVertexShader) { bd->pVertexShader->Release(); bd->pVertexShader = nullptr; } } -bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context) +bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context, ImGui_ImplDXGI_ColorSpace color_space) { ImGuiIO& io = *igGetIO(); IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); // Setup backend capabilities flags ImGui_ImplDX11_Data* bd = IM_NEW(ImGui_ImplDX11_Data)(); + bd->ColorSpace = color_space; io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "imgui_impl_dx11"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. diff --git a/dependencies/imgui/imgui_impl_dx11.h b/dependencies/imgui/imgui_impl_dx11.h index c0a5b73..e1be9f1 100644 --- a/dependencies/imgui/imgui_impl_dx11.h +++ b/dependencies/imgui/imgui_impl_dx11.h @@ -16,12 +16,13 @@ #pragma once #include +#include #ifndef IMGUI_DISABLE struct ID3D11Device; struct ID3D11DeviceContext; -IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context); +IMGUI_IMPL_API bool ImGui_ImplDX11_Init(ID3D11Device* device, ID3D11DeviceContext* device_context, ImGui_ImplDXGI_ColorSpace color_space = ImGui_ImplDXGI_ColorSpace_SDR); IMGUI_IMPL_API void ImGui_ImplDX11_Shutdown(); IMGUI_IMPL_API void ImGui_ImplDX11_NewFrame(); IMGUI_IMPL_API void ImGui_ImplDX11_RenderDrawData(ImDrawData* draw_data); diff --git a/dependencies/imgui/imgui_impl_dx12.cpp b/dependencies/imgui/imgui_impl_dx12.cpp index 7fe2a7a..f48d561 100644 --- a/dependencies/imgui/imgui_impl_dx12.cpp +++ b/dependencies/imgui/imgui_impl_dx12.cpp @@ -67,6 +67,7 @@ struct ImGui_ImplDX12_Data D3D12_GPU_DESCRIPTOR_HANDLE hFontSrvGpuDescHandle; ID3D12DescriptorHeap* pd3dSrvDescHeap; UINT numFramesInFlight; + ImGui_ImplDXGI_ColorSpace ColorSpace; ImGui_ImplDX12_Data() { memset((void*)this, 0, sizeof(*this)); } }; @@ -676,23 +677,13 @@ bool ImGui_ImplDX12_CreateDeviceObjects() // Create the pixel shader { - static const char* pixelShader = - "struct PS_INPUT\ - {\ - float4 pos : SV_POSITION;\ - float4 col : COLOR0;\ - float2 uv : TEXCOORD0;\ - };\ - SamplerState sampler0 : register(s0);\ - Texture2D texture0 : register(t0);\ - \ - float4 main(PS_INPUT input) : SV_Target\ - {\ - float4 out_col = input.col * texture0.Sample(sampler0, input.uv); \ - return out_col; \ - }"; + const D3D_SHADER_MACRO shaderDefines[] = + { + { "IMGUI_COLOR_SPACE", ImGui_ImplDXGI_GetColorSpaceShaderDefine(bd->ColorSpace) }, + { nullptr, nullptr }, + }; - if (FAILED(D3DCompile(pixelShader, strlen(pixelShader), nullptr, nullptr, nullptr, "main", "ps_5_0", 0, 0, &pixelShaderBlob, nullptr))) + if (FAILED(D3DCompile(ImGui_ImplDXGI_PixelShader, strlen(ImGui_ImplDXGI_PixelShader), nullptr, shaderDefines, nullptr, "main", "ps_5_0", 0, 0, &pixelShaderBlob, nullptr))) { vertexShaderBlob->Release(); return false; // NB: Pass ID3DBlob* pErrorBlob to D3DCompile() to get error showing in (const char*)pErrorBlob->GetBufferPointer(). Make sure to Release() the blob! @@ -774,7 +765,8 @@ void ImGui_ImplDX12_InvalidateDeviceObjects() } bool ImGui_ImplDX12_Init(ID3D12Device* device, int num_frames_in_flight, DXGI_FORMAT rtv_format, ID3D12DescriptorHeap* cbv_srv_heap, - D3D12_CPU_DESCRIPTOR_HANDLE font_srv_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE font_srv_gpu_desc_handle) + D3D12_CPU_DESCRIPTOR_HANDLE font_srv_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE font_srv_gpu_desc_handle, + ImGui_ImplDXGI_ColorSpace color_space) { ImGuiIO& io = *igGetIO(); IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); @@ -790,6 +782,7 @@ bool ImGui_ImplDX12_Init(ID3D12Device* device, int num_frames_in_flight, DXGI_FO bd->pd3dDevice = device; bd->RTVFormat = rtv_format; + bd->ColorSpace = color_space; bd->hFontSrvCpuDescHandle = font_srv_cpu_desc_handle; bd->hFontSrvGpuDescHandle = font_srv_gpu_desc_handle; bd->numFramesInFlight = num_frames_in_flight; diff --git a/dependencies/imgui/imgui_impl_dx12.h b/dependencies/imgui/imgui_impl_dx12.h index f7decd1..78b1ce2 100644 --- a/dependencies/imgui/imgui_impl_dx12.h +++ b/dependencies/imgui/imgui_impl_dx12.h @@ -19,6 +19,7 @@ #pragma once #include +#include #ifndef IMGUI_DISABLE #include // DXGI_FORMAT @@ -33,7 +34,8 @@ struct D3D12_GPU_DESCRIPTOR_HANDLE; // render target and descriptor heap that contains font_srv_cpu_desc_handle/font_srv_gpu_desc_handle. // font_srv_cpu_desc_handle and font_srv_gpu_desc_handle are handles to a single SRV descriptor to use for the internal font texture. IMGUI_IMPL_API bool ImGui_ImplDX12_Init(ID3D12Device* device, int num_frames_in_flight, DXGI_FORMAT rtv_format, ID3D12DescriptorHeap* cbv_srv_heap, - D3D12_CPU_DESCRIPTOR_HANDLE font_srv_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE font_srv_gpu_desc_handle); + D3D12_CPU_DESCRIPTOR_HANDLE font_srv_cpu_desc_handle, D3D12_GPU_DESCRIPTOR_HANDLE font_srv_gpu_desc_handle, + ImGui_ImplDXGI_ColorSpace color_space = ImGui_ImplDXGI_ColorSpace_SDR); IMGUI_IMPL_API void ImGui_ImplDX12_Shutdown(); IMGUI_IMPL_API void ImGui_ImplDX12_NewFrame(); IMGUI_IMPL_API void ImGui_ImplDX12_RenderDrawData(ImDrawData* draw_data, ID3D12GraphicsCommandList* graphics_command_list); diff --git a/dependencies/imgui/imgui_impl_dxgi_hdr.h b/dependencies/imgui/imgui_impl_dxgi_hdr.h new file mode 100644 index 0000000..cc9971f --- /dev/null +++ b/dependencies/imgui/imgui_impl_dxgi_hdr.h @@ -0,0 +1,89 @@ +#pragma once + +enum ImGui_ImplDXGI_ColorSpace +{ + ImGui_ImplDXGI_ColorSpace_SDR = 0, + ImGui_ImplDXGI_ColorSpace_scRGB, + ImGui_ImplDXGI_ColorSpace_HDR10, +}; + +inline const char* ImGui_ImplDXGI_GetColorSpaceShaderDefine(ImGui_ImplDXGI_ColorSpace color_space) +{ + switch (color_space) + { + case ImGui_ImplDXGI_ColorSpace_scRGB: + return "1"; + case ImGui_ImplDXGI_ColorSpace_HDR10: + return "2"; + default: + return "0"; + } +} + +// ImGui colors and textures are treated as sRGB/Rec.709. HDR output uses a +// 203-nit graphics white, as recommended by BT.2408, while SDR output remains +// byte-for-byte compatible with the stock Dear ImGui renderer backend. +inline constexpr const char* ImGui_ImplDXGI_PixelShader = R"( +#ifndef IMGUI_COLOR_SPACE +#define IMGUI_COLOR_SPACE 0 +#endif + +struct PS_INPUT +{ + float4 pos : SV_POSITION; + float4 col : COLOR0; + float2 uv : TEXCOORD0; +}; + +SamplerState sampler0 : register(s0); +Texture2D texture0 : register(t0); + +static const float GRAPHICS_WHITE_NITS = 203.0; + +float3 SrgbToLinear(float3 color) +{ + float3 low = color / 12.92; + float3 high = pow((color + 0.055) / 1.055, 2.4); + return lerp(low, high, step(0.04045, color)); +} + +float3 Rec709ToRec2020(float3 color) +{ + const float3x3 transform = + { + 0.6274040, 0.3292820, 0.0433136, + 0.0690970, 0.9195400, 0.0113612, + 0.0163916, 0.0880132, 0.8955950 + }; + return mul(transform, color); +} + +float3 LinearToPq(float3 color) +{ + const float m1 = 2610.0 / 16384.0; + const float m2 = 2523.0 / 32.0; + const float c1 = 3424.0 / 4096.0; + const float c2 = 2413.0 / 128.0; + const float c3 = 2392.0 / 128.0; + + float3 p = pow(max(color, 0.0), m1); + return pow((c1 + c2 * p) / (1.0 + c3 * p), m2); +} + +float4 main(PS_INPUT input) : SV_Target +{ + float4 out_col = input.col * texture0.Sample(sampler0, input.uv); + +#if IMGUI_COLOR_SPACE == 1 + // scRGB is linear Rec.709 and defines 1.0 as 80 nits. + out_col.rgb = SrgbToLinear(saturate(out_col.rgb)) * (GRAPHICS_WHITE_NITS / 80.0); +#elif IMGUI_COLOR_SPACE == 2 + // HDR10 uses Rec.2020 primaries and the absolute ST.2084/PQ transfer curve. + float3 linear_rec709 = SrgbToLinear(saturate(out_col.rgb)); + float3 linear_rec2020 = Rec709ToRec2020(linear_rec709); + out_col.rgb = LinearToPq(linear_rec2020 * (GRAPHICS_WHITE_NITS / 10000.0)); +#endif + + return out_col; +} +)"; diff --git a/mhw-cs-plugin-loader/D3DModule.cpp b/mhw-cs-plugin-loader/D3DModule.cpp index 0fc5397..a8f90d9 100644 --- a/mhw-cs-plugin-loader/D3DModule.cpp +++ b/mhw-cs-plugin-loader/D3DModule.cpp @@ -25,6 +25,28 @@ // DirectXTK12 References SerializeRootSignature so we need to link this #pragma comment(lib, "d3d12.lib") +namespace { + +// DXGI exposes methods to set a swap-chain color space, but not to query the +// active one. MHW uses these back-buffer formats for its two HDR output paths. +ImGui_ImplDXGI_ColorSpace infer_imgui_color_space(DXGI_FORMAT back_buffer_format) { + switch (back_buffer_format) { + case DXGI_FORMAT_R16G16B16A16_FLOAT: + dlog::debug("Using scRGB color management for ImGui"); + return ImGui_ImplDXGI_ColorSpace_scRGB; + + case DXGI_FORMAT_R10G10B10A2_UNORM: + dlog::debug("Using HDR10 color management for ImGui"); + return ImGui_ImplDXGI_ColorSpace_HDR10; + + default: + dlog::debug("Using SDR color management for ImGui"); + return ImGui_ImplDXGI_ColorSpace_SDR; + } +} + +} + void D3DModule::initialize(CoreClr* coreclr) { if (!preloader::LoaderConfig::get().get_imgui_rendering_enabled()) { dlog::debug("Skipping D3D module initialization because imgui rendering is disabled"); @@ -587,6 +609,8 @@ void D3DModule::d3d12_initialize_imgui(IDXGISwapChain* swap_chain) { rtv_handle.ptr += rtv_descriptor_size; } + const auto imgui_color_space = infer_imgui_color_space(back_buffer_format); + if (!ImGui_ImplWin32_Init(m_game_window)) { dlog::error("Failed to initialize ImGui Win32"); return; @@ -597,7 +621,8 @@ void D3DModule::d3d12_initialize_imgui(IDXGISwapChain* swap_chain) { if (!ImGui_ImplDX12_Init(m_d3d12_device, desc.BufferCount, back_buffer_format, m_d3d12_srv_heap.Get(), m_d3d12_srv_heap->GetCPUDescriptorHandleForHeapStart(), - m_d3d12_srv_heap->GetGPUDescriptorHandleForHeapStart())) { + m_d3d12_srv_heap->GetGPUDescriptorHandleForHeapStart(), + imgui_color_space)) { dlog::error("Failed to initialize ImGui D3D12"); return; } @@ -630,6 +655,16 @@ void D3DModule::d3d11_initialize_imgui(IDXGISwapChain* swap_chain) { return; } + DXGI_FORMAT back_buffer_format = desc.BufferDesc.Format; + ComPtr back_buffer; + if (SUCCEEDED(swap_chain->GetBuffer(0, IID_PPV_ARGS(back_buffer.GetAddressOf())))) { + D3D11_TEXTURE2D_DESC back_buffer_desc; + back_buffer->GetDesc(&back_buffer_desc); + back_buffer_format = back_buffer_desc.Format; + } + + const auto imgui_color_space = infer_imgui_color_space(back_buffer_format); + RECT client_rect; GetClientRect(desc.OutputWindow, &client_rect); @@ -650,7 +685,7 @@ void D3DModule::d3d11_initialize_imgui(IDXGISwapChain* swap_chain) { return; } - if (!ImGui_ImplDX11_Init(m_d3d11_device, m_d3d11_device_context)) { + if (!ImGui_ImplDX11_Init(m_d3d11_device, m_d3d11_device_context, imgui_color_space)) { dlog::error("Failed to initialize ImGui D3D11"); return; } diff --git a/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj b/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj index 06824ea..7b14df9 100644 --- a/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj +++ b/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj @@ -135,6 +135,7 @@ + @@ -206,4 +207,4 @@ - \ No newline at end of file + diff --git a/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj.filters b/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj.filters index 296310a..0d4f70c 100644 --- a/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj.filters +++ b/mhw-cs-plugin-loader/mhw-cs-plugin-loader.vcxproj.filters @@ -143,6 +143,9 @@ Header Files\ImGui + + Header Files\ImGui + Header Files\ImGui @@ -246,4 +249,4 @@ Resource Files - \ No newline at end of file +