mirror of
https://git.eden-emu.dev/eden-emu/eden.git
synced 2026-10-06 14:09:58 +00:00
1st phase of framepacer rework
This commit is contained in:
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user