From 13f11ebf49fdf2394784d046950a368805260382 Mon Sep 17 00:00:00 2001 From: MaranBr Date: Tue, 3 Feb 2026 18:25:15 +0100 Subject: [PATCH] [nvnflinger] Reimplement GetBufferHistory (#3394) Reimplements GetBufferHistory, enabling tracking and retrieval of recent buffer states. This can improve rendering performance and stability in some games. Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/3394 Reviewed-by: CamilleLaVey Co-authored-by: MaranBr Co-committed-by: MaranBr --- .../features/settings/model/BooleanSetting.kt | 1 + .../settings/model/view/SettingsItem.kt | 7 + .../settings/ui/SettingsFragmentPresenter.kt | 1 + .../app/src/main/res/values/strings.xml | 2 + src/common/settings.h | 8 + .../nvnflinger/buffer_queue_consumer.cpp | 7 +- .../service/nvnflinger/buffer_queue_core.cpp | 38 +++- .../service/nvnflinger/buffer_queue_core.h | 22 ++- .../nvnflinger/buffer_queue_producer.cpp | 182 ++++++++---------- .../nvnflinger/buffer_queue_producer.h | 4 +- src/core/hle/service/nvnflinger/buffer_slot.h | 3 +- src/qt_common/config/shared_translation.cpp | 4 + 12 files changed, 168 insertions(+), 111 deletions(-) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt index ee07e15474..157cc502b7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -24,6 +24,7 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { RENDERER_FORCE_MAX_CLOCK("force_max_clock"), RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"), RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"), + ENABLE_BUFFER_HISTORY("enable_buffer_history"), SYNC_MEMORY_OPERATIONS("sync_memory_operations"), BUFFER_REORDER_DISABLE("disable_buffer_reorder"), RENDERER_DEBUG("debug"), diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt index 0785e3fc8b..47932c2dd8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt @@ -745,6 +745,13 @@ abstract class SettingsItem( descriptionId = R.string.renderer_reactive_flushing_description ) ) + put( + SwitchSetting( + BooleanSetting.ENABLE_BUFFER_HISTORY, + titleId = R.string.enable_buffer_history, + descriptionId = R.string.enable_buffer_history_description + ) + ) put( SwitchSetting( BooleanSetting.SYNC_MEMORY_OPERATIONS, diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt index 5899382520..e7c2dbf609 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -275,6 +275,7 @@ class SettingsFragmentPresenter( add(BooleanSetting.RENDERER_USE_DISK_SHADER_CACHE.key) add(BooleanSetting.RENDERER_FORCE_MAX_CLOCK.key) add(BooleanSetting.RENDERER_REACTIVE_FLUSHING.key) + add(BooleanSetting.ENABLE_BUFFER_HISTORY.key) add(HeaderSetting(R.string.hacks)) diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 18cee9b331..3765b36323 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -493,6 +493,8 @@ Forces the GPU to run at the maximum possible clocks (thermal constraints will still be applied). Use reactive flushing Improves rendering accuracy in some games at the cost of performance. + Enable buffer history + Enables access to previous buffer states. This option may improve rendering quality and performance consistency in some games. Hacks diff --git a/src/common/settings.h b/src/common/settings.h index 878f65c973..85f3cb21cd 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -481,6 +481,14 @@ struct Values { SwitchableSetting barrier_feedback_loops{linkage, true, "barrier_feedback_loops", Category::RendererAdvanced}; + SwitchableSetting enable_buffer_history{linkage, + false, + "enable_buffer_history", + Category::RendererAdvanced, + Specialization::Default, + true, + true}; + // Renderer Hacks // SwitchableSetting fast_gpu_time{linkage, GpuOverclock::Medium, diff --git a/src/core/hle/service/nvnflinger/buffer_queue_consumer.cpp b/src/core/hle/service/nvnflinger/buffer_queue_consumer.cpp index 298ec4b811..c1bd58ee7e 100644 --- a/src/core/hle/service/nvnflinger/buffer_queue_consumer.cpp +++ b/src/core/hle/service/nvnflinger/buffer_queue_consumer.cpp @@ -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 2021 yuzu Emulator Project @@ -99,11 +99,6 @@ Status BufferQueueConsumer::AcquireBuffer(BufferItem* out_buffer, slots[slot].acquire_called = true; slots[slot].needs_cleanup_on_release = false; slots[slot].buffer_state = BufferState::Acquired; - - // TODO: for now, avoid resetting the fence, so that when we next return this - // slot to the producer, it will wait for the fence to pass. We should fix this - // by properly waiting for the fence in the BufferItemConsumer. - // slots[slot].fence = Fence::NoFence(); } // If the buffer has previously been acquired by the consumer, set graphic_buffer to nullptr to diff --git a/src/core/hle/service/nvnflinger/buffer_queue_core.cpp b/src/core/hle/service/nvnflinger/buffer_queue_core.cpp index 63e69ac124..30c145e545 100644 --- a/src/core/hle/service/nvnflinger/buffer_queue_core.cpp +++ b/src/core/hle/service/nvnflinger/buffer_queue_core.cpp @@ -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 2021 yuzu Emulator Project @@ -17,6 +17,39 @@ BufferQueueCore::BufferQueueCore() = default; BufferQueueCore::~BufferQueueCore() = default; +void BufferQueueCore::PushHistory(u64 frame_number, s64 queue_time, s64 presentation_time, BufferState state) { + std::lock_guard lk(buffer_history_mutex); + + auto it = buffer_history_map.find(frame_number); + if (it != buffer_history_map.end()) { + it->second.state = state; + return; + } + + buffer_history_map.emplace(frame_number, BufferHistoryInfo{ + frame_number, + queue_time, + presentation_time, + state + }); + buffer_history_order.push_back(frame_number); + + if (buffer_history_order.size() > BUFFER_HISTORY_SIZE) { + u64 oldest_frame = buffer_history_order.front(); + buffer_history_order.pop_front(); + buffer_history_map.erase(oldest_frame); + } +} + +void BufferQueueCore::UpdateHistory(u64 frame_number, BufferState state) { + std::lock_guard lk(buffer_history_mutex); + + auto it = buffer_history_map.find(frame_number); + if (it != buffer_history_map.end()) { + it->second.state = state; + } +} + void BufferQueueCore::SignalDequeueCondition() { dequeue_possible.store(true); dequeue_condition.notify_all(); @@ -30,7 +63,6 @@ bool BufferQueueCore::WaitForDequeueCondition(std::unique_lock& lk) } s32 BufferQueueCore::GetMinUndequeuedBufferCountLocked(bool async) const { - // If DequeueBuffer is allowed to error out, we don't have to add an extra buffer. if (!use_async_buffer) { return 0; } @@ -55,8 +87,6 @@ s32 BufferQueueCore::GetMaxBufferCountLocked(bool async) const { return override_max_buffer_count; } - // Any buffers that are dequeued by the producer or sitting in the queue waiting to be consumed - // need to have their slots preserved. for (s32 slot = max_buffer_count; slot < BufferQueueDefs::NUM_BUFFER_SLOTS; ++slot) { const auto state = slots[slot].buffer_state; if (state == BufferState::Queued || state == BufferState::Dequeued) { diff --git a/src/core/hle/service/nvnflinger/buffer_queue_core.h b/src/core/hle/service/nvnflinger/buffer_queue_core.h index 305c54b1b1..86ca19dad3 100644 --- a/src/core/hle/service/nvnflinger/buffer_queue_core.h +++ b/src/core/hle/service/nvnflinger/buffer_queue_core.h @@ -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 2021 yuzu Emulator Project @@ -10,20 +10,31 @@ #pragma once #include +#include #include #include #include #include #include +#include +#include #include "core/hle/service/nvnflinger/buffer_item.h" #include "core/hle/service/nvnflinger/buffer_queue_defs.h" +#include "core/hle/service/nvnflinger/buffer_slot.h" #include "core/hle/service/nvnflinger/pixel_format.h" #include "core/hle/service/nvnflinger/status.h" #include "core/hle/service/nvnflinger/window.h" namespace Service::android { +struct BufferHistoryInfo { + u64 frame_number{}; + s64 queue_time{}; + s64 presentation_time{}; + BufferState state{}; +}; + class IConsumerListener; class IProducerListener; @@ -33,10 +44,14 @@ class BufferQueueCore final { public: static constexpr s32 INVALID_BUFFER_SLOT = BufferItem::INVALID_BUFFER_SLOT; + static constexpr u32 BUFFER_HISTORY_SIZE = 8; BufferQueueCore(); ~BufferQueueCore(); + void PushHistory(u64 frame_number, s64 queue_time, s64 presentation_time, BufferState state); + void UpdateHistory(u64 frame_number, BufferState state); + private: void SignalDequeueCondition(); bool WaitForDequeueCondition(std::unique_lock& lk); @@ -72,6 +87,11 @@ private: const s32 max_acquired_buffer_count{}; // This is always zero on HOS bool buffer_has_been_queued{}; u64 frame_counter{}; + + std::unordered_map buffer_history_map{}; + mutable std::mutex buffer_history_mutex{}; + std::deque buffer_history_order; + u32 transform_hint{}; bool is_allocating{}; mutable std::condition_variable_any is_allocating_condition; diff --git a/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp b/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp index a2e9396878..2aae24ba4b 100644 --- a/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp +++ b/src/core/hle/service/nvnflinger/buffer_queue_producer.cpp @@ -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 2021 yuzu Emulator Project @@ -9,6 +9,7 @@ #include "common/assert.h" #include "common/logging/log.h" +#include "common/settings.h" #include "core/hle/kernel/k_event.h" #include "core/hle/kernel/k_readable_event.h" #include "core/hle/kernel/kernel.h" @@ -26,7 +27,7 @@ BufferQueueProducer::BufferQueueProducer(Service::KernelHelpers::ServiceContext& std::shared_ptr buffer_queue_core_, Service::Nvidia::NvCore::NvMap& nvmap_) : service_context{service_context_}, core{std::move(buffer_queue_core_)}, slots(core->slots), - nvmap(nvmap_) { + clock{Common::CreateOptimalClock()}, nvmap(nvmap_) { buffer_wait_event = service_context.CreateEvent("BufferQueue:WaitEvent"); } @@ -428,8 +429,7 @@ Status BufferQueueProducer::AttachBuffer(s32* out_slot, return return_flags; } -Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, - QueueBufferOutput* output) { +Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, QueueBufferOutput* output) { s64 timestamp{}; bool is_auto_timestamp{}; Common::Rectangle crop; @@ -440,8 +440,7 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, s32 swap_interval{}; Fence fence{}; - input.Deflate(×tamp, &is_auto_timestamp, &crop, &scaling_mode, &transform, - &sticky_transform_, &async, &swap_interval, &fence); + input.Deflate(×tamp, &is_auto_timestamp, &crop, &scaling_mode, &transform, &sticky_transform_, &async, &swap_interval, &fence); switch (scaling_mode) { case NativeWindowScalingMode::Freeze: @@ -455,10 +454,9 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, return Status::BadValue; } - std::shared_ptr frame_available_listener; - std::shared_ptr frame_replaced_listener; - s32 callback_ticket{}; BufferItem item; + std::shared_ptr listener_available; + std::shared_ptr listener_replaced; { std::scoped_lock lock{core->mutex}; @@ -469,127 +467,82 @@ Status BufferQueueProducer::QueueBuffer(s32 slot, const QueueBufferInput& input, } const s32 max_buffer_count = core->GetMaxBufferCountLocked(async); - if (async && core->override_max_buffer_count) { - if (core->override_max_buffer_count < max_buffer_count) { - LOG_ERROR(Service_Nvnflinger, "async mode is invalid with " - "buffer count override"); - return Status::BadValue; - } - } if (slot < 0 || slot >= max_buffer_count) { - LOG_ERROR(Service_Nvnflinger, "slot index {} out of range [0, {})", slot, - max_buffer_count); + LOG_ERROR(Service_Nvnflinger, "slot {} out of range [0, {})", slot, max_buffer_count); return Status::BadValue; - } else if (slots[slot].buffer_state != BufferState::Dequeued) { - LOG_ERROR(Service_Nvnflinger, - "slot {} is not owned by the producer " - "(state = {})", - slot, slots[slot].buffer_state); + } + if (slots[slot].buffer_state != BufferState::Dequeued) { + LOG_ERROR(Service_Nvnflinger, "slot {} is not owned by producer", slot); return Status::BadValue; - } else if (!slots[slot].request_buffer_called) { - LOG_ERROR(Service_Nvnflinger, - "slot {} was queued without requesting " - "a buffer", - slot); + } + if (!slots[slot].request_buffer_called) { + LOG_ERROR(Service_Nvnflinger, "slot {} was queued without request", slot); return Status::BadValue; } - LOG_DEBUG(Service_Nvnflinger, - "slot={} frame={} time={} crop=[{},{},{},{}] transform={} scale={}", slot, - core->frame_counter + 1, timestamp, crop.Left(), crop.Top(), crop.Right(), - crop.Bottom(), transform, scaling_mode); - - const std::shared_ptr& graphic_buffer(slots[slot].graphic_buffer); - Common::Rectangle buffer_rect(graphic_buffer->Width(), graphic_buffer->Height()); - Common::Rectangle cropped_rect; - [[maybe_unused]] const bool unused = crop.Intersect(buffer_rect, &cropped_rect); - - if (cropped_rect != crop) { - LOG_ERROR(Service_Nvnflinger, "crop rect is not contained within the buffer in slot {}", - slot); - return Status::BadValue; - } - - slots[slot].fence = fence; - slots[slot].buffer_state = BufferState::Queued; ++core->frame_counter; + slots[slot].buffer_state = BufferState::Queued; slots[slot].frame_number = core->frame_counter; + slots[slot].queue_time = timestamp; + slots[slot].presentation_time = clock->GetTimeNS().count(); + slots[slot].fence = fence; - item.acquire_called = slots[slot].acquire_called; + item.slot = slot; item.graphic_buffer = slots[slot].graphic_buffer; - item.crop = crop; - item.transform = transform & ~NativeWindowTransform::InverseDisplay; - item.transform_to_display_inverse = - (transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None; - item.scaling_mode = static_cast(scaling_mode); + item.frame_number = core->frame_counter; item.timestamp = timestamp; item.is_auto_timestamp = is_auto_timestamp; - item.frame_number = core->frame_counter; - item.slot = slot; + item.crop = crop; + item.transform = transform & ~NativeWindowTransform::InverseDisplay; + item.transform_to_display_inverse = (transform & NativeWindowTransform::InverseDisplay) != NativeWindowTransform::None; + item.scaling_mode = static_cast(scaling_mode); item.fence = fence; item.is_droppable = core->dequeue_buffer_cannot_block || async; item.swap_interval = swap_interval; + item.acquire_called = slots[slot].acquire_called; sticky_transform = sticky_transform_; if (core->queue.empty()) { - // When the queue is empty, we can simply queue this buffer core->queue.push_back(item); - frame_available_listener = core->consumer_listener; + listener_available = core->consumer_listener; } else { - // When the queue is not empty, we need to look at the front buffer - // state to see if we need to replace it - auto front(core->queue.begin()); + auto front = core->queue.begin(); + if (front->is_droppable && core->StillTracking(*front)) { + slots[front->slot].buffer_state = BufferState::Free; + if (Settings::values.enable_buffer_history.GetValue()) { + core->UpdateHistory(front->frame_number, BufferState::Free); + } + slots[front->slot].frame_number = 0; + } if (front->is_droppable) { - // If the front queued buffer is still being tracked, we first - // mark it as freed - if (core->StillTracking(*front)) { - slots[front->slot].buffer_state = BufferState::Free; - // Reset the frame number of the freed buffer so that it is the first in line to - // be dequeued again - slots[front->slot].frame_number = 0; - } - // Overwrite the droppable buffer with the incoming one *front = item; - frame_replaced_listener = core->consumer_listener; + listener_replaced = core->consumer_listener; } else { core->queue.push_back(item); - frame_available_listener = core->consumer_listener; + listener_available = core->consumer_listener; } } + if (Settings::values.enable_buffer_history.GetValue()) { + core->PushHistory(core->frame_counter, slots[slot].queue_time, slots[slot].presentation_time, BufferState::Queued); + } + core->buffer_has_been_queued = true; core->SignalDequeueCondition(); - output->Inflate(core->default_width, core->default_height, core->transform_hint, - static_cast(core->queue.size())); - // Take a ticket for the callback functions - callback_ticket = next_callback_ticket++; + output->Inflate(core->default_width, core->default_height, core->transform_hint, static_cast(core->queue.size())); } - // Don't send the GraphicBuffer through the callback, and don't send the slot number, since the - // consumer shouldn't need it item.graphic_buffer.reset(); item.slot = BufferItem::INVALID_BUFFER_SLOT; - // Call back without the main BufferQueue lock held, but with the callback lock held so we can - // ensure that callbacks occur in order - { - std::scoped_lock lock{callback_mutex}; - while (callback_ticket != current_callback_ticket) { - callback_condition.wait(callback_mutex); - } - - if (frame_available_listener != nullptr) { - frame_available_listener->OnFrameAvailable(item); - } else if (frame_replaced_listener != nullptr) { - frame_replaced_listener->OnFrameReplaced(item); - } - - ++current_callback_ticket; - callback_condition.notify_all(); + if (listener_available) { + listener_available->OnFrameAvailable(item); + } else if (listener_replaced) { + listener_replaced->OnFrameReplaced(item); } return Status::NoError; @@ -810,6 +763,10 @@ Status BufferQueueProducer::SetPreallocatedBuffer(s32 slot, return Status::NoError; } +Kernel::KReadableEvent* BufferQueueProducer::GetNativeHandle(u32 type_id) { + return &buffer_wait_event->GetReadableEvent(); +} + void BufferQueueProducer::Transact(u32 code, std::span parcel_data, std::span parcel_reply, u32 flags) { // Values used by BnGraphicBufferProducer onTransact @@ -929,9 +886,42 @@ void BufferQueueProducer::Transact(u32 code, std::span parcel_data, status = SetBufferCount(buffer_count); break; } - case TransactionId::GetBufferHistory: - LOG_DEBUG(Service_Nvnflinger, "(STUBBED) called, transaction=GetBufferHistory"); + case TransactionId::GetBufferHistory: { + if (!Settings::values.enable_buffer_history.GetValue()) { + LOG_DEBUG(Service_Nvnflinger, "(STUBBED) called"); + break; + } + + LOG_DEBUG(Service_Nvnflinger, "called, transaction=GetBufferHistory"); + + const s32 request = parcel_in.Read(); + if (request <= 0) { + parcel_out.Write(Status::BadValue); + parcel_out.Write(0); + break; + } + + std::vector snapshot; + + { + std::scoped_lock lk(core->buffer_history_mutex); + for (auto& [frame, info] : core->buffer_history_map) { + snapshot.push_back(info); + } + } + + std::sort(snapshot.begin(), snapshot.end(), [](auto& a, auto& b){ + return a.frame_number > b.frame_number; + }); + + const s32 limit = std::min(request, (s32)snapshot.size()); + parcel_out.Write(Status::NoError); + parcel_out.Write(limit); + for (s32 i = 0; i < limit; ++i) { + parcel_out.Write(snapshot[i]); + } break; + } default: ASSERT_MSG(false, "Unimplemented TransactionId {}", code); break; @@ -944,8 +934,4 @@ void BufferQueueProducer::Transact(u32 code, std::span parcel_data, (std::min)(parcel_reply.size(), serialized.size())); } -Kernel::KReadableEvent* BufferQueueProducer::GetNativeHandle(u32 type_id) { - return &buffer_wait_event->GetReadableEvent(); -} - } // namespace Service::android diff --git a/src/core/hle/service/nvnflinger/buffer_queue_producer.h b/src/core/hle/service/nvnflinger/buffer_queue_producer.h index 55e95b2ac9..06cb49cbad 100644 --- a/src/core/hle/service/nvnflinger/buffer_queue_producer.h +++ b/src/core/hle/service/nvnflinger/buffer_queue_producer.h @@ -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 2021 yuzu Emulator Project @@ -14,6 +14,7 @@ #include #include "common/common_funcs.h" +#include "common/wall_clock.h" #include "core/hle/service/nvdrv/nvdata.h" #include "core/hle/service/nvnflinger/binder.h" #include "core/hle/service/nvnflinger/buffer_queue_defs.h" @@ -88,6 +89,7 @@ private: s32 next_callback_ticket{}; s32 current_callback_ticket{}; std::condition_variable_any callback_condition; + std::unique_ptr clock; Service::Nvidia::NvCore::NvMap& nvmap; }; diff --git a/src/core/hle/service/nvnflinger/buffer_slot.h b/src/core/hle/service/nvnflinger/buffer_slot.h index b33eac35e0..c6a8f61a0d 100644 --- a/src/core/hle/service/nvnflinger/buffer_slot.h +++ b/src/core/hle/service/nvnflinger/buffer_slot.h @@ -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 2021 yuzu Emulator Project @@ -37,6 +37,7 @@ struct BufferSlot final { bool needs_cleanup_on_release{}; bool attached_by_consumer{}; bool is_preallocated{}; + s64 queue_time{}, presentation_time{}; }; } // namespace Service::android diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 163eb57138..0d15f9065c 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -329,6 +329,10 @@ std::unique_ptr InitializeTranslations(QObject* parent) barrier_feedback_loops, tr("Barrier feedback loops"), tr("Improves rendering of transparency effects in specific games.")); + INSERT(Settings, + enable_buffer_history, + tr("Enable buffer history"), + tr("Enables access to previous buffer states.\nThis option may improve rendering quality and performance consistency in some games.")); INSERT(Settings, fix_bloom_effects, tr("Fix bloom effects"),