Repository navigation
fix(app): follow a disc swap in the content identity; keep the .srm on the opened disc (#1010) - #1011
Conversation
d4c185a to
c7e7dfc
Compare
|
No automated review was posted on this PR â the plan is out of quota for the current billing cycle. This is a billing limit, not a repo or credential problem â it resumes on its own when the quota refreshes. This PR has not been machine-reviewed. Posted by the review workflow so that an unreviewed PR is not mistaken for a clean one; it updates in place rather than repeating on every push. |
Regression:
|
| ROM | Status | Details | Diff |
|---|---|---|---|
| jagniccc | â PASS | 0 pixels differ | - |
| yarc | â PASS | 0 pixels differ | - |
| jagniccc (determinism) | â PASS | identical across runs | - |
| yarc (determinism) | â PASS | identical across runs | - |
| jagniccc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| yarc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| jagniccc (save state) | â PASS | round-trip matches | - |
| yarc (save state) | â PASS | round-trip matches | - |
| jagniccc (rewind) | â PASS | rewind matches | - |
| yarc (rewind) | â PASS | rewind matches | - |
Platform: Linux aarch64
Updated by CI at 2026-10-17T23:45:26.296Z
Regression:
|
| ROM | Status | Details | Diff |
|---|---|---|---|
| jagniccc | â PASS | 0 pixels differ | - |
| yarc | â PASS | 0 pixels differ | - |
| jagniccc (determinism) | â PASS | identical across runs | - |
| yarc (determinism) | â PASS | identical across runs | - |
| jagniccc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| yarc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| jagniccc (save state) | â PASS | round-trip matches | - |
| yarc (save state) | â PASS | round-trip matches | - |
| jagniccc (rewind) | â PASS | rewind matches | - |
| yarc (rewind) | â PASS | rewind matches | - |
Platform: Linux x86_64
Updated by CI at 2026-10-18T00:53:17.747Z
Regression:
|
| ROM | Status | Details | Diff |
|---|---|---|---|
| jagniccc | â PASS | 0 pixels differ | - |
| yarc | â PASS | 0 pixels differ | - |
| jagniccc (determinism) | â PASS | identical across runs | - |
| yarc (determinism) | â PASS | identical across runs | - |
| jagniccc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| yarc (frameskip) | â PASS | skip=0 matches skip=3 | - |
| jagniccc (save state) | â PASS | round-trip matches | - |
| yarc (save state) | â PASS | round-trip matches | - |
| jagniccc (rewind) | â PASS | rewind matches | - |
| yarc (rewind) | â PASS | rewind matches | - |
Platform: Darwin arm64
Updated by CI at 2026-10-18T00:44:12.147Z
âĶn the opened disc After File > Disc / Add disc image... booted another disc, contentPath()/contentName() and everything keyed on them still named the first disc. CoreHost now keeps two identities: - contentPath()/contentName(): the disc in the tray. It moves only after an insert that worked and left the tray closed (followMountedDisc). The core moves its image index before it tries the insert and keeps it when the insert fails, so the index alone is not enough. Off the owner thread these answer from the snapshot, since discSetEject/discSetIndex may run as queued commands. - loadedPath()/loadedName(): what was opened. The .srm (and the second-copy fallback) is named after it, and Restart reopens it. The core keeps SAVE_RAM (the EEPROM pair and the Memory Track) across an insert and never reloads it, so re-keying the file would write disc A's Memory Track into B's .srm. App::followDiscSwap (main loop, addDisc, the headless check) catches up on a swap: - save-state slots and thumbnails follow the tray (statePath reads contentName): the core refuses a state taken on a different disc (savestate v14 disc identity); - achievements use rc_client_begin_identify_and_change_media, so the session and hardcore survive a disc of the same game; an unidentified first disc gets a fresh identify instead; - cheats re-key (the core re-reads the per-title row on insert, titledb_probe_disc); - a queued state load and the resume offer for the previous disc are dropped. The window title and Discord read contentPath every frame, so they follow without changes. Recents, the library's recently played and the per-game control overrides stay with the opened disc: reopening must start from its .srm. --disc-swap now checks after each step that the content and the state slot name the disc in the tray while loadedPath and saveRamPath stay on the opened disc, and saves a state on the second disc. The three disc-swap tests get their own --data-dir so that state lands in the build tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit b96ce11)
âĶapshot Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit c7e7dfc)
c7e7dfc to
1108a36
Compare
âĶ1008) Neither the core nor the app read .m3u. The app now does (app/src/m3u.h, RetroArch's format: one image per line, relative to the playlist or absolute; '#' lines, a BOM, CRLF and a "|label" suffix are ignored). - CoreHost::loadContent: an .m3u boots its first disc and appends every disc to the core's disc list (owner thread, inside the load). Each entry must exist and be a cue/cdi/chd/iso, or the load fails with the entry named: a missing disc would still register and only fail when picked. - Identity, on #1011's model: the playlist is what was opened (loadedPath/loadedName: one .srm per set, Restart, recents, the library's recently played, per-game controls); the disc in the tray is the content (contentPath/contentName: the window title, save-state slots, achievements, cheats) -- the first disc at boot, then whatever a swap boots. finishLoad takes the opened path for this. - File > Open filter takes .m3u; the headless load check, tv_script's load and the library's "load this state" compare against loadedPath. - Library: isDiscExt includes m3u; the playlist claims the discs it lists (they leave the grid) and is identified by its first disc's track-1 CRC. - Headless --expect-discs N: the disc list the menus read holds N images; at boot the content is disc 1 and the .srm / recents are the playlist's; after Open tray, File > Disc > last, Close tray the content and state slot are the last disc's while the .srm stays the playlist's. smoke_m3u_playlist (inline) and smoke_m3u_playlist_threaded (--emu-thread on, emu_thread label) load tests/fixtures/synth_jagcd_set.m3u (../-relative entries to test/roms/synth_jagcd{,_multi}.chd). - library_unit: parser cases and the scan dedupe / hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
âĶ1008) Neither the core nor the app read .m3u. The app now does (app/src/m3u.h, RetroArch's format: one image per line, relative to the playlist or absolute; '#' lines, a BOM, CRLF and a "|label" suffix are ignored). - CoreHost::loadContent: an .m3u boots its first disc and appends every disc to the core's disc list (owner thread, inside the load). Each entry must exist and be a cue/cdi/chd/iso, or the load fails with the entry named: a missing disc would still register and only fail when picked. - Identity, on #1011's model: the playlist is what was opened (loadedPath/loadedName: one .srm per set, Restart, recents, the library's recently played, per-game controls); the disc in the tray is the content (contentPath/contentName: the window title, save-state slots, achievements, cheats) -- the first disc at boot, then whatever a swap boots. finishLoad takes the opened path for this. - File > Open filter takes .m3u; the headless load check, tv_script's load and the library's "load this state" compare against loadedPath. - Library: isDiscExt includes m3u; the playlist claims the discs it lists (they leave the grid) and is identified by its first disc's track-1 CRC. - Headless --expect-discs N: the disc list the menus read holds N images; at boot the content is disc 1 and the .srm / recents are the playlist's; after Open tray, File > Disc > last, Close tray the content and state slot are the last disc's while the .srm stays the playlist's. smoke_m3u_playlist (inline) and smoke_m3u_playlist_threaded (--emu-thread on, emu_thread label) load tests/fixtures/synth_jagcd_set.m3u (../-relative entries to test/roms/synth_jagcd{,_multi}.chd). - library_unit: parser cases and the scan dedupe / hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
âĶ1008) (#1012) Neither the core nor the app read .m3u. The app now does (app/src/m3u.h, RetroArch's format: one image per line, relative to the playlist or absolute; '#' lines, a BOM, CRLF and a "|label" suffix are ignored). - CoreHost::loadContent: an .m3u boots its first disc and appends every disc to the core's disc list (owner thread, inside the load). Each entry must exist and be a cue/cdi/chd/iso, or the load fails with the entry named: a missing disc would still register and only fail when picked. - Identity, on #1011's model: the playlist is what was opened (loadedPath/loadedName: one .srm per set, Restart, recents, the library's recently played, per-game controls); the disc in the tray is the content (contentPath/contentName: the window title, save-state slots, achievements, cheats) -- the first disc at boot, then whatever a swap boots. finishLoad takes the opened path for this. - File > Open filter takes .m3u; the headless load check, tv_script's load and the library's "load this state" compare against loadedPath. - Library: isDiscExt includes m3u; the playlist claims the discs it lists (they leave the grid) and is identified by its first disc's track-1 CRC. - Headless --expect-discs N: the disc list the menus read holds N images; at boot the content is disc 1 and the .srm / recents are the playlist's; after Open tray, File > Disc > last, Close tray the content and state slot are the last disc's while the .srm stays the playlist's. smoke_m3u_playlist (inline) and smoke_m3u_playlist_threaded (--emu-thread on, emu_thread label) load tests/fixtures/synth_jagcd_set.m3u (../-relative entries to test/roms/synth_jagcd{,_multi}.chd). - library_unit: parser cases and the scan dedupe / hash. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
App packagesBuilt from
Updated by CI at 2026-10-18T03:54:36.836Z |
Closes #1010
Problem
After a disc swap (File > Disc, or Add disc image...), the core reboots on the new disc. The app still named the first disc as the content, so:
What follows what
contentPath())loadedPath()).srmfile, and the fallback copy a second app instance reads.rc_client_begin_identify_and_change_media. A disc of the same game keeps the session and hardcore. rcheevos decides what an unknown disc means.reloadContent).titledb_probe_disc).contentPathevery frame and needed no change.input.setContent). These are picked once per open; left unchanged to keep scope.Why the
.srmstays. The core's save RAM is the EEPROM pair plus the Memory Track. It survives an insert and is never reloaded (disk_set_eject_stateonly re-installs the NVM BIOS module). So the buffer still holds what was read from A's.srm, plus anything B wrote.B.srm.Keeping the save with the opened content is what RetroArch does. It is also what an
.m3usession would do.Recents follow the same reasoning. Reopening from recents is a fresh load, and it reads the
.srmof the entry it opens. Disc A's entry is the one that brings the session's saves back.Change
CoreHostcontentPath()/contentName()now name the disc in the tray.followMountedDiscmoves them only when an insert worked and left the tray closed.disk_indexbefore it tries the insert, and leaves it moved when the insert fails (it puts the previous disc back and leaves the tray open).EmuSnapshot, becausediscSetEject/discSetIndexmay run as queued commands.loadedPath()/loadedName()hold what was opened;saveRamPath()uses them. NewdiscSwaps()counts swaps.App::followDiscSwap()addDisc, and in the headless check, so it catches every disc menu: ImGui, native and TV.call(), it hands the swap to achievements and re-keys cheats, then drops the cached state thumbnails.Achievements â
onDiscChanged().chd,.cdi, or signed out), it starts a fresh identify.applyHardcore()would have turned it straight back on.Tests
--disc-swap(discSwapHeadlessCheck) now checks, after each step and as the UI reads it (the pinned snapshot when threaded):After the add:
contentPath,contentNameandstatePath(slot)name disc B.loadedPathandsaveRamPathare unchanged. A state is saved, andsynth_jagcd_multi.slot0.stateexists.With the tray open: the content is still disc B.
Back on disc A: the content is A again, and exactly one swap was counted.
New line: it prints
DISC content synth_jagcd -> synth_jagcd_multi -> synth_jagcd, save RAM synth_jagcd.Data dirs:
smoke_disc_swap,smoke_disc_swap_threadedandsmoke_disc_swap_bios_threadedeach get their own--data-dirin the build tree, so the saved state never reaches a user folder.Negative check: with
followMountedDiscdisabled,smoke_disc_swapfails withafter the add, the content is '.../synth_jagcd.chd'andthe state slot is .../synth_jagcd.slot0.state.Full app suite: 117/117 pass on macOS arm64 after the restack, including all 21
emu_threadtests.TSan, local (macOS): the 11
emu_threadtests that don't run the app binary pass. That includescore_host_thread, which queuesdiscSetEject/discSetIndex(the snapshot write path). macOS refuses to load the sanitizer into the app binary ("Sanitizer load violates platform policy"), so the threaded disc-swap tests get TSan only from the Linuxtsanjob.CI from the base: before fix(app): register the loaded CD with disk control so the disc menu works (#1003)Â #1006 was rebased, its
ThreadSanitizerjob was red on a lock-order inversion insmoke_yarc_threaded, a cart run with no disc. Its macOS job also failedemu_thread_unitonce. Both come from feat(app): emulation on its own thread, phase 1 (#967)Â #1000, and its 4d7668f targets them. This branch now sits on fix(app): register the loaded CD with disk control so the disc menu works (#1003)Â #1006 at 08b340f, on top of feat(app): emulation on its own thread, phase 1 (#967)Â #1000's tip c7de134.Not covered:
.chd, which rcheevos cannot hash.Not in this PR
B.srm. This follows from the table above. A real fix needs a multi-disc set identity, such as.m3u.ðĪ Generated with Claude Code