DolphinLibretro: defer Auto Load State until the EmuThread is live#430
DolphinLibretro: defer Auto Load State until the EmuThread is live#430dbartholomae wants to merge 1 commit intolibretro:masterfrom
Conversation
|
Could you please:
|
|
Thanks! I can reproduce the issue (that's what lead me to create the PR), but I'm not sure that I will be able to compile and test the fix unfortunately. |
|
Could you post a log with the issue? Also, are you using f1 then closing game, then selecting an new game? Have you chnaged any settings to do with savestates in retroarch? |
|
Yes, of course: They are not super helpful as they just stop right after it tries to load the auto-save, but they do show the point where things go wrong. |
|
Aha well actually the log is super helpful because it confirms the issue occurs exactly at that point and secondly it occurs even in the latest RetroArch version (the behavior may have changed in RetroArch). Can you remove the comments as they are over the top. Only this comment is required "// Savestate buffered by retro_unserialize() before the EmuThread was alive". Then could you squash those commits together. |
RetroArch's Auto Load State calls retro_unserialize between retro_load_game and the first retro_run. In the libretro fork the EmuThread is launched lazily from inside retro_run, so at auto-load time the core's HW, Memory, PowerPC, FIFO and video subsystems are uninitialised. RunOnCPUThread falls back to inline execution when the core state is not Running, so State::DoState runs on the frontend thread against null/garbage pointers and segfaults. Buffer any savestate delivered before the EmuThread is live in a new Libretro::g_pending_load_state, and drain it from retro_run inside the existing one-shot launch block, after the IsRunningOrStarting wait and the memory-map registration. At that point the execution environment matches a manual Load State. Also guard retro_serialize and retro_serialize_size against the same uninitialised state with an early return. Closes libretro#322.
2844b26 to
9b295c5
Compare
|
Thanks for the quick review! I've removed the other comments. There's only one commit. Anything else? :) |
|
I compiled the code and it just gives a blank screen loading Super Mario Sunshine. I also cannot reproduce the crash on Linux, it loads my auto save state without issue on SMS and Luigi's Mansion. |
|
Thanks! The issue happens on Windows, but I'll try to reproduce it also on a different OS. I'll convert this PR into a draft for now. |
Closes #322.
Summary
RetroArch's "Auto Load State" feature fires
retro_unserializebetweenretro_load_gameand the firstretro_run. In the libretro fork, theEmuThread (and therefore
HW::Init,CBoot::BootUp, the CPU thread, andthe GPU backend) is launched lazily inside
retro_run, so at the momentauto-load runs, the emulator's memory, PowerPC, FIFO, and video
subsystems don't exist yet.
State::DoStatethen dereferencesuninitialised state and the core segfaults.
This PR buffers any savestate delivered before the EmuThread is alive
and replays it from
retro_runimmediately after the emulator finishesbooting, giving it the same execution environment a manual Load State
sees.
Reproducer
RetroArch writes
<game>.state.auto.game.state.autois loaded.Workaround confirmed: renaming the file to
.state1and triggering Load State manually after boot succeeds, because by then
the core is fully initialised. The bytes are identical; only the timing
differs.
My assumption about the root cause
retro_unserializeinSource/Core/DolphinLibretro/Main.cpprunsState::DoStateviaCore::RunOnCPUThread:In
Source/Core/Core/Core.cpp,RunOnCPUThreadfalls back to inlineexecution when the core state isn't
Running:Meanwhile the libretro build branch of
Core::Initdeliberately doesnot start the EmuThread — it only stashes the boot params and
returns.
EmuThread(and all HW initialisation) is launched from insideretro_run, guarded byLibretro::g_emuthread_launched.When RetroArch's auto-load path calls
retro_unserializebefore thefirst
retro_run:s_state == Starting, soRunOnCPUThreadtakes the inline branch andexecutes
State::DoStateon the frontend thread.HW::Init,Memory::Init,PowerPC::Init,CBoot::BootUp, the JITand the video backend have not run.
State::DoStatetouches those subsystems and dereferences null/garbagepointers. Segfault.
Manual Load State from the Quick Menu works because by then many
retro_runiterations have occurred,g_emuthread_launched == true, andRunOnCPUThreadqueues the lambda onto a live CPU thread.Fix
Three small additions, 45 lines, zero deletions.
Common/Globals.{h,cpp}: addLibretro::g_pending_load_state, astd::vector<unsigned char>that holds a savestate delivered beforethe EmuThread is live.
Main.cpp / retro_unserialize: if!g_emuthread_launched, copy thebuffer into
g_pending_load_stateand returntrue. RetroArch seesa successful unserialize; the actual state apply is deferred.
Main.cpp / retro_run: at the end of the existing one-shotEmuThread-launch block — after the
IsRunningOrStartingwait andthe
RETRO_ENVIRONMENT_SET_MEMORY_MAPScall — draing_pending_load_statethrough the sameRunOnCPUThread+State::DoStatepath the manual load uses.Main.cpp / retro_serializeandretro_serialize_size: symmetricguards that return
false/0when!g_emuthread_launched. Cheapprotection against the same crash on the rarer save-before-boot path.
The drain runs exactly once per game session, in the same code block
that already initialises the GPU backend and memory maps, so it does not
add per-frame work.
What this does not change
AsyncRequests::SetPassthroughbracket introducedby Fix save/load state when in dual core mode #385 is preserved on the drain path.
DeclareAsCPUThreadwork from Fix macOS save state crash in dual-core mode #428 is orthogonal andcomposes cleanly — the drain path benefits from it if merged.
Testing
Full disclosure: this PR has not been behaviourally tested on hardware
yet. It was derived from source analysis of the current
master.What was verified:
retro_unserialize,Core::RunOnCPUThread,Core::Init,EmuThread, andLibretro::g_emuthread_launched.Core::RunOnCPUThread,AsyncRequests::{GetInstance, SetPassthrough},PointerWrap::Mode::Read,State::DoState,Libretro::g_emuthread_launched) cross-checkedagainst the actual headers on master.
cleanly under
g++ -std=c++20 -Wall -Wextra.masterand confirming thepatched core loads the state correctly.
I'd appreciate a reviewer with a working build environment running through
the reproducer above. Happy to iterate on wording or approach.
Alternatives considered
Simpler option: have
retro_unserializereturnfalseearly when!g_emuthread_launched. Four lines instead of 45. But that silentlybreaks Auto Load State rather than fixing it — users would see "failed
to load state" on every auto-boot and have to manually load after each
launch. The buffered-replay approach preserves the feature.