fix(menu): longshot_options.ini is replaced whole or not at all #550
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!550
Loading…
Reference in a new issue
No description provided.
Delete branch "zeldaootr/Longshot:menu-options-atomic-save"
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?
Type: fix · Risk: low · Tested on: Linux, no GPU (game-free checks); Steam Deck OLED (a tester's run, before the rebase) · Needs: Windows, any GPU
Why
If the disk was full, or the game was closed while saving the menu's settings,
longshot_options.iniwas left cut short, and the next start took the defaults for every setting past the cut (or, with an empty file, chose Auto over the player's preset). Since #729 the file can live in the launcher's user folder (--user,%APPDATA%\Longshoton Windows), shared by every build the launcher starts, so one cut-short save there loses the settings of all of them.How to reproduce
Seen on: read off the code, and reproduced game-free on Linux (
tests/test_menu_save_atomic.pyat the test-only commit)Settings and switches: defaults
Steps: fill the disk that holds
longshot_options.ini(an SD card on a Steam Deck), then change a row in the Insert menu; or close the game while it savesExpected: the file holds either the old settings or the new ones, whole
Actual: on
mainthe file is written in place and left cut where the disk ran out: in the test, 200 bytes of 1006, everything past the cut gone at the next startWhat changed
The menu's save writes the settings to a file beside the real one and moves it over once it is whole, so a failed save leaves the old file as it was. Where that can't be done it saves in place as before, and it says once a run, with the file's whole path, what went wrong. It applies to whichever file
options_path()names: the user folder's since #729, else the game folder's.save()ingpu/vk/vk_menu.cppwrites the lines to<file>.newbeside the file (beside its target when it is a link, so a synced file stays linked), with the old file's mode, checks the write and the close, and only then moves it over the old file (rename;MoveFileExAwithMOVEFILE_REPLACE_EXISTINGon Windows). A write that fails leaves the old file as it was. Where nothing can be made beside it or moved over it (a folder that is not writable, the file held open by another program) it writes in place as before; a read-only file is not written. Each failure is logged once a run, with the error and the whole path (menu: <folder>/longshot_options.ini.new could not be written whole (File too large): <folder>/longshot_options.ini is kept as it was), so a report shows which folder it was. A run with no settings file (options_path()empty: the host found no place for one and logged so at the start) saves nothing, as onmain, and makes no.newin the folder the game runs in.tests/test_menu_save_atomic.pyruns the realsave()against a file that runs out of room part way, a linked file, an unwritable folder or file, a file with its own mode, a file in a user folder away from the folder the game runs in (nothing made in the game's folder, the log line naming the whole path), and no settings file at all.tests/test_mesh_mode_switch.py,tests/test_menu_env_overrides.pyandtests/test_upscaler_round_trip.py, which compilesave()from the source, get its headers and alogfstand-in.Measurements
Verdict: passes: the check fails on
main(a save that runs out of room leaves the file cut short) and passes here, with g++ and clang, twice each, alternating. No frame rate changes; what the Deck run checked is the behaviour (Testing).Hardware: Linux (Ubuntu 24.04, kernel 6.8), x86-64, no GPU or game used; g++ 13.3.0, clang 23.1.5 (llvm-mingw's,
--target=x86_64-linux-gnu)Scene: no game: the game-free check
Method:
python3 -I -B tests/test_menu_save_atomic.py --cxx g++, and with clang, at the test-only commit540aaf3f(mainaa95d35fplus the test) and at2248676d, twice each, alternatingHow to reproduce: check out each commit and run the command above
aa95d35f(with the test,540aaf3f)2248676dlongshot_options.iniafter a save that did not fit (both runs, both compilers the same)Flags and settings
None: no switch, menu row or ini key is added or changed; the file's contents are the same, only how it is written.
Testing
Tested:
mainaa95d35f(which moved the file into the user folder, #729): the check fails at the test-only commit540aaf3fand passes at2248676d, as above. The 72 checks that read the files this PR changes (vk_menu.cppand the three edited checks) ran with g++ and with clang: 69 pass, and the three that fail (test_options_budget_once,test_pipeline_tour,test_walk_key) fail the same way on cleanmainaa95d35f.vk_menu.cpppasses a syntax-only compile for Windows (llvm-mingw 20261104,-Wall -Wextra: the same 6 warnings asmain, none new) and for Linux with clang (g++ stops at the same threerender/rx_nvn.herrors as onmain).python -I -B tests/run_all.py, Linux, g++,LONGSHOT_VULKAN_HEADERSset) ran once onmainaa95d35fwith this PR and the other open fixes from the same sweep merged locally (ac6a1e97, not pushed): 550 passed, 5 failed, 51 skipped. All five fail on cleanmainaa95d35ftoo:test_make_game_cache,test_crash_dialog(its repair, #626, needs one more line since #729),test_options_budget_once,test_mods_size_tableandtest_walk_key.5089cac6(in a build of main7ff5256ewith #550 to #558 merged) on a Steam Deck OLED, SteamOS 3.9.5, ext4 on the internal drive, started through Steam in Game Mode; the comment of 2026-11-14 22:32 on this PR has the details. Six rows changed in play through the menu's own setter were all in the file after a restart, the file kept its mode, and nolongshot_options.ini.newwas left in play or after the close. With the options file a symlink, it stayed a link and its target got the change. With the game folder read-only and the file writable, both changes were saved in place and the log said so once. No crash or error in 4 starts. (Rumble strength read back 50 after a set of 60: the control bridge set a value the menu's steps cannot make, and the start snaps it; the save kept what was set.) That run still holds for the Deck after the rebase: the Linux launcher passes no--useryet (#729), so on the Deckoptions_path()is still the game folder'slongshot_options.ini, the file the tester saved, and the save's steps are unchanged. Only the log line's wording changed: it now names the whole path (menu: <game folder>/longshot_options.ini.new cannot be made beside it (Permission denied): <game folder>/longshot_options.ini is written in place).Not tested: the rebased head
2248676din the game (no build here: the build base was in use). A real full disk or power loss in the game, and an exFAT SD card. The user folder in the game: on Windows%APPDATA%\Longshot(a launcher start); on Linux only withHOS_OPTIONS_FILEor a--usergiven by hand until the launcher passes one. Windows: the rebased head was compiled but not linked or run; the read-only check, the link (reparse point) written in place and theMoveFileExApath were not run.Must be tested:
%APPDATA%\Longshot): change a few Insert menu rows, close and start again, expecting the rows kept and nolongshot_options.ini.newleft in%APPDATA%\Longshotor the game folder (by hand: the folder andlogs\longshot_log.txt, which should have nomenu:line about the file)Risks
What could break: saving the menu's settings. There is no
fsyncbefore the rename, by design (a save comes with every arrow press in the menu): on a file system that can reorder the rename before the data, XFS among them, a power loss right after a save can leave a zero-lengthlongshot_options.ini, and the next start chooses Auto. A crash between the write and the rename can leave a.newfile, which the next save overwrites. Where the file is written in place (an unwritable folder, a Windows link), a full disk still cuts it short, as onmain. The Windows paths (the read-only check, a reparse point written in place, theMoveFileExAfallback) are untested.Turning it off: revert the commit
Compatibility: Nothing changes: the same file in the same place (
options_path()), with the same lines; alongshot_options.ini.newmay appear beside it for the moment of a save.Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.5089cac6, linked for both (longshot_vk.dllwith llvm-mingw 20261104, andlongshot_vk.so). The rebased head's only changed source file,gpu/vk/vk_menu.cpp, compiles (syntax-only, no full build: the build base was in use) for Windows with llvm-mingw 20261104'sx86_64-w64-mingw32-clang++ -Wall -Wextra, with the same 6 warnings asmainand none new, and for Linux with clang; it adds no new call or symbol over5089cac6. The path calls are the narrow ones the renderer already uses under the UTF-8 manifest (GetFileAttributesA,MoveFileExA), which take the user folder's UTF-8 path.longshot_options.ini: a full disk, a link, an unwritable folder or file, its mode 00e93fe0a0longshot_options.inibeside it and move it over once wholeFilled in: the full game-free suite on the combined branch, the Windows and Linux builds, and the test request T-SRCU. Still work in progress until a Steam Deck run is in.
Test request T-SRCU: pass
Hardware: Steam Deck OLED (AMD Custom APU 0932, RADV VANGOGH, 16 GB), Mesa 26.2.0-devel git-5c65d4fc31, SteamOS 3.9.5 (build
20261023.100), kernel 7.2.7-valve2. File system: ext4 on the internal NVMe (the game folder and the options file).
Built from main
7ff5256ewith #550 to #558 merged in order (all clean); this PR at its head5089cac6. Built for x86-64-v3 with therelease play profile and LLVM 23.1.5. The game ran in a test folder with its own options file (the defaults its first start wrote),
started through Steam in Game Mode, docked.
How the rows were changed. With the control bridge on (
HOS_CONTROL), each row was set with itsoption set, which goes throughthe Insert menu's own setter and saves as the menu does (docs/control.md).
What I checked
tab); Camera speed 100 to 120, Invert camera Y off to on, Stick dead zone 0 to 2, Rumble strength 100 to 60 (Controls tab).
-rw-r--r--).longshot_options.ini.newin the folder, in play or after the game closed.Invert camera Y on and Stick dead zone 2.
25, the bridge's set took 60 (which the menu's arrows can't make), and the start snaps it to the step (
rumble_step,"a hand-edited 60: 50"). The save kept exactly what was set.
longshot_options.inia link to a file in another folder, after a change (Text speed 2 to 1)it is still the link and its target has the change. No
.newbeside the target or in the game folder, before or after the close.chmod a-won the folder, the file itself writable, I made two changes (Text speed to 3,Field of view to 105). Both were saved, in place. The log said once:
menu: longshot_options.ini.new cannot be made beside it (Permission denied): the file is written in place.No
.new. The folder was made writable again afterwards.No crash or error in the 4 starts.
Not tested:
Thank you for the Deck run, dukenheimer: your result is in the description as you reported it, including what it did not cover. Marking this ready for review.
WIP: fix(menu): longshot_options.ini is replaced whole or not at allto fix(menu): longshot_options.ini is replaced whole or not at allRebased onto
mainaa95d35f(2248676d):test_upscaler_round_trip.pykeepsmain'sapply_auto(), andtest_menu_save_atomic.pygets #736's GPU card stand-ins. Since #729save()writes whatever fileoptions_path()names (the launcher's user folder's or the game folder's), and the atomic write already followed it there.Added for the new location: a user-folder case and a no-settings-file case in the check, log lines that name the file's whole path, and no stray
.newwhen a run has no settings file. The Deck result (T-SRCU) still holds, since on the Deck the file is still the game folder's.5089cac60e2248676d40