[vulkan] Add support for target FPS frame pacing (#3494)

This allows users to choose how the emulator manages frame pacing to reduce stuttering and provide a smoother and more consistent frame rate.

Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3494
Reviewed-by: CamilleLaVey <camillelavey99@gmail.com>
Co-authored-by: MaranBr <maranbr@outlook.com>
Co-committed-by: MaranBr <maranbr@outlook.com>
This commit is contained in:
MaranBr 2026-02-12 00:23:19 +01:00 committed by crueter
parent 2ab5b37137
commit 5f676a6a55
No known key found for this signature in database
GPG key ID: 425ACD2D4830EBC6
11 changed files with 121 additions and 8 deletions

View file

@ -347,7 +347,7 @@ void Scheduler::EndRenderPass()
Record([num_images = num_renderpass_images,
images = renderpass_images,
ranges = renderpass_image_ranges](vk::CommandBuffer cmdbuf) {
std::array<VkImageMemoryBarrier, 9> barriers;
std::vector<VkImageMemoryBarrier> barriers(num_images);
VkPipelineStageFlags src_stages = 0;
for (size_t i = 0; i < num_images; ++i) {
const VkImageSubresourceRange& range = ranges[i];

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@ -16,6 +16,7 @@
#include "common/alignment.h"
#include "common/common_types.h"
#include "common/settings.h"
#include "common/polyfill_thread.h"
#include "video_core/renderer_vulkan/vk_master_semaphore.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@ -111,15 +112,34 @@ public:
return master_semaphore->IsFree(tick);
}
/// Waits for the given tick to trigger on the GPU.
void Wait(u64 tick) {
if (tick >= master_semaphore->CurrentTick()) {
// Make sure we are not waiting for the current tick without signalling
/// Waits for the given GPU tick, optionally pacing frames.
void Wait(u64 tick, double target_fps = 0.0) {
if (Settings::values.use_speed_limit.GetValue() && target_fps > 0.0) {
auto frame_duration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(1.0 / target_fps));
auto now = std::chrono::steady_clock::now();
if (now < next_frame_time) {
std::this_thread::sleep_until(next_frame_time);
next_frame_time += frame_duration;
} else {
next_frame_time = now + frame_duration;
}
}
if (tick > master_semaphore->CurrentTick() && !chunk->Empty()) {
Flush();
}
master_semaphore->Wait(tick);
}
/// Resets the frame pacing state by setting the next frame time.
void ResetFramePacing(double target_fps = 0.0) {
if (target_fps > 0.0) {
auto frame_duration = std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(1.0 / target_fps));
next_frame_time = std::chrono::steady_clock::now() + frame_duration;
} else {
next_frame_time = std::chrono::steady_clock::time_point{};
}
}
/// Returns the master timeline semaphore.
[[nodiscard]] MasterSemaphore& GetMasterSemaphore() const noexcept {
return *master_semaphore;
@ -261,6 +281,8 @@ private:
std::mutex queue_mutex;
std::condition_variable_any event_cv;
std::jthread worker_thread;
std::chrono::steady_clock::time_point next_frame_time{};
};
} // namespace Vulkan

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2019 yuzu Emulator Project
@ -146,6 +146,25 @@ void Swapchain::Create(
{
is_outdated = false;
is_suboptimal = false;
switch (Settings::values.frame_pacing_mode.GetValue()) {
case Settings::FramePacingMode::Target_Auto:
scheduler.ResetFramePacing();
break;
case Settings::FramePacingMode::Target_30:
scheduler.ResetFramePacing(30.0);
break;
case Settings::FramePacingMode::Target_60:
scheduler.ResetFramePacing(60.0);
break;
case Settings::FramePacingMode::Target_120:
scheduler.ResetFramePacing(120.0);
break;
case Settings::FramePacingMode::Target_240:
scheduler.ResetFramePacing(240.0);
break;
}
width = width_;
height = height_;
#ifdef ANDROID
@ -194,7 +213,26 @@ bool Swapchain::AcquireNextImage() {
break;
}
scheduler.Wait(resource_ticks[image_index]);
if (resource_ticks[image_index] != 0 && !scheduler.IsFree(resource_ticks[image_index])) {
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_120:
scheduler.Wait(resource_ticks[image_index], 120.0);
break;
case Settings::FramePacingMode::Target_240:
scheduler.Wait(resource_ticks[image_index], 240.0);
break;
}
}
resource_ticks[image_index] = scheduler.CurrentTick();
return is_suboptimal || is_outdated;