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 000000000000..d802f10bff86 --- /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 f4c16b286cce..9a0dfbcd2634 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 f45d0c48116d..e24d47a39bb7 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 4edb9363434a..a8bc51aa69e6 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/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index 3c2c848d331d..e30228767789 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 000000000000..5946b9210e68 --- /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 000000000000..bcc27ce1c865 --- /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 c5889d2ad4af..c1c049592999 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 90d25a704068..56d8741b9cea 100644 --- a/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h +++ b/Source/Core/DolphinQt/Config/Graphics/EnhancementsWidget.h @@ -30,6 +30,7 @@ class EnhancementsWidget final : public QWidget void CreateWidgets(); void ConnectWidgets(); void AddDescriptions(); + void ConfigureColorCorrection(); void ConfigurePostProcessingShader(); void LoadPPShaders(); @@ -38,6 +39,7 @@ class EnhancementsWidget final : public QWidget 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 @@ class EnhancementsWidget final : public QWidget 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 cd0f3a7487b0..79b05b372bca 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -80,6 +80,7 @@ + @@ -283,6 +284,7 @@ + diff --git a/Source/Core/VideoBackends/D3D/D3DBase.cpp b/Source/Core/VideoBackends/D3D/D3DBase.cpp index 18dda662975a..34c3a97ce316 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 5bae0477c740..a4d1f5b8b910 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 4f797fcb262e..5ea3014c6cff 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 83c366bc8e51..a25014b208cb 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 c6f257c73517..3f8e6ae4f372 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 1cb4a3668b97..a29279724833 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 7b33ffe8d92d..bf156b2fdbb6 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 f82b7d961b8c..171455cf34a1 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 a6abc9159f5f..d0f98b35234d 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 debdd4d77a27..7d489cc0024a 100644 --- a/Source/Core/VideoBackends/D3DCommon/SwapChain.h +++ b/Source/Core/VideoBackends/D3DCommon/SwapChain.h @@ -25,8 +25,13 @@ class SwapChain // 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 @@ class SwapChain 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 @@ class SwapChain 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 77b037115421..73645414262b 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 5e76adf7880d..48102df64dc1 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) - m_texture_format = AbstractTextureFormat::RGBA8; + surface_format_RGBA8 = &surface_format; else if (format == VK_FORMAT_B8G8R8A8_UNORM) - m_texture_format = AbstractTextureFormat::BGRA8; + 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 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/VKTexture.cpp b/Source/Core/VideoBackends/Vulkan/VKTexture.cpp index 1f0b1ff0d2a1..f8923b4edce3 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; diff --git a/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp b/Source/Core/VideoBackends/Vulkan/VulkanContext.cpp index ed4d749878c7..7e53a02d7a19 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 7c0cc10355cf..4c386b12ca0d 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 fb8351ff8837..952369e11eb1 100644 --- a/Source/Core/VideoCommon/AbstractGfx.h +++ b/Source/Core/VideoCommon/AbstractGfx.h @@ -159,8 +159,8 @@ class AbstractGfx // 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 84e9e4b8439a..25b658146925 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 8a6aac553ff9..c97fec899182 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 d5949e356f98..50e191a7ae5a 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 c2f311376bc1..c3a245629f54 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 << " float ubo_align_" << unused_counter++ << "_;\n"; + 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"; + } } } @@ -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()); + + // 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 (const auto& it : m_config.GetOptions()) + 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 c7f94535f5d1..b8b648d2d085 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 @@ class PostProcessing 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 6cf27cd9bb10..b89c9fa32dd9 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 9284c8171f83..42bf17804727 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 33f2a06ebe80..20f4aa8a2812 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