diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index b2c3783970..378c0eb055 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -40,7 +40,8 @@ enum class IntSetting( VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1), DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0), TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0), - USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1); + USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1), + DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0); override var int: Int = defaultValue diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt index 9f504e6037..f949950bc0 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt @@ -729,6 +729,18 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) IntSetting.TEXTURE_FILTER.defaultValue ) ) + add( + SliderSetting( + IntSetting.DELAY_RENDER_THREAD_US, + R.string.delay_render_thread, + R.string.delay_render_thread_description, + 0, + 16000, + " μs", + IntSetting.DELAY_RENDER_THREAD_US.key, + IntSetting.DELAY_RENDER_THREAD_US.defaultValue.toFloat() + ) + ) add(HeaderSetting(R.string.stereoscopy)) add( diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp index 4e862f3a05..bafac31297 100644 --- a/src/android/app/src/main/jni/config.cpp +++ b/src/android/app/src/main/jni/config.cpp @@ -169,6 +169,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.bg_red); ReadSetting("Renderer", Settings::values.bg_green); ReadSetting("Renderer", Settings::values.bg_blue); + ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); // Layout Settings::values.layout_option = static_cast(sdl2_config->GetInteger( diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index c46395feab..7bf546e035 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -175,6 +175,10 @@ anaglyph_shader_name = # 0: Nearest, 1 (default): Linear filter_mode = +# Delays the game render thread by the specified amount of microseconds +# Set to 0 for no delay, only useful in dynamic-fps games to simulate GPU delay. +delay_game_render_thread_us = + [Layout] # Layout for the screen inside the render window. # 0 (default): Default Top Bottom Screen, 1: Single Screen Only, 2: Large Screen Small Screen, 3: Side by Side diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml index 7c7ba9e533..c4c9db23a0 100644 --- a/src/android/app/src/main/res/values-es/strings.xml +++ b/src/android/app/src/main/res/values-es/strings.xml @@ -663,5 +663,7 @@ Se esperan fallos gráficos temporales cuando ésta esté activado. Conectar con Artic Base Conectar con una consola real que esté ejecutando un servidor Artic Base Introduce la dirección del servidor Artic Base + Retrasa el hilo de dibujado del juego + Retrasa el hilo de dibujado del juego cuando envía datos a la GPU. Ayuda con problemas de rendimiento en los (muy pocos) juegos de fps dinámicos. diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 6614e97f18..4de0ba43bd 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -689,5 +689,7 @@ Connect to a real console that is running an Artic Base server Connect to Artic Base Enter Artic Base server address + Delay game render thread + Delay the game render thread when it submits data to the GPU. Helps with performance issues in the (very few) dynamic-fps games. diff --git a/src/citra/config.cpp b/src/citra/config.cpp index e474211020..bd60d47649 100644 --- a/src/citra/config.cpp +++ b/src/citra/config.cpp @@ -147,6 +147,7 @@ void Config::ReadValues() { ReadSetting("Renderer", Settings::values.use_vsync_new); ReadSetting("Renderer", Settings::values.texture_filter); ReadSetting("Renderer", Settings::values.texture_sampling); + ReadSetting("Renderer", Settings::values.delay_game_render_thread_us); ReadSetting("Renderer", Settings::values.mono_render_option); ReadSetting("Renderer", Settings::values.render_3d); diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp index e54fcee877..76ae4934d5 100644 --- a/src/citra_qt/configuration/config.cpp +++ b/src/citra_qt/configuration/config.cpp @@ -667,6 +667,8 @@ void Config::ReadRendererValues() { ReadGlobalSetting(Settings::values.texture_filter); ReadGlobalSetting(Settings::values.texture_sampling); + ReadGlobalSetting(Settings::values.delay_game_render_thread_us); + if (global) { ReadBasicSetting(Settings::values.use_shader_jit); } @@ -1168,6 +1170,8 @@ void Config::SaveRendererValues() { WriteGlobalSetting(Settings::values.texture_filter); WriteGlobalSetting(Settings::values.texture_sampling); + WriteGlobalSetting(Settings::values.delay_game_render_thread_us); + if (global) { WriteSetting(QStringLiteral("use_shader_jit"), Settings::values.use_shader_jit.GetValue(), true); diff --git a/src/citra_qt/configuration/configure_graphics.cpp b/src/citra_qt/configuration/configure_graphics.cpp index 80c2d138d3..2e244b33a8 100644 --- a/src/citra_qt/configuration/configure_graphics.cpp +++ b/src/citra_qt/configuration/configure_graphics.cpp @@ -26,6 +26,10 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo->setCurrentIndex(-1); + const auto width = static_cast(QString::fromStdString("000000000").size() * 6); + ui->delay_render_display_label->setMinimumWidth(width); + ui->delay_render_combo->setVisible(!Settings::IsConfiguringGlobal()); + auto graphics_api_combo_model = qobject_cast(ui->graphics_api_combo->model()); #ifndef ENABLE_SOFTWARE_RENDERER @@ -82,12 +86,25 @@ ConfigureGraphics::ConfigureGraphics(QString gl_renderer, std::spangraphics_api_combo, qOverload(&QComboBox::currentIndexChanged), this, &ConfigureGraphics::SetPhysicalDeviceComboVisibility); + connect(ui->delay_render_slider, &QSlider::valueChanged, this, [&](int value) { + ui->delay_render_display_label->setText( + QStringLiteral("%1 ms") + .arg(((double)value) / 1000.f, 0, 'f', 3) + .rightJustified(QString::fromStdString("000000000").size())); + }); + SetConfiguration(); } ConfigureGraphics::~ConfigureGraphics() = default; void ConfigureGraphics::SetConfiguration() { + ui->delay_render_slider->setValue(Settings::values.delay_game_render_thread_us.GetValue()); + ui->delay_render_display_label->setText( + QStringLiteral("%1 ms") + .arg(((double)ui->delay_render_slider->value()) / 1000, 0, 'f', 3) + .rightJustified(QString::fromStdString("000000000").size())); + if (!Settings::IsConfiguringGlobal()) { ConfigurationShared::SetHighlight(ui->graphics_api_group, !Settings::values.graphics_api.UsingGlobal()); @@ -101,6 +118,16 @@ void ConfigureGraphics::SetConfiguration() { &Settings::values.texture_sampling); ConfigurationShared::SetHighlight(ui->widget_texture_sampling, !Settings::values.texture_sampling.UsingGlobal()); + ConfigurationShared::SetHighlight( + ui->delay_render_layout, !Settings::values.delay_game_render_thread_us.UsingGlobal()); + + if (Settings::values.delay_game_render_thread_us.UsingGlobal()) { + ui->delay_render_combo->setCurrentIndex(0); + ui->delay_render_slider->setEnabled(false); + } else { + ui->delay_render_combo->setCurrentIndex(1); + ui->delay_render_slider->setEnabled(true); + } } else { ui->graphics_api_combo->setCurrentIndex( static_cast(Settings::values.graphics_api.GetValue())); @@ -144,6 +171,9 @@ void ConfigureGraphics::ApplyConfiguration() { ui->toggle_disk_shader_cache, use_disk_shader_cache); ConfigurationShared::ApplyPerGameSetting(&Settings::values.use_vsync_new, ui->toggle_vsync_new, use_vsync_new); + ConfigurationShared::ApplyPerGameSetting( + &Settings::values.delay_game_render_thread_us, ui->delay_render_combo, + [this](s32) { return ui->delay_render_slider->value(); }); if (Settings::IsConfiguringGlobal()) { Settings::values.use_shader_jit = ui->toggle_shader_jit->isChecked(); @@ -170,9 +200,16 @@ void ConfigureGraphics::SetupPerGameUI() { ui->toggle_async_present->setEnabled(Settings::values.async_presentation.UsingGlobal()); ui->graphics_api_combo->setEnabled(Settings::values.graphics_api.UsingGlobal()); ui->physical_device_combo->setEnabled(Settings::values.physical_device.UsingGlobal()); + ui->delay_render_combo->setEnabled( + Settings::values.delay_game_render_thread_us.UsingGlobal()); return; } + connect(ui->delay_render_combo, qOverload(&QComboBox::activated), this, [this](int index) { + ui->delay_render_slider->setEnabled(index == 1); + ConfigurationShared::SetHighlight(ui->delay_render_layout, index == 1); + }); + ui->toggle_shader_jit->setVisible(false); ConfigurationShared::SetColoredComboBox( diff --git a/src/citra_qt/configuration/configure_graphics.ui b/src/citra_qt/configuration/configure_graphics.ui index a052186cd8..122fdddcd1 100644 --- a/src/citra_qt/configuration/configure_graphics.ui +++ b/src/citra_qt/configuration/configure_graphics.ui @@ -307,6 +307,83 @@ + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + Use global + + + + + Use per-game + + + + + + + + Delay game render thread: + + + <html><head/><body><p>Delays the emulated game render thread the specified amount of milliseconds every time it submits render commands to the GPU.</p><p>Adjust this feature in the (very few) dynamic-fps games to fix performance issues.</p></body></html> + + + + + + + 0 + + + 16000 + + + 100 + + + 250 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + diff --git a/src/common/settings.cpp b/src/common/settings.cpp index 657747b617..e2f432a716 100644 --- a/src/common/settings.cpp +++ b/src/common/settings.cpp @@ -100,6 +100,7 @@ void LogSettings() { log_setting("Renderer_TextureFilter", GetTextureFilterName(values.texture_filter.GetValue())); log_setting("Renderer_TextureSampling", GetTextureSamplingName(values.texture_sampling.GetValue())); + log_setting("Renderer_DelayGameRenderThreasUs", values.delay_game_render_thread_us.GetValue()); log_setting("Stereoscopy_Render3d", values.render_3d.GetValue()); log_setting("Stereoscopy_Factor3d", values.factor_3d.GetValue()); log_setting("Stereoscopy_MonoRenderOption", values.mono_render_option.GetValue()); @@ -192,6 +193,7 @@ void RestoreGlobalState(bool is_powered_on) { values.frame_limit.SetGlobal(true); values.texture_filter.SetGlobal(true); values.texture_sampling.SetGlobal(true); + values.delay_game_render_thread_us.SetGlobal(true); values.layout_option.SetGlobal(true); values.swap_screen.SetGlobal(true); values.upright_screen.SetGlobal(true); diff --git a/src/common/settings.h b/src/common/settings.h index 64bba90ee4..b7ac91a0ba 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -479,6 +479,8 @@ struct Values { SwitchableSetting texture_filter{TextureFilter::None, "texture_filter"}; SwitchableSetting texture_sampling{TextureSampling::GameControlled, "texture_sampling"}; + SwitchableSetting delay_game_render_thread_us{0, 0, 16000, + "delay_game_render_thread_us"}; SwitchableSetting layout_option{LayoutOption::Default, "layout_option"}; SwitchableSetting swap_screen{false, "swap_screen"}; diff --git a/src/core/hle/service/gsp/gsp_gpu.cpp b/src/core/hle/service/gsp/gsp_gpu.cpp index 329c653c8f..1b2dd29f17 100644 --- a/src/core/hle/service/gsp/gsp_gpu.cpp +++ b/src/core/hle/service/gsp/gsp_gpu.cpp @@ -9,6 +9,7 @@ #include #include "common/archives.h" #include "common/bit_field.h" +#include "common/settings.h" #include "core/core.h" #include "core/hle/ipc_helpers.h" #include "core/hle/kernel/shared_memory.h" @@ -410,6 +411,9 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { auto* command_buffer = GetCommandBuffer(active_thread_id); auto& gpu = system.GPU(); + + bool requires_delay = false; + while (command_buffer->number_commands) { if (command_buffer->should_stop) { command_buffer->status.Assign(CommandBuffer::STATUS_STOPPED); @@ -420,6 +424,10 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { } Command command = command_buffer->commands[command_buffer->index]; + if (command.id == CommandId::SubmitCmdList && !requires_delay && + Settings::values.delay_game_render_thread_us.GetValue() != 0) { + requires_delay = true; + } // Decrease the number of commands remaining and increase the current index command_buffer->number_commands.Assign(command_buffer->number_commands - 1); @@ -435,8 +443,20 @@ void GSP_GPU::TriggerCmdReqQueue(Kernel::HLERequestContext& ctx) { } } - IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); - rb.Push(ResultSuccess); + if (requires_delay) { + ctx.RunAsync( + [](Kernel::HLERequestContext& ctx) { + return Settings::values.delay_game_render_thread_us.GetValue() * 1000; + }, + [](Kernel::HLERequestContext& ctx) { + IPC::RequestBuilder rb(ctx, 1, 0); + rb.Push(ResultSuccess); + }, + false); + } else { + IPC::RequestBuilder rb = rp.MakeBuilder(1, 0); + rb.Push(ResultSuccess); + } } void GSP_GPU::ImportDisplayCaptureInfo(Kernel::HLERequestContext& ctx) {