Framerate display corrections

This commit is contained in:
CamilleLaVey
2026-10-01 20:27:22 -04:00
parent f759409cc2
commit 7db354a25a
11 changed files with 94 additions and 110 deletions
@@ -64,8 +64,8 @@ void EmuWindow_Android::OnTouchReleased(int id) {
EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id); EmulationSession::GetInstance().GetInputSubsystem().GetTouchScreen()->TouchReleased(id);
} }
void EmuWindow_Android::OnFrameDisplayed() { void EmuWindow_Android::OnFrameDisplayed(u32 presented_frames) {
UpdateObservedFrameRate(); UpdateObservedFrameRate(presented_frames);
UpdateFrameRateHint(); UpdateFrameRateHint();
if (!m_first_frame) { if (!m_first_frame) {
@@ -75,15 +75,16 @@ void EmuWindow_Android::OnFrameDisplayed() {
} }
} }
void EmuWindow_Android::UpdateObservedFrameRate() { void EmuWindow_Android::UpdateObservedFrameRate(u32 presented_frames) {
m_presented_frames = static_cast<float>((std::max)(presented_frames, 1u));
const auto now = Clock::now(); const auto now = Clock::now();
if (m_last_frame_display_time.time_since_epoch().count() != 0) { if (m_last_frame_display_time.time_since_epoch().count() != 0) {
const auto frame_time = std::chrono::duration<float>(now - m_last_frame_display_time); const auto frame_time = std::chrono::duration<float>(now - m_last_frame_display_time);
const float seconds = frame_time.count(); const float seconds = frame_time.count();
if (seconds > 0.0f) { if (seconds > 0.0f) {
const float instantaneous_rate = 1.0f / seconds; const float instantaneous_rate = m_presented_frames / seconds;
if (std::isfinite(instantaneous_rate) && instantaneous_rate >= 1.0f && if (std::isfinite(instantaneous_rate) && instantaneous_rate >= 1.0f &&
instantaneous_rate <= 240.0f) { instantaneous_rate <= 240.0f * m_presented_frames) {
constexpr float SmoothingFactor = 0.15f; constexpr float SmoothingFactor = 0.15f;
if (m_smoothed_present_rate <= 0.0f) { if (m_smoothed_present_rate <= 0.0f) {
m_smoothed_present_rate = instantaneous_rate; m_smoothed_present_rate = instantaneous_rate;
@@ -124,18 +125,9 @@ float EmuWindow_Android::GetFrameTimeVerifiedHint() const {
return QuantizeFrameRateHint(verified_rate); return QuantizeFrameRateHint(verified_rate);
} }
float EmuWindow_Android::GetPresentedFrameMultiplier() {
if (!Settings::values.frame_gen.GetValue()) {
return 1.0f;
}
return static_cast<float>(std::clamp<u32>(Settings::values.frame_gen_multiplier.GetValue(), 2, 4));
}
float EmuWindow_Android::GetFrameRateHint() const { float EmuWindow_Android::GetFrameRateHint() const {
const float presented_multiplier = GetPresentedFrameMultiplier(); const float observed_rate = std::clamp(m_smoothed_present_rate, 0.0f, 240.0f);
const float observed_rate = const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * m_presented_frames;
std::clamp(m_smoothed_present_rate * presented_multiplier, 0.0f, 240.0f);
const float frame_time_verified_hint = GetFrameTimeVerifiedHint() * presented_multiplier;
if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) { if (m_last_frame_rate_hint > 0.0f && observed_rate > 0.0f) {
const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f); const float tolerance = std::max(m_last_frame_rate_hint * 0.12f, 4.0f);
@@ -159,7 +151,7 @@ float EmuWindow_Android::GetFrameRateHint() const {
return frame_time_verified_hint; return frame_time_verified_hint;
} }
const float nominal_rate = 60.0f * presented_multiplier; const float nominal_rate = 60.0f * m_presented_frames;
if (!Settings::values.use_speed_limit.GetValue()) { if (!Settings::values.use_speed_limit.GetValue()) {
return QuantizeFrameRateHint(nominal_rate); return QuantizeFrameRateHint(nominal_rate);
} }
@@ -41,7 +41,7 @@ public:
~EmuWindow_Android() = default; ~EmuWindow_Android() = default;
void OnSurfaceChanged(ANativeWindow* surface); void OnSurfaceChanged(ANativeWindow* surface);
void OnFrameDisplayed() override; void OnFrameDisplayed(u32 presented_frames) override;
void OnTouchPressed(int id, float x, float y); void OnTouchPressed(int id, float x, float y);
void OnTouchMoved(int id, float x, float y); void OnTouchMoved(int id, float x, float y);
@@ -58,10 +58,9 @@ private:
using Clock = std::chrono::steady_clock; using Clock = std::chrono::steady_clock;
void UpdateFrameRateHint(); void UpdateFrameRateHint();
void UpdateObservedFrameRate(); void UpdateObservedFrameRate(u32 presented_frames);
[[nodiscard]] float GetFrameRateHint() const; [[nodiscard]] float GetFrameRateHint() const;
[[nodiscard]] float GetFrameTimeVerifiedHint() const; [[nodiscard]] float GetFrameTimeVerifiedHint() const;
[[nodiscard]] static float GetPresentedFrameMultiplier();
[[nodiscard]] static float QuantizeFrameRateHint(float frame_rate); [[nodiscard]] static float QuantizeFrameRateHint(float frame_rate);
float m_window_width{}; float m_window_width{};
@@ -73,6 +72,7 @@ private:
float m_last_frame_rate_hint = -1.0f; float m_last_frame_rate_hint = -1.0f;
float m_pending_frame_rate_hint = -1.0f; float m_pending_frame_rate_hint = -1.0f;
float m_smoothed_present_rate = 0.0f; float m_smoothed_present_rate = 0.0f;
float m_presented_frames = 1.0f;
Clock::time_point m_last_frame_display_time{}; Clock::time_point m_last_frame_display_time{};
Clock::time_point m_pending_frame_rate_since{}; Clock::time_point m_pending_frame_rate_since{};
std::uint32_t m_pending_frame_rate_hint_votes = 0; std::uint32_t m_pending_frame_rate_hint_votes = 0;
+2 -2
View File
@@ -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-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2014 Citra Emulator Project // SPDX-FileCopyrightText: 2014 Citra Emulator Project
@@ -74,7 +74,7 @@ public:
}; };
/// Called from GPU thread when a frame is displayed. /// Called from GPU thread when a frame is displayed.
virtual void OnFrameDisplayed() {} virtual void OnFrameDisplayed([[maybe_unused]] u32 presented_frames) {}
/** /**
* Returns a GraphicsContext that the frontend provides to be used for rendering. * Returns a GraphicsContext that the frontend provides to be used for rendering.
+38 -52
View File
@@ -6,7 +6,9 @@
#pragma once #pragma once
#include "common/alignment.h" #include <algorithm>
#include <vector>
#include "common/common_types.h" #include "common/common_types.h"
namespace VideoCommon { namespace VideoCommon {
@@ -15,74 +17,58 @@ class UsageTracker {
// PAGE_SHIFT is a macro on FreeBSD // PAGE_SHIFT is a macro on FreeBSD
static constexpr size_t BUFFER_BYTES_PER_BITSHIFT = 6; static constexpr size_t BUFFER_BYTES_PER_BITSHIFT = 6;
static constexpr size_t BUFFER_PAGE_SHIFT = 6 + BUFFER_BYTES_PER_BITSHIFT; static constexpr size_t BUFFER_PAGE_SHIFT = 6 + BUFFER_BYTES_PER_BITSHIFT;
static constexpr size_t BUFFER_PAGE_BYTES = 1 << BUFFER_PAGE_SHIFT; static constexpr u64 BUFFER_PAGE_BYTES = u64{1} << BUFFER_PAGE_SHIFT;
struct Page {
u64 bits;
u64 tick;
};
public: public:
explicit UsageTracker(size_t size) { explicit UsageTracker(size_t size) : pages((size >> BUFFER_PAGE_SHIFT) + 1) {}
const size_t num_pages = (size >> BUFFER_PAGE_SHIFT) + 1;
pages.resize(num_pages, 0ULL);
}
void Reset() noexcept { void Track(u64 offset, u64 size, u64 tick, u64 gpu_tick) noexcept {
std::ranges::fill(pages, 0ULL); const u64 end = offset + size;
} if (size == 0 || ((end - 1) >> BUFFER_PAGE_SHIFT) >= pages.size()) {
void Track(u64 offset, u64 size) noexcept {
const size_t page = offset >> BUFFER_PAGE_SHIFT;
const size_t page_end = (offset + size) >> BUFFER_PAGE_SHIFT;
if (page_end < page || page_end >= pages.size()) {
return; return;
} }
TrackPage(page, offset, size); for (u64 page = offset >> BUFFER_PAGE_SHIFT; page <= (end - 1) >> BUFFER_PAGE_SHIFT;
if (page == page_end) { ++page) {
return; Page& entry = pages[page];
if (entry.tick <= gpu_tick) {
entry.bits = 0;
}
entry.bits |= PageMask(page, offset, end);
entry.tick = (std::max)(entry.tick, tick);
} }
for (size_t i = page + 1; i < page_end; i++) {
pages[i] = ~u64{0};
}
const size_t offset_end = offset + size;
const size_t offset_end_page_aligned = Common::AlignDown(offset_end, BUFFER_PAGE_BYTES);
TrackPage(page_end, offset_end_page_aligned, offset_end - offset_end_page_aligned);
} }
[[nodiscard]] bool IsUsed(u64 offset, u64 size) const noexcept { [[nodiscard]] bool IsUsed(u64 offset, u64 size, u64 gpu_tick) const noexcept {
const size_t page = offset >> BUFFER_PAGE_SHIFT; const u64 end = offset + size;
const size_t page_end = (offset + size) >> BUFFER_PAGE_SHIFT; if (size == 0 || ((end - 1) >> BUFFER_PAGE_SHIFT) >= pages.size()) {
if (page_end < page || page_end >= pages.size()) {
return false; return false;
} }
if (IsPageUsed(page, offset, size)) { for (u64 page = offset >> BUFFER_PAGE_SHIFT; page <= (end - 1) >> BUFFER_PAGE_SHIFT;
return true; ++page) {
} const Page& entry = pages[page];
for (size_t i = page + 1; i < page_end; i++) { if (entry.tick > gpu_tick && (entry.bits & PageMask(page, offset, end)) != 0) {
if (pages[i] != 0) {
return true; return true;
} }
} }
const size_t offset_end = offset + size; return false;
const size_t offset_end_page_aligned = Common::AlignDown(offset_end, BUFFER_PAGE_BYTES);
return IsPageUsed(page_end, offset_end_page_aligned, offset_end - offset_end_page_aligned);
} }
private: private:
void TrackPage(u64 page, u64 offset, u64 size) noexcept { [[nodiscard]] static u64 PageMask(u64 page, u64 offset, u64 end) noexcept {
const size_t offset_in_page = offset % BUFFER_PAGE_BYTES; const u64 page_begin = page << BUFFER_PAGE_SHIFT;
const size_t first_bit = offset_in_page >> BUFFER_BYTES_PER_BITSHIFT; const u64 first =
const size_t num_bits = std::min<size_t>(size, BUFFER_PAGE_BYTES) >> BUFFER_BYTES_PER_BITSHIFT; ((std::max)(offset, page_begin) - page_begin) >> BUFFER_BYTES_PER_BITSHIFT;
const size_t mask = ~u64{0} >> (64 - num_bits); const u64 last = ((std::min)(end, page_begin + BUFFER_PAGE_BYTES) - 1 - page_begin) >>
pages[page] |= (~u64{0} & mask) << first_bit; BUFFER_BYTES_PER_BITSHIFT;
return (~u64{0} >> (63 - last)) & (~u64{0} << first);
} }
bool IsPageUsed(u64 page, u64 offset, u64 size) const noexcept { std::vector<Page> pages;
const size_t offset_in_page = offset % BUFFER_PAGE_BYTES;
const size_t first_bit = offset_in_page >> BUFFER_BYTES_PER_BITSHIFT;
const size_t num_bits = std::min<size_t>(size, BUFFER_PAGE_BYTES) >> BUFFER_BYTES_PER_BITSHIFT;
const size_t mask = ~u64{0} >> (64 - num_bits);
const size_t mask2 = (~u64{0} & mask) << first_bit;
return (pages[page] & mask2) != 0;
}
private:
std::vector<u64> pages;
}; };
} // namespace VideoCommon } // namespace VideoCommon
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@@ -20,7 +23,7 @@ void RendererNull::Composite(std::span<const Tegra::FramebufferConfig> framebuff
} }
m_gpu.RendererFrameEndNotify(); m_gpu.RendererFrameEndNotify();
render_window.OnFrameDisplayed(); render_window.OnFrameDisplayed(1);
} }
std::vector<u8> RendererNull::GetAppletCaptureBuffer() { std::vector<u8> RendererNull::GetAppletCaptureBuffer() {
@@ -151,7 +151,7 @@ void RendererOpenGL::Composite(std::span<const Tegra::FramebufferConfig> framebu
rasterizer.TickFrame(); rasterizer.TickFrame();
context->SwapBuffers(); context->SwapBuffers();
render_window.OnFrameDisplayed(); render_window.OnFrameDisplayed(1);
} }
void RendererOpenGL::AddTelemetryFields() { void RendererOpenGL::AddTelemetryFields() {
@@ -185,8 +185,9 @@ RendererVulkan::~RendererVulkan() {
} }
void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebuffers) { void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebuffers) {
u32 pace_index = 0;
SCOPE_EXIT { SCOPE_EXIT {
render_window.OnFrameDisplayed(); render_window.OnFrameDisplayed(pace_index + 1);
}; };
RenderAppletCaptureLayer(framebuffers); RenderAppletCaptureLayer(framebuffers);
@@ -204,7 +205,6 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebu
present_manager.SwapchainImageCount(), present_manager.SwapchainImageCount(),
swapchain.GetImageViewFormat()); swapchain.GetImageViewFormat());
u32 pace_index = 0;
std::chrono::nanoseconds pace_step{}; std::chrono::nanoseconds pace_step{};
#ifdef HAS_LSFG #ifdef HAS_LSFG
void(frame_gen.WantedGenerations(present_manager.MaxExtraFrames())); void(frame_gen.WantedGenerations(present_manager.MaxExtraFrames()));
@@ -229,7 +229,7 @@ void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebu
output.View()); output.View());
scheduler.Flush(*generated->render_ready); scheduler.Flush(*generated->render_ready);
present_manager.Present(generated, static_cast<u32>(generation), generated_step); present_manager.Present(generated, static_cast<u32>(generation), generated_step);
pace_index = static_cast<u32>(generated_frames); pace_index = static_cast<u32>(generation + 1);
pace_step = generated_step; pace_step = generated_step;
} }
#endif #endif
@@ -109,7 +109,8 @@ vk::Buffer CreateBuffer(const Device& device, const MemoryAllocator& memory_allo
} // Anonymous namespace } // Anonymous namespace
Buffer::Buffer(BufferCacheRuntime& runtime, VideoCommon::NullBufferParams null_params) Buffer::Buffer(BufferCacheRuntime& runtime, VideoCommon::NullBufferParams null_params)
: VideoCommon::BufferBase(null_params), scheduler{&runtime.scheduler}, tracker{4096} { : VideoCommon::BufferBase(null_params), scheduler{&runtime.scheduler}, tracker{4096},
uploads{4096} {
if (runtime.device.HasNullDescriptor()) { if (runtime.device.HasNullDescriptor()) {
return; return;
} }
@@ -127,7 +128,7 @@ Buffer::Buffer(BufferCacheRuntime& runtime, DAddr cpu_addr_, u64 size_bytes_,
scheduler{&runtime.scheduler}, scheduler{&runtime.scheduler},
buffer{CreateBuffer(*device, runtime.memory_allocator, SizeBytes(), buffer{CreateBuffer(*device, runtime.memory_allocator, SizeBytes(),
runtime.SparseAlignmentFor(sparse_compatible_))}, runtime.SparseAlignmentFor(sparse_compatible_))},
tracker{SizeBytes()} { tracker{SizeBytes()}, uploads{SizeBytes()} {
sparse_compatible = sparse_compatible_; sparse_compatible = sparse_compatible_;
if (runtime.device.HasDebuggingToolAttached()) { if (runtime.device.HasDebuggingToolAttached()) {
buffer.SetObjectNameEXT(fmt::format("Buffer {:#x}", CpuAddr()).c_str()); buffer.SetObjectNameEXT(fmt::format("Buffer {:#x}", CpuAddr()).c_str());
@@ -137,13 +138,25 @@ Buffer::Buffer(BufferCacheRuntime& runtime, DAddr cpu_addr_, u64 size_bytes_,
} }
} }
void Buffer::MarkUsage(u64 offset, u64 size) noexcept { bool Buffer::IsRegionUsed(u64 offset, u64 size) const noexcept {
tracker.Track(offset, size); return tracker.IsUsed(offset, size, scheduler->GetMasterSemaphore().KnownGpuTick());
last_usage_tick = scheduler->CurrentTick();
} }
void Buffer::MarkUpload() noexcept { bool Buffer::IsRegionUploading(u64 offset, u64 size) const noexcept {
return uploads.IsUsed(offset, size, scheduler->GetMasterSemaphore().KnownGpuTick());
}
void Buffer::MarkUsage(u64 offset, u64 size) noexcept {
tracker.Track(offset, size, scheduler->CurrentTick(),
scheduler->GetMasterSemaphore().KnownGpuTick());
}
void Buffer::MarkUpload(std::span<const VideoCommon::BufferCopy> copies) noexcept {
last_upload_tick = scheduler->CurrentTick(); last_upload_tick = scheduler->CurrentTick();
const u64 gpu_tick = scheduler->GetMasterSemaphore().KnownGpuTick();
for (const VideoCommon::BufferCopy& copy : copies) {
uploads.Track(copy.dst_offset, copy.size, last_upload_tick, gpu_tick);
}
} }
std::span<u8> Buffer::CoherentMapping() noexcept { std::span<u8> Buffer::CoherentMapping() noexcept {
@@ -462,13 +475,8 @@ u32 BufferCacheRuntime::GetStorageBufferAlignment() const {
return static_cast<u32>(device.GetStorageBufferAlignment()); return static_cast<u32>(device.GetStorageBufferAlignment());
} }
void BufferCacheRuntime::TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept { void BufferCacheRuntime::TickFrame(Common::SlotVector<Buffer>&) noexcept {
multi_range_buffers.DrainRetired(scheduler); multi_range_buffers.DrainRetired(scheduler);
for (auto it = slot_buffers.begin(); it != slot_buffers.end(); it++) {
if (scheduler.IsFree(it->LastUsageTick())) {
it->ResetUsageTracking();
}
}
} }
u64 BufferCacheRuntime::CurrentTick() { u64 BufferCacheRuntime::CurrentTick() {
@@ -522,8 +530,11 @@ std::span<const u8> BufferCacheRuntime::DirectDownloadSpan(Buffer& buffer) {
std::span<u8> BufferCacheRuntime::DirectUploadSpan( std::span<u8> BufferCacheRuntime::DirectUploadSpan(
Buffer& buffer, std::span<const VideoCommon::BufferCopy> copies) { Buffer& buffer, std::span<const VideoCommon::BufferCopy> copies) {
const std::span<u8> mapping = buffer.CoherentMapping(); const std::span<u8> mapping = buffer.CoherentMapping();
if (mapping.empty() || !scheduler.IsFree(buffer.LastUploadTick()) || const bool uploading =
!CanReorderUpload(buffer, copies)) { std::ranges::any_of(copies, [&buffer](const VideoCommon::BufferCopy& copy) {
return buffer.IsRegionUploading(copy.dst_offset, copy.size);
});
if (mapping.empty() || uploading || !CanReorderUpload(buffer, copies)) {
return {}; return {};
} }
return mapping; return mapping;
@@ -55,21 +55,13 @@ public:
return buffer.Location(); return buffer.Location();
} }
[[nodiscard]] bool IsRegionUsed(u64 offset, u64 size) const noexcept { [[nodiscard]] bool IsRegionUsed(u64 offset, u64 size) const noexcept;
return tracker.IsUsed(offset, size);
} [[nodiscard]] bool IsRegionUploading(u64 offset, u64 size) const noexcept;
void MarkUsage(u64 offset, u64 size) noexcept; void MarkUsage(u64 offset, u64 size) noexcept;
void ResetUsageTracking() noexcept { void MarkUpload(std::span<const VideoCommon::BufferCopy> copies) noexcept;
tracker.Reset();
}
[[nodiscard]] u64 LastUsageTick() const noexcept {
return last_usage_tick;
}
void MarkUpload() noexcept;
[[nodiscard]] u64 LastUploadTick() const noexcept { [[nodiscard]] u64 LastUploadTick() const noexcept {
return last_upload_tick; return last_upload_tick;
@@ -94,8 +86,8 @@ private:
vk::Buffer buffer; vk::Buffer buffer;
std::vector<BufferView> views; std::vector<BufferView> views;
VideoCommon::UsageTracker tracker; VideoCommon::UsageTracker tracker;
VideoCommon::UsageTracker uploads;
VkDeviceAddress device_address{}; VkDeviceAddress device_address{};
u64 last_usage_tick{};
u64 last_upload_tick{}; u64 last_upload_tick{};
bool is_null{}; bool is_null{};
bool sparse_compatible{}; bool sparse_compatible{};
@@ -117,7 +109,7 @@ public:
ComputePassDescriptorQueue& compute_pass_descriptor_queue, ComputePassDescriptorQueue& compute_pass_descriptor_queue,
DescriptorPool& descriptor_pool); DescriptorPool& descriptor_pool);
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept; void TickFrame(Common::SlotVector<Buffer>&) noexcept;
u64 CurrentTick(); u64 CurrentTick();
@@ -165,7 +157,7 @@ public:
void CopyBuffer(Buffer& dst_buffer, VkBuffer src_buffer, void CopyBuffer(Buffer& dst_buffer, VkBuffer src_buffer,
std::span<const VideoCommon::BufferCopy> copies, bool barrier, std::span<const VideoCommon::BufferCopy> copies, bool barrier,
bool can_reorder_upload = false) { bool can_reorder_upload = false) {
dst_buffer.MarkUpload(); dst_buffer.MarkUpload(copies);
CopyBuffer(dst_buffer.Handle(), src_buffer, copies, barrier, can_reorder_upload); CopyBuffer(dst_buffer.Handle(), src_buffer, copies, barrier, can_reorder_upload);
} }
+1 -1
View File
@@ -157,7 +157,7 @@ GRenderWindow::~GRenderWindow() {
input_subsystem->Shutdown(); input_subsystem->Shutdown();
} }
void GRenderWindow::OnFrameDisplayed() { void GRenderWindow::OnFrameDisplayed([[maybe_unused]] u32 presented_frames) {
input_subsystem->GetTas()->UpdateThread(); input_subsystem->GetTas()->UpdateThread();
const InputCommon::TasInput::TasState new_tas_state = const InputCommon::TasInput::TasState new_tas_state =
std::get<0>(input_subsystem->GetTas()->GetStatus()); std::get<0>(input_subsystem->GetTas()->GetStatus());
+1 -1
View File
@@ -67,7 +67,7 @@ public:
~GRenderWindow() override; ~GRenderWindow() override;
// EmuWindow implementation. // EmuWindow implementation.
void OnFrameDisplayed() override; void OnFrameDisplayed(u32 presented_frames) override;
bool IsShown() const override; bool IsShown() const override;
std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override; std::unique_ptr<Core::Frontend::GraphicsContext> CreateSharedContext() const override;