diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt index 7f1b49a94a..e8087cef5b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -32,7 +32,6 @@ enum class IntSetting(override val key: String) : AbstractIntSetting { RENDERER_DYNA_STATE("dyna_state"), DMA_ACCURACY("dma_accuracy"), GPU_FENCE_BEHAVIOR("gpu_fence_behavior"), - FRAME_PACING_MODE("frame_pacing_mode"), AUDIO_OUTPUT_ENGINE("output_engine"), MAX_ANISOTROPY("max_anisotropy"), THEME("theme"), diff --git a/src/common/settings.h b/src/common/settings.h index 4ca2368064..e564a11cb5 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -552,16 +552,6 @@ struct Values { "accelerate_astc", Category::RendererAdvanced}; - SwitchableSetting frame_pacing_mode{linkage, - FramePacingMode::Target_Auto, - FramePacingMode::Target_Auto, - FramePacingMode::Target_120, - "frame_pacing_mode", - Category::RendererAdvanced, - Specialization::Default, - true, - true}; - SwitchableSetting sync_memory_operations{linkage, false, "sync_memory_operations", diff --git a/src/common/settings_enums.h b/src/common/settings_enums.h index 73568e257c..60e2b55742 100644 --- a/src/common/settings_enums.h +++ b/src/common/settings_enums.h @@ -130,7 +130,6 @@ ENUM(TimeZone, Auto, Default, Cet, Cst6Cdt, Cuba, Eet, Egypt, Eire, Est, Est5Edt Roc, Rok, Singapore, Turkey, Uct, Universal, Utc, WSu, Wet, Zulu); ENUM(AnisotropyMode, Automatic, Default, X2, X4, X8, X16); ENUM(AstcDecodeMode, Cpu, Gpu, CpuAsynchronous); -ENUM(FramePacingMode, Target_Auto, Target_30, Target_60, Target_90, Target_120); ENUM(VSyncMode, Immediate, Mailbox, Fifo, FifoRelaxed); ENUM(VramUsageMode, Conservative, Aggressive); ENUM(RendererBackend, OpenGL_GLSL, Vulkan, Null, OpenGL_GLASM, OpenGL_SPIRV); diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 2c459855e8..0bb2e0d52b 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -198,9 +198,6 @@ std::unique_ptr InitializeTranslations(QObject* parent) { "GPU: Use the GPU's compute shaders to decode ASTC textures (recommended).\n" "CPU Asynchronously: Use the CPU to decode ASTC textures on demand. Eliminates" "ASTC decoding\nstuttering but may present artifacts.")); - INSERT(Settings, frame_pacing_mode, tr("Frame Pacing Mode (Vulkan only)"), - tr("Controls how the emulator manages frame pacing to reduce stuttering and make the " - "frame rate smoother and more consistent.")); INSERT(Settings, vram_usage_mode, tr("VRAM Usage Mode:"), tr("Selects whether the emulator should prefer to conserve memory or make maximum usage " "of available video memory for performance.\nAggressive mode may impact performance " @@ -393,14 +390,6 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { PAIR(AstcDecodeMode, Gpu, tr("GPU")), PAIR(AstcDecodeMode, CpuAsynchronous, tr("CPU Asynchronous")), }}); - translations->insert({Settings::EnumMetadata::Index(), - { - PAIR(FramePacingMode, Target_Auto, tr("Auto")), - PAIR(FramePacingMode, Target_30, tr("30 FPS")), - PAIR(FramePacingMode, Target_60, tr("60 FPS")), - PAIR(FramePacingMode, Target_90, tr("90 FPS")), - PAIR(FramePacingMode, Target_120, tr("120 FPS")), - }}); translations->insert({Settings::EnumMetadata::Index(), { PAIR(VramUsageMode, Conservative, tr("Conservative")), diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp index 3ea8941ba9..dc57ad8428 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -272,6 +272,11 @@ size_t FrameGen::GeneratedFrameCount() const { return generated ? last_generations : 0; } +std::chrono::nanoseconds FrameGen::PaceStep() const { + const f32 step = plan.interval / static_cast(last_generations + 1); + return std::chrono::duration_cast(std::chrono::duration(step)); +} + const LsfgImage& FrameGen::Generate(const Device& device, size_t generation) { LsfgImage& output = outputs[generation]; chain->SetTarget(device, last_generations, generation, output.View()); diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h index 6993ffcb20..6b03db3f25 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen.h +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -28,6 +28,8 @@ public: [[nodiscard]] size_t GeneratedFrameCount() const; + [[nodiscard]] std::chrono::nanoseconds PaceStep() const; + [[nodiscard]] const LsfgImage& Generate(const Device& device, size_t generation); private: diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp index 80aca5ebe0..195a81be19 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp @@ -112,7 +112,7 @@ FrameGenPlan FrameGenPacer::Plan(size_t capacity) { limit = (std::min)(Settings::FrameGenGenerations(), ceiling); output_credit = 0.0f; issued_generations = limit; - return {.generations = limit, .warm = limit > 0}; + return {.generations = limit, .warm = limit > 0, .interval = smoothed_interval}; } UpdateLimit(now, 1.0f / smoothed_interval, target_rate, ceiling); @@ -137,7 +137,7 @@ FrameGenPlan FrameGenPacer::Plan(size_t capacity) { } issued_generations = generations; - return {.generations = generations, .warm = true}; + return {.generations = generations, .warm = true, .interval = smoothed_interval}; } void FrameGenPacer::UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.h b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h index 7f4b23920c..8c513af71c 100644 --- a/src/video_core/renderer_vulkan/present/frame_gen_pacer.h +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h @@ -13,6 +13,7 @@ namespace Vulkan { struct FrameGenPlan { size_t generations{}; bool warm{}; + f32 interval{}; }; class FrameGenPacer { diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp index d425844189..f963ef4aef 100644 --- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp +++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp @@ -204,6 +204,8 @@ void RendererVulkan::Composite(std::span framebu present_manager.SwapchainImageCount(), swapchain.GetImageViewFormat()); + u32 pace_index = 0; + std::chrono::nanoseconds pace_step{}; #ifdef HAS_LSFG void(frame_gen.WantedGenerations(present_manager.MaxExtraFrames())); @@ -212,6 +214,7 @@ void RendererVulkan::Composite(std::span framebu const Layout::FramebufferLayout layout = render_window.GetFramebufferLayout(); const size_t generated_frames = frame_gen.GeneratedFrameCount(); + const std::chrono::nanoseconds generated_step = frame_gen.PaceStep(); for (size_t generation = 0; generation < generated_frames; ++generation) { if (!blit_swapchain.IsGenerationFree(generation)) { break; @@ -225,13 +228,15 @@ void RendererVulkan::Composite(std::span framebu blit_swapchain.DrawGenerated(device, generated, layout, generation, output.Handle(), output.View()); scheduler.Flush(*generated->render_ready); - present_manager.Present(generated); + present_manager.Present(generated, static_cast(generation), generated_step); + pace_index = static_cast(generated_frames); + pace_step = generated_step; } #endif scheduler.Flush(*frame->render_ready); - present_manager.Present(frame); + present_manager.Present(frame, pace_index, pace_step); #ifdef HAS_LSFG scheduler.DispatchWork(); #endif diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp index 5a96fd7847..52d497e446 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.cpp +++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp @@ -117,7 +117,8 @@ PresentManager::PresentManager(const vk::Instance& instance_, , swapchain{swapchain_} , surface{surface_} , blit_supported{CanBlitToSwapchain(device.GetPhysical(), swapchain.GetImageViewFormat())} - , use_present_thread{Settings::values.async_presentation.GetValue()} + , use_present_thread{Settings::values.async_presentation.GetValue() || + Settings::values.frame_gen.GetValue()} { SetImageCount(); @@ -184,7 +185,9 @@ Frame* PresentManager::TryGetRenderFrame() { return frame; } -void PresentManager::Present(Frame* frame) { +void PresentManager::Present(Frame* frame, u32 pace_index, std::chrono::nanoseconds pace_step) { + frame->pace_index = pace_index; + frame->pace_step = pace_step; if (use_present_thread) { scheduler.Record([this, frame](vk::CommandBuffer) { std::unique_lock lock{queue_mutex}; @@ -279,21 +282,33 @@ void PresentManager::PresentThread(std::stop_token token) { Common::SetCurrentThreadName("VulkanPresent"); Common::SetCurrentThreadPriority(Common::ThreadPriority::High); Common::SetCurrentThreadToPerformanceCores(); + std::chrono::steady_clock::time_point pace_anchor{}; while (!token.stop_requested()) { std::unique_lock lock{queue_mutex}; // Wait for presentation frames frame_cv.wait(lock, token, [this] { return !present_queue.empty(); }); + if (!present_queue.empty() && present_queue.front()->pace_index != 0) { + const Frame* const held = present_queue.front(); + const auto next_anchor = [this] { + return std::any_of(present_queue.begin() + 1, present_queue.end(), + [](const Frame* next) { return next->pace_index == 0; }); + }; + void(frame_cv.wait_until(lock, token, + pace_anchor + held->pace_step * held->pace_index, + next_anchor)); + } if (!token.stop_requested()) { // Take the frame and notify anyone waiting Frame* frame = present_queue.front(); present_queue.pop_front(); frame_cv.notify_one(); - // By exchanging the lock ownership we take the swapchain lock - // before the queue lock goes out of scope. This way the swapchain - // lock in WaitPresent is guaranteed to occur after here. void(std::exchange(lock, std::unique_lock{swapchain_mutex})); CopyToSwapchain(frame); + if (frame->pace_index == 0 && frame->pace_step.count() != 0) { + frame->present_done.Wait(static_cast(frame->pace_step.count())); + pace_anchor = std::chrono::steady_clock::now(); + } // Free the frame for reuse std::scoped_lock fl{free_mutex}; diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h index a59d18f420..7e1a47bb45 100644 --- a/src/video_core/renderer_vulkan/vk_present_manager.h +++ b/src/video_core/renderer_vulkan/vk_present_manager.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include #include @@ -35,6 +36,8 @@ struct Frame { vk::CommandBuffer cmdbuf; vk::Semaphore render_ready; vk::Fence present_done; + std::chrono::nanoseconds pace_step{}; + u32 pace_index{}; }; class PresentManager { @@ -54,7 +57,7 @@ public: [[nodiscard]] Frame* TryGetRenderFrame(); /// Pushes a frame for presentation - void Present(Frame* frame); + void Present(Frame* frame, u32 pace_index, std::chrono::nanoseconds pace_step); /// Recreates the present frame to match the provided parameters void RecreateFrame(Frame* frame, u32 width, u32 height, VkFormat image_view_format); diff --git a/src/video_core/renderer_vulkan/vk_scheduler.h b/src/video_core/renderer_vulkan/vk_scheduler.h index ef04062b49..a0bdeedccc 100644 --- a/src/video_core/renderer_vulkan/vk_scheduler.h +++ b/src/video_core/renderer_vulkan/vk_scheduler.h @@ -161,39 +161,12 @@ public: return master_semaphore->IsFree(tick); } - /// Waits for the given GPU tick, optionally pacing frames. - void Wait(u64 tick, double target_fps = 0.0) { - if (tick > 0) { - if (tick >= master_semaphore->CurrentTick()) { - Flush(); - } - master_semaphore->Wait(tick); - } - if (Settings::values.use_speed_limit.GetValue() && target_fps > 0.0) { - auto now = std::chrono::steady_clock::now(); - if (last_target_fps != target_fps) { - frame_interval = std::chrono::duration_cast(std::chrono::duration(1.0 / target_fps)); - max_frame_count = static_cast(0.1 * target_fps); - last_target_fps = target_fps; - frame_counter = 0; - start_time = now; - } - frame_counter++; - auto target_time = start_time + frame_interval * frame_counter; - if (target_time >= now) { - constexpr auto spin_tail = std::chrono::milliseconds(1); - auto sleep_time = target_time - now; - if (sleep_time > spin_tail * 2) { - std::this_thread::sleep_for(sleep_time - spin_tail); - } - while (std::chrono::steady_clock::now() < target_time) { - std::this_thread::yield(); - } - } else if (frame_counter > max_frame_count) { - frame_counter = 0; - start_time = now; - } + /// Waits for the given tick to trigger on the GPU. + void Wait(u64 tick) { + if (tick >= master_semaphore->CurrentTick()) { + Flush(); } + master_semaphore->Wait(tick); } /// Returns the master timeline semaphore. @@ -368,12 +341,6 @@ private: std::mutex queue_mutex; std::condition_variable_any event_cv; std::jthread worker_thread; - - std::chrono::steady_clock::duration frame_interval{}; - std::chrono::steady_clock::time_point start_time{}; - double last_target_fps{}; - u64 max_frame_count{}; - u64 frame_counter{}; }; } // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_swapchain.cpp b/src/video_core/renderer_vulkan/vk_swapchain.cpp index fea5398557..328bb28897 100644 --- a/src/video_core/renderer_vulkan/vk_swapchain.cpp +++ b/src/video_core/renderer_vulkan/vk_swapchain.cpp @@ -9,10 +9,6 @@ #include #include -#ifdef __ANDROID__ -#include -#endif - #include "common/logging.h" #include "common/settings.h" #include "common/settings_enums.h" @@ -179,35 +175,7 @@ bool Swapchain::AcquireNextImage() { break; } - const auto wait_with_frame_pacing = [this] { - switch (Settings::values.frame_pacing_mode.GetValue()) { - case Settings::FramePacingMode::Target_Auto: - scheduler.Wait(resource_ticks[image_index]); - break; - case Settings::FramePacingMode::Target_30: - scheduler.Wait(resource_ticks[image_index], 30.0); - break; - case Settings::FramePacingMode::Target_60: - scheduler.Wait(resource_ticks[image_index], 60.0); - break; - case Settings::FramePacingMode::Target_90: - scheduler.Wait(resource_ticks[image_index], 90.0); - break; - case Settings::FramePacingMode::Target_120: - scheduler.Wait(resource_ticks[image_index], 120.0); - break; - } - }; - -#ifdef __ANDROID__ - if (android_get_device_api_level() >= 30) { - scheduler.Wait(resource_ticks[image_index]); - } else { - wait_with_frame_pacing(); - } -#else - wait_with_frame_pacing(); -#endif + scheduler.Wait(resource_ticks[image_index]); resource_ticks[image_index] = scheduler.CurrentTick();