diff --git a/common/Vulkan/Context.cpp b/common/Vulkan/Context.cpp index 3438708f85..0fcce8da84 100644 --- a/common/Vulkan/Context.cpp +++ b/common/Vulkan/Context.cpp @@ -373,6 +373,11 @@ namespace Vulkan m_optional_extensions.vk_khr_shader_draw_parameters = SupportsExtension(VK_KHR_SHADER_DRAW_PARAMETERS_EXTENSION_NAME, false); +#ifdef _WIN32 + m_optional_extensions.vk_ext_full_screen_exclusive = + SupportsExtension(VK_EXT_FULL_SCREEN_EXCLUSIVE_EXTENSION_NAME, false); +#endif + return true; } diff --git a/common/Vulkan/Context.h b/common/Vulkan/Context.h index 5a812d065d..4d220bd453 100644 --- a/common/Vulkan/Context.h +++ b/common/Vulkan/Context.h @@ -54,6 +54,7 @@ namespace Vulkan bool vk_ext_calibrated_timestamps : 1; bool vk_ext_line_rasterization : 1; bool vk_ext_rasterization_order_attachment_access : 1; + bool vk_ext_full_screen_exclusive : 1; bool vk_khr_driver_properties : 1; bool vk_khr_fragment_shader_barycentric : 1; bool vk_khr_shader_draw_parameters : 1; diff --git a/common/Vulkan/SwapChain.cpp b/common/Vulkan/SwapChain.cpp index 1094c75a4b..ca96c72f8f 100644 --- a/common/Vulkan/SwapChain.cpp +++ b/common/Vulkan/SwapChain.cpp @@ -29,10 +29,12 @@ namespace Vulkan { - SwapChain::SwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR preferred_present_mode) + SwapChain::SwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR preferred_present_mode, + std::optional exclusive_fullscreen_control) : m_window_info(wi) , m_surface(surface) , m_preferred_present_mode(preferred_present_mode) + , m_exclusive_fullscreen_control(exclusive_fullscreen_control) { } @@ -412,9 +414,10 @@ namespace Vulkan } std::unique_ptr SwapChain::Create(const WindowInfo& wi, VkSurfaceKHR surface, - VkPresentModeKHR preferred_present_mode) + VkPresentModeKHR preferred_present_mode, std::optional exclusive_fullscreen_control) { - std::unique_ptr swap_chain = std::make_unique(wi, surface, preferred_present_mode); + std::unique_ptr swap_chain = std::unique_ptr( + new SwapChain(wi, surface, preferred_present_mode, exclusive_fullscreen_control)); if (!swap_chain->CreateSwapChain() || !swap_chain->SetupSwapChainImages()) return nullptr; @@ -577,6 +580,7 @@ namespace Vulkan } // Store the old/current swap chain when recreating for resize + // Old swap chain is destroyed regardless of whether the create call succeeds VkSwapchainKHR old_swap_chain = m_swap_chain; m_swap_chain = VK_NULL_HANDLE; @@ -596,10 +600,27 @@ namespace Vulkan swap_chain_info.pQueueFamilyIndices = indices.data(); } - if (m_swap_chain == VK_NULL_HANDLE) +#ifdef _WIN32 + VkSurfaceFullScreenExclusiveInfoEXT exclusive_info = {VK_STRUCTURE_TYPE_SURFACE_FULL_SCREEN_EXCLUSIVE_INFO_EXT}; + if (g_vulkan_context->GetOptionalExtensions().vk_ext_full_screen_exclusive) { - res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr, &m_swap_chain); + exclusive_info.fullScreenExclusive = m_exclusive_fullscreen_control.has_value() ? + (m_exclusive_fullscreen_control.value() ? + VK_FULL_SCREEN_EXCLUSIVE_ALLOWED_EXT : + VK_FULL_SCREEN_EXCLUSIVE_DISALLOWED_EXT) : + VK_FULL_SCREEN_EXCLUSIVE_DEFAULT_EXT; + Util::AddPointerToChain(&swap_chain_info, &exclusive_info); } + else if (m_exclusive_fullscreen_control.has_value()) + { + Console.Error("Exclusive fullscreen control requested, but VK_EXT_full_screen_exclusive is not supported."); + } +#else + if (m_exclusive_fullscreen_control.has_value()) + Console.Error("Exclusive fullscreen control requested, but is not supported on this platform."); +#endif + + res = vkCreateSwapchainKHR(g_vulkan_context->GetDevice(), &swap_chain_info, nullptr, &m_swap_chain); if (res != VK_SUCCESS) { LOG_VULKAN_ERROR(res, "vkCreateSwapchainKHR failed: "); diff --git a/common/Vulkan/SwapChain.h b/common/Vulkan/SwapChain.h index eeca0140eb..fd556e8ea3 100644 --- a/common/Vulkan/SwapChain.h +++ b/common/Vulkan/SwapChain.h @@ -28,7 +28,6 @@ namespace Vulkan class SwapChain { public: - SwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR preferred_present_mode); ~SwapChain(); // Creates a vulkan-renderable surface for the specified window handle. @@ -49,7 +48,7 @@ namespace Vulkan // Create a new swap chain from a pre-existing surface. static std::unique_ptr Create(const WindowInfo& wi, VkSurfaceKHR surface, - VkPresentModeKHR preferred_present_mode); + VkPresentModeKHR preferred_present_mode, std::optional exclusive_fullscreen_control); __fi VkSurfaceKHR GetSurface() const { return m_surface; } __fi VkSurfaceFormatKHR GetSurfaceFormat() const { return m_surface_format; } @@ -91,6 +90,9 @@ namespace Vulkan } private: + SwapChain(const WindowInfo& wi, VkSurfaceKHR surface, VkPresentModeKHR preferred_present_mode, + std::optional exclusive_fullscreen_control); + bool SelectSurfaceFormat(); bool SelectPresentMode(); @@ -131,5 +133,6 @@ namespace Vulkan u32 m_current_image = 0; u32 m_current_semaphore = 0; std::optional m_image_acquire_result; + std::optional m_exclusive_fullscreen_control; }; } // namespace Vulkan diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp index de1c2ab9ab..be1fa9c9f9 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.cpp @@ -181,7 +181,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* ////////////////////////////////////////////////////////////////////////// // HW Renderer Fixes ////////////////////////////////////////////////////////////////////////// - SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.crcFixLevel, "EmuCore/GS", "crc_hack_level", static_cast(CRCHackLevel::Automatic), -1); + SettingWidgetBinder::BindWidgetToIntSetting( + sif, m_ui.crcFixLevel, "EmuCore/GS", "crc_hack_level", static_cast(CRCHackLevel::Automatic), -1); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.halfScreenFix, "EmuCore/GS", "UserHacks_Half_Bottom_Override", -1, -1); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.cpuSpriteRenderBW, "EmuCore/GS", "UserHacks_CPUSpriteRenderBW", 0); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.cpuSpriteRenderLevel, "EmuCore/GS", "UserHacks_CPUSpriteRenderLevel", 0); @@ -199,13 +200,14 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* SettingWidgetBinder::BindWidgetToIntSetting( sif, m_ui.textureInsideRt, "EmuCore/GS", "UserHacks_TextureInsideRt", static_cast(GSTextureInRtMode::Disabled)); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.readTCOnClose, "EmuCore/GS", "UserHacks_ReadTCOnClose", false); - SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.targetPartialInvalidation, "EmuCore/GS", "UserHacks_TargetPartialInvalidation", false); + SettingWidgetBinder::BindWidgetToBoolSetting( + sif, m_ui.targetPartialInvalidation, "EmuCore/GS", "UserHacks_TargetPartialInvalidation", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.estimateTextureRegion, "EmuCore/GS", "UserHacks_EstimateTextureRegion", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.gpuPaletteConversion, "EmuCore/GS", "paltex", false); connect(m_ui.cpuSpriteRenderBW, QOverload::of(&QComboBox::currentIndexChanged), this, &GraphicsSettingsWidget::onCPUSpriteRenderBWChanged); - connect(m_ui.textureInsideRt, QOverload::of(&QComboBox::currentIndexChanged), this, - &GraphicsSettingsWidget::onTextureInsideRtChanged); + connect( + m_ui.textureInsideRt, QOverload::of(&QComboBox::currentIndexChanged), this, &GraphicsSettingsWidget::onTextureInsideRtChanged); connect(m_ui.gpuPaletteConversion, QOverload::of(&QCheckBox::stateChanged), this, &GraphicsSettingsWidget::onGpuPaletteConversionChanged); onCPUSpriteRenderBWChanged(); @@ -244,6 +246,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.useDebugDevice, "EmuCore/GS", "UseDebugDevice", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.skipPresentingDuplicateFrames, "EmuCore/GS", "SkipDuplicateFrames", false); SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.threadedPresentation, "EmuCore/GS", "DisableThreadedPresentation", false); + SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.exclusiveFullscreenControl, "EmuCore/GS", "ExclusiveFullscreenControl", -1, -1); SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.overrideTextureBarriers, "EmuCore/GS", "OverrideTextureBarriers", -1, -1); SettingWidgetBinder::BindWidgetToIntSetting( sif, m_ui.gsDumpCompression, "EmuCore/GS", "GSDumpCompression", static_cast(GSDumpCompressionMethod::Zstandard)); @@ -295,6 +298,12 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* connect(m_ui.swTextureFiltering, &QComboBox::currentIndexChanged, this, &GraphicsSettingsWidget::onSWTextureFilteringChange); updateRendererDependentOptions(); +#ifndef _WIN32 + // Exclusive fullscreen control is Windows-only. + m_ui.advancedOptionsFormLayout->removeRow(2); + m_ui.exclusiveFullscreenControl = nullptr; +#endif + #ifndef PCSX2_DEVBUILD if (!m_dialog->isPerGameSettings()) { @@ -384,8 +393,8 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* dialog->registerWidgetHelp(m_ui.PCRTCOverscan, tr("Show Overscan"), tr("Unchecked"), tr("Enables the option to show the overscan area on games which draw more than the safe area of the screen.")); - dialog->registerWidgetHelp(m_ui.fmvAspectRatio, tr("FMV Aspect Ratio"), tr("Off (Default)"), - tr("Overrides the full-motion video (FMV) aspect ratio.")); + dialog->registerWidgetHelp( + m_ui.fmvAspectRatio, tr("FMV Aspect Ratio"), tr("Off (Default)"), tr("Overrides the full-motion video (FMV) aspect ratio.")); dialog->registerWidgetHelp(m_ui.PCRTCAntiBlur, tr("Anti-Blur"), tr("Checked"), tr("Enables internal Anti-Blur hacks. Less accurate to PS2 rendering but will make a lot of games look less blurry.")); @@ -422,17 +431,17 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* dialog->registerWidgetHelp(m_ui.fullscreenModes, tr("Fullscreen Mode"), tr("Borderless Fullscreen"), tr("Chooses the fullscreen resolution and frequency.")); - dialog->registerWidgetHelp(m_ui.cropLeft, tr("Left"), tr("0px"), - tr("Changes the number of pixels cropped from the left side of the display.")); + dialog->registerWidgetHelp( + m_ui.cropLeft, tr("Left"), tr("0px"), tr("Changes the number of pixels cropped from the left side of the display.")); - dialog->registerWidgetHelp(m_ui.cropTop, tr("Top"), tr("0px"), - tr("Changes the number of pixels cropped from the top of the display.")); + dialog->registerWidgetHelp( + m_ui.cropTop, tr("Top"), tr("0px"), tr("Changes the number of pixels cropped from the top of the display.")); - dialog->registerWidgetHelp(m_ui.cropRight, tr("Right"), tr("0px"), - tr("Changes the number of pixels cropped from the right side of the display.")); + dialog->registerWidgetHelp( + m_ui.cropRight, tr("Right"), tr("0px"), tr("Changes the number of pixels cropped from the right side of the display.")); - dialog->registerWidgetHelp(m_ui.cropBottom, tr("Bottom"), tr("0px"), - tr("Changes the number of pixels cropped from the bottom of the display.")); + dialog->registerWidgetHelp( + m_ui.cropBottom, tr("Bottom"), tr("0px"), tr("Changes the number of pixels cropped from the bottom of the display.")); } // Rendering tab @@ -639,8 +648,7 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* // OSD tab { - dialog->registerWidgetHelp( - m_ui.osdScale, tr("OSD Scale"), tr("100%"), tr("Scales the size of the onscreen OSD from 50% to 500%.")); + dialog->registerWidgetHelp(m_ui.osdScale, tr("OSD Scale"), tr("100%"), tr("Scales the size of the onscreen OSD from 50% to 500%.")); dialog->registerWidgetHelp(m_ui.osdShowMessages, tr("Show OSD Messages"), tr("Checked"), tr("Shows on-screen-display messages when events occur such as save states being " @@ -702,6 +710,10 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* "renderer. This usually results in slower performance, but may be required for some " "streaming applications, or to uncap framerates on some systems.")); + dialog->registerWidgetHelp(m_ui.exclusiveFullscreenControl, tr("Allow Exclusive Fullscreen"), tr("Automatic (Default)"), + tr("Overrides the driver's heuristics for enabling exclusive fullscreen, or direct flip/scanout.
" + "Disallowing exclusive fullscreen may enable smoother task switching and overlays, but increase input latency.")); + dialog->registerWidgetHelp(m_ui.useDebugDevice, tr("Use Debug Device"), tr("Unchecked"), tr("")); dialog->registerWidgetHelp(m_ui.disableDualSource, tr("Disable Dual Source Blending"), tr("Unchecked"), tr("")); @@ -710,10 +722,9 @@ GraphicsSettingsWidget::GraphicsSettingsWidget(SettingsDialog* dialog, QWidget* dialog->registerWidgetHelp(m_ui.skipPresentingDuplicateFrames, tr("Skip Presenting Duplicate Frames"), tr("Unchecked"), tr("Detects when idle frames are being presented in 25/30fps games, and skips presenting those frames. The frame is still " - "rendered, it just means " - "the GPU has more time to complete it (this is NOT frame skipping). Can smooth our frame time fluctuations when the CPU/GPU " - "are near maximum " - "utilization, but makes frame pacing more inconsistent and can increase input lag.")); + "rendered, it just means the GPU has more time to complete it (this is NOT frame skipping). Can smooth our frame time " + "fluctuations when the CPU/GPU are near maximum utilization, but makes frame pacing more inconsistent and can increase " + "input lag.")); dialog->registerWidgetHelp(m_ui.threadedPresentation, tr("Disable Threaded Presentation"), tr("Unchecked"), tr("Presents frames on the main GS thread instead of a worker thread. Used for debugging frametime issues. " @@ -911,6 +922,8 @@ void GraphicsSettingsWidget::updateRendererDependentOptions() const bool is_hardware = (type == GSRendererType::DX11 || type == GSRendererType::DX12 || type == GSRendererType::OGL || type == GSRendererType::VK || type == GSRendererType::Metal); const bool is_software = (type == GSRendererType::SW); + const bool is_auto = (type == GSRendererType::Auto); + const bool is_vk = (type == GSRendererType::VK); const bool hw_fixes = (is_hardware && m_ui.enableHWFixes && m_ui.enableHWFixes->checkState() == Qt::Checked); const int prev_tab = m_ui.tabs->currentIndex(); @@ -946,6 +959,8 @@ void GraphicsSettingsWidget::updateRendererDependentOptions() m_ui.disableFramebufferFetch->setDisabled(is_sw_dx); + m_ui.exclusiveFullscreenControl->setEnabled(is_auto || is_vk); + // populate adapters std::vector adapters; std::vector fullscreen_modes; diff --git a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui index d8bd66e063..0b21df78ca 100644 --- a/pcsx2-qt/Settings/GraphicsSettingsWidget.ui +++ b/pcsx2-qt/Settings/GraphicsSettingsWidget.ui @@ -2117,6 +2117,32 @@ + + + + Allow Exclusive Fullscreen: + + + + + + + + Automatic (Default) + + + + + Disallowed + + + + + Allowed + + + + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 2c9eb0f35a..dfcc3ff7f8 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -19,6 +19,7 @@ #include "common/General.h" #include #include +#include #include class SettingsInterface; @@ -626,6 +627,9 @@ struct Pcsx2Config static const char* GetRendererName(GSRendererType type); + /// Converts a tri-state option to an optional boolean value. + static std::optional TriStateToOptionalBoolean(int value); + static constexpr float DEFAULT_FRAME_RATE_NTSC = 59.94f; static constexpr float DEFAULT_FRAME_RATE_PAL = 50.00f; @@ -778,6 +782,7 @@ struct Pcsx2Config int SaveN = 0; int SaveL = 5000; + s8 ExclusiveFullscreenControl = -1; GSScreenshotSize ScreenshotSize = GSScreenshotSize::WindowResolution; GSScreenshotFormat ScreenshotFormat = GSScreenshotFormat::PNG; int ScreenshotQuality = 50; diff --git a/pcsx2/Frontend/FullscreenUI.cpp b/pcsx2/Frontend/FullscreenUI.cpp index 62ea3be507..758542642b 100644 --- a/pcsx2/Frontend/FullscreenUI.cpp +++ b/pcsx2/Frontend/FullscreenUI.cpp @@ -3325,6 +3325,10 @@ void FullscreenUI::DrawGraphicsSettingsPage() DrawIntListSetting(bsi, "Hardware Download Mode", "Changes synchronization behavior for GS downloads.", "EmuCore/GS", "HWDownloadMode", static_cast(GSHardwareDownloadMode::Enabled), s_hw_download, std::size(s_hw_download)); } + DrawIntListSetting(bsi, "Allow Exclusive Fullscreen", + "Overrides the driver's heuristics for enabling exclusive fullscreen, or direct flip/scanout.", "EmuCore/GS", + "ExclusiveFullscreenControl", -1, s_generic_options, std::size(s_generic_options), -1, + (renderer == GSRendererType::Auto || renderer == GSRendererType::VK)); DrawIntListSetting(bsi, "Override Texture Barriers", "Forces texture barrier functionality to the specified value.", "EmuCore/GS", "OverrideTextureBarriers", -1, s_generic_options, std::size(s_generic_options), -1); DrawIntListSetting(bsi, "GS Dump Compression", "Sets the compression algorithm for GS dumps.", "EmuCore/GS", "GSDumpCompression", diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index 23b8fcc85a..9d86c537e4 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -306,7 +306,8 @@ bool GSDeviceVK::UpdateWindow() return false; } - m_swap_chain = Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode)); + m_swap_chain = Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode), + Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)); if (!m_swap_chain) { Console.Error("Failed to create swap chain"); @@ -659,7 +660,8 @@ bool GSDeviceVK::CreateDeviceAndSwapChain() if (surface != VK_NULL_HANDLE) { m_swap_chain = - Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode)); + Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode), + Pcsx2Config::GSOptions::TriStateToOptionalBoolean(GSConfig.ExclusiveFullscreenControl)); if (!m_swap_chain) { Console.Error("Failed to create swap chain"); diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index de8af33aad..5feb45df82 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -395,6 +395,11 @@ const char* Pcsx2Config::GSOptions::GetRendererName(GSRendererType type) } } +std::optional Pcsx2Config::GSOptions::TriStateToOptionalBoolean(int value) +{ + return (value < 0) ? std::optional(std::nullopt) : std::optional((value != 0)); +} + Pcsx2Config::GSOptions::GSOptions() { bitset = 0; @@ -539,6 +544,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const OpEqu(SaveN) && OpEqu(SaveL) && + OpEqu(ExclusiveFullscreenControl) && OpEqu(ScreenshotSize) && OpEqu(ScreenshotFormat) && OpEqu(ScreenshotQuality) && @@ -574,7 +580,8 @@ bool Pcsx2Config::GSOptions::RestartOptionsAreEqual(const GSOptions& right) cons OpEqu(DisableDualSourceBlend) && OpEqu(DisableFramebufferFetch) && OpEqu(DisableThreadedPresentation) && - OpEqu(OverrideTextureBarriers); + OpEqu(OverrideTextureBarriers) && + OpEqu(ExclusiveFullscreenControl); } void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) @@ -727,6 +734,7 @@ void Pcsx2Config::GSOptions::LoadSave(SettingsWrapper& wrap) GSSettingInt(ShadeBoost_Brightness); GSSettingInt(ShadeBoost_Contrast); GSSettingInt(ShadeBoost_Saturation); + GSSettingInt(ExclusiveFullscreenControl); GSSettingIntEx(PNGCompressionLevel, "png_compression_level"); GSSettingIntEx(SaveN, "saven"); GSSettingIntEx(SaveL, "savel");