1st phase of framepacer rework

This commit is contained in:
CamilleLaVey
2026-09-30 17:54:48 -04:00
parent 148a7b24cd
commit 51584f5a63
13 changed files with 47 additions and 104 deletions
@@ -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"),
-10
View File
@@ -552,16 +552,6 @@ struct Values {
"accelerate_astc",
Category::RendererAdvanced};
SwitchableSetting<FramePacingMode, true> frame_pacing_mode{linkage,
FramePacingMode::Target_Auto,
FramePacingMode::Target_Auto,
FramePacingMode::Target_120,
"frame_pacing_mode",
Category::RendererAdvanced,
Specialization::Default,
true,
true};
SwitchableSetting<bool> sync_memory_operations{linkage,
false,
"sync_memory_operations",
-1
View File
@@ -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);
@@ -198,9 +198,6 @@ std::unique_ptr<TranslationMap> 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<ComboboxTranslationMap> ComboboxEnumeration(QObject* parent) {
PAIR(AstcDecodeMode, Gpu, tr("GPU")),
PAIR(AstcDecodeMode, CpuAsynchronous, tr("CPU Asynchronous")),
}});
translations->insert({Settings::EnumMetadata<Settings::FramePacingMode>::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<Settings::VramUsageMode>::Index(),
{
PAIR(VramUsageMode, Conservative, tr("Conservative")),
@@ -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<f32>(last_generations + 1);
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::duration<f32>(step));
}
const LsfgImage& FrameGen::Generate(const Device& device, size_t generation) {
LsfgImage& output = outputs[generation];
chain->SetTarget(device, last_generations, generation, output.View());
@@ -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:
@@ -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,
@@ -13,6 +13,7 @@ namespace Vulkan {
struct FrameGenPlan {
size_t generations{};
bool warm{};
f32 interval{};
};
class FrameGenPacer {
@@ -204,6 +204,8 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> 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<const Tegra::FramebufferConfig> 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<const Tegra::FramebufferConfig> 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<u32>(generation), generated_step);
pace_index = static_cast<u32>(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
@@ -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<u64>(frame->pace_step.count()));
pace_anchor = std::chrono::steady_clock::now();
}
// Free the frame for reuse
std::scoped_lock fl{free_mutex};
@@ -7,6 +7,7 @@
#pragma once
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <boost/container/deque.hpp>
@@ -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);
+5 -38
View File
@@ -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::steady_clock::duration>(std::chrono::duration<double>(1.0 / target_fps));
max_frame_count = static_cast<int>(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
@@ -9,10 +9,6 @@
#include <limits>
#include <vector>
#ifdef __ANDROID__
#include <android/api-level.h>
#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();