Add Skip Presenting Duplicate Frames Feature (#1867)

* added basic skip duplicate frames implementation

* fix secondary display and attempt to fix lcd color fill
This commit is contained in:
KojoZero
2026-07-03 03:37:27 -07:00
committed by GitHub
parent fcb10fb899
commit 8c4e8b77b2
19 changed files with 164 additions and 53 deletions
+1
View File
@@ -42,6 +42,7 @@ foreach(KEY IN ITEMS
"use_disk_shader_cache"
"shaders_accurate_mul"
"use_vsync"
"use_skip_duplicate_frames"
"use_display_refresh_rate_detection"
"use_shader_jit"
"resolution_factor"
@@ -39,6 +39,7 @@ object SettingKeys {
external fun use_disk_shader_cache(): String
external fun shaders_accurate_mul(): String
external fun use_vsync(): String
external fun use_skip_duplicate_frames(): String
external fun use_shader_jit(): String
external fun resolution_factor(): String
external fun frame_limit(): String
@@ -97,6 +97,11 @@ enum class BooleanSetting(
HW_SHADER(SettingKeys.use_hw_shader(), Settings.SECTION_RENDERER, true),
SHADER_JIT(SettingKeys.use_shader_jit(), Settings.SECTION_RENDERER, true),
VSYNC(SettingKeys.use_vsync(), Settings.SECTION_RENDERER, false),
USE_SKIP_DUPLICATE_FRAMES(
SettingKeys.use_skip_duplicate_frames(),
Settings.SECTION_RENDERER,
false
),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, true),
DEBUG_RENDERER(SettingKeys.renderer_debug(), Settings.SECTION_DEBUG, false),
DISABLE_RIGHT_EYE_RENDER(
@@ -1146,6 +1146,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.TEXTURE_SAMPLING.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.USE_SKIP_DUPLICATE_FRAMES,
R.string.use_skip_duplicate_frames,
R.string.use_skip_duplicate_frames_description,
BooleanSetting.USE_SKIP_DUPLICATE_FRAMES.key,
BooleanSetting.USE_SKIP_DUPLICATE_FRAMES.defaultValue
)
)
// Disabled until custom texture implementation gets rewrite, current one overloads RAM
// and crashes Citra.
+1
View File
@@ -152,6 +152,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.resolution_factor);
ReadSetting("Renderer", Settings::values.use_disk_shader_cache);
ReadSetting("Renderer", Settings::values.use_vsync);
ReadSetting("Renderer", Settings::values.use_skip_duplicate_frames);
ReadSetting("Renderer", Settings::values.texture_filter);
ReadSetting("Renderer", Settings::values.texture_sampling);
ReadSetting("Renderer", Settings::values.turbo_limit);
@@ -130,6 +130,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# 0 (default): Off, 1: On
)") DECLARE_KEY(use_vsync) BOOST_HANA_STRING(R"(
# Skips display of duplicated frames in 30 fps games
# 0: Off, 1 (default): On
)") DECLARE_KEY(use_skip_duplicate_frames) BOOST_HANA_STRING(R"(
# Reduce stuttering by storing and loading generated shaders to disk
# 0: Off, 1 (default. On)
)") DECLARE_KEY(use_disk_shader_cache) BOOST_HANA_STRING(R"(
@@ -271,6 +271,8 @@
<string name="advanced">Advanced</string>
<string name="texture_sampling_name">Texture Sampling</string>
<string name="texture_sampling_description">Overrides the sampling filter used by games. This can be useful in certain cases with poorly behaved games when upscaling. If unsure, set this to Game Controlled.</string>
<string name="use_skip_duplicate_frames">Skip Presenting Duplicate Frames</string>
<string name="use_skip_duplicate_frames_description">Skips the presentation of frames that are not unique. It also allows external frame generation tools to work correctly with 30fps games</string>
<string name="shaders_accurate_mul">Accurate Multiplication</string>
<string name="shaders_accurate_mul_description">Uses more accurate multiplication in hardware shaders, which may fix some graphical bugs. When enabled, performance will be reduced.</string>
<string name="asynchronous_gpu">Enable Asynchronous GPU Emulation</string>
+2
View File
@@ -715,6 +715,7 @@ void QtConfig::ReadRendererValues() {
ReadGlobalSetting(Settings::values.shaders_accurate_mul);
ReadGlobalSetting(Settings::values.use_disk_shader_cache);
ReadGlobalSetting(Settings::values.use_vsync);
ReadGlobalSetting(Settings::values.use_skip_duplicate_frames);
ReadGlobalSetting(Settings::values.use_display_refresh_rate_detection);
ReadGlobalSetting(Settings::values.resolution_factor);
ReadGlobalSetting(Settings::values.use_integer_scaling);
@@ -1265,6 +1266,7 @@ void QtConfig::SaveRendererValues() {
WriteGlobalSetting(Settings::values.shaders_accurate_mul);
WriteGlobalSetting(Settings::values.use_disk_shader_cache);
WriteGlobalSetting(Settings::values.use_vsync);
WriteGlobalSetting(Settings::values.use_skip_duplicate_frames);
WriteGlobalSetting(Settings::values.use_display_refresh_rate_detection);
WriteGlobalSetting(Settings::values.resolution_factor);
WriteGlobalSetting(Settings::values.use_integer_scaling);
@@ -144,6 +144,8 @@ void ConfigureGraphics::SetConfiguration() {
ui->toggle_accurate_mul->setChecked(Settings::values.shaders_accurate_mul.GetValue());
ui->toggle_disk_shader_cache->setChecked(Settings::values.use_disk_shader_cache.GetValue());
ui->toggle_vsync->setChecked(Settings::values.use_vsync.GetValue());
ui->toggle_skip_duplicate_frames->setChecked(
Settings::values.use_skip_duplicate_frames.GetValue());
ui->spirv_shader_gen->setChecked(Settings::values.spirv_shader_gen.GetValue());
ui->disable_spirv_optimizer->setChecked(Settings::values.disable_spirv_optimizer.GetValue());
ui->toggle_async_shaders->setChecked(Settings::values.async_shader_compilation.GetValue());
@@ -180,6 +182,9 @@ void ConfigureGraphics::ApplyConfiguration() {
ui->toggle_disk_shader_cache, use_disk_shader_cache);
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync, ui->toggle_vsync,
use_vsync);
ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_skip_duplicate_frames,
ui->toggle_skip_duplicate_frames,
use_skip_duplicate_frames);
ConfigurationShared::ApplyPerGameSetting(
&Settings::values.delay_game_render_thread_us, ui->delay_render_combo,
[this](s32) { return ui->delay_render_slider->value(); });
@@ -208,6 +213,9 @@ void ConfigureGraphics::SetupPerGameUI() {
Settings::values.use_disk_shader_cache.UsingGlobal());
ui->toggle_vsync->setEnabled(ui->toggle_vsync->isEnabled() &&
Settings::values.use_vsync.UsingGlobal());
ui->toggle_skip_duplicate_frames->setEnabled(
ui->toggle_skip_duplicate_frames->isEnabled() &&
Settings::values.use_skip_duplicate_frames.UsingGlobal());
ui->toggle_async_shaders->setEnabled(
Settings::values.async_shader_compilation.UsingGlobal());
ui->widget_texture_sampling->setEnabled(Settings::values.texture_sampling.UsingGlobal());
@@ -250,6 +258,9 @@ void ConfigureGraphics::SetupPerGameUI() {
use_disk_shader_cache);
ConfigurationShared::SetColoredTristate(ui->toggle_vsync, Settings::values.use_vsync,
use_vsync);
ConfigurationShared::SetColoredTristate(ui->toggle_skip_duplicate_frames,
Settings::values.use_skip_duplicate_frames,
use_skip_duplicate_frames);
ConfigurationShared::SetColoredTristate(ui->toggle_async_shaders,
Settings::values.async_shader_compilation,
async_shader_compilation);
@@ -39,6 +39,7 @@ private:
ConfigurationShared::CheckState shaders_accurate_mul;
ConfigurationShared::CheckState use_disk_shader_cache;
ConfigurationShared::CheckState use_vsync;
ConfigurationShared::CheckState use_skip_duplicate_frames;
ConfigurationShared::CheckState use_display_refresh_rate_detection;
ConfigurationShared::CheckState async_shader_compilation;
ConfigurationShared::CheckState async_presentation;
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>400</width>
<height>509</height>
<height>559</height>
</rect>
</property>
<property name="minimumSize">
@@ -317,6 +317,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_skip_duplicate_frames">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;This detects and skips the presentation of frames that are not unique. It also allows external frame generation tools to work correctly with 30fps games. Works in OpenGL and Vulkan.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="text">
<string>Skip Presenting Duplicate Frames</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="toggle_display_refresh_rate_detection">
<property name="toolTip">
+2
View File
@@ -104,6 +104,7 @@ void LogSettings() {
log_setting("Renderer_UseIntegerScaling", values.use_integer_scaling.GetValue());
log_setting("Renderer_FrameLimit", values.frame_limit.GetValue());
log_setting("Renderer_VSyncNew", values.use_vsync.GetValue());
log_setting("Renderer_SkipDuplicateFrames", values.use_skip_duplicate_frames.GetValue());
log_setting("Renderer_PostProcessingShader", values.pp_shader_name.GetValue());
log_setting("Renderer_FilterMode", values.filter_mode.GetValue());
log_setting("Renderer_TextureFilter", GetTextureFilterName(values.texture_filter.GetValue()));
@@ -216,6 +217,7 @@ void RestoreGlobalState(bool is_powered_on) {
values.use_disk_shader_cache.SetGlobal(true);
values.shaders_accurate_mul.SetGlobal(true);
values.use_vsync.SetGlobal(true);
values.use_skip_duplicate_frames.SetGlobal(true);
values.resolution_factor.SetGlobal(true);
values.use_integer_scaling.SetGlobal(true);
values.frame_limit.SetGlobal(true);
+1
View File
@@ -535,6 +535,7 @@ struct Values {
SwitchableSetting<bool> async_presentation{true, Keys::async_presentation};
SwitchableSetting<bool> use_hw_shader{true, Keys::use_hw_shader};
SwitchableSetting<bool> use_disk_shader_cache{true, Keys::use_disk_shader_cache};
SwitchableSetting<bool> use_skip_duplicate_frames{true, Keys::use_skip_duplicate_frames};
SwitchableSetting<bool> shaders_accurate_mul{true, Keys::shaders_accurate_mul};
#ifdef ANDROID // TODO: Fuck this -OS
SwitchableSetting<bool> use_vsync{false, Keys::use_vsync};
+3
View File
@@ -29,6 +29,8 @@ constexpr std::size_t IgnoreFrames = 5;
namespace Core {
bool PerfStats::game_frames_updated = true;
PerfStats::PerfStats(u64 title_id) : title_id(title_id) {}
PerfStats::~PerfStats() {
@@ -109,6 +111,7 @@ void PerfStats::EndGameFrame() {
std::scoped_lock lock{object_mutex};
game_frames += 1;
PerfStats::game_frames_updated = true;
}
double PerfStats::GetMeanFrametime() const {
+2
View File
@@ -120,6 +120,8 @@ public:
artic_events.Set(event, set);
}
}
/// Boolean representing whether game_frames has been updated since last time it was presented
static bool game_frames_updated;
private:
mutable std::mutex object_mutex;
@@ -93,11 +93,26 @@ void RendererOpenGL::SwapBuffers() {
// Maintain the rasterizer's state as a priority
OpenGLState prev_state = OpenGLState::GetCurState();
state.Apply();
#ifdef ANDROID
if (secondary_window) {
secondaryWindowEnabled = true;
} else {
secondaryWindowEnabled = false;
}
#else
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
secondaryWindowEnabled = true;
} else {
secondaryWindowEnabled = false;
}
#endif
render_window.SetupFramebuffer();
PrepareRendertarget();
RenderScreenshot();
isSecondaryWindow = false;
#ifdef HAVE_LIBRETRO
DrawScreens(render_window.GetFramebufferLayout(), false);
render_window.SwapBuffers();
@@ -110,6 +125,7 @@ void RendererOpenGL::SwapBuffers() {
// it means we have a second display
if (secondary_window) {
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
isSecondaryWindow = true;
RenderToMailbox(secondary_layout, secondary_window->mailbox, false);
secondary_window->PollEvents();
}
@@ -117,6 +133,7 @@ void RendererOpenGL::SwapBuffers() {
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
const auto& secondary_layout = secondary_window->GetFramebufferLayout();
isSecondaryWindow = true;
RenderToMailbox(secondary_layout, secondary_window->mailbox, false);
secondary_window->PollEvents();
}
@@ -196,51 +213,57 @@ void RendererOpenGL::PrepareRendertarget() {
void RendererOpenGL::RenderToMailbox(const Layout::FramebufferLayout& layout,
std::unique_ptr<Frontend::TextureMailbox>& mailbox,
bool flipped) {
if (!Settings::values.use_skip_duplicate_frames.GetValue() ||
Core::PerfStats::game_frames_updated) {
Frontend::Frame* frame;
{
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
Frontend::Frame* frame;
{
MICROPROFILE_SCOPE(OpenGL_WaitPresent);
frame = mailbox->GetRenderFrame();
frame = mailbox->GetRenderFrame();
// Clean up sync objects before drawing
// Clean up sync objects before drawing
// INTEL driver workaround. We can't delete the previous render sync object until we are
// sure that the presentation is done
if (frame->present_fence) {
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
}
// INTEL driver workaround. We can't delete the previous render sync object until we are
// sure that the presentation is done
if (frame->present_fence) {
glClientWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
// delete the draw fence if the frame wasn't presented
if (frame->render_fence) {
glDeleteSync(frame->render_fence);
frame->render_fence = nullptr;
}
// wait for the presentation to be done
if (frame->present_fence) {
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(frame->present_fence);
frame->present_fence = nullptr;
}
}
// delete the draw fence if the frame wasn't presented
if (frame->render_fence) {
glDeleteSync(frame->render_fence);
frame->render_fence = nullptr;
{
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
// Recreate the frame if the size of the window has changed
if (layout.width != frame->width || layout.height != frame->height) {
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
}
state.draw.draw_framebuffer = frame->render.handle;
state.Apply();
DrawScreens(layout, flipped);
// Create a fence for the frontend to wait on and swap this frame to OffTex
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
mailbox->ReleaseRenderFrame(frame);
}
// wait for the presentation to be done
if (frame->present_fence) {
glWaitSync(frame->present_fence, 0, GL_TIMEOUT_IGNORED);
glDeleteSync(frame->present_fence);
frame->present_fence = nullptr;
if ((secondaryWindowEnabled && isSecondaryWindow) || (!secondaryWindowEnabled)) {
Core::PerfStats::game_frames_updated = false;
}
}
{
MICROPROFILE_SCOPE(OpenGL_RenderFrame);
// Recreate the frame if the size of the window has changed
if (layout.width != frame->width || layout.height != frame->height) {
LOG_DEBUG(Render_OpenGL, "Reloading render frame");
mailbox->ReloadRenderFrame(frame, layout.width, layout.height);
}
state.draw.draw_framebuffer = frame->render.handle;
state.Apply();
DrawScreens(layout, flipped);
// Create a fence for the frontend to wait on and swap this frame to OffTex
frame->render_fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glFlush();
mailbox->ReleaseRenderFrame(frame);
}
}
/**
@@ -112,6 +112,8 @@ private:
GLuint attrib_tex_coord;
FrameDumperOpenGL frame_dumper;
bool isSecondaryWindow;
bool secondaryWindowEnabled;
};
} // namespace OpenGL
@@ -24,7 +24,6 @@
#include "video_core/host_shaders/vulkan_cursor_vert.h"
#include <vk_mem_alloc.h>
#if defined(__APPLE__) && !defined(HAVE_LIBRETRO)
#include "common/apple_utils.h"
#endif
@@ -237,23 +236,29 @@ void RendererVulkan::PrepareDraw(Frame* frame, const Layout::FramebufferLayout&
void RendererVulkan::RenderToWindow(PresentWindow& window, const Layout::FramebufferLayout& layout,
bool flipped) {
Frame* frame = window.GetRenderFrame();
if (!Settings::values.use_skip_duplicate_frames.GetValue() ||
Core::PerfStats::game_frames_updated) {
Frame* frame = window.GetRenderFrame();
if (layout.width != frame->width || layout.height != frame->height) {
window.WaitPresent();
scheduler.Finish();
window.RecreateFrame(frame, layout.width, layout.height);
if (layout.width != frame->width || layout.height != frame->height) {
window.WaitPresent();
scheduler.Finish();
window.RecreateFrame(frame, layout.width, layout.height);
}
clear_color.float32[0] = Settings::values.bg_red.GetValue();
clear_color.float32[1] = Settings::values.bg_green.GetValue();
clear_color.float32[2] = Settings::values.bg_blue.GetValue();
clear_color.float32[3] = 1.0f;
DrawScreens(frame, layout, flipped);
scheduler.Flush(frame->render_ready);
window.Present(frame);
if ((secondaryWindowEnabled && isSecondaryWindow) || (!secondaryWindowEnabled)) {
Core::PerfStats::game_frames_updated = false;
screenRendered = true;
}
}
clear_color.float32[0] = Settings::values.bg_red.GetValue();
clear_color.float32[1] = Settings::values.bg_green.GetValue();
clear_color.float32[2] = Settings::values.bg_blue.GetValue();
clear_color.float32[3] = 1.0f;
DrawScreens(frame, layout, flipped);
scheduler.Flush(frame->render_ready);
window.Present(frame);
}
void RendererVulkan::LoadFBToScreenInfo(const Pica::FramebufferConfig& framebuffer,
@@ -1116,9 +1121,28 @@ void RendererVulkan::DrawCursor(const Layout::FramebufferLayout& layout) {
void RendererVulkan::SwapBuffers() {
system.perf_stats->StartSwap();
screenRendered = false;
#ifndef ANDROID
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
ASSERT(secondary_window);
secondaryWindowEnabled = true;
} else {
secondaryWindowEnabled = false;
}
#endif
#ifdef ANDROID
if (secondary_window) {
secondaryWindowEnabled = true;
} else {
secondaryWindowEnabled = false;
}
#endif
const Layout::FramebufferLayout& layout = render_window.GetFramebufferLayout();
PrepareRendertarget();
RenderScreenshot();
isSecondaryWindow = false;
RenderToWindow(main_present_window, layout, false);
#ifndef ANDROID
if (Settings::values.layout_option.GetValue() == Settings::LayoutOption::SeparateWindows) {
@@ -1128,6 +1152,7 @@ void RendererVulkan::SwapBuffers() {
secondary_present_window_ptr = std::make_unique<PresentWindow>(
*secondary_window, instance, scheduler, IsLowRefreshRate());
}
isSecondaryWindow = true;
RenderToWindow(*secondary_present_window_ptr, secondary_layout, false);
secondary_window->PollEvents();
}
@@ -1140,10 +1165,14 @@ void RendererVulkan::SwapBuffers() {
secondary_present_window_ptr = std::make_unique<PresentWindow>(
*secondary_window, instance, scheduler, IsLowRefreshRate());
}
isSecondaryWindow = true;
RenderToWindow(*secondary_present_window_ptr, secondary_layout, false);
secondary_window->PollEvents();
}
#endif
if (!screenRendered) {
scheduler.Finish();
}
system.perf_stats->EndSwap();
rasterizer.TickFrame();
@@ -134,7 +134,6 @@ private:
DescriptorUpdateQueue update_queue;
RasterizerVulkan rasterizer;
std::unique_ptr<PresentWindow> secondary_present_window_ptr;
DescriptorHeap present_heap;
vk::UniquePipelineLayout present_pipeline_layout;
std::array<vk::Pipeline, PRESENT_PIPELINES> present_pipelines;
@@ -151,6 +150,9 @@ private:
vk::ShaderModule cursor_fragment_shader{};
vk::Pipeline cursor_pipeline{};
vk::UniquePipelineLayout cursor_pipeline_layout{};
bool isSecondaryWindow;
bool secondaryWindowEnabled;
bool screenRendered;
};
} // namespace Vulkan