Skip to content

DolphinLibretro: defer Auto Load State until the EmuThread is live#430

Draft
dbartholomae wants to merge 1 commit intolibretro:masterfrom
dbartholomae:fix/auto-load-state-segfault
Draft

DolphinLibretro: defer Auto Load State until the EmuThread is live#430
dbartholomae wants to merge 1 commit intolibretro:masterfrom
dbartholomae:fix/auto-load-state-segfault

Conversation

@dbartholomae
Copy link
Copy Markdown

@dbartholomae dbartholomae commented Apr 19, 2026

Closes #322.

Summary

RetroArch's "Auto Load State" feature fires retro_unserialize between
retro_load_game and the first retro_run. In the libretro fork, the
EmuThread (and therefore HW::Init, CBoot::BootUp, the CPU thread, and
the GPU backend) is launched lazily inside retro_run, so at the moment
auto-load runs, the emulator's memory, PowerPC, FIFO, and video
subsystems don't exist yet. State::DoState then dereferences
uninitialised state and the core segfaults.

This PR buffers any savestate delivered before the EmuThread is alive
and replays it from retro_run immediately after the emulator finishes
booting, giving it the same execution environment a manual Load State
sees.

Reproducer

  1. RetroArch 1.16+ with the Dolphin libretro core.
  2. Enable Settings → Saving → Auto Save State and Auto Load State.
  3. Load any GameCube game, play for a moment, then close content.
    RetroArch writes <game>.state.auto.
  4. Reload the same content. The core crashes (segfault) immediately after
    game.state.auto is loaded.

Workaround confirmed: renaming the file to .state1
and 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_unserialize in Source/Core/DolphinLibretro/Main.cpp runs
State::DoState via Core::RunOnCPUThread:

Core::RunOnCPUThread(Core::System::GetInstance(), [&] {
  PointerWrap p((u8**)&data, size, PointerWrap::Mode::Read);
  State::DoState(Core::System::GetInstance(), p);
}, true);

In Source/Core/Core/Core.cpp, RunOnCPUThread falls back to inline
execution when the core state isn't Running:

if (!IsRunning(system)) {
   function();                 // runs on the caller's thread
   wait_for_completion = false;
}

Meanwhile the libretro build branch of Core::Init deliberately does
not start the EmuThread — it only stashes the boot params and
returns. EmuThread (and all HW initialisation) is launched from inside
retro_run, guarded by Libretro::g_emuthread_launched.

When RetroArch's auto-load path calls retro_unserialize before the
first retro_run:

  • s_state == Starting, so RunOnCPUThread takes the inline branch and
    executes State::DoState on the frontend thread.
  • HW::Init, Memory::Init, PowerPC::Init, CBoot::BootUp, the JIT
    and the video backend have not run.
  • State::DoState touches those subsystems and dereferences null/garbage
    pointers. Segfault.

Manual Load State from the Quick Menu works because by then many
retro_run iterations have occurred, g_emuthread_launched == true, and
RunOnCPUThread queues the lambda onto a live CPU thread.

Fix

Three small additions, 45 lines, zero deletions.

  • Common/Globals.{h,cpp}: add Libretro::g_pending_load_state, a
    std::vector<unsigned char> that holds a savestate delivered before
    the EmuThread is live.
  • Main.cpp / retro_unserialize: if !g_emuthread_launched, copy the
    buffer into g_pending_load_state and return true. RetroArch sees
    a successful unserialize; the actual state apply is deferred.
  • Main.cpp / retro_run: at the end of the existing one-shot
    EmuThread-launch block — after the IsRunningOrStarting wait and
    the RETRO_ENVIRONMENT_SET_MEMORY_MAPS call — drain
    g_pending_load_state through the same
    RunOnCPUThread + State::DoState path the manual load uses.
  • Main.cpp / retro_serialize and retro_serialize_size: symmetric
    guards that return false/0 when !g_emuthread_launched. Cheap
    protection 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

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:

  • Root cause traced end-to-end through retro_unserialize,
    Core::RunOnCPUThread, Core::Init, EmuThread, and
    Libretro::g_emuthread_launched.
  • All symbols used by the patch (Core::RunOnCPUThread,
    AsyncRequests::{GetInstance, SetPassthrough}, PointerWrap::Mode::Read,
    State::DoState, Libretro::g_emuthread_launched) cross-checked
    against the actual headers on master.
  • A reduced harness exercising the patched code paths compiles
    cleanly under g++ -std=c++20 -Wall -Wextra.
  • Building the full core locally.
  • Reproducing the crash on unpatched master and confirming the
    patched core loads the state correctly.
  • Dual-core mode both ways.
  • Manual Load State still works post-patch.

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_unserialize return false early when
!g_emuthread_launched. Four lines instead of 45. But that silently
breaks 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.

@cscd98
Copy link
Copy Markdown

cscd98 commented Apr 20, 2026

Could you please:

  1. see if you can reproduce the issue
  2. setup a dev environment to actually test any fixes
  3. compile dolphin and confirm that it does indeed fix the reported issue

@dbartholomae
Copy link
Copy Markdown
Author

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.

@cscd98
Copy link
Copy Markdown

cscd98 commented Apr 21, 2026

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?

@dbartholomae
Copy link
Copy Markdown
Author

Yes, of course:
retroarch__2026_04_21__13_43_25.log

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.

@cscd98
Copy link
Copy Markdown

cscd98 commented Apr 21, 2026

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.
@dbartholomae dbartholomae force-pushed the fix/auto-load-state-segfault branch from 2844b26 to 9b295c5 Compare April 21, 2026 21:58
@dbartholomae
Copy link
Copy Markdown
Author

Thanks for the quick review! I've removed the other comments. There's only one commit. Anything else? :)

@cscd98
Copy link
Copy Markdown

cscd98 commented Apr 22, 2026

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.

@dbartholomae
Copy link
Copy Markdown
Author

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.

@dbartholomae dbartholomae marked this pull request as draft April 22, 2026 08:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dolphin Core crash when auto save state exists with segmentation fault

2 participants