From df3c0c18e4b71ecb5c4e009bfc07b9fd14fd39d9 Mon Sep 17 00:00:00 2001 From: OpenSauce Date: Sun, 29 Jun 2025 21:06:44 +0100 Subject: [PATCH] renderer_vulkan: Disable FIFO when refresh rate is lower than ~60hz or Apple low power mode is enabled (#1193) * renderer_vulkan: Disable FIFO when refresh rate is lower than ~60hz Also disables FIFO when Apple low power mode is enabled, as it can limit the application framerate to 30fps * renderer_vulkan.cpp: Put `IsLowRefreshRate` into anon namespace + make static --- .ci/license-header.rb | 2 +- CMakeLists.txt | 9 +++- src/common/CMakeLists.txt | 12 +++-- src/common/apple_utils.h | 9 ++++ src/common/apple_utils.mm | 13 +++++ src/video_core/CMakeLists.txt | 4 ++ .../renderer_vulkan/renderer_vulkan.cpp | 53 +++++++++++++++++-- .../renderer_vulkan/vk_present_window.cpp | 9 ++-- .../renderer_vulkan/vk_present_window.h | 5 +- .../renderer_vulkan/vk_swapchain.cpp | 13 +++-- src/video_core/renderer_vulkan/vk_swapchain.h | 8 +-- 11 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 src/common/apple_utils.h create mode 100644 src/common/apple_utils.mm diff --git a/.ci/license-header.rb b/.ci/license-header.rb index 063ef8d7e..9f5ca1a44 100755 --- a/.ci/license-header.rb +++ b/.ci/license-header.rb @@ -17,7 +17,7 @@ puts 'done' print 'Checking files...' issue_files = [] branch_changed_files.each do |file_name| - if file_name.end_with?('.cpp', '.h', '.kt', '.kts') and File.file?(file_name) + if file_name.end_with?('.cpp', '.h', '.kt', '.kts', '.m', '.mm') and File.file?(file_name) file_content = File.read(file_name, mode: 'r:bom|utf-8') if not file_content.start_with?(license_header) issue_files.push(file_name) diff --git a/CMakeLists.txt b/CMakeLists.txt index c23b03ea9..770fcda72 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,11 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modul include(DownloadExternals) include(CMakeDependentOption) -project(citra LANGUAGES C CXX ASM) +if (APPLE) + project(citra LANGUAGES C CXX OBJC OBJCXX ASM) +else() + project(citra LANGUAGES C CXX ASM) +endif() # Some submodules like to pick their own default build type if not specified. # Make sure we default to Release build type always, unless the generator has custom types. @@ -125,6 +129,9 @@ endif() if (ENABLE_ROOM) add_definitions(-DENABLE_ROOM) endif() +if (ENABLE_SDL2) + add_definitions(-DENABLE_SDL2) +endif() if (ENABLE_SDL2_FRONTEND) add_definitions(-DENABLE_SDL2_FRONTEND) endif() diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 8f90c9b00..113eb6b7e 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -165,10 +165,12 @@ if (UNIX AND NOT APPLE) endif() if (APPLE) - target_sources(citra_common PUBLIC - apple_authorization.h - apple_authorization.cpp - ) + target_sources(citra_common PUBLIC + apple_authorization.h + apple_authorization.cpp + apple_utils.h + apple_utils.mm + ) endif() if (MSVC) @@ -211,4 +213,4 @@ endif() if (SSE42_COMPILE_OPTION) target_compile_definitions(citra_common PRIVATE CITRA_HAS_SSE42) target_compile_options(citra_common PRIVATE ${SSE42_COMPILE_OPTION}) -endif() \ No newline at end of file +endif() diff --git a/src/common/apple_utils.h b/src/common/apple_utils.h new file mode 100644 index 000000000..472982874 --- /dev/null +++ b/src/common/apple_utils.h @@ -0,0 +1,9 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +namespace AppleUtils { + +int IsLowPowerModeEnabled(); + +} diff --git a/src/common/apple_utils.mm b/src/common/apple_utils.mm new file mode 100644 index 000000000..99a3d3235 --- /dev/null +++ b/src/common/apple_utils.mm @@ -0,0 +1,13 @@ +// Copyright Citra Emulator Project / Azahar Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#import + +namespace AppleUtils { + +int IsLowPowerModeEnabled() { + return (int)[NSProcessInfo processInfo].lowPowerModeEnabled; +} + +} // namespace AppleUtils diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index 4de7ed4fc..27076cbe4 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -203,6 +203,10 @@ if (ENABLE_VULKAN) target_link_libraries(video_core PRIVATE vulkan-headers vma sirit SPIRV glslang) endif() +if(ENABLE_SDL2) + target_link_libraries(video_core PRIVATE SDL2::SDL2) +endif() + add_dependencies(video_core host_shaders) target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE}) diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index 4f1fc0d25..eb3d05650 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -22,6 +22,14 @@ #include +#ifdef __APPLE__ +#include "common/apple_utils.h" +#endif + +#ifdef ENABLE_SDL2 +#include +#endif + MICROPROFILE_DEFINE(Vulkan_RenderFrame, "Vulkan", "Render Frame", MP_RGB(128, 128, 64)); namespace Vulkan { @@ -50,11 +58,48 @@ constexpr static std::array PRESENT_BINDINGS {0, vk::DescriptorType::eCombinedImageSampler, 3, vk::ShaderStageFlagBits::eFragment}, }}; +namespace { +static bool IsLowRefreshRate() { +#ifdef ENABLE_SDL2 + const auto sdl_init_status = SDL_Init(SDL_INIT_VIDEO); + if (sdl_init_status < 0) { + LOG_ERROR(Render_Vulkan, "SDL failed to initialize, unable to check refresh rate"); + } else { + SDL_DisplayMode cur_display_mode; + SDL_GetCurrentDisplayMode(0, &cur_display_mode); // TODO: Multimonitor handling. -OS + const auto cur_refresh_rate = cur_display_mode.refresh_rate; + SDL_Quit(); + + if (cur_refresh_rate < SCREEN_REFRESH_RATE) { + LOG_WARNING(Render_Vulkan, + "Detected refresh rate lower than the emulated 3DS screen: {}hz. FIFO will " + "be disabled", + cur_refresh_rate); + return true; + } + } +#endif + +#ifdef __APPLE__ + // Apple's low power mode sometimes limits applications to 30fps without changing the refresh + // rate, meaning the above code doesn't catch it. + if (AppleUtils::IsLowPowerModeEnabled()) { + LOG_WARNING(Render_Vulkan, "Apple's low power mode is enabled, assuming low application " + "framerate. FIFO will be disabled"); + return true; + } +#endif + + return false; +} +} // Anonymous namespace + RendererVulkan::RendererVulkan(Core::System& system, Pica::PicaCore& pica_, Frontend::EmuWindow& window, Frontend::EmuWindow* secondary_window) : RendererBase{system, window, secondary_window}, memory{system.Memory()}, pica{pica_}, instance{window, Settings::values.physical_device.GetValue()}, scheduler{instance}, - renderpass_cache{instance, scheduler}, main_window{window, instance, scheduler}, + renderpass_cache{instance, scheduler}, + main_window{window, instance, scheduler, IsLowRefreshRate()}, vertex_buffer{instance, scheduler, vk::BufferUsageFlagBits::eVertexBuffer, VERTEX_BUFFER_SIZE}, update_queue{instance}, @@ -66,7 +111,8 @@ RendererVulkan::RendererVulkan(Core::System& system, Pica::PicaCore& pica_, BuildLayouts(); BuildPipelines(); if (secondary_window) { - second_window = std::make_unique(*secondary_window, instance, scheduler); + second_window = std::make_unique(*secondary_window, instance, scheduler, + IsLowRefreshRate()); } } @@ -841,7 +887,8 @@ void RendererVulkan::SwapBuffers() { ASSERT(secondary_window); const auto& secondary_layout = secondary_window->GetFramebufferLayout(); if (!second_window) { - second_window = std::make_unique(*secondary_window, instance, scheduler); + second_window = std::make_unique(*secondary_window, instance, scheduler, + IsLowRefreshRate()); } RenderToWindow(*second_window, secondary_layout, false); secondary_window->PollEvents(); diff --git a/src/video_core/renderer_vulkan/vk_present_window.cpp b/src/video_core/renderer_vulkan/vk_present_window.cpp index 88cc1e219..7cdf455dc 100644 --- a/src/video_core/renderer_vulkan/vk_present_window.cpp +++ b/src/video_core/renderer_vulkan/vk_present_window.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -98,11 +98,12 @@ bool CanBlitToSwapchain(const vk::PhysicalDevice& physical_device, vk::Format fo } // Anonymous namespace PresentWindow::PresentWindow(Frontend::EmuWindow& emu_window_, const Instance& instance_, - Scheduler& scheduler_) + Scheduler& scheduler_, bool low_refresh_rate_) : emu_window{emu_window_}, instance{instance_}, scheduler{scheduler_}, + low_refresh_rate{low_refresh_rate_}, surface{CreateSurface(instance.GetInstance(), emu_window)}, next_surface{surface}, swapchain{instance, emu_window.GetFramebufferLayout().width, - emu_window.GetFramebufferLayout().height, surface}, + emu_window.GetFramebufferLayout().height, surface, low_refresh_rate_}, graphics_queue{instance.GetGraphicsQueue()}, present_renderpass{CreateRenderpass()}, vsync_enabled{Settings::values.use_vsync_new.GetValue()}, blit_supported{ @@ -355,7 +356,7 @@ void PresentWindow::CopyToSwapchain(Frame* frame) { #endif std::scoped_lock submit_lock{scheduler.submit_mutex}; graphics_queue.waitIdle(); - swapchain.Create(frame->width, frame->height, surface); + swapchain.Create(frame->width, frame->height, surface, low_refresh_rate); }; #ifndef ANDROID diff --git a/src/video_core/renderer_vulkan/vk_present_window.h b/src/video_core/renderer_vulkan/vk_present_window.h index 1e4b1e12a..012dbf81b 100644 --- a/src/video_core/renderer_vulkan/vk_present_window.h +++ b/src/video_core/renderer_vulkan/vk_present_window.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -37,7 +37,7 @@ struct Frame { class PresentWindow final { public: explicit PresentWindow(Frontend::EmuWindow& emu_window, const Instance& instance, - Scheduler& scheduler); + Scheduler& scheduler, bool low_refresh_rate); ~PresentWindow(); /// Waits for all queued frames to finish presenting. @@ -74,6 +74,7 @@ private: Frontend::EmuWindow& emu_window; const Instance& instance; Scheduler& scheduler; + bool low_refresh_rate; vk::SurfaceKHR surface; vk::SurfaceKHR next_surface{}; Swapchain swapchain; diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index 12678953f..ac5340653 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -15,11 +15,12 @@ MICROPROFILE_DEFINE(Vulkan_Present, "Vulkan", "Swapchain Present", MP_RGB(66, 18 namespace Vulkan { -Swapchain::Swapchain(const Instance& instance_, u32 width, u32 height, vk::SurfaceKHR surface_) +Swapchain::Swapchain(const Instance& instance_, u32 width, u32 height, vk::SurfaceKHR surface_, + bool low_refresh_rate) : instance{instance_}, surface{surface_} { FindPresentFormat(); SetPresentMode(); - Create(width, height, surface); + Create(width, height, surface, low_refresh_rate); } Swapchain::~Swapchain() { @@ -27,10 +28,11 @@ Swapchain::~Swapchain() { instance.GetInstance().destroySurfaceKHR(surface); } -void Swapchain::Create(u32 width_, u32 height_, vk::SurfaceKHR surface_) { +void Swapchain::Create(u32 width_, u32 height_, vk::SurfaceKHR surface_, bool low_refresh_rate_) { width = width_; height = height_; surface = surface_; + low_refresh_rate = low_refresh_rate_; needs_recreation = false; Destroy(); @@ -188,7 +190,8 @@ void Swapchain::SetPresentMode() { // If vsync is enabled attempt to use mailbox mode in case the user wants to speedup/slowdown // the game. If mailbox is not available use immediate and warn about it. - if (use_vsync && (frame_limit > 100 || frame_limit == 0)) { // 0 = unthrottled + if (use_vsync && + (frame_limit > 100 || frame_limit == 0 || low_refresh_rate)) { // 0 = unthrottled present_mode = has_mailbox ? vk::PresentModeKHR::eMailbox : vk::PresentModeKHR::eImmediate; if (!has_mailbox) { LOG_WARNING( @@ -275,4 +278,4 @@ void Swapchain::SetupImages() { } } -} // namespace Vulkan \ No newline at end of file +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_swapchain.h b/src/video_core/renderer_vulkan/vk_swapchain.h index c3f6c17d0..4f885727a 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.h +++ b/src/video_core/renderer_vulkan/vk_swapchain.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -16,11 +16,12 @@ class Scheduler; class Swapchain { public: - explicit Swapchain(const Instance& instance, u32 width, u32 height, vk::SurfaceKHR surface); + explicit Swapchain(const Instance& instance, u32 width, u32 height, vk::SurfaceKHR surface, + bool low_refresh_rate); ~Swapchain(); /// Creates (or recreates) the swapchain with a given size. - void Create(u32 width, u32 height, vk::SurfaceKHR surface); + void Create(u32 width, u32 height, vk::SurfaceKHR surface, bool low_refresh_rate); /// Acquires the next image in the swapchain. bool AcquireNextImage(); @@ -105,6 +106,7 @@ private: u32 image_index = 0; u32 frame_index = 0; bool needs_recreation = true; + bool low_refresh_rate; }; } // namespace Vulkan