From befbf57191d62fcd9c162362a5ed4050178e894b Mon Sep 17 00:00:00 2001 From: Stenzek Date: Tue, 25 Apr 2023 22:52:41 +1000 Subject: [PATCH] GS: Refactor exclusive fullscreen yet again Also acquire render window as late as possible. Limits the duration that the main window isn't displaying anything. --- common/Vulkan/Context.cpp | 110 ++-------- common/Vulkan/Context.h | 9 +- common/Vulkan/SwapChain.cpp | 4 +- pcsx2-gsrunner/Main.cpp | 8 +- pcsx2-qt/MainWindow.cpp | 105 ++++------ pcsx2-qt/MainWindow.h | 7 +- pcsx2-qt/QtHost.cpp | 42 ++-- pcsx2-qt/QtHost.h | 14 +- pcsx2/GS/GS.cpp | 98 ++------- pcsx2/GS/GS.h | 8 +- pcsx2/GS/Renderers/Common/GSDevice.cpp | 36 +++- pcsx2/GS/Renderers/Common/GSDevice.h | 17 +- pcsx2/GS/Renderers/DX11/D3D.cpp | 73 +++++++ pcsx2/GS/Renderers/DX11/D3D.h | 4 + pcsx2/GS/Renderers/DX11/GSDevice11.cpp | 229 ++++++++++----------- pcsx2/GS/Renderers/DX11/GSDevice11.h | 10 +- pcsx2/GS/Renderers/DX12/GSDevice12.cpp | 233 +++++++++++----------- pcsx2/GS/Renderers/DX12/GSDevice12.h | 10 +- pcsx2/GS/Renderers/Metal/GSDeviceMTL.h | 6 +- pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm | 34 ++-- pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp | 51 +++-- pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h | 7 +- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp | 205 +++++++++++++++---- pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h | 8 +- tests/ctest/core/StubHost.cpp | 7 +- 25 files changed, 710 insertions(+), 625 deletions(-) diff --git a/common/Vulkan/Context.cpp b/common/Vulkan/Context.cpp index bfa2dbccb6..2a94bc66bf 100644 --- a/common/Vulkan/Context.cpp +++ b/common/Vulkan/Context.cpp @@ -70,8 +70,7 @@ namespace Vulkan Context::~Context() = default; - VkInstance Context::CreateVulkanInstance( - const WindowInfo* wi, bool enable_debug_utils, bool enable_validation_layer) + VkInstance Context::CreateVulkanInstance(const WindowInfo& wi, bool enable_debug_utils, bool enable_validation_layer) { ExtensionList enabled_extensions; if (!SelectInstanceExtensions(&enabled_extensions, wi, enable_debug_utils)) @@ -118,7 +117,7 @@ namespace Vulkan return instance; } - bool Context::SelectInstanceExtensions(ExtensionList* extension_list, const WindowInfo* wi, bool enable_debug_utils) + bool Context::SelectInstanceExtensions(ExtensionList* extension_list, const WindowInfo& wi, bool enable_debug_utils) { u32 extension_count = 0; VkResult res = vkEnumerateInstanceExtensionProperties(nullptr, &extension_count, nullptr); @@ -155,34 +154,32 @@ namespace Vulkan }; // Common extensions - if (wi && wi->type != WindowInfo::Type::Surfaceless && !SupportsExtension(VK_KHR_SURFACE_EXTENSION_NAME, true)) + if (wi.type != WindowInfo::Type::Surfaceless && !SupportsExtension(VK_KHR_SURFACE_EXTENSION_NAME, true)) return false; #if defined(VK_USE_PLATFORM_WIN32_KHR) - if (wi && wi->type == WindowInfo::Type::Win32 && !SupportsExtension(VK_KHR_WIN32_SURFACE_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::Win32 && !SupportsExtension(VK_KHR_WIN32_SURFACE_EXTENSION_NAME, true)) return false; #endif #if defined(VK_USE_PLATFORM_XLIB_KHR) - if (wi && wi->type == WindowInfo::Type::X11 && !SupportsExtension(VK_KHR_XLIB_SURFACE_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::X11 && !SupportsExtension(VK_KHR_XLIB_SURFACE_EXTENSION_NAME, true)) return false; #endif #if defined(VK_USE_PLATFORM_WAYLAND_KHR) - if (wi && wi->type == WindowInfo::Type::Wayland && - !SupportsExtension(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::Wayland && !SupportsExtension(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME, true)) return false; #endif #if defined(VK_USE_PLATFORM_ANDROID_KHR) - if (wi && wi->type == WindowInfo::Type::Android && - !SupportsExtension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::Android && !SupportsExtension(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME, true)) return false; #endif #if defined(VK_USE_PLATFORM_METAL_EXT) - if (wi && wi->type == WindowInfo::Type::MacOS && !SupportsExtension(VK_EXT_METAL_SURFACE_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::MacOS && !SupportsExtension(VK_EXT_METAL_SURFACE_EXTENSION_NAME, true)) return false; #endif #if 0 - if (wi && wi->type == WindowInfo::Type::Display && !SupportsExtension(VK_KHR_DISPLAY_EXTENSION_NAME, true)) + if (wi.type == WindowInfo::Type::Display && !SupportsExtension(VK_KHR_DISPLAY_EXTENSION_NAME, true)) return false; #endif @@ -274,91 +271,11 @@ namespace Vulkan return gpu_names; } - bool Context::Create(std::string_view gpu_name, const WindowInfo* wi, std::unique_ptr* out_swap_chain, - VkPresentModeKHR preferred_present_mode, bool threaded_presentation, bool enable_debug_utils, - bool enable_validation_layer) + bool Context::Create(VkInstance instance, VkSurfaceKHR surface, VkPhysicalDevice physical_device, + bool threaded_presentation, bool enable_debug_utils, bool enable_validation_layer) { pxAssertMsg(!g_vulkan_context, "Has no current context"); - - if (!Vulkan::LoadVulkanLibrary()) - { - Console.Error("Failed to load Vulkan library"); - return false; - } - - const bool enable_surface = (wi && wi->type != WindowInfo::Type::Surfaceless); - VkInstance instance = CreateVulkanInstance(wi, enable_debug_utils, enable_validation_layer); - if (instance == VK_NULL_HANDLE) - { - if (enable_debug_utils || enable_validation_layer) - { - // Try again without the validation layer. - enable_debug_utils = false; - enable_validation_layer = false; - instance = CreateVulkanInstance(wi, enable_debug_utils, enable_validation_layer); - if (instance == VK_NULL_HANDLE) - { - Vulkan::UnloadVulkanLibrary(); - return false; - } - - Console.Error("Vulkan validation/debug layers requested but are unavailable. Creating non-debug device."); - } - } - - if (!Vulkan::LoadVulkanInstanceFunctions(instance)) - { - Console.Error("Failed to load Vulkan instance functions"); - vkDestroyInstance(instance, nullptr); - Vulkan::UnloadVulkanLibrary(); - return false; - } - - GPUList gpus = EnumerateGPUs(instance); - if (gpus.empty()) - { - vkDestroyInstance(instance, nullptr); - Vulkan::UnloadVulkanLibrary(); - return false; - } - - u32 gpu_index = 0; - GPUNameList gpu_names = EnumerateGPUNames(instance); - if (!gpu_name.empty()) - { - for (; gpu_index < static_cast(gpu_names.size()); gpu_index++) - { - Console.WriteLn("GPU %u: %s", static_cast(gpu_index), gpu_names[gpu_index].c_str()); - if (gpu_names[gpu_index] == gpu_name) - break; - } - - if (gpu_index == static_cast(gpu_names.size())) - { - Console.Warning("Requested GPU '%s' not found, using first (%s)", std::string(gpu_name).c_str(), - gpu_names[0].c_str()); - gpu_index = 0; - } - } - else - { - Console.WriteLn("No GPU requested, using first (%s)", gpu_names[0].c_str()); - } - - VkSurfaceKHR surface = VK_NULL_HANDLE; - WindowInfo wi_copy; - if (wi) - wi_copy = *wi; - - if (enable_surface && - (surface = SwapChain::CreateVulkanSurface(instance, gpus[gpu_index], &wi_copy)) == VK_NULL_HANDLE) - { - vkDestroyInstance(instance, nullptr); - Vulkan::UnloadVulkanLibrary(); - return false; - } - - g_vulkan_context.reset(new Context(instance, gpus[gpu_index])); + g_vulkan_context.reset(new Context(instance, physical_device)); if (enable_debug_utils) g_vulkan_context->EnableDebugUtils(); @@ -367,8 +284,7 @@ namespace Vulkan if (!g_vulkan_context->CreateDevice(surface, enable_validation_layer, nullptr, 0, nullptr, 0, nullptr) || !g_vulkan_context->CreateAllocator() || !g_vulkan_context->CreateGlobalDescriptorPool() || !g_vulkan_context->CreateCommandBuffers() || !g_vulkan_context->CreateTextureStreamBuffer() || - !g_vulkan_context->InitSpinResources() || - (enable_surface && (*out_swap_chain = SwapChain::Create(wi_copy, surface, preferred_present_mode)) == nullptr)) + !g_vulkan_context->InitSpinResources()) { // Since we are destroying the instance, we're also responsible for destroying the surface. if (surface != VK_NULL_HANDLE) diff --git a/common/Vulkan/Context.h b/common/Vulkan/Context.h index b5604a81ad..fc5ec4b3b4 100644 --- a/common/Vulkan/Context.h +++ b/common/Vulkan/Context.h @@ -63,7 +63,7 @@ namespace Vulkan // Helper method to create a Vulkan instance. static VkInstance CreateVulkanInstance( - const WindowInfo* wi, bool enable_debug_utils, bool enable_validation_layer); + const WindowInfo& wi, bool enable_debug_utils, bool enable_validation_layer); // Returns a list of Vulkan-compatible GPUs. using GPUList = std::vector; @@ -72,9 +72,8 @@ namespace Vulkan static GPUNameList EnumerateGPUNames(VkInstance instance); // Creates a new context and sets it up as global. - static bool Create(std::string_view gpu_name, const WindowInfo* wi, std::unique_ptr* out_swap_chain, - VkPresentModeKHR preferred_present_mode, bool threaded_presentation, bool enable_debug_utils, - bool enable_validation_layer); + static bool Create(VkInstance instance, VkSurfaceKHR surface, VkPhysicalDevice physical_device, + bool threaded_presentation, bool enable_debug_utils, bool enable_validation_layer); // Destroys context. static void Destroy(); @@ -267,7 +266,7 @@ namespace Vulkan using ExtensionList = std::vector; static bool SelectInstanceExtensions( - ExtensionList* extension_list, const WindowInfo* wi, bool enable_debug_utils); + ExtensionList* extension_list, const WindowInfo& wi, bool enable_debug_utils); bool SelectDeviceExtensions(ExtensionList* extension_list, bool enable_surface); bool SelectDeviceFeatures(const VkPhysicalDeviceFeatures* required_features); bool CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer, const char** required_device_extensions, diff --git a/common/Vulkan/SwapChain.cpp b/common/Vulkan/SwapChain.cpp index c973546928..1094c75a4b 100644 --- a/common/Vulkan/SwapChain.cpp +++ b/common/Vulkan/SwapChain.cpp @@ -569,8 +569,8 @@ namespace Vulkan } // Select swap chain flags, we only need a colour attachment - VkImageUsageFlags image_usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; - if (!(surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)) + VkImageUsageFlags image_usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + if ((surface_capabilities.supportedUsageFlags & image_usage) != image_usage) { Console.Error("Vulkan: Swap chain does not support usage as color attachment"); return false; diff --git a/pcsx2-gsrunner/Main.cpp b/pcsx2-gsrunner/Main.cpp index e3aff68396..68ea8bac29 100644 --- a/pcsx2-gsrunner/Main.cpp +++ b/pcsx2-gsrunner/Main.cpp @@ -254,17 +254,11 @@ void Host::SetRelativeMouseMode(bool enabled) { } -std::optional Host::AcquireRenderWindow() +std::optional Host::AcquireRenderWindow(bool recreate_window) { return GSRunner::GetPlatformWindowInfo(); } -std::optional Host::UpdateRenderWindow(bool recreate_window) -{ - // We shouldn't ever recreate with the runner, so this is okay.. - return GSRunner::GetPlatformWindowInfo(); -} - void Host::ReleaseRenderWindow() { } diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index d38c553840..f132650424 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -405,10 +405,9 @@ void MainWindow::connectVMThreadSignals(EmuThread* thread) connect(m_ui.actionStartFullscreenUI, &QAction::triggered, thread, &EmuThread::startFullscreenUI); connect(m_ui.actionStartFullscreenUI2, &QAction::triggered, thread, &EmuThread::startFullscreenUI); connect(thread, &EmuThread::messageConfirmed, this, &MainWindow::confirmMessage, Qt::BlockingQueuedConnection); - connect(thread, &EmuThread::onCreateDisplayRequested, this, &MainWindow::createDisplayWindow, Qt::BlockingQueuedConnection); - connect(thread, &EmuThread::onUpdateDisplayRequested, this, &MainWindow::updateDisplayWindow, Qt::BlockingQueuedConnection); - connect(thread, &EmuThread::onDestroyDisplayRequested, this, &MainWindow::destroyDisplay, Qt::BlockingQueuedConnection); - connect(thread, &EmuThread::onResizeDisplayRequested, this, &MainWindow::displayResizeRequested); + connect(thread, &EmuThread::onAcquireRenderWindowRequested, this, &MainWindow::acquireRenderWindow, Qt::BlockingQueuedConnection); + connect(thread, &EmuThread::onReleaseRenderWindowRequested, this, &MainWindow::releaseRenderWindow, Qt::BlockingQueuedConnection); + connect(thread, &EmuThread::onResizeRenderWindowRequested, this, &MainWindow::displayResizeRequested); connect(thread, &EmuThread::onRelativeMouseModeRequested, this, &MainWindow::relativeMouseModeRequested); connect(thread, &EmuThread::onVMStarting, this, &MainWindow::onVMStarting); connect(thread, &EmuThread::onVMStarted, this, &MainWindow::onVMStarted); @@ -1748,57 +1747,27 @@ bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr #endif -std::optional MainWindow::createDisplayWindow(bool fullscreen, bool render_to_main) +std::optional MainWindow::acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless) { - DevCon.WriteLn( - "createDisplayWindow() fullscreen=%s render_to_main=%s", fullscreen ? "true" : "false", render_to_main ? "true" : "false"); - - createDisplayWidget(fullscreen, render_to_main); - - // we need the surface visible.. this might be able to be replaced with something else - QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); - - std::optional wi = m_display_widget->getWindowInfo(); - if (!wi.has_value()) - { - QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget")); - destroyDisplayWidget(true); - return std::nullopt; - } - - g_emu_thread->connectDisplaySignals(m_display_widget); - m_display_created = true; - - updateWindowTitle(); - updateWindowState(); - - m_ui.actionStartFullscreenUI->setEnabled(false); - m_ui.actionStartFullscreenUI2->setEnabled(false); - - updateDisplayWidgetCursor(); - m_display_widget->setFocus(); - - return wi; -} - -std::optional MainWindow::updateDisplayWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless) -{ - DevCon.WriteLn("updateDisplayWindow() recreate=%s fullscreen=%s render_to_main=%s surfaceless=%s", recreate_window ? "true" : "false", + DevCon.WriteLn("acquireRenderWindow() recreate=%s fullscreen=%s render_to_main=%s surfaceless=%s", recreate_window ? "true" : "false", fullscreen ? "true" : "false", render_to_main ? "true" : "false", surfaceless ? "true" : "false"); QWidget* container = m_display_container ? static_cast(m_display_container) : static_cast(m_display_widget); const bool is_fullscreen = isRenderingFullscreen(); const bool is_rendering_to_main = isRenderingToMain(); const bool changing_surfaceless = (!m_display_widget != surfaceless); - if (!recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main && !changing_surfaceless) - return m_display_widget->getWindowInfo(); + if (m_display_created && !recreate_window && fullscreen == is_fullscreen && is_rendering_to_main == render_to_main && + !changing_surfaceless) + { + return m_display_widget ? m_display_widget->getWindowInfo() : WindowInfo(); + } // Skip recreating the surface if we're just transitioning between fullscreen and windowed with render-to-main off. // .. except on Wayland, where everything tends to break if you don't recreate. const bool has_container = (m_display_container != nullptr); const bool needs_container = DisplayContainer::isNeeded(fullscreen, render_to_main); - if (!recreate_window && !is_rendering_to_main && !render_to_main && has_container == needs_container && !needs_container && - !changing_surfaceless) + if (m_display_created && !recreate_window && !is_rendering_to_main && !render_to_main && has_container == needs_container && + !needs_container && !changing_surfaceless) { DevCon.WriteLn("Toggling to %s without recreating surface", (fullscreen ? "fullscreen" : "windowed")); @@ -1825,6 +1794,7 @@ std::optional MainWindow::updateDisplayWindow(bool recreate_window, } destroyDisplayWidget(surfaceless); + m_display_created = true; // if we're going to surfaceless, we're done here if (surfaceless) @@ -1832,10 +1802,13 @@ std::optional MainWindow::updateDisplayWindow(bool recreate_window, createDisplayWidget(fullscreen, render_to_main); + // we need the surface visible.. this might be able to be replaced with something else + QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + std::optional wi = m_display_widget->getWindowInfo(); if (!wi.has_value()) { - QMessageBox::critical(this, tr("Error"), tr("Failed to get new window info from widget")); + QMessageBox::critical(this, tr("Error"), tr("Failed to get window info from widget")); destroyDisplayWidget(true); return std::nullopt; } @@ -1845,6 +1818,9 @@ std::optional MainWindow::updateDisplayWindow(bool recreate_window, updateWindowTitle(); updateWindowState(); + m_ui.actionStartFullscreenUI->setEnabled(false); + m_ui.actionStartFullscreenUI2->setEnabled(false); + updateDisplayWidgetCursor(); m_display_widget->setFocus(); @@ -1954,7 +1930,7 @@ void MainWindow::relativeMouseModeRequested(bool enabled) updateDisplayWidgetCursor(); } -void MainWindow::destroyDisplay() +void MainWindow::releaseRenderWindow() { // Now we can safely destroy the display window. destroyDisplayWidget(true); @@ -2506,19 +2482,33 @@ void MainWindow::doDiscChange(CDVD_SourceType source, const QString& path) MainWindow::VMLock MainWindow::pauseAndLockVM() { - const bool was_fullscreen = isRenderingFullscreen(); + // To switch out of fullscreen when displaying a popup, or not to? + // For Windows, with driver's direct scanout, what renders behind tends to be hit and miss. + // We can't draw anything over exclusive fullscreen, so get out of it in that case. + // Wayland's a pain as usual, we need to recreate the window, which means there'll be a brief + // period when there's no window, and Qt might shut us down. So avoid it there. + // On MacOS, it forces a workspace switch, which is kinda jarring. +#ifndef __APPLE__ + const bool was_fullscreen = g_emu_thread->isFullscreen() && !s_use_central_widget; +#else + const bool was_fullscreen = false; +#endif const bool was_paused = s_vm_paused; - // We use surfaceless rather than switching out of fullscreen, because - // we're paused, so we're not going to be rendering anyway. - if (was_fullscreen) - g_emu_thread->setSurfaceless(true); if (!was_paused) g_emu_thread->setVMPaused(true); - // We want to parent dialogs to the display widget, except if we were fullscreen, - // since it's going to get destroyed by the surfaceless call above. - QWidget* dialog_parent = was_fullscreen ? static_cast(this) : getDisplayContainer(); + // We need to switch out of exclusive fullscreen before we can display our popup. + // However, we do not want to switch back to render-to-main, the window might have generated this event. + if (was_fullscreen) + { + g_emu_thread->setFullscreen(false, false); + while (QtHost::IsVMValid() && g_emu_thread->isFullscreen()) + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); + } + + // Now we'll either have a borderless window, or a regular window (if we were exclusive fullscreen). + QWidget* dialog_parent = getDisplayContainer(); return VMLock(dialog_parent, was_paused, was_fullscreen); } @@ -2548,17 +2538,10 @@ MainWindow::VMLock::VMLock(VMLock&& lock) MainWindow::VMLock::~VMLock() { if (m_was_fullscreen) - g_emu_thread->setSurfaceless(false); + g_emu_thread->setFullscreen(true, true); if (!m_was_paused) g_emu_thread->setVMPaused(false); - - if (m_was_fullscreen) - { - // Wait until we get the display widget back, so we're not the last window left and closing. - while (QtHost::IsVMValid() && !g_main_window->m_display_widget) - QApplication::processEvents(QEventLoop::ExcludeUserInputEvents, 1); - } } void MainWindow::VMLock::cancelResume() diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index a1ad4a2b6c..312d4920c0 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -69,7 +69,7 @@ public: void cancelResume(); private: - VMLock(QWidget* dialog_parent, bool was_paused, bool was_fullscreen); + VMLock(QWidget* dialog_parent, bool was_paused, bool was_exclusive_fullscreen); friend MainWindow; QWidget* m_dialog_parent; @@ -122,11 +122,10 @@ public Q_SLOTS: private Q_SLOTS: void onUpdateCheckComplete(); - std::optional createDisplayWindow(bool fullscreen, bool render_to_main); - std::optional updateDisplayWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); + std::optional acquireRenderWindow(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); void displayResizeRequested(qint32 width, qint32 height); void relativeMouseModeRequested(bool enabled); - void destroyDisplay(); + void releaseRenderWindow(); void focusDisplayWidget(); void onGameListRefreshComplete(); diff --git a/pcsx2-qt/QtHost.cpp b/pcsx2-qt/QtHost.cpp index d6f258493a..32d72ed303 100644 --- a/pcsx2-qt/QtHost.cpp +++ b/pcsx2-qt/QtHost.cpp @@ -196,8 +196,8 @@ void EmuThread::startFullscreenUI(bool fullscreen) // this should just set the flag so it gets automatically started ImGuiManager::InitializeFullscreenUI(); m_run_fullscreen_ui = true; - if (fullscreen) - m_is_fullscreen = true; + m_is_rendering_to_main = shouldRenderToMain(); + m_is_fullscreen = fullscreen; if (!GetMTGS().WaitForOpen()) { @@ -242,6 +242,7 @@ void EmuThread::startVM(std::shared_ptr boot_params) pxAssertRel(!VMManager::HasValidVM(), "VM is shut down"); // Determine whether to start fullscreen or not. + m_is_rendering_to_main = shouldRenderToMain(); if (boot_params->fullscreen.has_value()) m_is_fullscreen = boot_params->fullscreen.value(); else @@ -492,14 +493,14 @@ void EmuThread::toggleFullscreen() return; } - setFullscreen(!m_is_fullscreen); + setFullscreen(!m_is_fullscreen, true); } -void EmuThread::setFullscreen(bool fullscreen) +void EmuThread::setFullscreen(bool fullscreen, bool allow_render_to_main) { if (!isOnEmuThread()) { - QMetaObject::invokeMethod(this, "setFullscreen", Qt::QueuedConnection, Q_ARG(bool, fullscreen)); + QMetaObject::invokeMethod(this, "setFullscreen", Qt::QueuedConnection, Q_ARG(bool, fullscreen), Q_ARG(bool, allow_render_to_main)); return; } @@ -508,6 +509,7 @@ void EmuThread::setFullscreen(bool fullscreen) // This will call back to us on the MTGS thread. m_is_fullscreen = fullscreen; + m_is_rendering_to_main = allow_render_to_main && shouldRenderToMain(); GetMTGS().UpdateDisplayWindow(); GetMTGS().WaitGS(); @@ -893,32 +895,24 @@ void EmuThread::endCapture() GetMTGS().RunOnGSThread(&GSEndCapture); } -std::optional EmuThread::acquireRenderWindow() +std::optional EmuThread::acquireRenderWindow(bool recreate_window) { - m_is_rendering_to_main = shouldRenderToMain(); - m_is_surfaceless = false; + // Check if we're wanting to get exclusive fullscreen. This should be safe to read, since we're going to be calling from the GS thread. + m_is_exclusive_fullscreen = m_is_fullscreen && GSWantsExclusiveFullscreen(); + const bool window_fullscreen = m_is_fullscreen && !m_is_exclusive_fullscreen; + const bool render_to_main = !m_is_exclusive_fullscreen && !window_fullscreen && m_is_rendering_to_main; - return emit onCreateDisplayRequested(m_is_fullscreen, m_is_rendering_to_main); -} - -std::optional EmuThread::updateRenderWindow(bool recreate_window) -{ - return emit onUpdateDisplayRequested(recreate_window, m_is_fullscreen, !m_is_fullscreen && m_is_rendering_to_main, m_is_surfaceless); + return emit onAcquireRenderWindowRequested(recreate_window, window_fullscreen, render_to_main, m_is_surfaceless); } void EmuThread::releaseRenderWindow() { - emit onDestroyDisplayRequested(); + emit onReleaseRenderWindowRequested(); } -std::optional Host::AcquireRenderWindow() +std::optional Host::AcquireRenderWindow(bool recreate_window) { - return g_emu_thread->acquireRenderWindow(); -} - -std::optional Host::UpdateRenderWindow(bool recreate_window) -{ - return g_emu_thread->updateRenderWindow(recreate_window); + return g_emu_thread->acquireRenderWindow(recreate_window); } void Host::ReleaseRenderWindow() @@ -932,7 +926,7 @@ void Host::BeginPresentFrame() void Host::RequestResizeHostDisplay(s32 width, s32 height) { - g_emu_thread->onResizeDisplayRequested(width, height); + g_emu_thread->onResizeRenderWindowRequested(width, height); } void Host::OnVMStarting() @@ -1194,7 +1188,7 @@ bool Host::IsFullscreen() void Host::SetFullscreen(bool enabled) { - g_emu_thread->setFullscreen(enabled); + g_emu_thread->setFullscreen(enabled, true); } alignas(16) static SysMtgsThread s_mtgs_thread; diff --git a/pcsx2-qt/QtHost.h b/pcsx2-qt/QtHost.h index a63723726c..2f5cb01e94 100644 --- a/pcsx2-qt/QtHost.h +++ b/pcsx2-qt/QtHost.h @@ -61,6 +61,7 @@ public: __fi QEventLoop* getEventLoop() const { return m_event_loop; } __fi bool isFullscreen() const { return m_is_fullscreen; } + __fi bool isExclusiveFullscreen() const { return m_is_exclusive_fullscreen; } __fi bool isRenderingToMain() const { return m_is_rendering_to_main; } __fi bool isSurfaceless() const { return m_is_surfaceless; } __fi bool isRunningFullscreenUI() const { return m_run_fullscreen_ui; } @@ -69,8 +70,7 @@ public: bool shouldRenderToMain() const; /// Called back from the GS thread when the display state changes (e.g. fullscreen, render to main). - std::optional acquireRenderWindow(); - std::optional updateRenderWindow(bool recreate_window); + std::optional acquireRenderWindow(bool recreate_window); void connectDisplaySignals(DisplayWidget* widget); void releaseRenderWindow(); @@ -93,7 +93,7 @@ public Q_SLOTS: void saveState(const QString& filename); void saveStateToSlot(qint32 slot); void toggleFullscreen(); - void setFullscreen(bool fullscreen); + void setFullscreen(bool fullscreen, bool allow_render_to_main); void setSurfaceless(bool surfaceless); void applySettings(); void reloadGameSettings(); @@ -117,10 +117,9 @@ public Q_SLOTS: Q_SIGNALS: bool messageConfirmed(const QString& title, const QString& message); - std::optional onCreateDisplayRequested(bool fullscreen, bool render_to_main); - std::optional onUpdateDisplayRequested(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); - void onResizeDisplayRequested(qint32 width, qint32 height); - void onDestroyDisplayRequested(); + std::optional onAcquireRenderWindowRequested(bool recreate_window, bool fullscreen, bool render_to_main, bool surfaceless); + void onResizeRenderWindowRequested(qint32 width, qint32 height); + void onReleaseRenderWindowRequested(); void onRelativeMouseModeRequested(bool enabled); /// Called when the VM is starting initialization, but has not been completed yet. @@ -195,6 +194,7 @@ private: bool m_run_fullscreen_ui = false; bool m_is_rendering_to_main = false; bool m_is_fullscreen = false; + bool m_is_exclusive_fullscreen = false; bool m_is_surfaceless = false; bool m_save_state_on_shutdown = false; bool m_pause_on_focus_loss = false; diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index 33afa776bc..a624858d22 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -133,52 +133,8 @@ static RenderAPI GetAPIForRenderer(GSRendererType renderer) } } -static void UpdateExclusiveFullscreen(bool force_off) +static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool recreate_window) { - if (!g_gs_device->SupportsExclusiveFullscreen()) - return; - - std::string fullscreen_mode = Host::GetBaseStringSettingValue("EmuCore/GS", "FullscreenMode", ""); - u32 width, height; - float refresh_rate; - - const bool wants_fullscreen = !force_off && Host::IsFullscreen() && !fullscreen_mode.empty() && - GSDevice::ParseFullscreenMode(fullscreen_mode, &width, &height, &refresh_rate); - const bool is_fullscreen = g_gs_device->IsExclusiveFullscreen(); - if (wants_fullscreen == is_fullscreen) - return; - - if (wants_fullscreen) - { - Console.WriteLn("Trying to acquire exclusive fullscreen..."); - if (g_gs_device->SetExclusiveFullscreen(true, width, height, refresh_rate)) - { - Host::AddKeyedOSDMessage( - "UpdateExclusiveFullscreen", "Acquired exclusive fullscreen.", Host::OSD_INFO_DURATION); - } - else - { - Host::AddKeyedOSDMessage( - "UpdateExclusiveFullscreen", "Failed to acquire exclusive fullscreen.", Host::OSD_WARNING_DURATION); - } - } - else - { - Console.WriteLn("Leaving exclusive fullscreen..."); - g_gs_device->SetExclusiveFullscreen(false, 0, 0, 0.0f); - Host::AddKeyedOSDMessage("UpdateExclusiveFullscreen", "Lost exclusive fullscreen.", Host::OSD_INFO_DURATION); - } -} - -static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool reopening, bool recreate_window) -{ - std::optional wi = reopening ? Host::UpdateRenderWindow(recreate_window) : Host::AcquireRenderWindow(); - if (!wi.has_value()) - { - Console.Error("Failed to acquire render window."); - return false; - } - const RenderAPI new_api = GetAPIForRenderer(renderer); switch (new_api) { @@ -212,8 +168,7 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool return false; } - const VsyncMode vsync_mode = Host::GetEffectiveVSyncMode(); - bool okay = g_gs_device->Create(wi.value(), vsync_mode); + bool okay = g_gs_device->Create(); if (okay) { okay = ImGuiManager::Initialize(); @@ -239,24 +194,17 @@ static bool OpenGSDevice(GSRendererType renderer, bool clear_state_on_fail, bool Console.WriteLn(Color_StrongGreen, "%s Graphics Driver Info:", GSDevice::RenderAPIToString(new_api)); Console.Indent().WriteLn(g_gs_device->GetDriverInfo()); - // Switch to exclusive fullscreen if enabled. - UpdateExclusiveFullscreen(false); - return true; } -static void CloseGSDevice(bool clear_state, bool reopening) +static void CloseGSDevice(bool clear_state) { if (!g_gs_device) return; - UpdateExclusiveFullscreen(true); ImGuiManager::Shutdown(clear_state); g_gs_device->Destroy(); g_gs_device.reset(); - - if (!reopening) - Host::ReleaseRenderWindow(); } static bool OpenGSRenderer(GSRendererType renderer, u8* basemem) @@ -336,18 +284,18 @@ bool GSreopen(bool recreate_device, bool recreate_renderer, const Pcsx2Config::G { // We need a new render window when changing APIs. const bool recreate_window = (g_gs_device->GetRenderAPI() != GetAPIForRenderer(GSConfig.Renderer)); - CloseGSDevice(false, true); + CloseGSDevice(false); - if (!OpenGSDevice(GSConfig.Renderer, false, true, recreate_window) || + if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window) || (recreate_renderer && !OpenGSRenderer(GSConfig.Renderer, basemem))) { Host::AddKeyedOSDMessage( "GSReopenFailed", "Failed to reopen, restoring old configuration.", Host::OSD_CRITICAL_ERROR_DURATION); - CloseGSDevice(false, true); + CloseGSDevice(false); GSConfig = old_config; - if (!OpenGSDevice(GSConfig.Renderer, false, true, recreate_window) || + if (!OpenGSDevice(GSConfig.Renderer, false, recreate_window) || (recreate_renderer && !OpenGSRenderer(GSConfig.Renderer, basemem))) { pxFailRel("Failed to reopen GS on old config"); @@ -387,12 +335,12 @@ bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* b GSConfig = config; GSConfig.Renderer = renderer; - bool res = OpenGSDevice(renderer, true, false, false); + bool res = OpenGSDevice(renderer, true, false); if (res) { res = OpenGSRenderer(renderer, basemem); if (!res) - CloseGSDevice(true, false); + CloseGSDevice(true); } if (!res) @@ -410,7 +358,7 @@ bool GSopen(const Pcsx2Config::GSOptions& config, GSRendererType renderer, u8* b void GSclose() { CloseGSRenderer(); - CloseGSDevice(true, false); + CloseGSDevice(true); } void GSreset(bool hardware_reset) @@ -626,24 +574,12 @@ void GSResizeDisplayWindow(int width, int height, float scale) void GSUpdateDisplayWindow() { - UpdateExclusiveFullscreen(true); - g_gs_device->DestroySurface(); - - const std::optional wi = Host::UpdateRenderWindow(false); - if (!wi.has_value()) + if (!g_gs_device->UpdateWindow()) { - pxFailRel("Failed to get window info after update."); + Host::ReportErrorAsync("Error", "Failed to change window after update. The log may contain more information."); return; } - if (!g_gs_device->ChangeWindow(wi.value())) - { - pxFailRel("Failed to change window after update."); - return; - } - - UpdateExclusiveFullscreen(false); - ImGuiManager::WindowResized(); } @@ -652,6 +588,16 @@ void GSSetVSyncMode(VsyncMode mode) g_gs_device->SetVSync(mode); } +bool GSWantsExclusiveFullscreen() +{ + if (!g_gs_device || !g_gs_device->SupportsExclusiveFullscreen()) + return false; + + u32 width, height; + float refresh_rate; + return GSDevice::GetRequestedExclusiveFullscreenMode(&width, &height, &refresh_rate); +} + bool GSGetHostRefreshRate(float* refresh_rate) { if (!g_gs_device) diff --git a/pcsx2/GS/GS.h b/pcsx2/GS/GS.h index 522593a577..6de431550a 100644 --- a/pcsx2/GS/GS.h +++ b/pcsx2/GS/GS.h @@ -99,6 +99,7 @@ void GSResizeDisplayWindow(int width, int height, float scale); void GSUpdateDisplayWindow(); void GSSetVSyncMode(VsyncMode mode); +bool GSWantsExclusiveFullscreen(); bool GSGetHostRefreshRate(float* refresh_rate); void GSGetAdaptersAndFullscreenModes( GSRendererType renderer, std::vector* adapters, std::vector* fullscreen_modes); @@ -128,11 +129,8 @@ struct GSRecoverableError : GSError namespace Host { /// Called when the GS is creating a render device. - std::optional AcquireRenderWindow(); - - /// Called on the MTGS thread when a request to update the display is received. - /// This could be a fullscreen transition, for example. - std::optional UpdateRenderWindow(bool recreate_window); + /// This could also be fullscreen transition. + std::optional AcquireRenderWindow(bool recreate_window); /// Called before drawing the OSD and other display elements. void BeginPresentFrame(); diff --git a/pcsx2/GS/Renderers/Common/GSDevice.cpp b/pcsx2/GS/Renderers/Common/GSDevice.cpp index c14e875ca8..78e3d78968 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.cpp +++ b/pcsx2/GS/Renderers/Common/GSDevice.cpp @@ -18,6 +18,7 @@ #include "GS/GSGL.h" #include "GS/GS.h" #include "Host.h" +#include "HostSettings.h" #include "common/Align.h" #include "common/StringUtil.h" @@ -113,14 +114,16 @@ const char* GSDevice::RenderAPIToString(RenderAPI api) } } -bool GSDevice::ParseFullscreenMode(const std::string_view& mode, u32* width, u32* height, float* refresh_rate) +bool GSDevice::GetRequestedExclusiveFullscreenMode(u32* width, u32* height, float* refresh_rate) { + const std::string mode = Host::GetBaseStringSettingValue("EmuCore/GS", "FullscreenMode", ""); if (!mode.empty()) { + const std::string_view mode_view = mode; std::string_view::size_type sep1 = mode.find('x'); if (sep1 != std::string_view::npos) { - std::optional owidth = StringUtil::FromChars(mode.substr(0, sep1)); + std::optional owidth = StringUtil::FromChars(mode_view.substr(0, sep1)); sep1++; while (sep1 < mode.length() && std::isspace(mode[sep1])) @@ -131,7 +134,7 @@ bool GSDevice::ParseFullscreenMode(const std::string_view& mode, u32* width, u32 std::string_view::size_type sep2 = mode.find('@', sep1); if (sep2 != std::string_view::npos) { - std::optional oheight = StringUtil::FromChars(mode.substr(sep1, sep2 - sep1)); + std::optional oheight = StringUtil::FromChars(mode_view.substr(sep1, sep2 - sep1)); sep2++; while (sep2 < mode.length() && std::isspace(mode[sep2])) @@ -139,7 +142,7 @@ bool GSDevice::ParseFullscreenMode(const std::string_view& mode, u32* width, u32 if (oheight.has_value() && sep2 < mode.length()) { - std::optional orefresh_rate = StringUtil::FromChars(mode.substr(sep2)); + std::optional orefresh_rate = StringUtil::FromChars(mode_view.substr(sep2)); if (orefresh_rate.has_value()) { *width = owidth.value(); @@ -181,10 +184,9 @@ void GSDevice::GenerateExpansionIndexBuffer(void* buffer) } } -bool GSDevice::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDevice::Create() { - m_window_info = wi; - m_vsync_mode = vsync; + m_vsync_mode = Host::GetEffectiveVSyncMode(); return true; } @@ -200,6 +202,26 @@ void GSDevice::Destroy() PurgePool(); } +bool GSDevice::AcquireWindow(bool recreate_window) +{ + std::optional wi = Host::AcquireRenderWindow(recreate_window); + if (!wi.has_value()) + { + Console.Error("Failed to acquire render window."); + Host::ReportErrorAsync("Error", "Failed to acquire render window. The log may have more information."); + return false; + } + + m_window_info = std::move(wi.value()); + return true; +} + +void GSDevice::ReleaseWindow() +{ + Host::ReleaseRenderWindow(); + m_window_info = WindowInfo(); +} + bool GSDevice::GetHostRefreshRate(float* refresh_rate) { if (m_window_info.surface_refresh_rate > 0.0f) diff --git a/pcsx2/GS/Renderers/Common/GSDevice.h b/pcsx2/GS/Renderers/Common/GSDevice.h index 6f6ee0ab4c..e1ce9b9d77 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.h +++ b/pcsx2/GS/Renderers/Common/GSDevice.h @@ -779,6 +779,9 @@ protected: bool m_rbswapped = false; FeatureSupport m_features; + bool AcquireWindow(bool recreate_window); + void ReleaseWindow(); + virtual GSTexture* CreateSurface(GSTexture::Type type, int width, int height, int levels, GSTexture::Format format) = 0; GSTexture* FetchSurface(GSTexture::Type type, int width, int height, int levels, GSTexture::Format format, bool clear, bool prefer_reuse); @@ -800,8 +803,8 @@ public: /// Returns a string representing the specified API. static const char* RenderAPIToString(RenderAPI api); - /// Parses a fullscreen mode into its components (width * height @ refresh hz) - static bool ParseFullscreenMode(const std::string_view& mode, u32* width, u32* height, float* refresh_rate); + /// Parses the configured fullscreen mode into its components (width * height @ refresh hz) + static bool GetRequestedExclusiveFullscreenMode(u32* width, u32* height, float* refresh_rate); /// Converts a fullscreen mode to a string. static std::string GetFullscreenModeString(u32 width, u32 height, float refresh_rate); @@ -830,7 +833,7 @@ public: /// Recreates the font, call when the window scaling changes. bool UpdateImGuiFontTexture(); - virtual bool Create(const WindowInfo& wi, VsyncMode vsync); + virtual bool Create(); virtual void Destroy(); /// Returns the graphics API used by this device. @@ -843,7 +846,7 @@ public: virtual void DestroySurface() = 0; /// Switches to a new window/surface. - virtual bool ChangeWindow(const WindowInfo& wi) = 0; + virtual bool UpdateWindow() = 0; /// Call when the window size changes externally to recreate any resources. virtual void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) = 0; @@ -851,12 +854,6 @@ public: /// Returns true if exclusive fullscreen is supported. virtual bool SupportsExclusiveFullscreen() const = 0; - /// Returns true if exclusive fullscreen is active. - virtual bool IsExclusiveFullscreen() = 0; - - /// Attempts to switch to the specified mode in exclusive fullscreen. - virtual bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) = 0; - /// Returns false if the window was completely occluded. If frame_skip is set, the frame won't be /// displayed, but the GPU command queue will still be flushed. virtual PresentResult BeginPresent(bool frame_skip) = 0; diff --git a/pcsx2/GS/Renderers/DX11/D3D.cpp b/pcsx2/GS/Renderers/DX11/D3D.cpp index dee6346bcb..fdeef46ce9 100644 --- a/pcsx2/GS/Renderers/DX11/D3D.cpp +++ b/pcsx2/GS/Renderers/DX11/D3D.cpp @@ -120,6 +120,79 @@ std::vector D3D::GetFullscreenModes(IDXGIFactory5* factory, const s return modes; } +bool D3D::GetRequestedExclusiveFullscreenModeDesc(IDXGIFactory5* factory, const RECT& window_rect, u32 width, + u32 height, float refresh_rate, DXGI_FORMAT format, DXGI_MODE_DESC* fullscreen_mode, IDXGIOutput** output) +{ + // We need to find which monitor the window is located on. + const GSVector4i client_rc_vec = GSVector4i::load(&window_rect); + + // The window might be on a different adapter to which we are rendering.. so we have to enumerate them all. + HRESULT hr; + wil::com_ptr_nothrow first_output, intersecting_output; + + for (u32 adapter_index = 0; !intersecting_output; adapter_index++) + { + wil::com_ptr_nothrow adapter; + hr = factory->EnumAdapters1(adapter_index, adapter.put()); + if (hr == DXGI_ERROR_NOT_FOUND) + break; + else if (FAILED(hr)) + continue; + + for (u32 output_index = 0;; output_index++) + { + wil::com_ptr_nothrow this_output; + DXGI_OUTPUT_DESC output_desc; + hr = adapter->EnumOutputs(output_index, this_output.put()); + if (hr == DXGI_ERROR_NOT_FOUND) + break; + else if (FAILED(hr) || FAILED(this_output->GetDesc(&output_desc))) + continue; + + const GSVector4i output_rc = GSVector4i::load(&output_desc.DesktopCoordinates); + if (!client_rc_vec.rintersect(output_rc).rempty()) + { + intersecting_output = std::move(this_output); + break; + } + + // Fallback to the first monitor. + if (!first_output) + first_output = std::move(this_output); + } + } + + if (!intersecting_output) + { + if (!first_output) + { + Console.Error("No DXGI output found. Can't use exclusive fullscreen."); + return false; + } + + Console.Warning("No DXGI output found for window, using first."); + intersecting_output = std::move(first_output); + } + + DXGI_MODE_DESC request_mode = {}; + request_mode.Width = width; + request_mode.Height = height; + request_mode.Format = format; + request_mode.RefreshRate.Numerator = static_cast(std::floor(refresh_rate * 1000.0f)); + request_mode.RefreshRate.Denominator = 1000u; + + if (FAILED(hr = intersecting_output->FindClosestMatchingMode(&request_mode, fullscreen_mode, nullptr)) || + request_mode.Format != format) + { + Console.Error("Failed to find closest matching mode, hr=%08X", hr); + return false; + } + + *output = intersecting_output.get(); + intersecting_output->AddRef(); + return true; +} + wil::com_ptr_nothrow D3D::GetAdapterByName(IDXGIFactory5* factory, const std::string_view& name) { if (name.empty()) diff --git a/pcsx2/GS/Renderers/DX11/D3D.h b/pcsx2/GS/Renderers/DX11/D3D.h index aea293f887..aeb6e1e358 100644 --- a/pcsx2/GS/Renderers/DX11/D3D.h +++ b/pcsx2/GS/Renderers/DX11/D3D.h @@ -36,6 +36,10 @@ namespace D3D // returns a list of fullscreen modes for the specified adapter std::vector GetFullscreenModes(IDXGIFactory5* factory, const std::string_view& adapter_name); + // returns the fullscreen mode to use for the specified dimensions + bool GetRequestedExclusiveFullscreenModeDesc(IDXGIFactory5* factory, const RECT& window_rect, u32 width, u32 height, + float refresh_rate, DXGI_FORMAT format, DXGI_MODE_DESC* fullscreen_mode, IDXGIOutput** output); + // get an adapter based on name wil::com_ptr_nothrow GetAdapterByName(IDXGIFactory5* factory, const std::string_view& name); diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp index cc282e3136..0dd3ed038b 100644 --- a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp +++ b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp @@ -37,6 +37,8 @@ // #define REPORT_LEAKED_OBJECTS 1 +static constexpr std::array s_present_clear_color = {}; + static bool SupportsTextureFormat(ID3D11Device* dev, DXGI_FORMAT format) { UINT support; @@ -75,9 +77,9 @@ RenderAPI GSDevice11::GetRenderAPI() const return RenderAPI::D3D11; } -bool GSDevice11::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDevice11::Create() { - if (!GSDevice::Create(wi, vsync)) + if (!GSDevice::Create()) return false; UINT create_flags = 0; @@ -150,7 +152,7 @@ bool GSDevice11::Create(const WindowInfo& wi, VsyncMode vsync) DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allow_tearing_supported, sizeof(allow_tearing_supported)); m_allow_tearing_supported = (SUCCEEDED(hr) && allow_tearing_supported == TRUE); - if (wi.type != WindowInfo::Type::Surfaceless && !CreateSwapChain(nullptr)) + if (!AcquireWindow(true) || (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain())) return false; D3D11_BUFFER_DESC bd; @@ -490,7 +492,8 @@ bool GSDevice11::Create(const WindowInfo& wi, VsyncMode vsync) void GSDevice11::Destroy() { GSDevice::Destroy(); - GSDevice11::DestroySurface(); + DestroySwapChain(); + ReleaseWindow(); DestroyTimestampQueries(); m_convert = {}; @@ -561,7 +564,7 @@ bool GSDevice11::HasSurface() const bool GSDevice11::GetHostRefreshRate(float* refresh_rate) { - if (m_swap_chain && IsExclusiveFullscreen()) + if (m_swap_chain && m_is_exclusive_fullscreen) { DXGI_SWAP_CHAIN_DESC desc; if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 && @@ -583,50 +586,84 @@ void GSDevice11::SetVSync(VsyncMode mode) m_vsync_mode = mode; } -bool GSDevice11::CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode) +bool GSDevice11::CreateSwapChain() { + constexpr DXGI_FORMAT swap_chain_format = DXGI_FORMAT_R8G8B8A8_UNORM; + if (m_window_info.type != WindowInfo::Type::Win32) return false; - m_using_flip_model_swap_chain = !EmuConfig.GS.UseBlitSwapChain || fullscreen_mode; - const HWND window_hwnd = reinterpret_cast(m_window_info.window_handle); RECT client_rc{}; GetClientRect(window_hwnd, &client_rc); - const u32 width = static_cast(client_rc.right - client_rc.left); - const u32 height = static_cast(client_rc.bottom - client_rc.top); + + DXGI_MODE_DESC fullscreen_mode; + wil::com_ptr_nothrow fullscreen_output; + if (Host::IsFullscreen()) + { + u32 fullscreen_width, fullscreen_height; + float fullscreen_refresh_rate; + m_is_exclusive_fullscreen = + GetRequestedExclusiveFullscreenMode(&fullscreen_width, &fullscreen_height, &fullscreen_refresh_rate) && + D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width, + fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode, + fullscreen_output.put()); + } + else + { + m_is_exclusive_fullscreen = false; + } + + m_using_flip_model_swap_chain = !EmuConfig.GS.UseBlitSwapChain || m_is_exclusive_fullscreen; DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = {}; - swap_chain_desc.Width = width; - swap_chain_desc.Height = height; - swap_chain_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + swap_chain_desc.Width = static_cast(client_rc.right - client_rc.left); + swap_chain_desc.Height = static_cast(client_rc.bottom - client_rc.top); + swap_chain_desc.Format = swap_chain_format; swap_chain_desc.SampleDesc.Count = 1; swap_chain_desc.BufferCount = 3; swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swap_chain_desc.SwapEffect = m_using_flip_model_swap_chain ? DXGI_SWAP_EFFECT_FLIP_DISCARD : DXGI_SWAP_EFFECT_DISCARD; - m_using_allow_tearing = (m_allow_tearing_supported && m_using_flip_model_swap_chain && !fullscreen_mode); + m_using_allow_tearing = (m_allow_tearing_supported && m_using_flip_model_swap_chain && !m_is_exclusive_fullscreen); if (m_using_allow_tearing) swap_chain_desc.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; - DXGI_SWAP_CHAIN_FULLSCREEN_DESC fs_desc = {}; - if (fullscreen_mode) + HRESULT hr = S_OK; + + if (m_is_exclusive_fullscreen) { - swap_chain_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; - swap_chain_desc.Width = fullscreen_mode->Width; - swap_chain_desc.Height = fullscreen_mode->Height; - fs_desc.RefreshRate = fullscreen_mode->RefreshRate; - fs_desc.ScanlineOrdering = fullscreen_mode->ScanlineOrdering; - fs_desc.Scaling = fullscreen_mode->Scaling; + DXGI_SWAP_CHAIN_DESC1 fs_sd_desc = swap_chain_desc; + DXGI_SWAP_CHAIN_FULLSCREEN_DESC fs_desc = {}; + + fs_sd_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + fs_sd_desc.Width = fullscreen_mode.Width; + fs_sd_desc.Height = fullscreen_mode.Height; + fs_desc.RefreshRate = fullscreen_mode.RefreshRate; + fs_desc.ScanlineOrdering = fullscreen_mode.ScanlineOrdering; + fs_desc.Scaling = fullscreen_mode.Scaling; fs_desc.Windowed = FALSE; + + Console.WriteLn("Creating a %dx%d exclusive fullscreen swap chain", fs_sd_desc.Width, fs_sd_desc.Height); + hr = m_dxgi_factory->CreateSwapChainForHwnd( + m_dev.get(), window_hwnd, &fs_sd_desc, &fs_desc, fullscreen_output.get(), m_swap_chain.put()); + if (FAILED(hr)) + { + Console.Warning("Failed to create fullscreen swap chain, trying windowed."); + m_is_exclusive_fullscreen = false; + m_using_allow_tearing = m_allow_tearing_supported && m_using_flip_model_swap_chain; + } } - Console.WriteLn("Creating a %dx%d %s %s swap chain", swap_chain_desc.Width, swap_chain_desc.Height, - m_using_flip_model_swap_chain ? "flip-discard" : "discard", fullscreen_mode ? "full-screen" : "windowed"); + if (!m_is_exclusive_fullscreen) + { + Console.WriteLn("Creating a %dx%d %s windowed swap chain", swap_chain_desc.Width, swap_chain_desc.Height, + m_using_flip_model_swap_chain ? "flip-discard" : "discard"); + hr = m_dxgi_factory->CreateSwapChainForHwnd( + m_dev.get(), window_hwnd, &swap_chain_desc, nullptr, nullptr, m_swap_chain.put()); + } - HRESULT hr = m_dxgi_factory->CreateSwapChainForHwnd(m_dev.get(), window_hwnd, &swap_chain_desc, - fullscreen_mode ? &fs_desc : nullptr, nullptr, m_swap_chain.put()); if (FAILED(hr) && m_using_flip_model_swap_chain) { Console.Warning("Failed to create a flip-discard swap chain, trying discard."); @@ -635,8 +672,8 @@ bool GSDevice11::CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode) m_using_flip_model_swap_chain = false; m_using_allow_tearing = false; - hr = m_dxgi_factory->CreateSwapChainForHwnd(m_dev.get(), window_hwnd, &swap_chain_desc, - fullscreen_mode ? &fs_desc : nullptr, nullptr, m_swap_chain.put()); + hr = m_dxgi_factory->CreateSwapChainForHwnd( + m_dev.get(), window_hwnd, &swap_chain_desc, nullptr, nullptr, m_swap_chain.put()); if (FAILED(hr)) { Console.Error("CreateSwapChainForHwnd failed: 0x%08X", hr); @@ -648,7 +685,16 @@ bool GSDevice11::CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode) if (FAILED(hr)) Console.Warning("MakeWindowAssociation() to disable ALT+ENTER failed"); - return CreateSwapChainRTV(); + if (!CreateSwapChainRTV()) + { + DestroySwapChain(); + return false; + } + + // Render a frame as soon as possible to clear out whatever was previously being displayed. + m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data()); + m_swap_chain->Present(0, m_using_allow_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0); + return true; } bool GSDevice11::CreateSwapChainRTV() @@ -670,6 +716,7 @@ bool GSDevice11::CreateSwapChainRTV() if (FAILED(hr)) { Console.Error("CreateRenderTargetView for swap chain failed: 0x%08X", hr); + m_swap_chain_rtv.reset(); return false; } @@ -696,25 +743,42 @@ bool GSDevice11::CreateSwapChainRTV() return true; } -bool GSDevice11::ChangeWindow(const WindowInfo& new_wi) +void GSDevice11::DestroySwapChain() { - DestroySurface(); + if (!m_swap_chain) + return; - m_window_info = new_wi; - if (new_wi.type == WindowInfo::Type::Surfaceless) - return true; + m_swap_chain_rtv.reset(); - return CreateSwapChain(nullptr); + // switch out of fullscreen before destroying + BOOL is_fullscreen; + if (SUCCEEDED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) && is_fullscreen) + m_swap_chain->SetFullscreenState(FALSE, nullptr); + + m_swap_chain.reset(); + m_is_exclusive_fullscreen = false; +} + +bool GSDevice11::UpdateWindow() +{ + DestroySwapChain(); + + if (!AcquireWindow(false)) + return false; + + if (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain()) + { + Console.WriteLn("Failed to create swap chain on updated window"); + ReleaseWindow(); + return false; + } + + return true; } void GSDevice11::DestroySurface() { - m_swap_chain_rtv.reset(); - - if (IsExclusiveFullscreen()) - SetExclusiveFullscreen(false, 0, 0, 0.0f); - - m_swap_chain.reset(); + DestroySwapChain(); } std::string GSDevice11::GetDriverInfo() const @@ -768,7 +832,7 @@ std::string GSDevice11::GetDriverInfo() const void GSDevice11::ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) { - if (!m_swap_chain) + if (!m_swap_chain || m_is_exclusive_fullscreen) return; m_window_info.surface_scale = new_window_scale; @@ -792,79 +856,21 @@ bool GSDevice11::SupportsExclusiveFullscreen() const return true; } -bool GSDevice11::IsExclusiveFullscreen() -{ - BOOL is_fullscreen = FALSE; - return (m_swap_chain && SUCCEEDED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) && is_fullscreen); -} - -bool GSDevice11::SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) -{ - if (!m_swap_chain) - return false; - - BOOL is_fullscreen = FALSE; - HRESULT hr = m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr); - if (!fullscreen) - { - // leaving fullscreen - if (is_fullscreen) - return SUCCEEDED(m_swap_chain->SetFullscreenState(FALSE, nullptr)); - else - return true; - } - - IDXGIOutput* output; - if (FAILED(hr = m_swap_chain->GetContainingOutput(&output))) - return false; - - DXGI_SWAP_CHAIN_DESC current_desc; - hr = m_swap_chain->GetDesc(¤t_desc); - if (FAILED(hr)) - return false; - - DXGI_MODE_DESC new_mode = current_desc.BufferDesc; - new_mode.Width = width; - new_mode.Height = height; - new_mode.RefreshRate.Numerator = static_cast(std::floor(refresh_rate * 1000.0f)); - new_mode.RefreshRate.Denominator = 1000u; - - DXGI_MODE_DESC closest_mode; - if (FAILED(hr = output->FindClosestMatchingMode(&new_mode, &closest_mode, nullptr)) || - new_mode.Format != current_desc.BufferDesc.Format) - { - Console.Error("Failed to find closest matching mode, hr=%08X", hr); - return false; - } - - if (new_mode.Width == current_desc.BufferDesc.Width && new_mode.Height == current_desc.BufferDesc.Height && - new_mode.RefreshRate.Numerator == current_desc.BufferDesc.RefreshRate.Numerator && - new_mode.RefreshRate.Denominator == current_desc.BufferDesc.RefreshRate.Denominator) - { - DevCon.WriteLn("Fullscreen mode already set"); - return true; - } - - m_swap_chain_rtv.reset(); - m_swap_chain.reset(); - - if (!CreateSwapChain(&closest_mode)) - { - Console.Error("Failed to create a fullscreen swap chain"); - if (!CreateSwapChain(nullptr)) - pxFailRel("Failed to recreate windowed swap chain"); - - return false; - } - - return true; -} - GSDevice::PresentResult GSDevice11::BeginPresent(bool frame_skip) { if (frame_skip || !m_swap_chain) return PresentResult::FrameSkipped; + // Check if we lost exclusive fullscreen. If so, notify the host, so it can switch to windowed mode. + // This might get called repeatedly if it takes a while to switch back, that's the host's problem. + BOOL is_fullscreen; + if (m_is_exclusive_fullscreen && + (FAILED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) || !is_fullscreen)) + { + Host::RunOnCPUThread([]() { Host::SetFullscreen(false); }); + return PresentResult::FrameSkipped; + } + // When using vsync, the time here seems to include the time for the buffer to become available. // This blows our our GPU usage number considerably, so read the timestamp before the final blit // in this configuration. It does reduce accuracy a little, but better than seeing 100% all of @@ -872,8 +878,7 @@ GSDevice::PresentResult GSDevice11::BeginPresent(bool frame_skip) if (m_vsync_mode != VsyncMode::Off && m_gpu_timing_enabled) PopTimestampQuery(); - static constexpr std::array clear_color = {}; - m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), clear_color.data()); + m_ctx->ClearRenderTargetView(m_swap_chain_rtv.get(), s_present_clear_color.data()); m_ctx->OMSetRenderTargets(1, m_swap_chain_rtv.addressof(), nullptr); if (m_state.rt_view) m_state.rt_view->Release(); diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.h b/pcsx2/GS/Renderers/DX11/GSDevice11.h index 91fc0a4054..78561ee4de 100644 --- a/pcsx2/GS/Renderers/DX11/GSDevice11.h +++ b/pcsx2/GS/Renderers/DX11/GSDevice11.h @@ -120,8 +120,9 @@ private: void SetFeatures(); - bool CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode); + bool CreateSwapChain(); bool CreateSwapChainRTV(); + void DestroySwapChain(); bool CreateTimestampQueries(); void DestroyTimestampQueries(); @@ -160,6 +161,7 @@ private: bool m_allow_tearing_supported = false; bool m_using_flip_model_swap_chain = true; bool m_using_allow_tearing = false; + bool m_is_exclusive_fullscreen = false; struct { @@ -281,16 +283,14 @@ public: __fi ID3D11Device1* GetD3DDevice() const { return m_dev.get(); } __fi ID3D11DeviceContext1* GetD3DContext() const { return m_ctx.get(); } - bool Create(const WindowInfo& wi, VsyncMode vsync) override; + bool Create() override; void Destroy() override; RenderAPI GetRenderAPI() const override; - bool ChangeWindow(const WindowInfo& new_wi) override; + bool UpdateWindow() override; void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) override; bool SupportsExclusiveFullscreen() const override; - bool IsExclusiveFullscreen() override; - bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) override; bool HasSurface() const override; void DestroySurface() override; std::string GetDriverInfo() const override; diff --git a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp index ddbc99e2aa..7ea559f579 100644 --- a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp +++ b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp @@ -44,6 +44,8 @@ static bool IsDATEModePrimIDInit(u32 flag) { return flag == 1 || flag == 2; } static constexpr std::array s_primitive_topology_mapping = {{D3D_PRIMITIVE_TOPOLOGY_POINTLIST, D3D_PRIMITIVE_TOPOLOGY_LINELIST, D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST}}; +static constexpr std::array s_present_clear_color = {}; + static D3D12_RENDER_PASS_BEGINNING_ACCESS_TYPE GetLoadOpForTexture(GSTexture12* tex) { if (!tex) @@ -116,9 +118,9 @@ bool GSDevice12::HasSurface() const return static_cast(m_swap_chain); } -bool GSDevice12::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDevice12::Create() { - if (!GSDevice::Create(wi, vsync)) + if (!GSDevice::Create()) return false; m_dxgi_factory = D3D::CreateFactory(EmuConfig.GS.UseDebugDevice); @@ -144,7 +146,7 @@ bool GSDevice12::Create(const WindowInfo& wi, VsyncMode vsync) return false; } - if (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain(nullptr)) + if (!AcquireWindow(true) || (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain())) return false; { @@ -215,16 +217,15 @@ void GSDevice12::Destroy() EndRenderPass(); ExecuteCommandList(true); DestroyResources(); - - g_d3d12_context->WaitForGPUIdle(); - GSDevice12::DestroySurface(); + DestroySwapChain(); + ReleaseWindow(); g_d3d12_context->Destroy(); } } bool GSDevice12::GetHostRefreshRate(float* refresh_rate) { - if (m_swap_chain && IsExclusiveFullscreen()) + if (m_swap_chain && m_is_exclusive_fullscreen) { DXGI_SWAP_CHAIN_DESC desc; if (SUCCEEDED(m_swap_chain->GetDesc(&desc)) && desc.BufferDesc.RefreshRate.Numerator > 0 && @@ -246,59 +247,101 @@ void GSDevice12::SetVSync(VsyncMode mode) m_vsync_mode = mode; } - -bool GSDevice12::CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode) +bool GSDevice12::CreateSwapChain() { + constexpr DXGI_FORMAT swap_chain_format = DXGI_FORMAT_R8G8B8A8_UNORM; + if (m_window_info.type != WindowInfo::Type::Win32) return false; const HWND window_hwnd = reinterpret_cast(m_window_info.window_handle); RECT client_rc{}; GetClientRect(window_hwnd, &client_rc); - const u32 width = static_cast(client_rc.right - client_rc.left); - const u32 height = static_cast(client_rc.bottom - client_rc.top); + + DXGI_MODE_DESC fullscreen_mode; + wil::com_ptr_nothrow fullscreen_output; + if (Host::IsFullscreen()) + { + u32 fullscreen_width, fullscreen_height; + float fullscreen_refresh_rate; + m_is_exclusive_fullscreen = + GetRequestedExclusiveFullscreenMode(&fullscreen_width, &fullscreen_height, &fullscreen_refresh_rate) && + D3D::GetRequestedExclusiveFullscreenModeDesc(m_dxgi_factory.get(), client_rc, fullscreen_width, + fullscreen_height, fullscreen_refresh_rate, swap_chain_format, &fullscreen_mode, + fullscreen_output.put()); + } + else + { + m_is_exclusive_fullscreen = false; + } DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = {}; - swap_chain_desc.Width = width; - swap_chain_desc.Height = height; - swap_chain_desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + swap_chain_desc.Width = static_cast(client_rc.right - client_rc.left); + swap_chain_desc.Height = static_cast(client_rc.bottom - client_rc.top); + swap_chain_desc.Format = swap_chain_format; swap_chain_desc.SampleDesc.Count = 1; swap_chain_desc.BufferCount = 3; swap_chain_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; swap_chain_desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; - m_using_allow_tearing = (m_allow_tearing_supported && !fullscreen_mode); + m_using_allow_tearing = (m_allow_tearing_supported && !m_is_exclusive_fullscreen); if (m_using_allow_tearing) swap_chain_desc.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING; - DXGI_SWAP_CHAIN_FULLSCREEN_DESC fs_desc = {}; - if (fullscreen_mode) + HRESULT hr = S_OK; + + if (m_is_exclusive_fullscreen) { - swap_chain_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; - swap_chain_desc.Width = fullscreen_mode->Width; - swap_chain_desc.Height = fullscreen_mode->Height; - fs_desc.RefreshRate = fullscreen_mode->RefreshRate; - fs_desc.ScanlineOrdering = fullscreen_mode->ScanlineOrdering; - fs_desc.Scaling = fullscreen_mode->Scaling; + DXGI_SWAP_CHAIN_DESC1 fs_sd_desc = swap_chain_desc; + DXGI_SWAP_CHAIN_FULLSCREEN_DESC fs_desc = {}; + + fs_sd_desc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + fs_sd_desc.Width = fullscreen_mode.Width; + fs_sd_desc.Height = fullscreen_mode.Height; + fs_desc.RefreshRate = fullscreen_mode.RefreshRate; + fs_desc.ScanlineOrdering = fullscreen_mode.ScanlineOrdering; + fs_desc.Scaling = fullscreen_mode.Scaling; fs_desc.Windowed = FALSE; + + Console.WriteLn("Creating a %dx%d exclusive fullscreen swap chain", fs_sd_desc.Width, fs_sd_desc.Height); + hr = m_dxgi_factory->CreateSwapChainForHwnd(g_d3d12_context->GetCommandQueue(), window_hwnd, &fs_sd_desc, + &fs_desc, fullscreen_output.get(), m_swap_chain.put()); + if (FAILED(hr)) + { + Console.Warning("Failed to create fullscreen swap chain, trying windowed."); + m_is_exclusive_fullscreen = false; + m_using_allow_tearing = m_allow_tearing_supported; + } } - DevCon.WriteLn("Creating a %dx%d %s swap chain", swap_chain_desc.Width, swap_chain_desc.Height, - fullscreen_mode ? "full-screen" : "windowed"); - - HRESULT hr = m_dxgi_factory->CreateSwapChainForHwnd(g_d3d12_context->GetCommandQueue(), window_hwnd, - &swap_chain_desc, fullscreen_mode ? &fs_desc : nullptr, nullptr, m_swap_chain.put()); - if (FAILED(hr)) + if (!m_is_exclusive_fullscreen) { - Console.Error("CreateSwapChainForHwnd failed: 0x%08X", hr); - return false; + Console.WriteLn("Creating a %dx%d windowed swap chain", swap_chain_desc.Width, swap_chain_desc.Height); + hr = m_dxgi_factory->CreateSwapChainForHwnd( + g_d3d12_context->GetCommandQueue(), window_hwnd, &swap_chain_desc, nullptr, nullptr, m_swap_chain.put()); } hr = m_dxgi_factory->MakeWindowAssociation(window_hwnd, DXGI_MWA_NO_WINDOW_CHANGES); if (FAILED(hr)) Console.Warning("MakeWindowAssociation() to disable ALT+ENTER failed"); - return CreateSwapChainRTV(); + if (!CreateSwapChainRTV()) + { + DestroySwapChain(); + return false; + } + + // Render a frame as soon as possible to clear out whatever was previously being displayed. + EndRenderPass(); + D3D12::Texture& swap_chain_buf = m_swap_chain_buffers[m_current_swap_chain_buffer]; + ID3D12GraphicsCommandList4* cmdlist = g_d3d12_context->GetCommandList(); + m_current_swap_chain_buffer = ((m_current_swap_chain_buffer + 1) % static_cast(m_swap_chain_buffers.size())); + swap_chain_buf.TransitionToState(cmdlist, D3D12_RESOURCE_STATE_RENDER_TARGET); + cmdlist->ClearRenderTargetView(swap_chain_buf.GetWriteDescriptor(), s_present_clear_color.data(), 0, nullptr); + swap_chain_buf.TransitionToState(cmdlist, D3D12_RESOURCE_STATE_PRESENT); + ExecuteCommandList(false); + m_swap_chain->Present(0, m_using_allow_tearing ? DXGI_PRESENT_ALLOW_TEARING : 0); + return true; } bool GSDevice12::CreateSwapChainRTV() @@ -315,6 +358,7 @@ bool GSDevice12::CreateSwapChainRTV() if (FAILED(hr)) { Console.Error("GetBuffer for RTV failed: 0x%08X", hr); + m_swap_chain_buffers.clear(); return false; } @@ -322,6 +366,7 @@ bool GSDevice12::CreateSwapChainRTV() if (!tex.Adopt(std::move(backbuffer), DXGI_FORMAT_UNKNOWN, swap_chain_desc.BufferDesc.Format, DXGI_FORMAT_UNKNOWN, D3D12_RESOURCE_STATE_PRESENT)) { + m_swap_chain_buffers.clear(); return false; } @@ -360,26 +405,44 @@ void GSDevice12::DestroySwapChainRTVs() m_current_swap_chain_buffer = 0; } -bool GSDevice12::ChangeWindow(const WindowInfo& new_wi) +void GSDevice12::DestroySwapChain() { - DestroySurface(); + if (!m_swap_chain) + return; - m_window_info = new_wi; - if (new_wi.type == WindowInfo::Type::Surfaceless) - return true; + DestroySwapChainRTVs(); - return CreateSwapChain(nullptr); + // switch out of fullscreen before destroying + BOOL is_fullscreen; + if (SUCCEEDED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) && is_fullscreen) + m_swap_chain->SetFullscreenState(FALSE, nullptr); + + m_swap_chain.reset(); + m_is_exclusive_fullscreen = false; +} + +bool GSDevice12::UpdateWindow() +{ + ExecuteCommandList(true); + DestroySwapChain(); + + if (!AcquireWindow(false)) + return false; + + if (m_window_info.type != WindowInfo::Type::Surfaceless && !CreateSwapChain()) + { + Console.WriteLn("Failed to create swap chain on updated window"); + ReleaseWindow(); + return false; + } + + return true; } void GSDevice12::DestroySurface() { ExecuteCommandList(true); - - if (IsExclusiveFullscreen()) - SetExclusiveFullscreen(false, 0, 0, 0.0f); - - DestroySwapChainRTVs(); - m_swap_chain.reset(); + DestroySwapChain(); } std::string GSDevice12::GetDriverInfo() const @@ -452,75 +515,6 @@ bool GSDevice12::SupportsExclusiveFullscreen() const return true; } -bool GSDevice12::IsExclusiveFullscreen() -{ - BOOL is_fullscreen = FALSE; - return (m_swap_chain && SUCCEEDED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) && is_fullscreen); -} - -bool GSDevice12::SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) -{ - if (!m_swap_chain) - return false; - - BOOL is_fullscreen = FALSE; - HRESULT hr = m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr); - if (!fullscreen) - { - // leaving fullscreen - if (is_fullscreen) - return SUCCEEDED(m_swap_chain->SetFullscreenState(FALSE, nullptr)); - else - return true; - } - - IDXGIOutput* output; - if (FAILED(hr = m_swap_chain->GetContainingOutput(&output))) - return false; - - DXGI_SWAP_CHAIN_DESC current_desc; - hr = m_swap_chain->GetDesc(¤t_desc); - if (FAILED(hr)) - return false; - - DXGI_MODE_DESC new_mode = current_desc.BufferDesc; - new_mode.Width = width; - new_mode.Height = height; - new_mode.RefreshRate.Numerator = static_cast(std::floor(refresh_rate * 1000.0f)); - new_mode.RefreshRate.Denominator = 1000u; - - DXGI_MODE_DESC closest_mode; - if (FAILED(hr = output->FindClosestMatchingMode(&new_mode, &closest_mode, nullptr)) || - new_mode.Format != current_desc.BufferDesc.Format) - { - Console.Error("Failed to find closest matching mode, hr=%08X", hr); - return false; - } - - if (new_mode.Width == current_desc.BufferDesc.Width && new_mode.Height == current_desc.BufferDesc.Width && - new_mode.RefreshRate.Numerator == current_desc.BufferDesc.RefreshRate.Numerator && - new_mode.RefreshRate.Denominator == current_desc.BufferDesc.RefreshRate.Denominator) - { - DevCon.WriteLn("Fullscreen mode already set"); - return true; - } - - g_d3d12_context->ExecuteCommandList(D3D12::Context::WaitType::Sleep); - DestroySwapChainRTVs(); - m_swap_chain.reset(); - - if (!CreateSwapChain(&closest_mode)) - { - Console.Error("Failed to create a fullscreen swap chain"); - if (!CreateSwapChain(nullptr)) - pxFailRel("Failed to recreate windowed swap chain"); - - return false; - } - - return true; -} - GSDevice::PresentResult GSDevice12::BeginPresent(bool frame_skip) { EndRenderPass(); @@ -531,12 +525,21 @@ GSDevice::PresentResult GSDevice12::BeginPresent(bool frame_skip) if (frame_skip || !m_swap_chain) return PresentResult::FrameSkipped; - static constexpr std::array clear_color = {}; + // Check if we lost exclusive fullscreen. If so, notify the host, so it can switch to windowed mode. + // This might get called repeatedly if it takes a while to switch back, that's the host's problem. + BOOL is_fullscreen; + if (m_is_exclusive_fullscreen && + (FAILED(m_swap_chain->GetFullscreenState(&is_fullscreen, nullptr)) || !is_fullscreen)) + { + Host::RunOnCPUThread([]() { Host::SetFullscreen(false); }); + return PresentResult::FrameSkipped; + } + D3D12::Texture& swap_chain_buf = m_swap_chain_buffers[m_current_swap_chain_buffer]; ID3D12GraphicsCommandList* cmdlist = g_d3d12_context->GetCommandList(); swap_chain_buf.TransitionToState(cmdlist, D3D12_RESOURCE_STATE_RENDER_TARGET); - cmdlist->ClearRenderTargetView(swap_chain_buf.GetWriteDescriptor(), clear_color.data(), 0, nullptr); + cmdlist->ClearRenderTargetView(swap_chain_buf.GetWriteDescriptor(), s_present_clear_color.data(), 0, nullptr); cmdlist->OMSetRenderTargets(1, &swap_chain_buf.GetWriteDescriptor().cpu_handle, FALSE, nullptr); g_perfmon.Put(GSPerfMon::RenderPasses, 1); diff --git a/pcsx2/GS/Renderers/DX12/GSDevice12.h b/pcsx2/GS/Renderers/DX12/GSDevice12.h index 0d5b403228..9711b44140 100644 --- a/pcsx2/GS/Renderers/DX12/GSDevice12.h +++ b/pcsx2/GS/Renderers/DX12/GSDevice12.h @@ -145,6 +145,7 @@ private: bool m_allow_tearing_supported = false; bool m_using_allow_tearing = false; + bool m_is_exclusive_fullscreen = false; bool m_device_lost = false; ComPtr m_tfx_root_signature; @@ -191,9 +192,10 @@ private: void LookupNativeFormat(GSTexture::Format format, DXGI_FORMAT* d3d_format, DXGI_FORMAT* srv_format, DXGI_FORMAT* rtv_format, DXGI_FORMAT* dsv_format) const; - bool CreateSwapChain(const DXGI_MODE_DESC* fullscreen_mode); + bool CreateSwapChain(); bool CreateSwapChainRTV(); void DestroySwapChainRTVs(); + void DestroySwapChain(); GSTexture* CreateSurface(GSTexture::Type type, int width, int height, int levels, GSTexture::Format format) override; @@ -243,14 +245,12 @@ public: RenderAPI GetRenderAPI() const override; bool HasSurface() const override; - bool Create(const WindowInfo& wi, VsyncMode vsync) override; + bool Create() override; void Destroy() override; - bool ChangeWindow(const WindowInfo& new_wi) override; + bool UpdateWindow() override; void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) override; bool SupportsExclusiveFullscreen() const override; - bool IsExclusiveFullscreen() override; - bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) override; void DestroySurface() override; std::string GetDriverInfo() const override; diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h index b3aebdccc3..de16233f2b 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.h @@ -378,7 +378,7 @@ public: MRCOwned> LoadShader(NSString* name); MRCOwned> MakePipeline(MTLRenderPipelineDescriptor* desc, id vertex, id fragment, NSString* name); MRCOwned> MakeComputePipeline(id compute, NSString* name); - bool Create(const WindowInfo& wi, VsyncMode vsync) override; + bool Create() override; void Destroy() override; void AttachSurfaceOnMainThread(); @@ -387,10 +387,8 @@ public: RenderAPI GetRenderAPI() const override; bool HasSurface() const override; void DestroySurface() override; - bool ChangeWindow(const WindowInfo& wi) override; + bool UpdateWindow() override; bool SupportsExclusiveFullscreen() const override; - bool IsExclusiveFullscreen() override; - bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) override; std::string GetDriverInfo() const override; void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) override; diff --git a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm index 7afb15952e..6bef4a1a3e 100644 --- a/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm +++ b/pcsx2/GS/Renderers/Metal/GSDeviceMTL.mm @@ -764,9 +764,9 @@ static MRCOwned> CreatePrivateBufferWithContent( return actual; } -bool GSDeviceMTL::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDeviceMTL::Create() { @autoreleasepool { - if (!GSDevice::Create(wi, vsync)) + if (!GSDevice::Create()) return false; NSString* ns_adapter_name = [NSString stringWithUTF8String:EmuConfig.GS.Adapter.c_str()]; @@ -810,11 +810,16 @@ bool GSDeviceMTL::Create(const WindowInfo& wi, VsyncMode vsync) if (m_dev.IsOk() && m_queue) { + // This is a little less than ideal, pinging back and forward between threads, but we don't really + // have any other option, because Qt uses a blocking queued connection for window acquire. + if (!AcquireWindow(true)) + return false; + OnMainThread([this] { AttachSurfaceOnMainThread(); }); - SetVSync(vsync); + [m_layer setDisplaySyncEnabled:m_vsync_mode != VsyncMode::Off]; } else { @@ -1165,6 +1170,7 @@ void GSDeviceMTL::Destroy() GSDevice::Destroy(); GSDeviceMTL::DestroySurface(); + ReleaseWindow(); m_queue = nullptr; m_dev.Reset(); }} @@ -1177,20 +1183,21 @@ void GSDeviceMTL::DestroySurface() m_layer = nullptr; } -bool GSDeviceMTL::ChangeWindow(const WindowInfo& wi) +bool GSDeviceMTL::UpdateWindow() { - OnMainThread([this, &wi] - { - DetachSurfaceOnMainThread(); - m_window_info = wi; - AttachSurfaceOnMainThread(); - }); + DestroySurface(); + + if (!AcquireWindow(false)) + return false; + + if (m_window_info.type == WindowInfo::Type::Surfaceless) + return true; + + OnMainThread([this] { AttachSurfaceOnMainThread(); }); return true; } bool GSDeviceMTL::SupportsExclusiveFullscreen() const { return false; } -bool GSDeviceMTL::IsExclusiveFullscreen() { return false; } -bool GSDeviceMTL::SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) { return false; } std::string GSDeviceMTL::GetDriverInfo() const { @autoreleasepool { @@ -1339,6 +1346,9 @@ void GSDeviceMTL::EndPresent() void GSDeviceMTL::SetVSync(VsyncMode mode) { + if (m_vsync_mode == mode) + return; + [m_layer setDisplaySyncEnabled:mode != VsyncMode::Off]; m_vsync_mode = mode; } diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp index 8cde898022..10be0cce33 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp @@ -85,9 +85,13 @@ void GSDeviceOGL::SetVSync(VsyncMode mode) m_vsync_mode = mode; } -bool GSDeviceOGL::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDeviceOGL::Create() { - if (!GSDevice::Create(wi, vsync)) + if (!GSDevice::Create()) + return false; + + // GL is a pain and needs the window super early to create the context. + if (!AcquireWindow(true)) return false; // We need at least GL3.3. @@ -95,7 +99,7 @@ bool GSDeviceOGL::Create(const WindowInfo& wi, VsyncMode vsync) {GL::Context::Profile::Core, 4, 5}, {GL::Context::Profile::Core, 4, 4}, {GL::Context::Profile::Core, 4, 3}, {GL::Context::Profile::Core, 4, 2}, {GL::Context::Profile::Core, 4, 1}, {GL::Context::Profile::Core, 4, 0}, {GL::Context::Profile::Core, 3, 3}}; - m_gl_context = GL::Context::Create(wi, version_list); + m_gl_context = GL::Context::Create(m_window_info, version_list); if (!m_gl_context) { Console.Error("Failed to create any GL context"); @@ -109,6 +113,10 @@ bool GSDeviceOGL::Create(const WindowInfo& wi, VsyncMode vsync) return false; } + // Render a frame as soon as possible to clear out whatever was previously being displayed. + if (m_window_info.type != WindowInfo::Type::Surfaceless) + RenderBlankFrame(); + if (!GLLoader::check_gl_requirements()) return false; @@ -543,6 +551,8 @@ void GSDeviceOGL::Destroy() m_gl_context->DoneCurrent(); m_gl_context.reset(); + + ReleaseWindow(); } } @@ -651,23 +661,31 @@ void GSDeviceOGL::DestroyResources() glDeleteFramebuffers(1, &m_fbo_write); } -bool GSDeviceOGL::ChangeWindow(const WindowInfo& new_wi) +bool GSDeviceOGL::UpdateWindow() { pxAssert(m_gl_context); - if (!m_gl_context->ChangeSurface(new_wi)) + DestroySurface(); + + if (!AcquireWindow(false)) + return false; + + if (!m_gl_context->ChangeSurface(m_window_info)) { Console.Error("Failed to change surface"); + ReleaseWindow(); return false; } m_window_info = m_gl_context->GetWindowInfo(); - if (new_wi.type != WindowInfo::Type::Surfaceless) + if (m_window_info.type != WindowInfo::Type::Surfaceless) { // reset vsync rate, since it (usually) gets lost if (m_vsync_mode != VsyncMode::Adaptive || !m_gl_context->SetSwapInterval(-1)) m_gl_context->SetSwapInterval(static_cast(m_vsync_mode != VsyncMode::Off)); + + RenderBlankFrame(); } return true; @@ -691,16 +709,6 @@ bool GSDeviceOGL::SupportsExclusiveFullscreen() const return false; } -bool GSDeviceOGL::IsExclusiveFullscreen() -{ - return false; -} - -bool GSDeviceOGL::SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) -{ - return false; -} - void GSDeviceOGL::DestroySurface() { m_window_info = {}; @@ -2039,6 +2047,17 @@ void GSDeviceOGL::RenderImGui() glScissor(GLState::scissor.x, GLState::scissor.y, GLState::scissor.width(), GLState::scissor.height()); } +void GSDeviceOGL::RenderBlankFrame() +{ + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); + glDisable(GL_SCISSOR_TEST); + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + m_gl_context->SwapBuffers(); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLState::fbo); + glEnable(GL_SCISSOR_TEST); +} + void GSDeviceOGL::OMAttachRt(GSTextureOGL* rt) { if (rt) diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h index fe69b4205e..f161f7e3b7 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.h @@ -262,6 +262,7 @@ private: bool CreateImGuiProgram(); void RenderImGui(); + void RenderBlankFrame(); void OMAttachRt(GSTextureOGL* rt = nullptr); void OMAttachDs(GSTextureOGL* ds = nullptr); @@ -286,14 +287,12 @@ public: RenderAPI GetRenderAPI() const override; bool HasSurface() const override; - bool Create(const WindowInfo& wi, VsyncMode vsync) override; + bool Create() override; void Destroy() override; - bool ChangeWindow(const WindowInfo& new_wi) override; + bool UpdateWindow() override; void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) override; bool SupportsExclusiveFullscreen() const override; - bool IsExclusiveFullscreen() override; - bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) override; void DestroySurface() override; std::string GetDriverInfo() const override; diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index 2ae953162e..e3e30c55aa 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -70,6 +70,8 @@ static VkPresentModeKHR GetPreferredPresentModeForVsyncMode(VsyncMode mode) return VK_PRESENT_MODE_IMMEDIATE_KHR; } +static constexpr VkClearValue s_present_clear_color = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; + GSDeviceVK::GSDeviceVK() { #ifdef ENABLE_OGL_DEBUG @@ -105,7 +107,7 @@ void GSDeviceVK::GetAdaptersAndFullscreenModes( if (Vulkan::LoadVulkanLibrary()) { ScopedGuard lib_guard([]() { Vulkan::UnloadVulkanLibrary(); }); - const VkInstance instance = Vulkan::Context::CreateVulkanInstance(nullptr, false, false); + const VkInstance instance = Vulkan::Context::CreateVulkanInstance(WindowInfo(), false, false); if (instance != VK_NULL_HANDLE) { if (Vulkan::LoadVulkanInstanceFunctions(instance)) @@ -171,26 +173,17 @@ bool GSDeviceVK::HasSurface() const return static_cast(m_swap_chain); } -bool GSDeviceVK::Create(const WindowInfo& wi, VsyncMode vsync) +bool GSDeviceVK::Create() { - if (!GSDevice::Create(wi, vsync)) + if (!GSDevice::Create()) return false; - WindowInfo local_wi(wi); - const bool debug_device = GSConfig.UseDebugDevice; - if (!Vulkan::Context::Create(GSConfig.Adapter, &local_wi, &m_swap_chain, GetPreferredPresentModeForVsyncMode(vsync), - !GSConfig.DisableThreadedPresentation, debug_device, debug_device)) - { - Console.Error("Failed to create Vulkan context"); + if (!CreateDeviceAndSwapChain()) return false; - } Vulkan::ShaderCache::Create(GSConfig.DisableShaderCache ? std::string_view() : std::string_view(EmuFolders::Cache), SHADER_CACHE_VERSION, GSConfig.UseDebugDevice); - // NOTE: This is assigned afterwards, because some platforms can modify the window info (e.g. Metal). - m_window_info = m_swap_chain ? m_swap_chain->GetWindowInfo() : local_wi; - if (!CheckFeatures()) { Console.Error("Your GPU does not support the required Vulkan features."); @@ -264,29 +257,31 @@ void GSDeviceVK::Destroy() g_vulkan_context->WaitForGPUIdle(); m_swap_chain.reset(); + ReleaseWindow(); Vulkan::ShaderCache::Destroy(); Vulkan::Context::Destroy(); } } -bool GSDeviceVK::ChangeWindow(const WindowInfo& new_wi) +bool GSDeviceVK::UpdateWindow() { - if (new_wi.type == WindowInfo::Type::Surfaceless) - { - ExecuteCommandBuffer(true); - m_swap_chain.reset(); - m_window_info = new_wi; + DestroySurface(); + + if (!AcquireWindow(false)) + return false; + + if (m_window_info.type == WindowInfo::Type::Surfaceless) return true; - } // make sure previous frames are presented + ExecuteCommandBuffer(false); g_vulkan_context->WaitForGPUIdle(); // recreate surface in existing swap chain if it already exists if (m_swap_chain) { - if (m_swap_chain->RecreateSurface(new_wi)) + if (m_swap_chain->RecreateSurface(m_window_info)) { m_window_info = m_swap_chain->GetWindowInfo(); return true; @@ -295,24 +290,26 @@ bool GSDeviceVK::ChangeWindow(const WindowInfo& new_wi) m_swap_chain.reset(); } - WindowInfo wi_copy(new_wi); VkSurfaceKHR surface = Vulkan::SwapChain::CreateVulkanSurface( - g_vulkan_context->GetVulkanInstance(), g_vulkan_context->GetPhysicalDevice(), &wi_copy); + g_vulkan_context->GetVulkanInstance(), g_vulkan_context->GetPhysicalDevice(), &m_window_info); if (surface == VK_NULL_HANDLE) { Console.Error("Failed to create new surface for swap chain"); + ReleaseWindow(); return false; } - m_swap_chain = Vulkan::SwapChain::Create(wi_copy, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode)); + m_swap_chain = Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode)); if (!m_swap_chain) { Console.Error("Failed to create swap chain"); - Vulkan::SwapChain::DestroyVulkanSurface(g_vulkan_context->GetVulkanInstance(), &wi_copy, surface); + Vulkan::SwapChain::DestroyVulkanSurface(g_vulkan_context->GetVulkanInstance(), &m_window_info, surface); + ReleaseWindow(); return false; } m_window_info = m_swap_chain->GetWindowInfo(); + RenderBlankFrame(); return true; } @@ -344,16 +341,6 @@ bool GSDeviceVK::SupportsExclusiveFullscreen() const return false; } -bool GSDeviceVK::IsExclusiveFullscreen() -{ - return false; -} - -bool GSDeviceVK::SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) -{ - return false; -} - void GSDeviceVK::DestroySurface() { g_vulkan_context->WaitForGPUIdle(); @@ -453,10 +440,9 @@ GSDevice::PresentResult GSDeviceVK::BeginPresent(bool frame_skip) swap_chain_texture.OverrideImageLayout(VK_IMAGE_LAYOUT_UNDEFINED); swap_chain_texture.TransitionToLayout(cmdbuffer, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL); - const VkClearValue clear_value = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; const VkRenderPassBeginInfo rp = {VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO, nullptr, m_swap_chain->GetClearRenderPass(), m_swap_chain->GetCurrentFramebuffer(), - {{0, 0}, {swap_chain_texture.GetWidth(), swap_chain_texture.GetHeight()}}, 1u, &clear_value}; + {{0, 0}, {swap_chain_texture.GetWidth(), swap_chain_texture.GetHeight()}}, 1u, &s_present_clear_color}; vkCmdBeginRenderPass(g_vulkan_context->GetCurrentCommandBuffer(), &rp, VK_SUBPASS_CONTENTS_INLINE); const VkViewport vp{0.0f, 0.0f, static_cast(swap_chain_texture.GetWidth()), @@ -570,6 +556,128 @@ void GSDeviceVK::InsertDebugMessage(DebugMessageCategory category, const char* f #endif } +bool GSDeviceVK::CreateDeviceAndSwapChain() +{ + bool enable_debug_utils = GSConfig.UseDebugDevice; + bool enable_validation_layer = GSConfig.UseDebugDevice; + + if (!Vulkan::LoadVulkanLibrary()) + { + Host::ReportErrorAsync("Error", "Failed to load Vulkan library. Does your GPU and/or driver support Vulkan?"); + return false; + } + + ScopedGuard library_cleanup(&Vulkan::UnloadVulkanLibrary); + + if (!AcquireWindow(true)) + return false; + + ScopedGuard window_cleanup = [this]() { ReleaseWindow(); }; + + VkInstance instance = + Vulkan::Context::CreateVulkanInstance(m_window_info, enable_debug_utils, enable_validation_layer); + if (instance == VK_NULL_HANDLE) + { + if (enable_debug_utils || enable_validation_layer) + { + // Try again without the validation layer. + enable_debug_utils = false; + enable_validation_layer = false; + instance = + Vulkan::Context::CreateVulkanInstance(m_window_info, enable_debug_utils, enable_validation_layer); + if (instance == VK_NULL_HANDLE) + { + Host::ReportErrorAsync( + "Error", "Failed to create Vulkan instance. Does your GPU and/or driver support Vulkan?"); + return false; + } + + Console.Error("Vulkan validation/debug layers requested but are unavailable. Creating non-debug device."); + } + } + + ScopedGuard instance_cleanup = [&instance]() { vkDestroyInstance(instance, nullptr); }; + if (!Vulkan::LoadVulkanInstanceFunctions(instance)) + { + Console.Error("Failed to load Vulkan instance functions"); + return false; + } + + Vulkan::Context::GPUList gpus = Vulkan::Context::EnumerateGPUs(instance); + if (gpus.empty()) + { + Host::ReportErrorAsync("Error", "No physical devices found. Does your GPU and/or driver support Vulkan?"); + return false; + } + + u32 gpu_index = 0; + Vulkan::Context::GPUNameList gpu_names = Vulkan::Context::EnumerateGPUNames(instance); + if (!GSConfig.Adapter.empty()) + { + for (; gpu_index < static_cast(gpu_names.size()); gpu_index++) + { + Console.WriteLn(fmt::format("GPU {}: {}", gpu_index, gpu_names[gpu_index])); + if (gpu_names[gpu_index] == GSConfig.Adapter) + break; + } + + if (gpu_index == static_cast(gpu_names.size())) + { + Console.Warning( + fmt::format("Requested GPU '{}' not found, using first ({})", GSConfig.Adapter, gpu_names[0])); + gpu_index = 0; + } + } + else + { + Console.WriteLn("No GPU requested, using first (%s)", gpu_names[0].c_str()); + } + + VkSurfaceKHR surface = VK_NULL_HANDLE; + ScopedGuard surface_cleanup = [&instance, &surface]() { + if (surface != VK_NULL_HANDLE) + vkDestroySurfaceKHR(instance, surface, nullptr); + }; + if (m_window_info.type != WindowInfo::Type::Surfaceless) + { + surface = Vulkan::SwapChain::CreateVulkanSurface(instance, gpus[gpu_index], &m_window_info); + if (surface == VK_NULL_HANDLE) + return false; + } + + if (!Vulkan::Context::Create(instance, surface, gpus[gpu_index], enable_debug_utils, enable_validation_layer, + !GSConfig.DisableThreadedPresentation)) + { + Console.Error("Failed to create Vulkan context"); + return false; + } + + // NOTE: This is assigned afterwards, because some platforms can modify the window info (e.g. Metal). + if (surface != VK_NULL_HANDLE) + { + m_swap_chain = + Vulkan::SwapChain::Create(m_window_info, surface, GetPreferredPresentModeForVsyncMode(m_vsync_mode)); + if (!m_swap_chain) + { + Console.Error("Failed to create swap chain"); + return false; + } + + m_window_info = m_swap_chain->GetWindowInfo(); + } + + surface_cleanup.Cancel(); + instance_cleanup.Cancel(); + window_cleanup.Cancel(); + library_cleanup.Cancel(); + + // Render a frame as soon as possible to clear out whatever was previously being displayed. + if (m_window_info.type != WindowInfo::Type::Surfaceless) + RenderBlankFrame(); + + return true; +} + bool GSDeviceVK::CheckFeatures() { const VkPhysicalDeviceProperties& properties = g_vulkan_context->GetDeviceProperties(); @@ -2369,6 +2477,29 @@ void GSDeviceVK::RenderImGui() } } +void GSDeviceVK::RenderBlankFrame() +{ + VkResult res = m_swap_chain->AcquireNextImage(); + if (res != VK_SUCCESS) + { + Console.Error("Failed to acquire image for blank frame present"); + return; + } + + VkCommandBuffer cmdbuffer = g_vulkan_context->GetCurrentCommandBuffer(); + Vulkan::Texture& sctex = m_swap_chain->GetCurrentTexture(); + sctex.TransitionToLayout(cmdbuffer, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); + + constexpr VkImageSubresourceRange srr = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(cmdbuffer, sctex.GetImage(), sctex.GetLayout(), &s_present_clear_color.color, 1, &srr); + + m_swap_chain->GetCurrentTexture().TransitionToLayout(cmdbuffer, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR); + g_vulkan_context->SubmitCommandBuffer(m_swap_chain.get(), !m_swap_chain->IsPresentModeSynchronizing()); + g_vulkan_context->MoveToNextCommandBuffer(); + + InvalidateCachedState(); +} + bool GSDeviceVK::DoCAS(GSTexture* sTex, GSTexture* dTex, bool sharpen_only, const std::array& constants) { EndRenderPass(); diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h index f0375a0f96..69403253a7 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.h @@ -190,6 +190,7 @@ private: VkShaderModule GetUtilityVertexShader(const std::string& source, const char* replace_main); VkShaderModule GetUtilityFragmentShader(const std::string& source, const char* replace_main); + bool CreateDeviceAndSwapChain(); bool CheckFeatures(); bool CreateNullTexture(); bool CreateBuffers(); @@ -205,6 +206,7 @@ private: bool CompileImGuiPipeline(); void RenderImGui(); + void RenderBlankFrame(); void DestroyResources(); @@ -230,14 +232,12 @@ public: RenderAPI GetRenderAPI() const override; bool HasSurface() const override; - bool Create(const WindowInfo& wi, VsyncMode vsync) override; + bool Create() override; void Destroy() override; - bool ChangeWindow(const WindowInfo& new_wi) override; + bool UpdateWindow() override; void ResizeWindow(s32 new_window_width, s32 new_window_height, float new_window_scale) override; bool SupportsExclusiveFullscreen() const override; - bool IsExclusiveFullscreen() override; - bool SetExclusiveFullscreen(bool fullscreen, u32 width, u32 height, float refresh_rate) override; void DestroySurface() override; std::string GetDriverInfo() const override; diff --git a/tests/ctest/core/StubHost.cpp b/tests/ctest/core/StubHost.cpp index 8dd87289c5..165d3fdefa 100644 --- a/tests/ctest/core/StubHost.cpp +++ b/tests/ctest/core/StubHost.cpp @@ -105,12 +105,7 @@ void Host::SetRelativeMouseMode(bool enabled) { } -std::optional Host::AcquireRenderWindow() -{ - return std::nullopt; -} - -std::optional Host::UpdateRenderWindow(bool recreate_window) +std::optional Host::AcquireRenderWindow(bool recreate_window) { return std::nullopt; }