From 0584f8ae2a4158d363703af87442ea326fcd798d Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sun, 18 Jun 2023 12:40:59 +0300 Subject: [PATCH 1/3] Video: fix Vulkan RGBA10_2 format to the actual most common one (used as swapchain format) --- Source/Core/VideoBackends/Vulkan/VKTexture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/VideoBackends/Vulkan/VKTexture.cpp b/Source/Core/VideoBackends/Vulkan/VKTexture.cpp index 1f0b1ff0d2..f8923b4edc 100644 --- a/Source/Core/VideoBackends/Vulkan/VKTexture.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKTexture.cpp @@ -190,7 +190,7 @@ VkFormat VKTexture::GetVkFormatForHostTextureFormat(AbstractTextureFormat format return VK_FORMAT_B8G8R8A8_UNORM; case AbstractTextureFormat::RGB10_A2: - return VK_FORMAT_A2R10G10B10_UNORM_PACK32; + return VK_FORMAT_A2B10G10R10_UNORM_PACK32; case AbstractTextureFormat::RGBA16F: return VK_FORMAT_R16G16B16A16_SFLOAT; From a2702c6e27f72d227f4892561ae45915a99096e5 Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 10 Jun 2023 11:48:05 +0300 Subject: [PATCH 2/3] Video: implement color correction to match the NTSC and PAL color spaces (and gamma) that GC and Wii targeted. To further increase the accuracy of the post process phase, I've added (scRGB) HDR support, which is necessary to fully display the PAL and NTSC-J color spaces, and also to improve the quality of post process texture samplings and do them in linear space instead of gamma space (which is very important when playing at low resolutions). For SDR, the quality is also slightly increased, at least if any post process runs, as the buffer is now R10G10B10A2 (on Vulkan, DX11 and DX12) if supported; previously it was R8G8B8A8 but the alpha bits were wasted. Gamma correction is arguably the most important thing as Dolphin on Windows outputted in "sRGB" (implicitly) as that's what Windows expects by default, though sRGB gamma is very different from the gamma commonly used by video standards dating to the pre HDR era (roughly gamma 2.35). Additionally, the addition of HDR support (which is pretty straight forward and minimal), added support for our own custom AutoHDR shaders, which would allow us to achieve decent looking HDR in Dolphin games without having to use SpecialK or Windows 11 AutoHDR. Both of which don't necessarily play nice with older games with strongly different and simpler lighting. HDR should also be supported in Linux. Development of my own AutoHDR shader is almost complete and will come next. This has been carefully tested and there should be no regression in any of the different features that Dolphin offers, like multisampling, stereo rendering, other post processes, etc etc. Fixes: https://bugs.dolphin-emu.org/issues/8941 Co-authored-by: EndlesslyFlowering Co-authored-by: Dogway --- .../Sys/Shaders/default_pre_post_process.glsl | 86 +++ Source/Core/Core/Config/GraphicsSettings.cpp | 16 + Source/Core/Core/Config/GraphicsSettings.h | 21 + Source/Core/Core/DolphinAnalytics.cpp | 1 + Source/Core/VideoBackends/D3D/D3DBase.cpp | 7 +- Source/Core/VideoBackends/D3D/D3DGfx.cpp | 3 + Source/Core/VideoBackends/D3D/D3DMain.cpp | 1 + .../Core/VideoBackends/D3D/D3DSwapChain.cpp | 2 +- Source/Core/VideoBackends/D3D12/D3D12Gfx.cpp | 6 + .../VideoBackends/D3D12/D3D12SwapChain.cpp | 2 +- .../Core/VideoBackends/D3D12/DX12Context.cpp | 5 +- .../Core/VideoBackends/D3D12/VideoBackend.cpp | 1 + .../VideoBackends/D3DCommon/SwapChain.cpp | 80 ++- .../Core/VideoBackends/D3DCommon/SwapChain.h | 14 +- Source/Core/VideoBackends/Vulkan/VKGfx.cpp | 2 +- .../Core/VideoBackends/Vulkan/VKSwapChain.cpp | 55 +- .../VideoBackends/Vulkan/VulkanContext.cpp | 1 + Source/Core/VideoCommon/AbstractGfx.cpp | 2 +- Source/Core/VideoCommon/AbstractGfx.h | 4 +- .../Core/VideoCommon/FramebufferShaderGen.cpp | 11 +- .../Core/VideoCommon/FramebufferShaderGen.h | 2 +- Source/Core/VideoCommon/OnScreenUI.cpp | 6 +- Source/Core/VideoCommon/PostProcessing.cpp | 491 ++++++++++++++---- Source/Core/VideoCommon/PostProcessing.h | 30 +- Source/Core/VideoCommon/Present.cpp | 4 +- Source/Core/VideoCommon/VideoConfig.cpp | 14 +- Source/Core/VideoCommon/VideoConfig.h | 29 ++ 27 files changed, 755 insertions(+), 141 deletions(-) create mode 100644 Data/Sys/Shaders/default_pre_post_process.glsl diff --git a/Data/Sys/Shaders/default_pre_post_process.glsl b/Data/Sys/Shaders/default_pre_post_process.glsl new file mode 100644 index 0000000000..d802f10bff --- /dev/null +++ b/Data/Sys/Shaders/default_pre_post_process.glsl @@ -0,0 +1,86 @@ +// References: +// https://www.unravel.com.au/understanding-color-spaces + +// SMPTE 170M - BT.601 (NTSC-M) -> BT.709 +mat3 from_NTSCM = transpose(mat3( + 0.939497225737661, 0.0502268452914346, 0.0102759289709032, + 0.0177558637510127, 0.965824605885027, 0.0164195303639603, + -0.00162163209967010, -0.00437400622653655, 1.00599563832621)); + +// ARIB TR-B9 (9300K+27MPCD with chromatic adaptation) (NTSC-J) -> BT.709 +mat3 from_NTSCJ = transpose(mat3( + 0.823613036967492, -0.0943227111084757, 0.00799341532931119, + 0.0289258355537324, 1.02310733489462, 0.00243547111576797, + -0.00569501554980891, 0.0161828357559315, 1.22328453915712)); + +// EBU - BT.470BG/BT.601 (PAL) -> BT.709 +mat3 from_PAL = transpose(mat3( + 1.04408168421813, -0.0440816842181253, 0.000000000000000, + 0.000000000000000, 1.00000000000000, 0.000000000000000, + 0.000000000000000, 0.0118044782106489, 0.988195521789351)); + +float3 LinearTosRGBGamma(float3 color) +{ + float a = 0.055; + + for (int i = 0; i < 3; ++i) + { + float x = color[i]; + if (x <= 0.0031308) + x = x * 12.92; + else + x = (1.0 + a) * pow(x, 1.0 / 2.4) - a; + color[i] = x; + } + + return color; +} + +void main() +{ + // Note: sampling in gamma space is "wrong" if the source + // and target resolution don't match exactly. + // Fortunately at the moment here they always should but to do this correctly, + // we'd need to sample from 4 pixels, de-apply the gamma from each of these, + // and then do linear sampling on their corrected value. + float4 color = Sample(); + + // Convert to linear space to do any other kind of operation + color.rgb = pow(color.rgb, game_gamma.xxx); + + if (OptionEnabled(correct_color_space)) + { + if (game_color_space == 0) + color.rgb = color.rgb * from_NTSCM; + else if (game_color_space == 1) + color.rgb = color.rgb * from_NTSCJ; + else if (game_color_space == 2) + color.rgb = color.rgb * from_PAL; + } + + if (OptionEnabled(hdr_output)) + { + const float hdr_paper_white = hdr_paper_white_nits / hdr_sdr_white_nits; + color.rgb *= hdr_paper_white; + } + + if (OptionEnabled(linear_space_output)) + { + // Nothing to do here + } + // Correct the SDR gamma for sRGB (PC/Monitor) or ~2.2 (Common TV gamma) + else if (OptionEnabled(correct_gamma)) + { + if (OptionEnabled(sdr_display_gamma_sRGB)) + color.rgb = LinearTosRGBGamma(color.rgb); + else + color.rgb = pow(color.rgb, (1.0 / sdr_display_custom_gamma).xxx); + } + // Restore the original gamma without changes + else + { + color.rgb = pow(color.rgb, (1.0 / game_gamma).xxx); + } + + SetOutput(color); +} \ No newline at end of file diff --git a/Source/Core/Core/Config/GraphicsSettings.cpp b/Source/Core/Core/Config/GraphicsSettings.cpp index f4c16b286c..9a0dfbcd26 100644 --- a/Source/Core/Core/Config/GraphicsSettings.cpp +++ b/Source/Core/Core/Config/GraphicsSettings.cpp @@ -128,6 +128,22 @@ const Info GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION{ {System::GFX, "Enhancements", "ArbitraryMipmapDetection"}, true}; const Info GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION_THRESHOLD{ {System::GFX, "Enhancements", "ArbitraryMipmapDetectionThreshold"}, 14.0f}; +const Info GFX_ENHANCE_HDR_OUTPUT{{System::GFX, "Enhancements", "HDROutput"}, false}; + +// Color.Correction + +const Info GFX_CC_CORRECT_COLOR_SPACE{{System::GFX, "ColorCorrection", "CorrectColorSpace"}, + false}; +const Info GFX_CC_GAME_COLOR_SPACE{ + {System::GFX, "ColorCorrection", "GameColorSpace"}, ColorCorrectionRegion::SMPTE_NTSCM}; +const Info GFX_CC_CORRECT_GAMMA{{System::GFX, "ColorCorrection", "CorrectGamma"}, false}; +const Info GFX_CC_GAME_GAMMA{{System::GFX, "ColorCorrection", "GameGamma"}, 2.35f}; +const Info GFX_CC_SDR_DISPLAY_GAMMA_SRGB{ + {System::GFX, "ColorCorrection", "SDRDisplayGammaSRGB"}, true}; +const Info GFX_CC_SDR_DISPLAY_CUSTOM_GAMMA{ + {System::GFX, "ColorCorrection", "SDRDisplayCustomGamma"}, 2.2f}; +const Info GFX_CC_HDR_PAPER_WHITE_NITS{{System::GFX, "ColorCorrection", "HDRPaperWhiteNits"}, + 200.f}; // Graphics.Stereoscopy diff --git a/Source/Core/Core/Config/GraphicsSettings.h b/Source/Core/Core/Config/GraphicsSettings.h index f45d0c4811..e24d47a39b 100644 --- a/Source/Core/Core/Config/GraphicsSettings.h +++ b/Source/Core/Core/Config/GraphicsSettings.h @@ -11,6 +11,7 @@ enum class AspectMode : int; enum class ShaderCompilationMode : int; enum class StereoMode : int; enum class TextureFilteringMode : int; +enum class ColorCorrectionRegion : int; enum class TriState : int; namespace Config @@ -105,6 +106,26 @@ extern const Info GFX_ENHANCE_FORCE_TRUE_COLOR; extern const Info GFX_ENHANCE_DISABLE_COPY_FILTER; extern const Info GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION; extern const Info GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION_THRESHOLD; +extern const Info GFX_ENHANCE_HDR_OUTPUT; + +// Color.Correction + +static constexpr float GFX_CC_GAME_GAMMA_MIN = 2.2f; +static constexpr float GFX_CC_GAME_GAMMA_MAX = 2.8f; + +static constexpr float GFX_CC_DISPLAY_GAMMA_MIN = 2.2f; +static constexpr float GFX_CC_DISPLAY_GAMMA_MAX = 2.4f; + +static constexpr float GFX_CC_HDR_PAPER_WHITE_NITS_MIN = 80.f; +static constexpr float GFX_CC_HDR_PAPER_WHITE_NITS_MAX = 400.f; + +extern const Info GFX_CC_CORRECT_COLOR_SPACE; +extern const Info GFX_CC_GAME_COLOR_SPACE; +extern const Info GFX_CC_CORRECT_GAMMA; +extern const Info GFX_CC_GAME_GAMMA; +extern const Info GFX_CC_SDR_DISPLAY_GAMMA_SRGB; +extern const Info GFX_CC_SDR_DISPLAY_CUSTOM_GAMMA; +extern const Info GFX_CC_HDR_PAPER_WHITE_NITS; // Graphics.Stereoscopy diff --git a/Source/Core/Core/DolphinAnalytics.cpp b/Source/Core/Core/DolphinAnalytics.cpp index 4edb936343..a8bc51aa69 100644 --- a/Source/Core/Core/DolphinAnalytics.cpp +++ b/Source/Core/Core/DolphinAnalytics.cpp @@ -380,6 +380,7 @@ void DolphinAnalytics::MakePerGameBuilder() builder.AddData("cfg-gfx-internal-resolution", g_Config.iEFBScale); builder.AddData("cfg-gfx-tc-samples", g_Config.iSafeTextureCache_ColorSamples); builder.AddData("cfg-gfx-stereo-mode", static_cast(g_Config.stereo_mode)); + builder.AddData("cfg-gfx-hdr", static_cast(g_Config.bHDR)); builder.AddData("cfg-gfx-per-pixel-lighting", g_Config.bEnablePixelLighting); builder.AddData("cfg-gfx-shader-compilation-mode", GetShaderCompilationMode(g_Config)); builder.AddData("cfg-gfx-wait-for-shaders", g_Config.bWaitForShadersBeforeStarting); diff --git a/Source/Core/VideoBackends/D3D/D3DBase.cpp b/Source/Core/VideoBackends/D3D/D3DBase.cpp index 18dda66297..34c3a97ce3 100644 --- a/Source/Core/VideoBackends/D3D/D3DBase.cpp +++ b/Source/Core/VideoBackends/D3D/D3DBase.cpp @@ -15,6 +15,7 @@ #include "VideoBackends/D3D/D3DState.h" #include "VideoBackends/D3D/DXTexture.h" #include "VideoBackends/D3DCommon/D3DCommon.h" +#include "VideoCommon/FramebufferManager.h" #include "VideoCommon/VideoConfig.h" namespace DX11 @@ -202,12 +203,14 @@ std::vector GetAAModes(u32 adapter_index) if (temp_feature_level == D3D_FEATURE_LEVEL_10_0) return {}; + const DXGI_FORMAT target_format = + D3DCommon::GetDXGIFormatForAbstractFormat(FramebufferManager::GetEFBColorFormat(), false); std::vector aa_modes; for (u32 samples = 1; samples <= D3D11_MAX_MULTISAMPLE_SAMPLE_COUNT; ++samples) { UINT quality_levels = 0; - if (SUCCEEDED(temp_device->CheckMultisampleQualityLevels(DXGI_FORMAT_R8G8B8A8_UNORM, samples, - &quality_levels)) && + if (SUCCEEDED( + temp_device->CheckMultisampleQualityLevels(target_format, samples, &quality_levels)) && quality_levels > 0) { aa_modes.push_back(samples); diff --git a/Source/Core/VideoBackends/D3D/D3DGfx.cpp b/Source/Core/VideoBackends/D3D/D3DGfx.cpp index 5bae0477c7..a4d1f5b8b9 100644 --- a/Source/Core/VideoBackends/D3D/D3DGfx.cpp +++ b/Source/Core/VideoBackends/D3D/D3DGfx.cpp @@ -175,6 +175,9 @@ void Gfx::OnConfigChanged(u32 bits) // Quad-buffer changes require swap chain recreation. if (bits & CONFIG_CHANGE_BIT_STEREO_MODE && m_swap_chain) m_swap_chain->SetStereo(SwapChain::WantsStereo()); + + if (bits & CONFIG_CHANGE_BIT_HDR && m_swap_chain) + m_swap_chain->SetHDR(SwapChain::WantsHDR()); } void Gfx::CheckForSwapChainChanges() diff --git a/Source/Core/VideoBackends/D3D/D3DMain.cpp b/Source/Core/VideoBackends/D3D/D3DMain.cpp index 4f797fcb26..5ea3014c6c 100644 --- a/Source/Core/VideoBackends/D3D/D3DMain.cpp +++ b/Source/Core/VideoBackends/D3D/D3DMain.cpp @@ -114,6 +114,7 @@ void VideoBackend::FillBackendInfo() g_Config.backend_info.bSupportsSettingObjectNames = true; g_Config.backend_info.bSupportsPartialMultisampleResolve = true; g_Config.backend_info.bSupportsDynamicVertexLoader = false; + g_Config.backend_info.bSupportsHDROutput = true; g_Config.backend_info.Adapters = D3DCommon::GetAdapterNames(); g_Config.backend_info.AAModes = D3D::GetAAModes(g_Config.iAdapter); diff --git a/Source/Core/VideoBackends/D3D/D3DSwapChain.cpp b/Source/Core/VideoBackends/D3D/D3DSwapChain.cpp index 83c366bc8e..a25014b208 100644 --- a/Source/Core/VideoBackends/D3D/D3DSwapChain.cpp +++ b/Source/Core/VideoBackends/D3D/D3DSwapChain.cpp @@ -21,7 +21,7 @@ std::unique_ptr SwapChain::Create(const WindowSystemInfo& wsi) { std::unique_ptr swap_chain = std::make_unique(wsi, D3D::dxgi_factory.Get(), D3D::device.Get()); - if (!swap_chain->CreateSwapChain(WantsStereo())) + if (!swap_chain->CreateSwapChain(WantsStereo(), WantsHDR())) return nullptr; return swap_chain; diff --git a/Source/Core/VideoBackends/D3D12/D3D12Gfx.cpp b/Source/Core/VideoBackends/D3D12/D3D12Gfx.cpp index c6f257c735..3f8e6ae4f3 100644 --- a/Source/Core/VideoBackends/D3D12/D3D12Gfx.cpp +++ b/Source/Core/VideoBackends/D3D12/D3D12Gfx.cpp @@ -421,6 +421,12 @@ void Gfx::OnConfigChanged(u32 bits) m_swap_chain->SetStereo(SwapChain::WantsStereo()); } + if (m_swap_chain && bits & CONFIG_CHANGE_BIT_HDR) + { + ExecuteCommandList(true); + m_swap_chain->SetHDR(SwapChain::WantsHDR()); + } + // Wipe sampler cache if force texture filtering or anisotropy changes. if (bits & (CONFIG_CHANGE_BIT_ANISOTROPY | CONFIG_CHANGE_BIT_FORCE_TEXTURE_FILTERING)) { diff --git a/Source/Core/VideoBackends/D3D12/D3D12SwapChain.cpp b/Source/Core/VideoBackends/D3D12/D3D12SwapChain.cpp index 1cb4a3668b..a292797248 100644 --- a/Source/Core/VideoBackends/D3D12/D3D12SwapChain.cpp +++ b/Source/Core/VideoBackends/D3D12/D3D12SwapChain.cpp @@ -22,7 +22,7 @@ std::unique_ptr SwapChain::Create(const WindowSystemInfo& wsi) { std::unique_ptr swap_chain = std::make_unique( wsi, g_dx_context->GetDXGIFactory(), g_dx_context->GetCommandQueue()); - if (!swap_chain->CreateSwapChain(WantsStereo())) + if (!swap_chain->CreateSwapChain(WantsStereo(), WantsHDR())) return nullptr; return swap_chain; diff --git a/Source/Core/VideoBackends/D3D12/DX12Context.cpp b/Source/Core/VideoBackends/D3D12/DX12Context.cpp index 7b33ffe8d9..bf156b2fdb 100644 --- a/Source/Core/VideoBackends/D3D12/DX12Context.cpp +++ b/Source/Core/VideoBackends/D3D12/DX12Context.cpp @@ -16,6 +16,7 @@ #include "VideoBackends/D3D12/Common.h" #include "VideoBackends/D3D12/D3D12StreamBuffer.h" #include "VideoBackends/D3D12/DescriptorHeapManager.h" +#include "VideoCommon/FramebufferManager.h" #include "VideoCommon/VideoConfig.h" namespace DX12 @@ -65,11 +66,13 @@ std::vector DXContext::GetAAModes(u32 adapter_index) return {}; } + const DXGI_FORMAT target_format = + D3DCommon::GetDXGIFormatForAbstractFormat(FramebufferManager::GetEFBColorFormat(), false); std::vector aa_modes; for (u32 samples = 1; samples < D3D12_MAX_MULTISAMPLE_SAMPLE_COUNT; ++samples) { D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS multisample_quality_levels = {}; - multisample_quality_levels.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + multisample_quality_levels.Format = target_format; multisample_quality_levels.SampleCount = samples; temp_device->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, diff --git a/Source/Core/VideoBackends/D3D12/VideoBackend.cpp b/Source/Core/VideoBackends/D3D12/VideoBackend.cpp index f82b7d961b..171455cf34 100644 --- a/Source/Core/VideoBackends/D3D12/VideoBackend.cpp +++ b/Source/Core/VideoBackends/D3D12/VideoBackend.cpp @@ -90,6 +90,7 @@ void VideoBackend::FillBackendInfo() g_Config.backend_info.bSupportsPartialMultisampleResolve = true; g_Config.backend_info.bSupportsDynamicVertexLoader = true; g_Config.backend_info.bSupportsVSLinePointExpand = true; + g_Config.backend_info.bSupportsHDROutput = true; // We can only check texture support once we have a device. if (g_dx_context) diff --git a/Source/Core/VideoBackends/D3DCommon/SwapChain.cpp b/Source/Core/VideoBackends/D3DCommon/SwapChain.cpp index a6abc9159f..d0f98b3523 100644 --- a/Source/Core/VideoBackends/D3DCommon/SwapChain.cpp +++ b/Source/Core/VideoBackends/D3DCommon/SwapChain.cpp @@ -51,13 +51,18 @@ bool SwapChain::WantsStereo() return g_ActiveConfig.stereo_mode == StereoMode::QuadBuffer; } +bool SwapChain::WantsHDR() +{ + return g_ActiveConfig.bHDR; +} + u32 SwapChain::GetSwapChainFlags() const { // This flag is necessary if we want to use a flip-model swapchain without locking the framerate return m_allow_tearing_supported ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; } -bool SwapChain::CreateSwapChain(bool stereo) +bool SwapChain::CreateSwapChain(bool stereo, bool hdr) { RECT client_rc; if (GetClientRect(static_cast(m_wsi.render_surface), &client_rc)) @@ -66,6 +71,9 @@ bool SwapChain::CreateSwapChain(bool stereo) m_height = client_rc.bottom - client_rc.top; } + m_stereo = false; + m_hdr = false; + // Try using the Win8 version if available. Microsoft::WRL::ComPtr dxgi_factory2; HRESULT hr = m_dxgi_factory.As(&dxgi_factory2); @@ -81,6 +89,7 @@ bool SwapChain::CreateSwapChain(bool stereo) swap_chain_desc.SampleDesc.Count = 1; swap_chain_desc.SampleDesc.Quality = 0; swap_chain_desc.Format = GetDXGIFormatForAbstractFormat(m_texture_format, false); + swap_chain_desc.Scaling = DXGI_SCALING_STRETCH; swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; swap_chain_desc.Stereo = stereo; @@ -108,6 +117,8 @@ bool SwapChain::CreateSwapChain(bool stereo) // support the newer DXGI interface aren't going to support DX12 anyway. if (FAILED(hr)) { + hdr = false; + DXGI_SWAP_CHAIN_DESC desc = {}; desc.BufferDesc.Width = m_width; desc.BufferDesc.Height = m_height; @@ -138,6 +149,37 @@ bool SwapChain::CreateSwapChain(bool stereo) WARN_LOG_FMT(VIDEO, "MakeWindowAssociation() failed: {}", Common::HRWrap(hr)); m_stereo = stereo; + + if (hdr) + { + // Only try to activate HDR here, to avoid failing when creating the swapchain + // (we can't know if the format is supported upfront) + Microsoft::WRL::ComPtr swap_chain4; + hr = m_swap_chain->QueryInterface(IID_PPV_ARGS(&swap_chain4)); + if (SUCCEEDED(hr)) + { + UINT color_space_support = 0; + // Note that this should succeed even if HDR is not currently engaged on the monitor, + // but it should display fine nonetheless. + // We need to check for DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020 as checking for + // scRGB always returns false (DX bug). + hr = swap_chain4->CheckColorSpaceSupport(DXGI_COLOR_SPACE_RGB_FULL_G2084_NONE_P2020, + &color_space_support); + if (SUCCEEDED(hr) && (color_space_support & DXGI_SWAP_CHAIN_COLOR_SPACE_SUPPORT_FLAG_PRESENT)) + { + hr = swap_chain4->ResizeBuffers(SWAP_CHAIN_BUFFER_COUNT, 0, 0, + GetDXGIFormatForAbstractFormat(m_texture_format_hdr, false), + GetSwapChainFlags()); + if (SUCCEEDED(hr)) + { + hr = swap_chain4->SetColorSpace1(DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709); + if (SUCCEEDED(hr)) + m_hdr = hdr; + } + } + } + } + if (!CreateSwapChainBuffers()) { PanicAlertFmt("Failed to create swap chain buffers"); @@ -164,12 +206,19 @@ bool SwapChain::ResizeSwapChain() { DestroySwapChainBuffers(); - HRESULT hr = m_swap_chain->ResizeBuffers(SWAP_CHAIN_BUFFER_COUNT, 0, 0, - GetDXGIFormatForAbstractFormat(m_texture_format, false), + // The swap chain fills up the size of the window if no size is specified + HRESULT hr = m_swap_chain->ResizeBuffers(SWAP_CHAIN_BUFFER_COUNT, 0, 0, DXGI_FORMAT_UNKNOWN, GetSwapChainFlags()); + if (FAILED(hr)) WARN_LOG_FMT(VIDEO, "ResizeBuffers() failed: {}", Common::HRWrap(hr)); + Microsoft::WRL::ComPtr swap_chain4; + hr = m_swap_chain->QueryInterface(IID_PPV_ARGS(&swap_chain4)); + if (SUCCEEDED(hr)) + hr = swap_chain4->SetColorSpace1(m_hdr ? DXGI_COLOR_SPACE_RGB_FULL_G10_NONE_P709 : + DXGI_COLOR_SPACE_RGB_FULL_G22_NONE_P709); + DXGI_SWAP_CHAIN_DESC desc; if (SUCCEEDED(m_swap_chain->GetDesc(&desc))) { @@ -186,10 +235,28 @@ void SwapChain::SetStereo(bool stereo) return; DestroySwapChain(); - if (!CreateSwapChain(stereo)) + // Do not try to re-activate HDR here if it had already failed + if (!CreateSwapChain(stereo, m_hdr)) { PanicAlertFmt("Failed to switch swap chain stereo mode"); - CreateSwapChain(false); + CreateSwapChain(false, false); + } +} + +void SwapChain::SetHDR(bool hdr) +{ + if (m_hdr == hdr) + return; + + // NOTE: as an optimization here we could just call "ResizeSwapChain()" + // by adding some code to check if we could change the format to HDR. + + DestroySwapChain(); + // Do not try to re-activate stereo mode here if it had already failed + if (!CreateSwapChain(m_stereo, hdr)) + { + PanicAlertFmt("Failed to switch swap chain SDR/HDR mode"); + CreateSwapChain(false, false); } } @@ -249,7 +316,8 @@ bool SwapChain::ChangeSurface(void* native_handle) { DestroySwapChain(); m_wsi.render_surface = native_handle; - return CreateSwapChain(m_stereo); + // We only keep the swap chain settings (HDR/Stereo) that had successfully applied beofre + return CreateSwapChain(m_stereo, m_hdr); } } // namespace D3DCommon diff --git a/Source/Core/VideoBackends/D3DCommon/SwapChain.h b/Source/Core/VideoBackends/D3DCommon/SwapChain.h index debdd4d77a..7d489cc002 100644 --- a/Source/Core/VideoBackends/D3DCommon/SwapChain.h +++ b/Source/Core/VideoBackends/D3DCommon/SwapChain.h @@ -25,8 +25,13 @@ public: // Returns true if the stereo mode is quad-buffering. static bool WantsStereo(); + static bool WantsHDR(); + IDXGISwapChain* GetDXGISwapChain() const { return m_swap_chain.Get(); } - AbstractTextureFormat GetFormat() const { return m_texture_format; } + AbstractTextureFormat GetFormat() const + { + return m_hdr ? m_texture_format_hdr : m_texture_format; + } u32 GetWidth() const { return m_width; } u32 GetHeight() const { return m_height; } @@ -43,10 +48,11 @@ public: bool ChangeSurface(void* native_handle); bool ResizeSwapChain(); void SetStereo(bool stereo); + void SetHDR(bool hdr); protected: u32 GetSwapChainFlags() const; - bool CreateSwapChain(bool stereo); + bool CreateSwapChain(bool stereo = false, bool hdr = false); void DestroySwapChain(); virtual bool CreateSwapChainBuffers() = 0; @@ -56,12 +62,14 @@ protected: Microsoft::WRL::ComPtr m_dxgi_factory; Microsoft::WRL::ComPtr m_swap_chain; Microsoft::WRL::ComPtr m_d3d_device; - AbstractTextureFormat m_texture_format = AbstractTextureFormat::RGBA8; + const AbstractTextureFormat m_texture_format = AbstractTextureFormat::RGB10_A2; + const AbstractTextureFormat m_texture_format_hdr = AbstractTextureFormat::RGBA16F; u32 m_width = 1; u32 m_height = 1; bool m_stereo = false; + bool m_hdr = false; bool m_allow_tearing_supported = false; bool m_has_fullscreen = false; bool m_fullscreen_request = false; diff --git a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp index 77b0371154..7364541426 100644 --- a/Source/Core/VideoBackends/Vulkan/VKGfx.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKGfx.cpp @@ -399,7 +399,7 @@ void VKGfx::OnConfigChanged(u32 bits) } // For quad-buffered stereo we need to change the layer count, so recreate the swap chain. - if (m_swap_chain && bits & CONFIG_CHANGE_BIT_STEREO_MODE) + if (m_swap_chain && (bits & CONFIG_CHANGE_BIT_STEREO_MODE) || (bits & CONFIG_CHANGE_BIT_HDR)) { ExecuteCommandBuffer(false, true); m_swap_chain->RecreateSwapChain(); diff --git a/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp b/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp index 5e76adf788..48102df64d 100644 --- a/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp +++ b/Source/Core/VideoBackends/Vulkan/VKSwapChain.cpp @@ -154,7 +154,7 @@ bool SwapChain::SelectSurfaceFormat() &format_count, surface_formats.data()); ASSERT(res == VK_SUCCESS); - // If there is a single undefined surface format, the device doesn't care, so we'll just use RGBA + // If there is a single undefined surface format, the device doesn't care, so we'll just use RGBA8 if (surface_formats[0].format == VK_FORMAT_UNDEFINED) { m_surface_format.format = VK_FORMAT_R8G8B8A8_UNORM; @@ -162,22 +162,61 @@ bool SwapChain::SelectSurfaceFormat() return true; } - // Try to find a suitable format. + const VkSurfaceFormatKHR* surface_format_RGBA8 = nullptr; + const VkSurfaceFormatKHR* surface_format_BGRA8 = nullptr; + const VkSurfaceFormatKHR* surface_format_RGB10_A2 = nullptr; + const VkSurfaceFormatKHR* surface_format_RGBA16F_scRGB = nullptr; + + // Try to find all suitable formats. for (const VkSurfaceFormatKHR& surface_format : surface_formats) { - // Some drivers seem to return a SRGB format here (Intel Mesa). - // This results in gamma correction when presenting to the screen, which we don't want. - // Use a linear format instead, if this is the case. + // Some drivers seem to return a RGBA8 SRGB format here (Intel Mesa). + // Some other drivers return both a RGBA8 SRGB and UNORM formats (Nvidia). + // This results in gamma correction when presenting to the screen, which we don't want, + // because we already apply gamma ourselves, and we might not use sRGB gamma. + // Force using a linear format instead, if this is the case. VkFormat format = VKTexture::GetLinearFormat(surface_format.format); + if (format == VK_FORMAT_R8G8B8A8_UNORM) + surface_format_RGBA8 = &surface_format; + else if (format == VK_FORMAT_B8G8R8A8_UNORM) + surface_format_BGRA8 = &surface_format; + else if (format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 && + surface_format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) + surface_format_RGB10_A2 = &surface_format; + else if (format == VK_FORMAT_R16G16B16A16_SFLOAT && + surface_format.colorSpace == VK_COLOR_SPACE_EXTENDED_SRGB_LINEAR_EXT) + surface_format_RGBA16F_scRGB = &surface_format; + else + continue; + } + + const VkSurfaceFormatKHR* surface_format = nullptr; + + // Pick the best format. + // "g_ActiveConfig" might not have been been updated yet. + if (g_Config.bHDR && surface_format_RGBA16F_scRGB) + surface_format = surface_format_RGBA16F_scRGB; + else if (surface_format_RGB10_A2) + surface_format = surface_format_RGB10_A2; + else if (surface_format_RGBA8) + surface_format = surface_format_RGBA8; + else if (surface_format_BGRA8) + surface_format = surface_format_BGRA8; + + if (surface_format) + { + const VkFormat format = VKTexture::GetLinearFormat(surface_format->format); if (format == VK_FORMAT_R8G8B8A8_UNORM) m_texture_format = AbstractTextureFormat::RGBA8; else if (format == VK_FORMAT_B8G8R8A8_UNORM) m_texture_format = AbstractTextureFormat::BGRA8; - else - continue; + else if (format == VK_FORMAT_A2B10G10R10_UNORM_PACK32) + m_texture_format = AbstractTextureFormat::RGB10_A2; + else if (format == VK_FORMAT_R16G16B16A16_SFLOAT) + m_texture_format = AbstractTextureFormat::RGBA16F; m_surface_format.format = format; - m_surface_format.colorSpace = surface_format.colorSpace; + m_surface_format.colorSpace = surface_format->colorSpace; return true; } diff --git a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp index ed4d749878..7e53a02d7a 100644 --- a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp +++ b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp @@ -382,6 +382,7 @@ void VulkanContext::PopulateBackendInfo(VideoConfig* config) config->backend_info.bSupportsPartialMultisampleResolve = true; // Assumed support. config->backend_info.bSupportsDynamicVertexLoader = true; // Assumed support. config->backend_info.bSupportsVSLinePointExpand = true; // Assumed support. + config->backend_info.bSupportsHDROutput = true; // Assumed support. } void VulkanContext::PopulateBackendInfoAdapters(VideoConfig* config, const GPUList& gpu_list) diff --git a/Source/Core/VideoCommon/AbstractGfx.cpp b/Source/Core/VideoCommon/AbstractGfx.cpp index 7c0cc10355..4c386b12ca 100644 --- a/Source/Core/VideoCommon/AbstractGfx.cpp +++ b/Source/Core/VideoCommon/AbstractGfx.cpp @@ -177,5 +177,5 @@ bool AbstractGfx::UseGeometryShaderForUI() const // OpenGL doesn't render to a 2-layer backbuffer like D3D/Vulkan for quad-buffered stereo, // instead drawing twice and the eye selected by glDrawBuffer() (see Presenter::RenderXFBToScreen) return g_ActiveConfig.stereo_mode == StereoMode::QuadBuffer && - g_ActiveConfig.backend_info.api_type != APIType::OpenGL; + !g_ActiveConfig.backend_info.bUsesExplictQuadBuffering; } diff --git a/Source/Core/VideoCommon/AbstractGfx.h b/Source/Core/VideoCommon/AbstractGfx.h index fb8351ff88..952369e11e 100644 --- a/Source/Core/VideoCommon/AbstractGfx.h +++ b/Source/Core/VideoCommon/AbstractGfx.h @@ -159,8 +159,8 @@ public: // Called when the configuration changes, and backend structures need to be updated. virtual void OnConfigChanged(u32 changed_bits); - // Returns true if a layer-expanding geometry shader should be used when rendering the user - // interface and final XFB. + // Returns true if a layer-expanding geometry shader should be used when rendering + // the user interface on the output buffer. bool UseGeometryShaderForUI() const; // Returns info about the main surface (aka backbuffer) diff --git a/Source/Core/VideoCommon/FramebufferShaderGen.cpp b/Source/Core/VideoCommon/FramebufferShaderGen.cpp index 84e9e4b843..25b6581469 100644 --- a/Source/Core/VideoCommon/FramebufferShaderGen.cpp +++ b/Source/Core/VideoCommon/FramebufferShaderGen.cpp @@ -672,7 +672,7 @@ std::string GenerateImGuiVertexShader() return code.GetBuffer(); } -std::string GenerateImGuiPixelShader() +std::string GenerateImGuiPixelShader(bool linear_space_output) { ShaderCode code; EmitSamplerDeclarations(code, 0, 1, false); @@ -680,8 +680,13 @@ std::string GenerateImGuiPixelShader() code.Write("{{\n" " ocol0 = "); EmitSampleTexture(code, 0, "float3(v_tex0.xy, 0.0)"); - code.Write(" * v_col0;\n" - "}}\n"); + // We approximate to gamma 2.2 instead of sRGB as it barely matters for this case. + // Note that if HDR is enabled, ideally we should multiply by + // the paper white brightness for readability. + if (linear_space_output) + code.Write(" * pow(v_col0, float4(2.2f, 2.2f, 2.2f, 1.0f));\n}}\n"); + else + code.Write(" * v_col0;\n}}\n"); return code.GetBuffer(); } diff --git a/Source/Core/VideoCommon/FramebufferShaderGen.h b/Source/Core/VideoCommon/FramebufferShaderGen.h index 8a6aac553f..c97fec8991 100644 --- a/Source/Core/VideoCommon/FramebufferShaderGen.h +++ b/Source/Core/VideoCommon/FramebufferShaderGen.h @@ -24,6 +24,6 @@ std::string GenerateFormatConversionShader(EFBReinterpretType convtype, u32 samp std::string GenerateTextureReinterpretShader(TextureFormat from_format, TextureFormat to_format); std::string GenerateEFBRestorePixelShader(); std::string GenerateImGuiVertexShader(); -std::string GenerateImGuiPixelShader(); +std::string GenerateImGuiPixelShader(bool linear_space_output = false); } // namespace FramebufferShaderGen diff --git a/Source/Core/VideoCommon/OnScreenUI.cpp b/Source/Core/VideoCommon/OnScreenUI.cpp index d5949e356f..50e191a7ae 100644 --- a/Source/Core/VideoCommon/OnScreenUI.cpp +++ b/Source/Core/VideoCommon/OnScreenUI.cpp @@ -120,11 +120,15 @@ bool OnScreenUI::RecompileImGuiPipeline() return true; } + const bool linear_space_output = + g_presenter->GetBackbufferFormat() == AbstractTextureFormat::RGBA16F; + std::unique_ptr vertex_shader = g_gfx->CreateShaderFromSource( ShaderStage::Vertex, FramebufferShaderGen::GenerateImGuiVertexShader(), "ImGui vertex shader"); std::unique_ptr pixel_shader = g_gfx->CreateShaderFromSource( - ShaderStage::Pixel, FramebufferShaderGen::GenerateImGuiPixelShader(), "ImGui pixel shader"); + ShaderStage::Pixel, FramebufferShaderGen::GenerateImGuiPixelShader(linear_space_output), + "ImGui pixel shader"); if (!vertex_shader || !pixel_shader) { PanicAlertFmt("Failed to compile ImGui shaders"); diff --git a/Source/Core/VideoCommon/PostProcessing.cpp b/Source/Core/VideoCommon/PostProcessing.cpp index c2f311376b..c3a245629f 100644 --- a/Source/Core/VideoCommon/PostProcessing.cpp +++ b/Source/Core/VideoCommon/PostProcessing.cpp @@ -33,7 +33,32 @@ namespace VideoCommon { -static const char s_default_shader[] = "void main() { SetOutput(Sample()); }\n"; +static const char s_empty_pixel_shader[] = "void main() { SetOutput(Sample()); }\n"; +static const char s_default_pixel_shader_name[] = "default_pre_post_process"; +// Keep the highest quality possible to avoid losing quality on subtle gamma conversions. +// RGBA16F should have enough quality even if we store colors in gamma space on it. +static const AbstractTextureFormat s_intermediary_buffer_format = AbstractTextureFormat::RGBA16F; + +bool LoadShaderFromFile(const std::string& shader, const std::string& sub_dir, + std::string& out_code) +{ + std::string path = File::GetUserPath(D_SHADERS_IDX) + sub_dir + shader + ".glsl"; + + if (!File::Exists(path)) + { + // Fallback to shared user dir + path = File::GetSysDirectory() + SHADERS_DIR DIR_SEP + sub_dir + shader + ".glsl"; + } + + if (!File::ReadFileToString(path, out_code)) + { + out_code = ""; + ERROR_LOG_FMT(VIDEO, "Post-processing shader not found: {}", path); + return false; + } + + return true; +} PostProcessingConfiguration::PostProcessingConfiguration() = default; @@ -60,24 +85,16 @@ void PostProcessingConfiguration::LoadShader(const std::string& shader) sub_dir = PASSIVE_DIR DIR_SEP; } - // loading shader code std::string code; - std::string path = File::GetUserPath(D_SHADERS_IDX) + sub_dir + shader + ".glsl"; - - if (!File::Exists(path)) + if (!LoadShaderFromFile(shader, sub_dir, code)) { - // Fallback to shared user dir - path = File::GetSysDirectory() + SHADERS_DIR DIR_SEP + sub_dir + shader + ".glsl"; - } - - if (!File::ReadFileToString(path, code)) - { - ERROR_LOG_FMT(VIDEO, "Post-processing shader not found: {}", path); LoadDefaultShader(); return; } LoadOptions(code); + // Note that this will build the shaders with the custom options values users + // might have set in the settings LoadOptionsConfiguration(); m_current_shader_code = code; } @@ -86,7 +103,8 @@ void PostProcessingConfiguration::LoadDefaultShader() { m_options.clear(); m_any_options_dirty = false; - m_current_shader_code = s_default_shader; + m_current_shader = ""; + m_current_shader_code = s_empty_pixel_shader; } void PostProcessingConfiguration::LoadOptions(const std::string& code) @@ -242,6 +260,7 @@ void PostProcessingConfiguration::LoadOptionsConfiguration() ini.Load(File::GetUserPath(F_DOLPHINCONFIG_IDX)); std::string section = m_current_shader + "-options"; + // We already expect all the options to be marked as "dirty" when we reach here for (auto& it : m_options) { switch (it.second.m_type) @@ -375,6 +394,8 @@ static std::vector GetShaders(const std::string& sub_dir = "") { std::string name; SplitPath(path, nullptr, &name, nullptr); + if (name == s_default_pixel_shader_name) + continue; result.push_back(name); } return result; @@ -409,8 +430,15 @@ bool PostProcessing::Initialize(AbstractTextureFormat format) void PostProcessing::RecompileShader() { + // Note: for simplicity we already recompile all the shaders + // and pipelines even if there might not be need to. + + m_default_pipeline.reset(); m_pipeline.reset(); + m_default_pixel_shader.reset(); m_pixel_shader.reset(); + m_default_vertex_shader.reset(); + m_vertex_shader.reset(); if (!CompilePixelShader()) return; if (!CompileVertexShader()) @@ -421,10 +449,27 @@ void PostProcessing::RecompileShader() void PostProcessing::RecompilePipeline() { + m_default_pipeline.reset(); m_pipeline.reset(); CompilePipeline(); } +bool PostProcessing::IsColorCorrectionActive() const +{ + // We can skip the color correction pass if none of these settings are on + // (it might have still helped with gamma correct sampling, but it's not worth running it). + return g_ActiveConfig.color_correction.bCorrectColorSpace || + g_ActiveConfig.color_correction.bCorrectGamma || + m_framebuffer_format == AbstractTextureFormat::RGBA16F; +} + +bool PostProcessing::NeedsIntermediaryBuffer() const +{ + // If we have no user selected post process shader, + // there's no point in having an intermediary buffer doing nothing. + return !m_config.GetShader().empty(); +} + void PostProcessing::BlitFromTexture(const MathUtil::Rectangle& dst, const MathUtil::Rectangle& src, const AbstractTexture* src_tex, int src_layer) @@ -435,69 +480,183 @@ void PostProcessing::BlitFromTexture(const MathUtil::Rectangle& dst, RecompilePipeline(); } - if (!m_pipeline) - return; + // By default all source layers will be copied into the respective target layers + const bool copy_all_layers = src_layer < 0; + src_layer = std::max(src_layer, 0); - FillUniformBuffer(src, src_tex, src_layer); - g_vertex_manager->UploadUtilityUniforms(m_uniform_staging_buffer.data(), - static_cast(m_uniform_staging_buffer.size())); - - g_gfx->SetViewportAndScissor( - g_gfx->ConvertFramebufferRectangle(dst, g_gfx->GetCurrentFramebuffer())); - g_gfx->SetPipeline(m_pipeline.get()); - g_gfx->SetTexture(0, src_tex); + MathUtil::Rectangle src_rect = src; g_gfx->SetSamplerState(0, RenderState::GetLinearSamplerState()); - g_gfx->Draw(0, 3); + g_gfx->SetTexture(0, src_tex); + + const bool is_color_correction_active = IsColorCorrectionActive(); + const bool needs_intermediary_buffer = NeedsIntermediaryBuffer(); + const AbstractPipeline* final_pipeline = m_pipeline.get(); + std::vector* uniform_staging_buffer = &m_default_uniform_staging_buffer; + bool default_uniform_staging_buffer = true; + + // Intermediary pass. + // We draw to a high quality intermediary texture for two reasons: + // -Keep quality for gamma and gamut conversions, and HDR output + // (low bit depths lose too much quality with gamma conversions) + // -We make a texture of the exact same res as the source one, + // because all the post process shaders we already had assume that + // the source texture size (EFB) is different from the swap chain + // texture size (which matches the window size). + if (m_default_pipeline && is_color_correction_active && needs_intermediary_buffer) + { + AbstractFramebuffer* const previous_framebuffer = g_gfx->GetCurrentFramebuffer(); + + // We keep the min number of layers as the render target, + // as in case of OpenGL, the source FBX will have two layers, + // but we will render onto two separate frame buffers (one by one), + // so it would be a waste to allocate two layers (see "bUsesExplictQuadBuffering"). + const u32 target_layers = copy_all_layers ? src_tex->GetLayers() : 1; + + if (!m_intermediary_frame_buffer || !m_intermediary_color_texture || + m_intermediary_color_texture.get()->GetWidth() != static_cast(src_rect.GetWidth()) || + m_intermediary_color_texture.get()->GetHeight() != static_cast(src_rect.GetHeight()) || + m_intermediary_color_texture.get()->GetLayers() != target_layers) + { + const TextureConfig intermediary_color_texture_config( + src_rect.GetWidth(), src_rect.GetHeight(), 1, target_layers, src_tex->GetSamples(), + s_intermediary_buffer_format, AbstractTextureFlag_RenderTarget); + m_intermediary_color_texture = g_gfx->CreateTexture(intermediary_color_texture_config, + "Intermediary post process texture"); + + m_intermediary_frame_buffer = + g_gfx->CreateFramebuffer(m_intermediary_color_texture.get(), nullptr); + } + + g_gfx->SetFramebuffer(m_intermediary_frame_buffer.get()); + + FillUniformBuffer(src_rect, src_tex, src_layer, g_gfx->GetCurrentFramebuffer()->GetRect(), + g_presenter->GetTargetRectangle(), uniform_staging_buffer->data(), + !default_uniform_staging_buffer); + g_vertex_manager->UploadUtilityUniforms(uniform_staging_buffer->data(), + static_cast(uniform_staging_buffer->size())); + + g_gfx->SetViewportAndScissor(g_gfx->ConvertFramebufferRectangle( + m_intermediary_color_texture->GetRect(), m_intermediary_frame_buffer.get())); + g_gfx->SetPipeline(m_default_pipeline.get()); + g_gfx->Draw(0, 3); + + g_gfx->SetFramebuffer(previous_framebuffer); + src_rect = m_intermediary_color_texture->GetRect(); + src_tex = m_intermediary_color_texture.get(); + g_gfx->SetTexture(0, src_tex); + // The "m_intermediary_color_texture" has already copied + // from the specified source layer onto its first one. + // If we query for a layer that the source texture doesn't have, + // it will fall back on the first one anyway. + src_layer = 0; + uniform_staging_buffer = &m_uniform_staging_buffer; + default_uniform_staging_buffer = false; + } + else + { + // If we have no custom user shader selected, and color correction + // is active, directly run the fixed pipeline shader instead of + // doing two passes, with the second one doing nothing useful. + if (m_default_pipeline && is_color_correction_active) + { + final_pipeline = m_default_pipeline.get(); + } + else + { + uniform_staging_buffer = &m_uniform_staging_buffer; + default_uniform_staging_buffer = false; + } + + m_intermediary_frame_buffer.release(); + m_intermediary_color_texture.release(); + } + + // TODO: ideally we'd do the user selected post process pass in the intermediary buffer in linear + // space (instead of gamma space), so the shaders could act more accurately (and sample in linear + // space), though that would break the look of some of current post processes we have, and thus is + // better avoided for now. + + // Final pass, either a user selected shader or the default (fixed) shader. + if (final_pipeline) + { + FillUniformBuffer(src_rect, src_tex, src_layer, g_gfx->GetCurrentFramebuffer()->GetRect(), + g_presenter->GetTargetRectangle(), uniform_staging_buffer->data(), + !default_uniform_staging_buffer); + g_vertex_manager->UploadUtilityUniforms(uniform_staging_buffer->data(), + static_cast(uniform_staging_buffer->size())); + + g_gfx->SetViewportAndScissor( + g_gfx->ConvertFramebufferRectangle(dst, g_gfx->GetCurrentFramebuffer())); + g_gfx->SetPipeline(final_pipeline); + g_gfx->Draw(0, 3); + } } -std::string PostProcessing::GetUniformBufferHeader() const +std::string PostProcessing::GetUniformBufferHeader(bool user_post_process) const { std::ostringstream ss; u32 unused_counter = 1; ss << "UBO_BINDING(std140, 1) uniform PSBlock {\n"; - // Builtin uniforms - ss << " float4 resolution;\n"; + // Builtin uniforms: + + ss << " float4 resolution;\n"; // Source resolution + ss << " float4 target_resolution;\n"; ss << " float4 window_resolution;\n"; + // How many horizontal and vertical stereo views do we have? (set to 1 when we use layers instead) + ss << " int2 stereo_views;\n"; ss << " float4 src_rect;\n"; + // The first (but not necessarily only) source layer we target ss << " int src_layer;\n"; ss << " uint time;\n"; - for (u32 i = 0; i < 2; i++) - ss << " uint ubo_align_" << unused_counter++ << "_;\n"; - ss << "\n"; - // Custom options/uniforms - for (const auto& it : m_config.GetOptions()) + ss << " int correct_color_space;\n"; + ss << " int game_color_space;\n"; + ss << " int correct_gamma;\n"; + ss << " float game_gamma;\n"; + ss << " int sdr_display_gamma_sRGB;\n"; + ss << " float sdr_display_custom_gamma;\n"; + ss << " int linear_space_output;\n"; + ss << " int hdr_output;\n"; + ss << " float hdr_paper_white_nits;\n"; + ss << " float hdr_sdr_white_nits;\n"; + + if (user_post_process) { - if (it.second.m_type == PostProcessingConfiguration::ConfigurationOption::OptionType::Bool) + ss << "\n"; + // Custom options/uniforms + for (const auto& it : m_config.GetOptions()) { - ss << fmt::format(" int {};\n", it.first); - for (u32 i = 0; i < 3; i++) - ss << " int ubo_align_" << unused_counter++ << "_;\n"; - } - else if (it.second.m_type == - PostProcessingConfiguration::ConfigurationOption::OptionType::Integer) - { - u32 count = static_cast(it.second.m_integer_values.size()); - if (count == 1) + if (it.second.m_type == PostProcessingConfiguration::ConfigurationOption::OptionType::Bool) + { ss << fmt::format(" int {};\n", it.first); - else - ss << fmt::format(" int{} {};\n", count, it.first); + for (u32 i = 0; i < 3; i++) + ss << " int ubo_align_" << unused_counter++ << "_;\n"; + } + else if (it.second.m_type == + PostProcessingConfiguration::ConfigurationOption::OptionType::Integer) + { + u32 count = static_cast(it.second.m_integer_values.size()); + if (count == 1) + ss << fmt::format(" int {};\n", it.first); + else + ss << fmt::format(" int{} {};\n", count, it.first); - for (u32 i = count; i < 4; i++) - ss << " int ubo_align_" << unused_counter++ << "_;\n"; - } - else if (it.second.m_type == - PostProcessingConfiguration::ConfigurationOption::OptionType::Float) - { - u32 count = static_cast(it.second.m_float_values.size()); - if (count == 1) - ss << fmt::format(" float {};\n", it.first); - else - ss << fmt::format(" float{} {};\n", count, it.first); + for (u32 i = count; i < 4; i++) + ss << " int ubo_align_" << unused_counter++ << "_;\n"; + } + else if (it.second.m_type == + PostProcessingConfiguration::ConfigurationOption::OptionType::Float) + { + u32 count = static_cast(it.second.m_float_values.size()); + if (count == 1) + ss << fmt::format(" float {};\n", it.first); + else + ss << fmt::format(" float{} {};\n", count, it.first); - for (u32 i = count; i < 4; i++) - ss << " float ubo_align_" << unused_counter++ << "_;\n"; + for (u32 i = count; i < 4; i++) + ss << " float ubo_align_" << unused_counter++ << "_;\n"; + } } } @@ -505,11 +664,12 @@ std::string PostProcessing::GetUniformBufferHeader() const return ss.str(); } -std::string PostProcessing::GetHeader() const +std::string PostProcessing::GetHeader(bool user_post_process) const { std::ostringstream ss; - ss << GetUniformBufferHeader(); + ss << GetUniformBufferHeader(user_post_process); ss << "SAMPLER_BINDING(0) uniform sampler2DArray samp0;\n"; + ss << "SAMPLER_BINDING(1) uniform sampler2DArray samp1;\n"; if (g_ActiveConfig.backend_info.bSupportsGeometryShaders) { @@ -530,6 +690,16 @@ float4 SampleLocation(float2 location) { return texture(samp0, float3(location, float4 SampleLayer(int layer) { return texture(samp0, float3(v_tex0.xy, float(layer))); } #define SampleOffset(offset) textureOffset(samp0, v_tex0, offset) +float2 GetTargetResolution() +{ + return target_resolution.xy; +} + +float2 GetInvTargetResolution() +{ + return target_resolution.zw; +} + float2 GetWindowResolution() { return window_resolution.xy; @@ -585,7 +755,9 @@ std::string PostProcessing::GetFooter() const bool PostProcessing::CompileVertexShader() { std::ostringstream ss; - ss << GetUniformBufferHeader(); + // We never need the user selected post process custom uniforms in the vertex shader + const bool user_post_process = false; + ss << GetUniformBufferHeader(user_post_process); if (g_ActiveConfig.backend_info.bSupportsGeometryShaders) { @@ -605,16 +777,28 @@ bool PostProcessing::CompileVertexShader() ss << " opos = float4(v_tex0.xy * float2(2.0f, -2.0f) + float2(-1.0f, 1.0f), 0.0f, 1.0f);\n"; ss << " v_tex0 = float3(src_rect.xy + (src_rect.zw * v_tex0.xy), float(src_layer));\n"; + // Vulkan Y needs to be inverted on every pass if (g_ActiveConfig.backend_info.api_type == APIType::Vulkan) ss << " opos.y = -opos.y;\n"; + std::string s2 = ss.str(); + s2 += "}\n"; + m_default_vertex_shader = g_gfx->CreateShaderFromSource(ShaderStage::Vertex, s2, + "Default post-processing vertex shader"); + + // OpenGL Y needs to be inverted once only (in the last pass) + if (g_ActiveConfig.backend_info.api_type == APIType::OpenGL) + ss << " opos.y = -opos.y;\n"; + ss << "}\n"; m_vertex_shader = g_gfx->CreateShaderFromSource(ShaderStage::Vertex, ss.str(), "Post-processing vertex shader"); - if (!m_vertex_shader) + if (!m_default_vertex_shader || !m_vertex_shader) { PanicAlertFmt("Failed to compile post-processing vertex shader"); + m_default_vertex_shader.reset(); + m_vertex_shader.reset(); return false; } @@ -623,44 +807,86 @@ bool PostProcessing::CompileVertexShader() struct BuiltinUniforms { - float resolution[4]; - float window_resolution[4]; - float src_rect[4]; + // bools need to be represented as "s32" + + std::array source_resolution; + std::array target_resolution; + std::array window_resolution; + std::array stereo_views; + std::array src_rect; s32 src_layer; u32 time; - u32 padding[2]; + s32 correct_color_space; + s32 game_color_space; + s32 correct_gamma; + float game_gamma; + s32 sdr_display_gamma_sRGB; + float sdr_display_custom_gamma; + s32 linear_space_output; + s32 hdr_output; + float hdr_paper_white_nits; + float hdr_sdr_white_nits; }; -size_t PostProcessing::CalculateUniformsSize() const +size_t PostProcessing::CalculateUniformsSize(bool user_post_process) const { // Allocate a vec4 for each uniform to simplify allocation. - return sizeof(BuiltinUniforms) + m_config.GetOptions().size() * sizeof(float) * 4; + return sizeof(BuiltinUniforms) + + (user_post_process ? m_config.GetOptions().size() : 0) * sizeof(float) * 4; } void PostProcessing::FillUniformBuffer(const MathUtil::Rectangle& src, - const AbstractTexture* src_tex, int src_layer) + const AbstractTexture* src_tex, int src_layer, + const MathUtil::Rectangle& dst, + const MathUtil::Rectangle& wnd, u8* buffer, + bool user_post_process) { - const auto& window_rect = g_presenter->GetTargetRectangle(); const float rcp_src_width = 1.0f / src_tex->GetWidth(); const float rcp_src_height = 1.0f / src_tex->GetHeight(); - BuiltinUniforms builtin_uniforms = { - {static_cast(src_tex->GetWidth()), static_cast(src_tex->GetHeight()), - rcp_src_width, rcp_src_height}, - {static_cast(window_rect.GetWidth()), static_cast(window_rect.GetHeight()), - 1.0f / static_cast(window_rect.GetWidth()), - 1.0f / static_cast(window_rect.GetHeight())}, - {static_cast(src.left) * rcp_src_width, static_cast(src.top) * rcp_src_height, - static_cast(src.GetWidth()) * rcp_src_width, - static_cast(src.GetHeight()) * rcp_src_height}, - static_cast(src_layer), - static_cast(m_timer.ElapsedMs()), - }; - u8* buf = m_uniform_staging_buffer.data(); - std::memcpy(buf, &builtin_uniforms, sizeof(builtin_uniforms)); - buf += sizeof(builtin_uniforms); + BuiltinUniforms builtin_uniforms; + builtin_uniforms.source_resolution = {static_cast(src_tex->GetWidth()), + static_cast(src_tex->GetHeight()), rcp_src_width, + rcp_src_height}; + builtin_uniforms.target_resolution = { + static_cast(dst.GetWidth()), static_cast(dst.GetHeight()), + 1.0f / static_cast(dst.GetWidth()), 1.0f / static_cast(dst.GetHeight())}; + builtin_uniforms.window_resolution = { + static_cast(wnd.GetWidth()), static_cast(wnd.GetHeight()), + 1.0f / static_cast(wnd.GetWidth()), 1.0f / static_cast(wnd.GetHeight())}; + builtin_uniforms.src_rect = {static_cast(src.left) * rcp_src_width, + static_cast(src.top) * rcp_src_height, + static_cast(src.GetWidth()) * rcp_src_width, + static_cast(src.GetHeight()) * rcp_src_height}; + builtin_uniforms.src_layer = static_cast(src_layer); + builtin_uniforms.time = static_cast(m_timer.ElapsedMs()); - for (const auto& it : m_config.GetOptions()) + // Color correction related uniforms. + // These are mainly used by the "m_default_pixel_shader", + // but should also be accessible to all other shaders. + builtin_uniforms.correct_color_space = g_ActiveConfig.color_correction.bCorrectColorSpace; + builtin_uniforms.game_color_space = + static_cast(g_ActiveConfig.color_correction.game_color_space); + builtin_uniforms.correct_gamma = g_ActiveConfig.color_correction.bCorrectGamma; + builtin_uniforms.game_gamma = g_ActiveConfig.color_correction.fGameGamma; + builtin_uniforms.sdr_display_gamma_sRGB = g_ActiveConfig.color_correction.bSDRDisplayGammaSRGB; + builtin_uniforms.sdr_display_custom_gamma = + g_ActiveConfig.color_correction.fSDRDisplayCustomGamma; + // scRGB (RGBA16F) expects linear values as opposed to sRGB gamma + builtin_uniforms.linear_space_output = m_framebuffer_format == AbstractTextureFormat::RGBA16F; + // Implies ouput values can be beyond the 0-1 range + builtin_uniforms.hdr_output = m_framebuffer_format == AbstractTextureFormat::RGBA16F; + builtin_uniforms.hdr_paper_white_nits = g_ActiveConfig.color_correction.fHDRPaperWhiteNits; + // A value of 1 1 1 usually matches 80 nits in HDR + builtin_uniforms.hdr_sdr_white_nits = 80.f; + + std::memcpy(buffer, &builtin_uniforms, sizeof(builtin_uniforms)); + buffer += sizeof(builtin_uniforms); + + if (!user_post_process) + return; + + for (auto& it : m_config.GetOptions()) { union { @@ -688,53 +914,116 @@ void PostProcessing::FillUniformBuffer(const MathUtil::Rectangle& src, break; } - std::memcpy(buf, &value, sizeof(value)); - buf += sizeof(value); + it.second.m_dirty = false; + + std::memcpy(buffer, &value, sizeof(value)); + buffer += sizeof(value); } + + m_config.SetDirty(false); } bool PostProcessing::CompilePixelShader() { - m_pipeline.reset(); + m_default_pixel_shader.reset(); m_pixel_shader.reset(); - // Generate GLSL and compile the new shader. + // Generate GLSL and compile the new shaders: + + std::string default_pixel_shader_code; + if (LoadShaderFromFile(s_default_pixel_shader_name, "", default_pixel_shader_code)) + { + m_default_pixel_shader = g_gfx->CreateShaderFromSource( + ShaderStage::Pixel, GetHeader(false) + default_pixel_shader_code + GetFooter(), + "Default post-processing pixel shader"); + // We continue even if all of this failed, it doesn't matter + m_default_uniform_staging_buffer.resize(CalculateUniformsSize(false)); + } + else + { + m_default_uniform_staging_buffer.resize(0); + } + m_config.LoadShader(g_ActiveConfig.sPostProcessingShader); m_pixel_shader = g_gfx->CreateShaderFromSource( - ShaderStage::Pixel, GetHeader() + m_config.GetShaderCode() + GetFooter(), - fmt::format("Post-processing pixel shader: {}", m_config.GetShader())); + ShaderStage::Pixel, GetHeader(true) + m_config.GetShaderCode() + GetFooter(), + fmt::format("User post-processing pixel shader: {}", m_config.GetShader())); if (!m_pixel_shader) { - PanicAlertFmt("Failed to compile post-processing shader {}", m_config.GetShader()); + PanicAlertFmt("Failed to compile user post-processing shader {}", m_config.GetShader()); // Use default shader. m_config.LoadDefaultShader(); m_pixel_shader = g_gfx->CreateShaderFromSource( - ShaderStage::Pixel, GetHeader() + m_config.GetShaderCode() + GetFooter(), - "Default post-processing pixel shader"); + ShaderStage::Pixel, GetHeader(true) + m_config.GetShaderCode() + GetFooter(), + "Default user post-processing pixel shader"); if (!m_pixel_shader) + { + m_uniform_staging_buffer.resize(0); return false; + } } - m_uniform_staging_buffer.resize(CalculateUniformsSize()); + m_uniform_staging_buffer.resize(CalculateUniformsSize(true)); return true; } +bool UseGeometryShaderForPostProcess(bool is_intermediary_buffer) +{ + // We only return true on stereo modes that need to copy + // both source texture layers into the target texture layers. + // Any other case is handled manually with multiple copies, thus + // it doesn't need a geom shader. + switch (g_ActiveConfig.stereo_mode) + { + case StereoMode::QuadBuffer: + return !g_ActiveConfig.backend_info.bUsesExplictQuadBuffering; + case StereoMode::Anaglyph: + case StereoMode::Passive: + return is_intermediary_buffer; + case StereoMode::SBS: + case StereoMode::TAB: + case StereoMode::Off: + default: + return false; + } +} + bool PostProcessing::CompilePipeline() { + // Not needed. Some backends don't like making pipelines with no targets, + // and in any case, we don't need to render anything if that happened. if (m_framebuffer_format == AbstractTextureFormat::Undefined) - return true; // Not needed (some backends don't like making pipelines with no targets) + return true; + + // If this is true, the "m_default_pipeline" won't be the only one that runs + const bool needs_intermediary_buffer = NeedsIntermediaryBuffer(); AbstractPipelineConfig config = {}; - config.vertex_shader = m_vertex_shader.get(); - config.geometry_shader = - g_gfx->UseGeometryShaderForUI() ? g_shader_cache->GetTexcoordGeometryShader() : nullptr; - config.pixel_shader = m_pixel_shader.get(); + config.vertex_shader = + needs_intermediary_buffer ? m_vertex_shader.get() : m_default_vertex_shader.get(); + // This geometry shader will take care of reading both layer 0 and 1 on the source texture, + // and writing to both layer 0 and 1 on the render target. + config.geometry_shader = UseGeometryShaderForPostProcess(needs_intermediary_buffer) ? + g_shader_cache->GetTexcoordGeometryShader() : + nullptr; + config.pixel_shader = m_default_pixel_shader.get(); config.rasterization_state = RenderState::GetNoCullRasterizationState(PrimitiveType::Triangles); config.depth_state = RenderState::GetNoDepthTestingDepthState(); config.blending_state = RenderState::GetNoBlendingBlendState(); - config.framebuffer_state = RenderState::GetColorFramebufferState(m_framebuffer_format); + config.framebuffer_state = RenderState::GetColorFramebufferState( + needs_intermediary_buffer ? s_intermediary_buffer_format : m_framebuffer_format); config.usage = AbstractPipelineUsage::Utility; + // We continue even if it failed, it will be skipped later on + if (config.pixel_shader) + m_default_pipeline = g_gfx->CreatePipeline(config); + + config.vertex_shader = m_default_vertex_shader.get(); + config.geometry_shader = UseGeometryShaderForPostProcess(false) ? + g_shader_cache->GetTexcoordGeometryShader() : + nullptr; + config.pixel_shader = m_pixel_shader.get(); + config.framebuffer_state = RenderState::GetColorFramebufferState(m_framebuffer_format); m_pipeline = g_gfx->CreatePipeline(config); if (!m_pipeline) return false; diff --git a/Source/Core/VideoCommon/PostProcessing.h b/Source/Core/VideoCommon/PostProcessing.h index c7f94535f5..b8b648d2d0 100644 --- a/Source/Core/VideoCommon/PostProcessing.h +++ b/Source/Core/VideoCommon/PostProcessing.h @@ -16,12 +16,14 @@ class AbstractPipeline; class AbstractShader; class AbstractTexture; +class AbstractFramebuffer; namespace VideoCommon { class PostProcessingConfiguration { public: + // User defined post process struct ConfigurationOption { enum class OptionType @@ -105,29 +107,43 @@ public: void RecompilePipeline(); void BlitFromTexture(const MathUtil::Rectangle& dst, const MathUtil::Rectangle& src, - const AbstractTexture* src_tex, int src_layer); + const AbstractTexture* src_tex, int src_layer = -1); + + bool IsColorCorrectionActive() const; + bool NeedsIntermediaryBuffer() const; protected: - std::string GetUniformBufferHeader() const; - std::string GetHeader() const; + std::string GetUniformBufferHeader(bool user_post_process) const; + std::string GetHeader(bool user_post_process) const; std::string GetFooter() const; bool CompileVertexShader(); bool CompilePixelShader(); bool CompilePipeline(); - size_t CalculateUniformsSize() const; + size_t CalculateUniformsSize(bool user_post_process) const; void FillUniformBuffer(const MathUtil::Rectangle& src, const AbstractTexture* src_tex, - int src_layer); + int src_layer, const MathUtil::Rectangle& dst, + const MathUtil::Rectangle& wnd, u8* buffer, bool user_post_process); // Timer for determining our time value Common::Timer m_timer; - PostProcessingConfiguration m_config; + // Dolphin fixed post process: + PostProcessingConfiguration::ConfigMap m_default_options; + std::unique_ptr m_default_vertex_shader; + std::unique_ptr m_default_pixel_shader; + std::unique_ptr m_default_pipeline; + std::unique_ptr m_intermediary_frame_buffer; + std::unique_ptr m_intermediary_color_texture; + std::vector m_default_uniform_staging_buffer; + // User post process: + PostProcessingConfiguration m_config; std::unique_ptr m_vertex_shader; std::unique_ptr m_pixel_shader; std::unique_ptr m_pipeline; - AbstractTextureFormat m_framebuffer_format = AbstractTextureFormat::Undefined; std::vector m_uniform_staging_buffer; + + AbstractTextureFormat m_framebuffer_format = AbstractTextureFormat::Undefined; }; } // namespace VideoCommon diff --git a/Source/Core/VideoCommon/Present.cpp b/Source/Core/VideoCommon/Present.cpp index 6cf27cd9bb..b89c9fa32d 100644 --- a/Source/Core/VideoCommon/Present.cpp +++ b/Source/Core/VideoCommon/Present.cpp @@ -514,9 +514,11 @@ void Presenter::RenderXFBToScreen(const MathUtil::Rectangle& target_rc, m_post_processor->BlitFromTexture(left_rc, source_rc, source_texture, 0); m_post_processor->BlitFromTexture(right_rc, source_rc, source_texture, 1); } + // Every other case will be treated the same (stereo or not). + // If there's multiple source layers, they should all be copied. else { - m_post_processor->BlitFromTexture(target_rc, source_rc, source_texture, 0); + m_post_processor->BlitFromTexture(target_rc, source_rc, source_texture); } } diff --git a/Source/Core/VideoCommon/VideoConfig.cpp b/Source/Core/VideoCommon/VideoConfig.cpp index 9284c8171f..42bf178047 100644 --- a/Source/Core/VideoCommon/VideoConfig.cpp +++ b/Source/Core/VideoCommon/VideoConfig.cpp @@ -139,6 +139,15 @@ void VideoConfig::Refresh() bArbitraryMipmapDetection = Config::Get(Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION); fArbitraryMipmapDetectionThreshold = Config::Get(Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION_THRESHOLD); + bHDR = Config::Get(Config::GFX_ENHANCE_HDR_OUTPUT); + + color_correction.bCorrectColorSpace = Config::Get(Config::GFX_CC_CORRECT_COLOR_SPACE); + color_correction.game_color_space = Config::Get(Config::GFX_CC_GAME_COLOR_SPACE); + color_correction.bCorrectGamma = Config::Get(Config::GFX_CC_CORRECT_GAMMA); + color_correction.fGameGamma = Config::Get(Config::GFX_CC_GAME_GAMMA); + color_correction.bSDRDisplayGammaSRGB = Config::Get(Config::GFX_CC_SDR_DISPLAY_GAMMA_SRGB); + color_correction.fSDRDisplayCustomGamma = Config::Get(Config::GFX_CC_SDR_DISPLAY_CUSTOM_GAMMA); + color_correction.fHDRPaperWhiteNits = Config::Get(Config::GFX_CC_HDR_PAPER_WHITE_NITS); stereo_mode = Config::Get(Config::GFX_STEREO_MODE); iStereoDepth = Config::Get(Config::GFX_STEREO_DEPTH); @@ -263,6 +272,7 @@ void CheckForConfigChanges() const AspectMode old_suggested_aspect_mode = g_ActiveConfig.suggested_aspect_mode; const bool old_widescreen_hack = g_ActiveConfig.bWidescreenHack; const auto old_post_processing_shader = g_ActiveConfig.sPostProcessingShader; + const auto old_hdr = g_ActiveConfig.bHDR; UpdateActiveConfig(); FreeLook::UpdateActiveConfig(); @@ -314,6 +324,8 @@ void CheckForConfigChanges() changed_bits |= CONFIG_CHANGE_BIT_ASPECT_RATIO; if (old_post_processing_shader != g_ActiveConfig.sPostProcessingShader) changed_bits |= CONFIG_CHANGE_BIT_POST_PROCESSING_SHADER; + if (old_hdr != g_ActiveConfig.bHDR) + changed_bits |= CONFIG_CHANGE_BIT_HDR; // No changes? if (changed_bits == 0) @@ -323,7 +335,7 @@ void CheckForConfigChanges() // Framebuffer changed? if (changed_bits & (CONFIG_CHANGE_BIT_MULTISAMPLES | CONFIG_CHANGE_BIT_STEREO_MODE | - CONFIG_CHANGE_BIT_TARGET_SIZE)) + CONFIG_CHANGE_BIT_TARGET_SIZE | CONFIG_CHANGE_BIT_HDR)) { g_framebuffer_manager->RecreateEFBFramebuffer(); } diff --git a/Source/Core/VideoCommon/VideoConfig.h b/Source/Core/VideoCommon/VideoConfig.h index 33f2a06ebe..20f4aa8a28 100644 --- a/Source/Core/VideoCommon/VideoConfig.h +++ b/Source/Core/VideoCommon/VideoConfig.h @@ -52,6 +52,13 @@ enum class TextureFilteringMode : int Linear, }; +enum class ColorCorrectionRegion : int +{ + SMPTE_NTSCM, + SYSTEMJ_NTSCJ, + EBU_PAL, +}; + enum class TriState : int { Off, @@ -72,6 +79,7 @@ enum ConfigChangeBits : u32 CONFIG_CHANGE_BIT_BBOX = (1 << 7), CONFIG_CHANGE_BIT_ASPECT_RATIO = (1 << 8), CONFIG_CHANGE_BIT_POST_PROCESSING_SHADER = (1 << 9), + CONFIG_CHANGE_BIT_HDR = (1 << 10), }; // NEVER inherit from this class. @@ -101,6 +109,26 @@ struct VideoConfig final bool bDisableCopyFilter = false; bool bArbitraryMipmapDetection = false; float fArbitraryMipmapDetectionThreshold = 0; + bool bHDR = false; + + // Color Correction + struct + { + // Color Space Correction: + bool bCorrectColorSpace = false; + ColorCorrectionRegion game_color_space = ColorCorrectionRegion::SMPTE_NTSCM; + + // Gamma Correction: + bool bCorrectGamma = false; + float fGameGamma = 2.35f; + bool bSDRDisplayGammaSRGB = true; + // Custom gamma when the display is not sRGB + float fSDRDisplayCustomGamma = 2.2f; + + // HDR: + // 200 is a good default value that matches the brightness of many SDR screens + float fHDRPaperWhiteNits = 200.f; + } color_correction; // Information bool bShowFPS = false; @@ -275,6 +303,7 @@ struct VideoConfig final bool bSupportsDynamicVertexLoader = false; bool bSupportsVSLinePointExpand = false; bool bSupportsGLLayerInFS = true; + bool bSupportsHDROutput = false; } backend_info; // Utility From daddf4cd9cf7c45820f5cadd1539c6a6b366cfbc Mon Sep 17 00:00:00 2001 From: Filoppi Date: Sat, 10 Jun 2023 11:48:40 +0300 Subject: [PATCH 3/3] Qt: add color correction and HDR Qt settings widgets --- Source/Core/DolphinQt/CMakeLists.txt | 2 + .../Graphics/ColorCorrectionConfigWindow.cpp | 181 ++++++++++++++++++ .../Graphics/ColorCorrectionConfigWindow.h | 36 ++++ .../Config/Graphics/EnhancementsWidget.cpp | 36 ++++ .../Config/Graphics/EnhancementsWidget.h | 3 + Source/Core/DolphinQt/DolphinQt.vcxproj | 2 + 6 files changed, 260 insertions(+) create mode 100644 Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.cpp create mode 100644 Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 3c2c848d33..e302287677 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -87,6 +87,8 @@ add_executable(dolphin-emu Config/Graphics/GraphicsWindow.h Config/Graphics/HacksWidget.cpp Config/Graphics/HacksWidget.h + Config/Graphics/ColorCorrectionConfigWindow.cpp + Config/Graphics/ColorCorrectionConfigWindow.h Config/Graphics/PostProcessingConfigWindow.cpp Config/Graphics/PostProcessingConfigWindow.h Config/GraphicsModListWidget.cpp diff --git a/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.cpp b/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.cpp new file mode 100644 index 0000000000..5946b9210e --- /dev/null +++ b/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.cpp @@ -0,0 +1,181 @@ +// Copyright 2018 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h" + +#include +#include +#include +#include +#include + +#include "Core/Config/GraphicsSettings.h" + +#include "DolphinQt/Config/ConfigControls/ConfigBool.h" +#include "DolphinQt/Config/ConfigControls/ConfigChoice.h" +#include "DolphinQt/Config/ConfigControls/ConfigFloatSlider.h" + +#include "VideoCommon/VideoConfig.h" + +ColorCorrectionConfigWindow::ColorCorrectionConfigWindow(QWidget* parent) : QDialog(parent) +{ + setWindowTitle(tr("Color Correction Configuration")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + Create(); + ConnectWidgets(); +} + +void ColorCorrectionConfigWindow::Create() +{ + static const char TR_COLOR_SPACE_CORRECTION_DESCRIPTION[] = QT_TR_NOOP( + "Converts the colors to the color spaces that GC/Wii were meant to work with to sRGB/Rec.709." + "

There's no way of knowing what exact color space games were meant for," + "
given there were multiple standards and most games didn't acknowledge them," + "
so it's not correct to assume a format from the game disc region." + "
Just pick the one that looks more natural to you," + " or match it with the region the game was developed in." + "

HDR output is required to show all the colors from the PAL and NTSC-J color spaces." + "

If unsure, leave this unchecked."); + static const char TR_GAME_GAMMA_DESCRIPTION[] = + QT_TR_NOOP("NTSC-M and NTSC-J target gamma ~2.2. PAL targets gamma ~2.8." + "
None of the two were necessarily followed by games or TVs. 2.35 is a good " + "generic value for all regions." + "
If a game allows you to chose a gamma value, match it here."); + static const char TR_GAMMA_CORRECTION_DESCRIPTION[] = QT_TR_NOOP( + "Converts the gamma from what the game targeted to what your current SDR display targets." + "
Monitors often target sRGB. TVs often target 2.2." + "

If unsure, leave this unchecked."); + + // Color Space: + + auto* const color_space_box = new QGroupBox(tr("Color Space")); + auto* const color_space_layout = new QGridLayout(); + color_space_layout->setVerticalSpacing(7); + color_space_layout->setColumnStretch(1, 1); + color_space_box->setLayout(color_space_layout); + + m_correct_color_space = + new ConfigBool(tr("Correct Color Space"), Config::GFX_CC_CORRECT_COLOR_SPACE); + color_space_layout->addWidget(m_correct_color_space, 0, 0); + m_correct_color_space->SetDescription(tr(TR_COLOR_SPACE_CORRECTION_DESCRIPTION)); + + // "ColorCorrectionRegion" + const QStringList game_color_space_enum{tr("NTSC-M (SMPTE 170M)"), tr("NTSC-J (ARIB TR-B9)"), + tr("PAL (EBU)")}; + + m_game_color_space = new ConfigChoice(game_color_space_enum, Config::GFX_CC_GAME_COLOR_SPACE); + color_space_layout->addWidget(new QLabel(tr("Game Color Space")), 1, 0); + color_space_layout->addWidget(m_game_color_space, 1, 1); + + m_game_color_space->setEnabled(m_correct_color_space->isChecked()); + + // Gamma: + + auto* const gamma_box = new QGroupBox(tr("Gamma")); + auto* const gamma_layout = new QGridLayout(); + gamma_layout->setVerticalSpacing(7); + gamma_layout->setColumnStretch(1, 1); + gamma_box->setLayout(gamma_layout); + + m_game_gamma = new ConfigFloatSlider(Config::GFX_CC_GAME_GAMMA_MIN, Config::GFX_CC_GAME_GAMMA_MAX, + Config::GFX_CC_GAME_GAMMA, 0.01f); + gamma_layout->addWidget(new QLabel(tr("Game Gamma")), 0, 0); + gamma_layout->addWidget(m_game_gamma, 0, 1); + m_game_gamma->SetDescription(tr(TR_GAME_GAMMA_DESCRIPTION)); + m_game_gamma_value = new QLabel(tr("")); + gamma_layout->addWidget(m_game_gamma_value, 0, 2); + + m_correct_gamma = new ConfigBool(tr("Correct SDR Gamma"), Config::GFX_CC_CORRECT_GAMMA); + gamma_layout->addWidget(m_correct_gamma, 1, 0); + m_correct_gamma->SetDescription(tr(TR_GAMMA_CORRECTION_DESCRIPTION)); + + m_sdr_display_gamma_srgb = + new ConfigBool(tr("SDR Display Gamma sRGB"), Config::GFX_CC_SDR_DISPLAY_GAMMA_SRGB); + gamma_layout->addWidget(m_sdr_display_gamma_srgb, 2, 0); + + m_sdr_display_custom_gamma = + new ConfigFloatSlider(Config::GFX_CC_DISPLAY_GAMMA_MIN, Config::GFX_CC_DISPLAY_GAMMA_MAX, + Config::GFX_CC_SDR_DISPLAY_CUSTOM_GAMMA, 0.01f); + gamma_layout->addWidget(new QLabel(tr("SDR Display Custom Gamma")), 3, 0); + gamma_layout->addWidget(m_sdr_display_custom_gamma, 3, 1); + m_sdr_display_custom_gamma_value = new QLabel(tr("")); + gamma_layout->addWidget(m_sdr_display_custom_gamma_value, 3, 2); + + m_sdr_display_gamma_srgb->setEnabled(m_correct_gamma->isChecked()); + m_sdr_display_custom_gamma->setEnabled(m_correct_gamma->isChecked() && + !m_sdr_display_gamma_srgb->isChecked()); + m_game_gamma_value->setText(QString::asprintf("%f", m_game_gamma->GetValue())); + m_sdr_display_custom_gamma_value->setText( + QString::asprintf("%f", m_sdr_display_custom_gamma->GetValue())); + + // HDR: + + auto* const hdr_box = new QGroupBox(tr("HDR")); + auto* const hdr_layout = new QGridLayout(); + hdr_layout->setVerticalSpacing(7); + hdr_layout->setColumnStretch(1, 1); + hdr_box->setLayout(hdr_layout); + + m_hdr_paper_white_nits = new ConfigFloatSlider(Config::GFX_CC_HDR_PAPER_WHITE_NITS_MIN, + Config::GFX_CC_HDR_PAPER_WHITE_NITS_MAX, + Config::GFX_CC_HDR_PAPER_WHITE_NITS, 1.f); + hdr_layout->addWidget(new QLabel(tr("HDR Paper White Nits")), 0, 0); + hdr_layout->addWidget(m_hdr_paper_white_nits, 0, 1); + m_hdr_paper_white_nits_value = new QLabel(tr("")); + hdr_layout->addWidget(m_hdr_paper_white_nits_value, 0, 2); + + m_hdr_paper_white_nits_value->setText( + QString::asprintf("%f", m_hdr_paper_white_nits->GetValue())); + + // Other: + + m_button_box = new QDialogButtonBox(QDialogButtonBox::Close); + + auto* layout = new QVBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setAlignment(Qt::AlignTop); + layout->addWidget(color_space_box); + layout->addWidget(gamma_box); + layout->addWidget(hdr_box); + layout->addWidget(m_button_box); + setLayout(layout); +} + +void ColorCorrectionConfigWindow::ConnectWidgets() +{ + connect(m_correct_color_space, &QCheckBox::toggled, this, + [this] { m_game_color_space->setEnabled(m_correct_color_space->isChecked()); }); + + connect(m_game_gamma, &ConfigFloatSlider::valueChanged, this, [this] { + m_game_gamma_value->setText(QString::asprintf("%f", m_game_gamma->GetValue())); + }); + + connect(m_correct_gamma, &QCheckBox::toggled, this, [this] { + // The "m_game_gamma" shouldn't be grayed out as it can still affect the color space correction + + // For the moment we leave this enabled even when we are outputting in HDR + // (which means they'd have no influence on the final image), + // mostly because we don't have a simple way to determine if HDR is engaged from here + m_sdr_display_gamma_srgb->setEnabled(m_correct_gamma->isChecked()); + m_sdr_display_custom_gamma->setEnabled(m_correct_gamma->isChecked() && + !m_sdr_display_gamma_srgb->isChecked()); + }); + + connect(m_sdr_display_gamma_srgb, &QCheckBox::toggled, this, [this] { + m_sdr_display_custom_gamma->setEnabled(m_correct_gamma->isChecked() && + !m_sdr_display_gamma_srgb->isChecked()); + }); + + connect(m_sdr_display_custom_gamma, &ConfigFloatSlider::valueChanged, this, [this] { + m_sdr_display_custom_gamma_value->setText( + QString::asprintf("%f", m_sdr_display_custom_gamma->GetValue())); + }); + + connect(m_hdr_paper_white_nits, &ConfigFloatSlider::valueChanged, this, [this] { + m_hdr_paper_white_nits_value->setText( + QString::asprintf("%f", m_hdr_paper_white_nits->GetValue())); + }); + + connect(m_button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); +} diff --git a/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h b/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h new file mode 100644 index 0000000000..bcc27ce1c8 --- /dev/null +++ b/Source/Core/DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h @@ -0,0 +1,36 @@ +// Copyright 2018 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class QWidget; +class QLabel; +class ConfigBool; +class ConfigChoice; +class ConfigFloatSlider; +class QDialogButtonBox; + +class ColorCorrectionConfigWindow final : public QDialog +{ + Q_OBJECT +public: + explicit ColorCorrectionConfigWindow(QWidget* parent); + +private: + void Create(); + void ConnectWidgets(); + + ConfigBool* m_correct_color_space; + ConfigChoice* m_game_color_space; + ConfigFloatSlider* m_game_gamma; + QLabel* m_game_gamma_value; + ConfigBool* m_correct_gamma; + ConfigBool* m_sdr_display_gamma_srgb; + ConfigFloatSlider* m_sdr_display_custom_gamma; + QLabel* m_sdr_display_custom_gamma_value; + ConfigFloatSlider* m_hdr_paper_white_nits; + QLabel* m_hdr_paper_white_nits_value; + QDialogButtonBox* m_button_box; +}; diff --git a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp index c5889d2ad4..c1c0495929 100644 --- a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp +++ b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.cpp @@ -18,6 +18,7 @@ #include "DolphinQt/Config/ConfigControls/ConfigChoice.h" #include "DolphinQt/Config/ConfigControls/ConfigRadio.h" #include "DolphinQt/Config/ConfigControls/ConfigSlider.h" +#include "DolphinQt/Config/Graphics/ColorCorrectionConfigWindow.h" #include "DolphinQt/Config/Graphics/GraphicsWindow.h" #include "DolphinQt/Config/Graphics/PostProcessingConfigWindow.h" #include "DolphinQt/QtUtils/NonDefaultQPushButton.h" @@ -102,6 +103,8 @@ void EnhancementsWidget::CreateWidgets() m_texture_filtering_combo->addItem(tr("Force Linear and 16x Anisotropic"), TEXTURE_FILTERING_FORCE_LINEAR_ANISO_16X); + m_configure_color_correction = new NonDefaultQPushButton(tr("Configure")); + m_pp_effect = new ToolTipComboBox(); m_configure_pp_effect = new NonDefaultQPushButton(tr("Configure")); m_scaled_efb_copy = new ConfigBool(tr("Scaled EFB Copy"), Config::GFX_HACK_COPY_EFB_SCALED); @@ -116,6 +119,7 @@ void EnhancementsWidget::CreateWidgets() new ConfigBool(tr("Disable Copy Filter"), Config::GFX_ENHANCE_DISABLE_COPY_FILTER); m_arbitrary_mipmap_detection = new ConfigBool(tr("Arbitrary Mipmap Detection"), Config::GFX_ENHANCE_ARBITRARY_MIPMAP_DETECTION); + m_hdr = new ConfigBool(tr("HDR Post-Processing"), Config::GFX_ENHANCE_HDR_OUTPUT); int row = 0; enhancements_layout->addWidget(new QLabel(tr("Internal Resolution:")), row, 0); @@ -130,6 +134,10 @@ void EnhancementsWidget::CreateWidgets() enhancements_layout->addWidget(m_texture_filtering_combo, row, 1, 1, -1); ++row; + enhancements_layout->addWidget(new QLabel(tr("Color Correction:")), row, 0); + enhancements_layout->addWidget(m_configure_color_correction, row, 1, 1, -1); + ++row; + enhancements_layout->addWidget(new QLabel(tr("Post-Processing Effect:")), row, 0); enhancements_layout->addWidget(m_pp_effect, row, 1); enhancements_layout->addWidget(m_configure_pp_effect, row, 2); @@ -148,6 +156,7 @@ void EnhancementsWidget::CreateWidgets() ++row; enhancements_layout->addWidget(m_disable_copy_filter, row, 0); + enhancements_layout->addWidget(m_hdr, row, 1, 1, -1); ++row; // Stereoscopy @@ -188,11 +197,14 @@ void EnhancementsWidget::ConnectWidgets() [this](int) { SaveSettings(); }); connect(m_3d_mode, qOverload(&QComboBox::currentIndexChanged), [this] { m_block_save = true; + m_configure_color_correction->setEnabled(g_Config.backend_info.bSupportsPostProcessing); LoadPPShaders(); m_block_save = false; SaveSettings(); }); + connect(m_configure_color_correction, &QPushButton::clicked, this, + &EnhancementsWidget::ConfigureColorCorrection); connect(m_configure_pp_effect, &QPushButton::clicked, this, &EnhancementsWidget::ConfigurePostProcessingShader); } @@ -311,6 +323,8 @@ void EnhancementsWidget::LoadSettings() break; } + m_configure_color_correction->setEnabled(g_Config.backend_info.bSupportsPostProcessing); + // Post Processing Shader LoadPPShaders(); @@ -320,6 +334,9 @@ void EnhancementsWidget::LoadSettings() m_3d_convergence->setEnabled(supports_stereoscopy); m_3d_depth->setEnabled(supports_stereoscopy); m_3d_swap_eyes->setEnabled(supports_stereoscopy); + + m_hdr->setEnabled(g_Config.backend_info.bSupportsHDROutput); + m_block_save = false; } @@ -436,6 +453,9 @@ void EnhancementsWidget::AddDescriptions() "scaling filter selected by the game.

Any option except 'Default' will alter the look " "of the game's textures and might cause issues in a small number of " "games.

If unsure, select 'Default'."); + static const char TR_COLOR_CORRECTION_DESCRIPTION[] = + QT_TR_NOOP("A group of features to make the colors more accurate," + " matching the color space Wii and GC games were meant for."); static const char TR_POSTPROCESSING_DESCRIPTION[] = QT_TR_NOOP("Applies a post-processing effect after rendering a frame.

If unsure, select (off)."); @@ -498,6 +518,13 @@ void EnhancementsWidget::AddDescriptions() "reduce stutter in games that frequently load new textures. This feature is not compatible " "with GPU Texture Decoding.

If unsure, leave this " "checked."); + static const char TR_HDR_DESCRIPTION[] = QT_TR_NOOP( + "Enables scRGB HDR output (if supported by your graphics backend and monitor)." + " Fullscreen might be required." + "

This gives post process shaders more room for accuracy, allows \"AutoHDR\" " + "post-process shaders to work, and allows to fully display the PAL and NTSC-J color spaces." + "

Note that games still render in SDR internally." + "

If unsure, leave this unchecked."); m_ir_combo->SetTitle(tr("Internal Resolution")); m_ir_combo->SetDescription(tr(TR_INTERNAL_RESOLUTION_DESCRIPTION)); @@ -508,6 +535,8 @@ void EnhancementsWidget::AddDescriptions() m_texture_filtering_combo->SetTitle(tr("Texture Filtering")); m_texture_filtering_combo->SetDescription(tr(TR_FORCE_TEXTURE_FILTERING_DESCRIPTION)); + m_configure_color_correction->setToolTip(tr(TR_COLOR_CORRECTION_DESCRIPTION)); + m_pp_effect->SetTitle(tr("Post-Processing Effect")); m_pp_effect->SetDescription(tr(TR_POSTPROCESSING_DESCRIPTION)); @@ -525,6 +554,8 @@ void EnhancementsWidget::AddDescriptions() m_arbitrary_mipmap_detection->SetDescription(tr(TR_ARBITRARY_MIPMAP_DETECTION_DESCRIPTION)); + m_hdr->SetDescription(tr(TR_HDR_DESCRIPTION)); + m_3d_mode->SetTitle(tr("Stereoscopic 3D Mode")); m_3d_mode->SetDescription(tr(TR_3D_MODE_DESCRIPTION)); @@ -537,6 +568,11 @@ void EnhancementsWidget::AddDescriptions() m_3d_swap_eyes->SetDescription(tr(TR_3D_SWAP_EYES_DESCRIPTION)); } +void EnhancementsWidget::ConfigureColorCorrection() +{ + ColorCorrectionConfigWindow(this).exec(); +} + void EnhancementsWidget::ConfigurePostProcessingShader() { const std::string shader = Config::Get(Config::GFX_ENHANCE_POST_SHADER); diff --git a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h index 90d25a7040..56d8741b9c 100644 --- a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h @@ -30,6 +30,7 @@ private: void CreateWidgets(); void ConnectWidgets(); void AddDescriptions(); + void ConfigureColorCorrection(); void ConfigurePostProcessingShader(); void LoadPPShaders(); @@ -38,6 +39,7 @@ private: ToolTipComboBox* m_aa_combo; ToolTipComboBox* m_texture_filtering_combo; ToolTipComboBox* m_pp_effect; + QPushButton* m_configure_color_correction; QPushButton* m_configure_pp_effect; ConfigBool* m_scaled_efb_copy; ConfigBool* m_per_pixel_lighting; @@ -46,6 +48,7 @@ private: ConfigBool* m_force_24bit_color; ConfigBool* m_disable_copy_filter; ConfigBool* m_arbitrary_mipmap_detection; + ConfigBool* m_hdr; // Stereoscopy ConfigChoice* m_3d_mode; diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index cd0f3a7487..79b05b372b 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -80,6 +80,7 @@ + @@ -283,6 +284,7 @@ +