[video_core] Improve synchronization and refactor buffer cache timing logic (#4182)

This simplifies the GPU accuracy setting by removing the intermediate Balanced mode and setting High as the default on desktop platforms.

It introduces a dedicated setting for GPU fence behavior, allowing the synchronization policy to be configured independently of GPU accuracy.

The Vulkan buffer cache now tracks GPU recording timeline ticks and waits only when necessary, reducing unnecessary synchronization while maintaining correctness for hard-to-trace graphical bugs.

GPU buffer readback has also been refined to synchronize only the affected upload regions when needed, and default DMA behavior has been updated to align with the new GPU accuracy model.

### TL;DR

The fix for particles freezing and unfreezing in mid-air in `Super Mario Odyssey` has been improved, resulting in less of a performance hit.
This game requires the new `Enable GPU Buffer Readback` option to be enabled to fix this issue.

The vertex explosions that occurred in `Super Mario Bros. Wonder`, especially in World 4, have been completely eliminated. You can now enjoy a smooth experience without graphical glitches exploding across the screen.
This game requires the new `GPU Fence Behavior` option to be set to `Strict` to fully fix this issue.

The flickering issue inside certain Shrines in `The Legend of Zelda: Tears of the Kingdom` has also been fixed.
For now, this game requires the new `GPU Fence Behavior` option to be set to `Accurate` to fully fix this issue.

These options are intended to fix graphical bugs in games that require better synchronization behavior between CPU and GPU, so other games may be affected as well.

Co-authored-by: xbzk <xbzk@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4182
Reviewed-by: Lizzie <lizzie@eden-emu.dev>
This commit is contained in:
MaranBr
2026-07-10 05:25:12 +02:00
committed by crueter
parent 5606edd1a6
commit a27d35463e
27 changed files with 151 additions and 104 deletions
@@ -121,12 +121,21 @@ public:
return size_bytes;
}
u64 getWriteTick() const noexcept {
return write_tick;
}
void setWriteTick(u64 write_tick_) {
write_tick = write_tick_;
}
private:
VAddr cpu_addr = 0;
BufferFlagBits flags{};
int stream_score = 0;
size_t lru_id = SIZE_MAX;
size_t size_bytes = 0;
u64 write_tick = 0;
};
} // namespace VideoCommon
+32 -17
View File
@@ -1430,6 +1430,10 @@ void BufferCache<P>::UpdateComputeTextureBuffers() {
template <class P>
void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u32 size) {
if constexpr (!IS_OPENGL) {
Buffer& buffer = slot_buffers[buffer_id];
buffer.setWriteTick(runtime.CurrentTick());
}
memory_tracker.MarkRegionAsGpuModified(device_addr, size);
gpu_modified_ranges.Add(device_addr, size);
uncommitted_gpu_modified_ranges.Add(device_addr, size);
@@ -1442,16 +1446,32 @@ BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size) {
}
const u64 page = device_addr >> CACHING_PAGEBITS;
const BufferId buffer_id = page_table[page];
if (!buffer_id) {
return CreateBuffer(device_addr, size);
}
const Buffer& buffer = slot_buffers[buffer_id];
if (buffer.IsInBounds(device_addr, size)) {
return buffer_id;
if (buffer_id) {
Buffer& buffer = slot_buffers[buffer_id];
WaitForGpuFenceIfNeeded(buffer);
if (buffer.IsInBounds(device_addr, size)) {
return buffer_id;
}
}
return CreateBuffer(device_addr, size);
}
template <class P>
void BufferCache<P>::WaitForGpuFenceIfNeeded(Buffer& buffer) {
if constexpr (!IS_OPENGL) {
const bool gpu_fence_accurate = Settings::IsGPUFenceBehaviorAccurate();
const bool gpu_fence_strict = Settings::IsGPUFenceBehaviorStrict();
if (gpu_fence_accurate || gpu_fence_strict) {
const u64 gpu_tick_delay = gpu_fence_strict ? 0 : 3;
const u64 buffer_tick = buffer.getWriteTick();
const u64 gpu_tick = runtime.KnownGpuTick();
if (buffer_tick > gpu_tick + gpu_tick_delay) {
runtime.Wait(buffer_tick);
}
}
}
}
template <class P>
typename BufferCache<P>::OverlapResult BufferCache<P>::ResolveOverlaps(DAddr device_addr,
u32 wanted_size) {
@@ -1634,17 +1654,6 @@ bool BufferCache<P>::SynchronizeBuffer(Buffer& buffer, DAddr device_addr, u32 si
if (total_size_bytes == 0) {
return true;
}
if (Settings::values.enable_gpu_buffer_readback.GetValue()) {
u64 min_offset = (std::numeric_limits<u64>::max)();
u64 max_offset = 0;
for (const auto& copy : upload_copies) {
min_offset = (std::min)(min_offset, copy.dst_offset);
max_offset = (std::max)(max_offset, copy.dst_offset + copy.size);
}
const DAddr sync_addr = buffer.CpuAddr() + min_offset;
const u64 sync_size = max_offset - min_offset;
DownloadBufferMemory(buffer, sync_addr, sync_size);
}
const std::span<BufferCopy> copies_span(upload_copies.data(), upload_copies.size());
UploadMemory(buffer, total_size_bytes, largest_copy, copies_span);
any_buffer_uploaded = true;
@@ -1679,6 +1688,9 @@ void BufferCache<P>::ImmediateUploadMemory([[maybe_unused]] Buffer& buffer,
if (immediate_buffer.empty()) {
immediate_buffer = ImmediateBuffer(largest_copy);
}
if (Settings::values.enable_gpu_buffer_readback.GetValue()) {
DownloadBufferMemory(buffer, device_addr, copy.size);
}
device_memory.ReadBlockUnsafe(device_addr, immediate_buffer.data(), copy.size);
upload_span = immediate_buffer.subspan(0, copy.size);
}
@@ -1697,6 +1709,9 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
for (BufferCopy& copy : copies) {
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
if (Settings::values.enable_gpu_buffer_readback.GetValue()) {
DownloadBufferMemory(buffer, device_addr, copy.size);
}
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
// Apply the staging offset
copy.src_offset += upload_staging.offset;
@@ -416,6 +416,8 @@ private:
[[nodiscard]] BufferId FindBuffer(DAddr device_addr, u32 size);
void WaitForGpuFenceIfNeeded(Buffer& buffer);
[[nodiscard]] OverlapResult ResolveOverlaps(DAddr device_addr, u32 wanted_size);
void JoinOverlap(BufferId new_buffer_id, BufferId overlap_id, bool accumulate_stream_score);
+2 -1
View File
@@ -78,7 +78,8 @@ bool DmaPusher::Step() {
}
if (header.size > 0) {
if (Settings::IsDMALevelDefault() ? (Settings::IsGPULevelMedium() || Settings::IsGPULevelHigh()) : Settings::IsDMALevelSafe()) {
const bool use_safe = Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe();
if (use_safe) {
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::SafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
ProcessCommands(headers);
} else {
+3 -6
View File
@@ -72,16 +72,13 @@ public:
}
void SignalFence(std::function<void()>&& func) {
const bool delay_fence = Settings::IsGPUFenceBehaviorDefault() ? Settings::IsGPULevelHigh() : Settings::IsGPUFenceBehaviorBalanced() || Settings::IsGPUFenceBehaviorAccurate() || Settings::IsGPUFenceBehaviorStrict();
const bool should_flush = ShouldFlush();
if constexpr (!can_async_check) {
TryReleasePendingFences<false>();
}
const bool should_flush = ShouldFlush();
const bool antiflicker_toggled = Settings::values.antiflicker.GetValue();
const bool delay_fence = Settings::IsGPULevelHigh() ||
(Settings::IsGPULevelMedium() && should_flush) ||
antiflicker_toggled;
CommitAsyncFlushes();
TFence new_fence = CreateFence(!should_flush && !antiflicker_toggled);
TFence new_fence = CreateFence(!should_flush);
if constexpr (can_async_check) {
guard.lock();
}
+1 -1
View File
@@ -260,7 +260,7 @@ void QueryCacheBase<Traits>::CounterReport(GPUVAddr addr, QueryType counter_type
};
u8* pointer = impl->device_memory.template GetPointer<u8>(cpu_addr);
u8* pointer_timestamp = impl->device_memory.template GetPointer<u8>(cpu_addr + 8);
bool is_synced = !Settings::IsGPULevelHigh() && is_fence;
bool is_synced = (Settings::IsGPUFenceBehaviorDefault() ? !Settings::IsGPULevelHigh() : !Settings::IsGPUFenceBehaviorBalanced() && !Settings::IsGPUFenceBehaviorAccurate() && !Settings::IsGPUFenceBehaviorStrict()) && is_fence;
std::function<void()> operation([this, is_synced, streamer, query_base = query, query_location,
pointer, pointer_timestamp] {
if (True(query_base->flags & QueryFlagBits::IsInvalidated)) {
@@ -396,6 +396,18 @@ void BufferCacheRuntime::TickFrame(Common::SlotVector<Buffer>& slot_buffers) noe
}
}
u64 BufferCacheRuntime::CurrentTick() {
return scheduler.GetMasterSemaphore().CurrentTick();
}
u64 BufferCacheRuntime::KnownGpuTick() {
return scheduler.GetMasterSemaphore().KnownGpuTick();
}
void BufferCacheRuntime::Wait(u64 buffer_tick) {
scheduler.Wait(buffer_tick);
}
void BufferCacheRuntime::Finish() {
scheduler.Finish();
}
@@ -92,6 +92,12 @@ public:
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept;
u64 CurrentTick();
u64 KnownGpuTick();
void Wait(u64 buffer_tick);
void Finish();
u64 GetDeviceLocalMemory() const;
@@ -27,8 +27,6 @@
namespace Vulkan {
constexpr u64 MAX_PENDING_FLUSHES = 5;
void Scheduler::CommandChunk::ExecuteAll(vk::CommandBuffer cmdbuf,
vk::CommandBuffer upload_cmdbuf) {
auto command = first;
@@ -49,15 +47,6 @@ Scheduler::Scheduler(const Device& device_, StateTracker& state_tracker_)
master_semaphore{std::make_unique<MasterSemaphore>(device)},
command_pool{std::make_unique<CommandPool>(*master_semaphore, device)} {
/*// PRE-OPTIMIZATION: Warm up the pool to prevent mid-frame spikes
{
std::scoped_lock rl{reserve_mutex};
chunk_reserve.reserve(2048); // Prevent vector resizing
for (int i = 0; i < 1024; ++i) {
chunk_reserve.push_back(std::make_unique<CommandChunk>());
}
}*/
AcquireNewChunk();
AllocateWorkerCommandBuffer();
worker_thread = std::jthread([this](std::stop_token token) { WorkerThread(token); });
@@ -66,18 +55,7 @@ Scheduler::Scheduler(const Device& device_, StateTracker& state_tracker_)
Scheduler::~Scheduler() = default;
u64 Scheduler::Flush(VkSemaphore signal_semaphore, VkSemaphore wait_semaphore) {
// Prevent the CPU from getting too far ahead of the GPU by limiting pending flushes.
const bool should_throttle = Settings::IsGPULevelHigh();
if (should_throttle) {
const u64 current_tick = master_semaphore->CurrentTick();
const u64 gap = current_tick > last_submitted_tick ? current_tick - last_submitted_tick : 0;
const u64 step = (std::min)(MAX_PENDING_FLUSHES, gap);
const u64 new_tick = last_submitted_tick + step;
if (new_tick < current_tick) {
last_submitted_tick = new_tick;
master_semaphore->Wait(last_submitted_tick);
}
}
// When flushing, we only send data to the worker thread; no waiting is necessary.
const u64 signal_value = SubmitExecution(signal_semaphore, wait_semaphore);
AllocateNewContext();
return signal_value;
@@ -298,8 +298,6 @@ private:
double last_target_fps{};
u64 max_frame_count{};
u64 frame_counter{};
u64 last_submitted_tick = 0;
};
} // namespace Vulkan