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"),