From e01d66d18e85449a63fb97bfdfa8f7d3d3330a92 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sun, 24 Jan 2021 02:52:52 +1000 Subject: [PATCH] System: Implement runahead --- src/core/analog_controller.cpp | 19 +- src/core/analog_joystick.cpp | 19 +- src/core/digital_controller.cpp | 16 +- src/core/gpu_sw.cpp | 6 + src/core/gpu_sw.h | 1 + src/core/host_interface.cpp | 16 +- src/core/settings.cpp | 4 + src/core/settings.h | 2 + src/core/spu.cpp | 8 +- src/core/spu.h | 4 + src/core/system.cpp | 170 +++++++++++++++--- src/core/system.h | 2 +- .../emulationsettingswidget.cpp | 9 + src/duckstation-qt/emulationsettingswidget.h | 1 + src/duckstation-qt/emulationsettingswidget.ui | 36 ++++ 15 files changed, 278 insertions(+), 35 deletions(-) diff --git a/src/core/analog_controller.cpp b/src/core/analog_controller.cpp index 0a362cb6e..05e3e459b 100644 --- a/src/core/analog_controller.cpp +++ b/src/core/analog_controller.cpp @@ -117,6 +117,9 @@ void AnalogController::SetAxisState(s32 axis_code, float value) void AnalogController::SetAxisState(Axis axis, u8 value) { + if (value != m_axis_state[static_cast(axis)]) + System::SetRunaheadReplayFlag(); + m_axis_state[static_cast(axis)] = value; } @@ -131,10 +134,22 @@ void AnalogController::SetButtonState(Button button, bool pressed) return; } + const u16 bit = u16(1) << static_cast(button); + if (pressed) - m_button_state &= ~(u16(1) << static_cast(button)); + { + if (m_button_state & bit) + System::SetRunaheadReplayFlag(); + + m_button_state &= ~(bit); + } else - m_button_state |= u16(1) << static_cast(button); + { + if (!(m_button_state & bit)) + System::SetRunaheadReplayFlag(); + + m_button_state |= bit; + } } void AnalogController::SetButtonState(s32 button_code, bool pressed) diff --git a/src/core/analog_joystick.cpp b/src/core/analog_joystick.cpp index 017e996aa..398d10ac9 100644 --- a/src/core/analog_joystick.cpp +++ b/src/core/analog_joystick.cpp @@ -83,6 +83,9 @@ void AnalogJoystick::SetAxisState(s32 axis_code, float value) void AnalogJoystick::SetAxisState(Axis axis, u8 value) { + if (m_axis_state[static_cast(axis)] != value) + System::SetRunaheadReplayFlag(); + m_axis_state[static_cast(axis)] = value; } @@ -96,10 +99,22 @@ void AnalogJoystick::SetButtonState(Button button, bool pressed) return; } + const u16 bit = u16(1) << static_cast(button); + if (pressed) - m_button_state &= ~(u16(1) << static_cast(button)); + { + if (m_button_state & bit) + System::SetRunaheadReplayFlag(); + + m_button_state &= ~bit; + } else - m_button_state |= (u16(1) << static_cast(button)); + { + if (!(m_button_state & bit)) + System::SetRunaheadReplayFlag(); + + m_button_state |= bit; + } } void AnalogJoystick::SetButtonState(s32 button_code, bool pressed) diff --git a/src/core/digital_controller.cpp b/src/core/digital_controller.cpp index b95b5f7a4..389c5f91b 100644 --- a/src/core/digital_controller.cpp +++ b/src/core/digital_controller.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "common/state_wrapper.h" #include "host_interface.h" +#include "system.h" DigitalController::DigitalController() = default; @@ -45,10 +46,21 @@ void DigitalController::SetAxisState(s32 axis_code, float value) {} void DigitalController::SetButtonState(Button button, bool pressed) { + const u16 bit = u16(1) << static_cast(button); if (pressed) - m_button_state &= ~(u16(1) << static_cast(button)); + { + if (m_button_state & bit) + System::SetRunaheadReplayFlag(); + + m_button_state &= ~bit; + } else - m_button_state |= u16(1) << static_cast(button); + { + if (!(m_button_state & bit)) + System::SetRunaheadReplayFlag(); + + m_button_state |= bit; + } } void DigitalController::SetButtonState(s32 button_code, bool pressed) diff --git a/src/core/gpu_sw.cpp b/src/core/gpu_sw.cpp index bd7ea537c..20cd5301b 100644 --- a/src/core/gpu_sw.cpp +++ b/src/core/gpu_sw.cpp @@ -75,6 +75,12 @@ bool GPU_SW::Initialize(HostDisplay* host_display) return true; } +bool GPU_SW::DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) +{ + // ignore the host texture for software mode, since we want to save vram here + return GPU::DoState(sw, nullptr, update_display); +} + void GPU_SW::Reset(bool clear_vram) { GPU::Reset(clear_vram); diff --git a/src/core/gpu_sw.h b/src/core/gpu_sw.h index d67c83bff..16b0be885 100644 --- a/src/core/gpu_sw.h +++ b/src/core/gpu_sw.h @@ -18,6 +18,7 @@ public: bool IsHardwareRenderer() const override; bool Initialize(HostDisplay* host_display) override; + bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display) override; void Reset(bool clear_vram) override; void UpdateSettings() override; diff --git a/src/core/host_interface.cpp b/src/core/host_interface.cpp index 81abeb809..a1e81938f 100644 --- a/src/core/host_interface.cpp +++ b/src/core/host_interface.cpp @@ -16,6 +16,7 @@ #include "host_display.h" #include "pgxp.h" #include "save_state_version.h" +#include "spu.h" #include "system.h" #include "texture_replacements.h" #include @@ -78,6 +79,9 @@ void HostInterface::CreateAudioStream() } m_audio_stream->SetOutputVolume(GetAudioOutputVolume()); + + if (System::IsValid()) + g_spu.SetAudioStream(m_audio_stream.get()); } s32 HostInterface::GetAudioOutputVolume() const @@ -491,6 +495,8 @@ void HostInterface::SetDefaultSettings(SettingsInterface& si) si.SetBoolValue("Main", "RewindEnable", false); si.SetFloatValue("Main", "RewindFrequency", 10.0f); si.SetIntValue("Main", "RewindSaveSlots", 10); + si.SetBoolValue("Main", "RunaheadEnable", false); + si.SetFloatValue("Main", "RunaheadFrames", 1); si.SetStringValue("CPU", "ExecutionMode", Settings::GetCPUExecutionModeName(Settings::DEFAULT_CPU_EXECUTION_MODE)); si.SetBoolValue("CPU", "RecompilerMemoryExceptions", false); @@ -660,7 +666,8 @@ void HostInterface::FixIncompatibleSettings(bool display_osd_messages) #endif // rewinding causes issues with mmap fastmem, so just use LUT - if (g_settings.rewind_enable && g_settings.IsUsingFastmem() && g_settings.cpu_fastmem_mode == CPUFastmemMode::MMap) + if ((g_settings.rewind_enable || g_settings.runahead_enable) && g_settings.IsUsingFastmem() && + g_settings.cpu_fastmem_mode == CPUFastmemMode::MMap) { Log_WarningPrintf("Disabling mmap fastmem due to rewind being enabled"); g_settings.cpu_fastmem_mode = CPUFastmemMode::LUT; @@ -768,7 +775,8 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) g_settings.display_active_end_offset != old_settings.display_active_end_offset || g_settings.display_line_start_offset != old_settings.display_line_start_offset || g_settings.display_line_end_offset != old_settings.display_line_end_offset || - g_settings.rewind_enable != old_settings.rewind_enable) + g_settings.rewind_enable != old_settings.rewind_enable || + g_settings.runahead_enable != old_settings.runahead_enable) { if (g_settings.IsUsingCodeCache()) CPU::CodeCache::Reinitialize(); @@ -808,7 +816,9 @@ void HostInterface::CheckForSettingsChanges(const Settings& old_settings) if (g_settings.rewind_enable != old_settings.rewind_enable || g_settings.rewind_save_frequency != old_settings.rewind_save_frequency || - g_settings.rewind_save_slots != old_settings.rewind_save_slots) + g_settings.rewind_save_slots != old_settings.rewind_save_slots || + g_settings.runahead_enable != old_settings.runahead_enable || + g_settings.runahead_frames != old_settings.runahead_frames) { System::UpdateMemorySaveStateSettings(); } diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 82c88a5bc..de629bda6 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -126,6 +126,8 @@ void Settings::Load(SettingsInterface& si) rewind_enable = si.GetBoolValue("Main", "RewindEnable", false); rewind_save_frequency = si.GetFloatValue("Main", "RewindFrequency", 10.0f); rewind_save_slots = static_cast(si.GetIntValue("Main", "RewindSaveSlots", 10)); + runahead_enable = si.GetBoolValue("Main", "RunaheadEnable", false); + runahead_frames = static_cast(si.GetIntValue("Main", "RunaheadFrames", 1)); cpu_execution_mode = ParseCPUExecutionMode( @@ -301,6 +303,8 @@ void Settings::Save(SettingsInterface& si) const si.SetBoolValue("Main", "RewindEnable", rewind_enable); si.SetFloatValue("Main", "RewindFrequency", rewind_save_frequency); si.SetIntValue("Main", "RewindSaveSlots", rewind_save_slots); + si.SetBoolValue("Main", "RunaheadEnable", runahead_enable); + si.SetFloatValue("Main", "RunaheadFrames", runahead_frames); si.SetStringValue("CPU", "ExecutionMode", GetCPUExecutionModeName(cpu_execution_mode)); si.SetBoolValue("CPU", "OverclockEnable", cpu_overclock_enable); diff --git a/src/core/settings.h b/src/core/settings.h index 67f189e20..0e6ef5a38 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -95,8 +95,10 @@ struct Settings bool disable_all_enhancements = false; bool rewind_enable = false; + bool runahead_enable = false; float rewind_save_frequency = 10.0f; u32 rewind_save_slots = 10; + u32 runahead_frames = 1; GPURenderer gpu_renderer = GPURenderer::Software; std::string gpu_adapter; diff --git a/src/core/spu.cpp b/src/core/spu.cpp index 4aad31f6b..ed5d46b9d 100644 --- a/src/core/spu.cpp +++ b/src/core/spu.cpp @@ -31,6 +31,7 @@ void SPU::Initialize() "SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD, [](void* param, TickCount ticks, TickCount ticks_late) { static_cast(param)->ExecuteTransfer(ticks); }, this, false); + m_audio_stream = g_host_interface->GetAudioStream(); Reset(); } @@ -49,6 +50,7 @@ void SPU::Shutdown() m_tick_event.reset(); m_transfer_event.reset(); m_dump_writer.reset(); + m_audio_stream = nullptr; } void SPU::Reset() @@ -172,7 +174,6 @@ bool SPU::DoState(StateWrapper& sw) if (sw.IsReading()) { - g_host_interface->GetAudioStream()->EmptyBuffers(); UpdateEventInterval(); UpdateTransferEvent(); } @@ -1731,10 +1732,9 @@ void SPU::Execute(TickCount ticks) while (remaining_frames > 0) { - AudioStream* const output_stream = g_host_interface->GetAudioStream(); s16* output_frame_start; u32 output_frame_space = remaining_frames; - output_stream->BeginWrite(&output_frame_start, &output_frame_space); + m_audio_stream->BeginWrite(&output_frame_start, &output_frame_space); s16* output_frame = output_frame_start; const u32 frames_in_this_batch = std::min(remaining_frames, output_frame_space); @@ -1837,7 +1837,7 @@ void SPU::Execute(TickCount ticks) if (m_dump_writer) m_dump_writer->WriteFrames(output_frame_start, frames_in_this_batch); - output_stream->EndWrite(frames_in_this_batch); + m_audio_stream->EndWrite(frames_in_this_batch); remaining_frames -= frames_in_this_batch; } } diff --git a/src/core/spu.h b/src/core/spu.h index 980acfb10..0bd17e9f4 100644 --- a/src/core/spu.h +++ b/src/core/spu.h @@ -57,6 +57,9 @@ public: const std::array& GetRAM() const { return m_ram; } std::array& GetRAM() { return m_ram; } + /// Change output stream - used for runahead. + ALWAYS_INLINE void SetAudioStream(AudioStream* stream) { m_audio_stream = stream; } + private: static constexpr u32 SPU_BASE = 0x1F801C00; static constexpr u32 NUM_VOICES = 24; @@ -373,6 +376,7 @@ private: std::unique_ptr m_tick_event; std::unique_ptr m_transfer_event; std::unique_ptr m_dump_writer; + AudioStream* m_audio_stream = nullptr; TickCount m_ticks_carry = 0; TickCount m_cpu_ticks_per_spu_tick = 0; TickCount m_cpu_tick_divider = 0; diff --git a/src/core/system.cpp b/src/core/system.cpp index 6e5d59a6e..4113f7472 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -61,6 +61,9 @@ struct MemorySaveState std::unique_ptr state_stream; }; +static bool SaveMemoryState(MemorySaveState* mss); +static bool LoadMemoryState(const MemorySaveState& mss); + static bool LoadEXE(const char* filename); static bool SetExpansionROM(const char* filename); @@ -70,10 +73,16 @@ static std::unique_ptr OpenCDImage(const char* path, bool force_preload static bool DoLoadState(ByteStream* stream, bool force_software_renderer, bool update_display); static bool DoState(StateWrapper& sw, HostDisplayTexture** host_texture, bool update_display); static void DoRunFrame(); -static void DoRewind(); -static void DoMemorySaveStates(); static bool CreateGPU(GPURenderer renderer); +static bool SaveRewindState(); +static void DoRewind(); + +static void SaveRunaheadState(); +static void DoRunahead(); + +static void DoMemorySaveStates(); + static bool Initialize(bool force_software_renderer); static void UpdateRunningGame(const char* path, CDImage* image); @@ -126,6 +135,11 @@ static s32 s_rewind_save_frequency = -1; static s32 s_rewind_save_counter = -1; static bool s_rewinding_first_save = false; +static std::deque s_runahead_states; +static std::unique_ptr s_runahead_audio_stream; +static bool s_runahead_replay_pending = false; +static u32 s_runahead_frames = 0; + State GetState() { return s_state; @@ -835,6 +849,7 @@ void Shutdown() return; ClearMemorySaveStates(); + s_runahead_audio_stream.reset(); g_texture_replacements.Shutdown(); @@ -1150,6 +1165,7 @@ bool DoLoadState(ByteStream* state, bool force_software_renderer, bool update_di if (s_state == State::Starting) s_state = State::Running; + g_host_interface->GetAudioStream()->EmptyBuffers(); return true; } @@ -1301,6 +1317,9 @@ void RunFrame() return; } + if (s_runahead_frames > 0) + DoRunahead(); + DoRunFrame(); if (s_memory_saves_enabled) @@ -1960,6 +1979,7 @@ void CalculateRewindMemoryUsage(u32 num_saves, u64* ram_usage, u64* vram_usage) void ClearMemorySaveStates() { s_rewind_states.clear(); + s_runahead_states.clear(); } void UpdateMemorySaveStateSettings() @@ -1984,6 +2004,60 @@ void UpdateMemorySaveStateSettings() s_rewind_save_frequency = -1; s_rewind_save_counter = -1; } + + s_rewind_load_frequency = -1; + s_rewind_load_counter = -1; + + s_runahead_frames = g_settings.runahead_enable ? g_settings.runahead_frames : 0; + s_runahead_replay_pending = false; + if (s_runahead_frames > 0) + { + Log_InfoPrintf("Runahead is active with %u frames", s_runahead_frames); + + if (!s_runahead_audio_stream) + { + // doesn't matter if it's not resampled here since it eats everything anyway, nom nom nom. + s_runahead_audio_stream = AudioStream::CreateNullAudioStream(); + s_runahead_audio_stream->Reconfigure(HostInterface::AUDIO_SAMPLE_RATE, HostInterface::AUDIO_SAMPLE_RATE, + HostInterface::AUDIO_CHANNELS); + } + } + else + { + s_runahead_audio_stream.reset(); + } +} + +bool LoadMemoryState(const MemorySaveState& mss) +{ + mss.state_stream->SeekAbsolute(0); + + StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Read, SAVE_STATE_VERSION); + HostDisplayTexture* host_texture = mss.vram_texture.get(); + if (!DoState(sw, &host_texture, true)) + { + g_host_interface->ReportError("Failed to load memory save state, resetting."); + Reset(); + return false; + } + + return true; +} + +bool SaveMemoryState(MemorySaveState* mss) +{ + mss->state_stream = std::make_unique(nullptr, MAX_SAVE_STATE_SIZE); + + HostDisplayTexture* host_texture = nullptr; + StateWrapper sw(mss->state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION); + if (!DoState(sw, &host_texture, false)) + { + Log_ErrorPrint("Failed to create rewind state."); + return false; + } + + mss->vram_texture.reset(host_texture); + return true; } bool SaveRewindState() @@ -1995,17 +2069,9 @@ bool SaveRewindState() s_rewind_states.pop_front(); MemorySaveState mss; - mss.state_stream = std::make_unique(nullptr, MAX_SAVE_STATE_SIZE); - - HostDisplayTexture* host_texture = nullptr; - StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Write, SAVE_STATE_VERSION); - if (!DoState(sw, &host_texture, false)) - { - Log_ErrorPrint("Failed to create rewind state."); + if (!SaveMemoryState(&mss)) return false; - } - mss.vram_texture.reset(host_texture); s_rewind_states.push_back(std::move(mss)); Log_DevPrintf("Saved rewind state (%u bytes, took %.4f ms)", s_rewind_states.back().state_stream->GetSize(), @@ -2027,17 +2093,8 @@ bool LoadRewindState(u32 skip_saves /*= 0*/, bool consume_state /*=true */) Common::Timer load_timer; - const MemorySaveState& mss = s_rewind_states.back(); - mss.state_stream->SeekAbsolute(0); - - StateWrapper sw(mss.state_stream.get(), StateWrapper::Mode::Read, SAVE_STATE_VERSION); - HostDisplayTexture* host_texture = mss.vram_texture.get(); - if (!DoState(sw, &host_texture, true)) - { - g_host_interface->ReportError("Failed to load rewind state from memory, resetting."); - Reset(); + if (!LoadMemoryState(s_rewind_states.back())) return false; - } if (consume_state) s_rewind_states.pop_back(); @@ -2082,6 +2139,68 @@ void DoRewind() } } +void SaveRunaheadState() +{ + if (s_runahead_states.size() >= s_runahead_frames) + s_runahead_states.pop_front(); + + MemorySaveState mss; + if (!SaveMemoryState(&mss)) + { + Log_ErrorPrint("Failed to save runahead state."); + return; + } + + s_runahead_states.push_back(std::move(mss)); +} + +void DoRunahead() +{ + Common::Timer timer; + Log_DevPrintf("runahead starting at frame %u", s_frame_number); + + if (s_runahead_replay_pending) + { + // we need to replay and catch up - load the state, + s_runahead_replay_pending = false; + if (!LoadMemoryState(s_runahead_states.front())) + return; + + // and throw away all the states, forcing us to catch up below + // TODO: can we leave one frame here and run, avoiding the extra save? + s_runahead_states.clear(); + Log_VerbosePrintf("Rewound to frame %u, took %.2f ms", s_frame_number, timer.GetTimeMilliseconds()); + } + + // run the frames with no audio + s32 frames_to_run = static_cast(s_runahead_frames) - static_cast(s_runahead_states.size()); + if (frames_to_run > 0) + { + Common::Timer timer2; + const s32 temp = frames_to_run; + + g_spu.SetAudioStream(s_runahead_audio_stream.get()); + + while (frames_to_run > 0) + { + DoRunFrame(); + SaveRunaheadState(); + frames_to_run--; + } + + g_spu.SetAudioStream(g_host_interface->GetAudioStream()); + + Log_VerbosePrintf("Running %d frames to catch up took %.2f ms", temp, timer2.GetTimeMilliseconds()); + } + else + { + // save this frame + SaveRunaheadState(); + } + + Log_DevPrintf("runahead ending at frame %u, took %.2f ms", s_frame_number, timer.GetTimeMilliseconds()); +} + void DoMemorySaveStates() { if (s_rewind_save_counter >= 0) @@ -2096,6 +2215,15 @@ void DoMemorySaveStates() s_rewind_save_counter--; } } + + if (s_runahead_frames > 0) + SaveRunaheadState(); +} + +void SetRunaheadReplayFlag() +{ + Log_DevPrintf("Runahead rewind pending..."); + s_runahead_replay_pending = true; } } // namespace System \ No newline at end of file diff --git a/src/core/system.h b/src/core/system.h index 4d85b575e..c8a36f295 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -234,8 +234,8 @@ void SetCheatList(std::unique_ptr cheats); void CalculateRewindMemoryUsage(u32 num_saves, u64* ram_usage, u64* vram_usage); void ClearMemorySaveStates(); void UpdateMemorySaveStateSettings(); -bool SaveRewindState(); bool LoadRewindState(u32 skip_saves = 0, bool consume_state = true); void SetRewinding(bool enabled); +void SetRunaheadReplayFlag(); } // namespace System diff --git a/src/duckstation-qt/emulationsettingswidget.cpp b/src/duckstation-qt/emulationsettingswidget.cpp index ea2d94caa..45db861a2 100644 --- a/src/duckstation-qt/emulationsettingswidget.cpp +++ b/src/duckstation-qt/emulationsettingswidget.cpp @@ -18,6 +18,8 @@ EmulationSettingsWidget::EmulationSettingsWidget(QtHostInterface* host_interface SettingWidgetBinder::BindWidgetToFloatSetting(m_host_interface, m_ui.rewindSaveFrequency, "Main", "RewindFrequency", 10.0f); SettingWidgetBinder::BindWidgetToIntSetting(m_host_interface, m_ui.rewindSaveSlots, "Main", "RewindSaveSlots", 10); + SettingWidgetBinder::BindWidgetToBoolSetting(m_host_interface, m_ui.runaheadEnable, "Main", "RunaheadEnable", false); + SettingWidgetBinder::BindWidgetToIntSetting(m_host_interface, m_ui.runaheadFrames, "Main", "RunaheadFrames", 1); QtUtils::FillComboBoxWithEmulationSpeeds(m_ui.emulationSpeed); const int emulation_speed_index = @@ -46,6 +48,7 @@ EmulationSettingsWidget::EmulationSettingsWidget(QtHostInterface* host_interface &EmulationSettingsWidget::updateRewindSummaryLabel); connect(m_ui.rewindSaveSlots, QOverload::of(&QSpinBox::valueChanged), this, &EmulationSettingsWidget::updateRewindSummaryLabel); + connect(m_ui.runaheadEnable, &QCheckBox::stateChanged, this, &EmulationSettingsWidget::updateRunaheadFields); dialog->registerWidgetHelp( m_ui.emulationSpeed, tr("Emulation Speed"), "100%", @@ -67,6 +70,7 @@ EmulationSettingsWidget::EmulationSettingsWidget(QtHostInterface* host_interface "should disable this option.")); updateRewindSummaryLabel(); + updateRunaheadFields(); } EmulationSettingsWidget::~EmulationSettingsWidget() = default; @@ -124,3 +128,8 @@ void EmulationSettingsWidget::updateRewindSummaryLabel() m_ui.rewindSaveSlots->setEnabled(false); } } + +void EmulationSettingsWidget::updateRunaheadFields() +{ + m_ui.runaheadFrames->setEnabled(m_ui.runaheadEnable->isChecked()); +} diff --git a/src/duckstation-qt/emulationsettingswidget.h b/src/duckstation-qt/emulationsettingswidget.h index 799b9e0a3..c89afed74 100644 --- a/src/duckstation-qt/emulationsettingswidget.h +++ b/src/duckstation-qt/emulationsettingswidget.h @@ -20,6 +20,7 @@ private Q_SLOTS: void onFastForwardSpeedIndexChanged(int index); void onTurboSpeedIndexChanged(int index); void updateRewindSummaryLabel(); + void updateRunaheadFields(); private: diff --git a/src/duckstation-qt/emulationsettingswidget.ui b/src/duckstation-qt/emulationsettingswidget.ui index 345e04817..3a109a4a2 100644 --- a/src/duckstation-qt/emulationsettingswidget.ui +++ b/src/duckstation-qt/emulationsettingswidget.ui @@ -138,6 +138,42 @@ + + + + Runahead + + + + + + Enable Runahead + + + + + + + Runahead Frames: + + + + + + + Frames + + + 1 + + + 20 + + + + + +