WIP: fix(saves): journaled save commits: a crash mid-save keeps the last save #2453
Draft
hckrman101 wants to merge 6 commits from
hckrman101/fix/journaled-saves into main
pull from: hckrman101/fix/journaled-saves
merge into: MagicBeanDev:main
MagicBeanDev:main
MagicBeanDev:github/pr/2893
MagicBeanDev:github/pr/2890
MagicBeanDev:github/pr/2887
MagicBeanDev:github/pr/2878
MagicBeanDev:github/pr/2875
MagicBeanDev:github/pr/2874
MagicBeanDev:github/pr/2873
MagicBeanDev:github/pr/2871
MagicBeanDev:github/pr/2756
MagicBeanDev:github/pr/2897
MagicBeanDev:github/pr/2896
MagicBeanDev:github/pr/2718
MagicBeanDev:github/pr/2842
MagicBeanDev:github/pr/2691
MagicBeanDev:github/pr/2895
MagicBeanDev:github/pr/2743
MagicBeanDev:github/pr/2765
MagicBeanDev:github/pr/2708
MagicBeanDev:github/pr/2732
MagicBeanDev:github/pr/2755
MagicBeanDev:github/pr/2817
MagicBeanDev:github/pr/2876
MagicBeanDev:release-please--branches--main
MagicBeanDev:release-please--branches--main--release-notes
MagicBeanDev:github/pr/2778
MagicBeanDev:github/pr/2888
MagicBeanDev:github/pr/2866
MagicBeanDev:github/pr/2877
MagicBeanDev:github/pr/2800
MagicBeanDev:github/pr/2829
MagicBeanDev:github/pr/2850
MagicBeanDev:github/pr/2889
MagicBeanDev:github/pr/2882
MagicBeanDev:github/pr/2860
MagicBeanDev:github/pr/2849
MagicBeanDev:github/pr/2886
MagicBeanDev:github/pr/2794
MagicBeanDev:github/pr/2840
MagicBeanDev:github/pr/2830
MagicBeanDev:github/pr/2834
MagicBeanDev:github/pr/2719
MagicBeanDev:github/pr/2742
MagicBeanDev:github/pr/2741
MagicBeanDev:github/pr/2820
MagicBeanDev:github/pr/2771
MagicBeanDev:github/pr/2859
MagicBeanDev:github/pr/2858
MagicBeanDev:github/pr/2823
MagicBeanDev:github/pr/2757
MagicBeanDev:github/pr/2769
MagicBeanDev:github/pr/2844
MagicBeanDev:github/pr/2843
MagicBeanDev:github/pr/2673
MagicBeanDev:github/pr/2855
MagicBeanDev:github/pr/2739
MagicBeanDev:github/pr/2807
MagicBeanDev:github/pr/2851
MagicBeanDev:github/pr/2789
MagicBeanDev:github/pr/2814
MagicBeanDev:github/pr/2818
MagicBeanDev:github/pr/2774
MagicBeanDev:github/pr/2772
MagicBeanDev:github/pr/2750
MagicBeanDev:github/pr/2786
MagicBeanDev:github/pr/2785
MagicBeanDev:github/pr/2782
MagicBeanDev:github/pr/2779
MagicBeanDev:github/pr/2775
MagicBeanDev:github/pr/2706
MagicBeanDev:github/pr/2766
MagicBeanDev:github/pr/2747
MagicBeanDev:github/pr/2759
MagicBeanDev:github/pr/2745
MagicBeanDev:github/pr/2740
MagicBeanDev:github/pr/2733
MagicBeanDev:github/pr/2725
MagicBeanDev:github/pr/2727
MagicBeanDev:github/pr/2728
MagicBeanDev:github/pr/2729
MagicBeanDev:github/pr/2721
MagicBeanDev:github/pr/2722
MagicBeanDev:github/pr/2715
MagicBeanDev:github/pr/2714
MagicBeanDev:github/pr/2707
MagicBeanDev:github/pr/2703
MagicBeanDev:github/pr/2705
MagicBeanDev:github/pr/2697
MagicBeanDev:github/pr/2679
MagicBeanDev:github/pr/2676
MagicBeanDev:github/pr/2669
MagicBeanDev:docs/what-hookshot-connects-to
MagicBeanDev:fix/hdr-encode-test
MagicBeanDev:fix/main-tests
MagicBeanDev:fix/play-cache-files
MagicBeanDev:perf/game-barrier-order
MagicBeanDev:feat/heap-lazy
MagicBeanDev:feat/texture-detail
MagicBeanDev:perf/astc-opaque-bc1
MagicBeanDev:fix/fs-dir-lock
MagicBeanDev:fix/framegen-native-kept
MagicBeanDev:fix/update-no-downgrade
MagicBeanDev:port/k10-gcn-f16-mirrors
MagicBeanDev:fix/framegen-mult-cap
MagicBeanDev:chore/release-as-2.1.1
MagicBeanDev:perf/pcache-load-switch
MagicBeanDev:fix/prepare-progress-log
MagicBeanDev:fix/update-closes-stale-game
MagicBeanDev:fix/hdr-scaled-sampled
MagicBeanDev:perf/first-start-preset
MagicBeanDev:perf/zero-upload
MagicBeanDev:fix/vcore-escape-2s
MagicBeanDev:test/fake-gh-atomic
MagicBeanDev:perf/batch-idle-flush
MagicBeanDev:test/finish-stub-a
MagicBeanDev:chore/record-2.1.0
MagicBeanDev:fix/main-red-tests
MagicBeanDev:fix/test-display-startup-driver-cache
MagicBeanDev:feat/mesh-shader-mode
MagicBeanDev:fix/menu-probes-at-startup
MagicBeanDev:fix/fps-counter-classic
MagicBeanDev:fix/test-log-batch-flake
MagicBeanDev:fix/narrow-screen-hud
MagicBeanDev:ci/prereleases
MagicBeanDev:release/v2.0.15
MagicBeanDev:hotfix/v2.0.15
MagicBeanDev:fix/present-ahead-replay
MagicBeanDev:release/v2.0.13
MagicBeanDev:fix/bracket-paths-2013
MagicBeanDev:fix/hdr-overlays-2013
MagicBeanDev:fix/fps-counter-scale-2013
MagicBeanDev:fix/stick-seen-at-rest
MagicBeanDev:hotfix/menu-fits-screen
MagicBeanDev:fix/f1-always-opens-menu
MagicBeanDev:fix/ini-keep-lines
MagicBeanDev:claude/cranky-pike-7cf247
MagicBeanDev:perf/texture-answer-flat
MagicBeanDev:fix/stamp-test-modules
MagicBeanDev:fix/appimage-fonts
MagicBeanDev:fix/display-awake
MagicBeanDev:release/v2.0.12
MagicBeanDev:fix/discord-windows
MagicBeanDev:rc/v2.0.12-tests
MagicBeanDev:fix/dlss-keep-feature
MagicBeanDev:rc/v2.0.12
MagicBeanDev:button-icons
No reviewers
Labels
Clear labels
accessibility
Barrier affecting people with disabilities
autorelease: pending
autorelease: tagged
bug
Something isn't working
dependencies
Pull requests that update a dependency file
documentation
Improvements or additions to documentation
duplicate
This issue or pull request already exists
enhancement
New feature or request
github_actions
Pull requests that update GitHub Actions code
good first issue
Good for newcomers
help wanted
Extra attention is needed
invalid
This doesn't seem right
question
Further information is requested
testing wanted
wontfix
This will not be worked on
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
Milestone
Clear milestone
No items
No milestone
Assignees
Clear assignees
No assignees
1 participant
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2453
Loading…
Reference in a new issue
No description provided.
Delete branch "hckrman101/fix/journaled-saves"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A crash, a kill or a power cut in the middle of a save can leave a save slot half written. Hookshot's
nn::fs::Commitdid nothing, so the game's writes went straight onto the save files.
What changes
The save mount's root is now a working copy,
save-tx/<user id>/work. A Commit only renames folders, so it needs nofree space:
ready-<n>. This is the commit point.save/<user id>is renamedold, andready-<n>is renamedsave/<user id>.oldgoes tosave-backup/<user id>/<UTC time>. The newest 3 are kept.Then the game's next working copy is made. That is the only step that needs space. If the disk is too full for it,
the rest of the run writes the save in place, as Hookshot did before, and the log says so. The next start uses the
journal again.
At the next start, a commit that was cut short is finished if it passed the commit point and rolled back if not. A
Commit that changed nothing writes nothing.
When things go wrong:
are never committed or deleted either: they are renamed to
save-tx/<user id>/uncommitted-<time>, and the newest 3are kept. A working copy that cannot be renamed is not mounted again; that run writes in place.
save stays as it was and the working copy stays mounted. The next Commit or the last Unmount tries again. The game is
told the save worked either way; whether it would show its own "save failed" dialog was not tried.
save-tx/<user id>/lock, flock orLockFileEx). A second Hookshot gets no journal: it writes in place, as before, and logs why.
the commit waits in
save-txand is the save the game loads. It is put in place only over the save it was made over,checked by a fingerprint of names, sizes and times. If something replaced the save since, such as
HOS_SAVE_JOURNAL=0or a copied-in save, the newer save stays. The waiting commit goes tosave-backup/<user id>/unapplied-<time>, or, if it cannot be moved, stays insave-txwithout being loaded.drive than
save-txandsave-backup. The save is written in place. On Windows only symbolic links and junctionscount as links; OneDrive placeholders are copied like any file.
empty save. A run without the journal stops too.
Other details:
save/<user id>keeps the emulators' layout, so existing saves load as they are.docs/saves.mddescribes
save-txandsave-backup.save-txis the save that gets written.MountSaveDataReadOnlymounts the newest save.scripts/oot_saves.pyrefuses to copy while a commit waits insave-tx.savescommand lists the working copy, which is what the game has now.\\?\for long paths. Save files areopened with FILE_SHARE_DELETE, so the game's own read handles never block the renames.
HOS_SAVE_JOURNAL=0writes the save in place as before.HOS_SAVE_CRASH_AT=<n>ends the process at acommit step, for testing.
Credit
host/save_journal.cis adapted from valentderah'stotk-pc-recomp
runtime/fs_host.c, which isunder the MIT licence (Copyright (c) 2026 Valiantsin Dzerakh). That file is itself adapted from MacNativeGames
games/totk/runtime/fs_host.c(MIT). Both are named at the top of the file, with the permission notice, and in thereadme's licence list and
docs/design.md's reuse table. Hookshot's changes:scripts/oot_saves.pyand players write save files in place);Verified
Game-free test.
tests/test_save_journal.pybuildshost/save_journal.cinto a small driver at-O2 -Werror.It ends the process at each of the commit's steps. After each one, the next start has the old save or the new one,
whole, and the new one exactly from the commit point on. It also checks:
Faults (Linux). A second driver makes
write()fail with ENOSPC and makesrename()fail, or end the process,at a chosen call. It covers:
Each ends with the old save or the new one, whole, never mixed, torn or silently rolled back.
Copy script test.
tests/test_oot_saves.py.Full test suite.
python -I -B tests/run_all.pypasses, apart from 3 tests this PC cannot run (no zstandard ortkinter).
Compiles.
host/nn_fs.candhost/save_journal.ccompile without warnings for Linux (gcc, clang) and Windows(llvm-mingw).
In the game. On Linux (RTX 3090), headless, on a copy of a test save, with this branch merged with main:
next working copy.
save/<user id>held the new slot afterwards, and the replaced saves were insave-backup(the newest 3).the saved slot, and its Commit at load wrote nothing.
Not tested
game.
open by another program, long paths, or a OneDrive folder. The Windows CI runs the game-free test.
thread, and making the next working copy is a copy of the save (about 1 MB).
Nearby: #2317 moves saves to a per-user data folder. This PR keeps
save-txandsave-backupbesidesaveunderhos_user_dir, so it should follow that move unchanged.The journal is on by default (
HOS_SAVE_JOURNAL=0turns it off), but no in-game save with changes, reload, or kill during a save has been run yet. Uncommitted changes are now dropped, where beforeCommitdid nothing, so any save path that never callsCommitwould silently lose progress. The game is also told a save worked when it failed, and the review fixes haven't been built. The crash-recovery design is solid in the game-free test. One real save/reload/kill run before marking ready would settle it.