feat(build): a new version's game files hard-linked from another game folder of the same .nup #360
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
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/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!360
Loading…
Reference in a new issue
No description provided.
Delete branch "madebyjordan/Longshot:feat/romfs-from"
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?
Why
Every new version and every update a launcher builds into its own folder unpacked the player's .nup again. That's 26 GB written and 26 GB more disk space each time, for game files the PC already has unpacked beside it. With this PR, a build can hard-link them from another game folder made from the same .nup: no disk space taken and nothing written. On an NVMe drive it's also about 10% quicker; on a hard disk, where writing 26 GB is slow, it should save more (not measured).
What changed
build:
romfs_from_build()innx2/make_game.pyhandles--nupwith--romfs-from <folder>, orLONGSHOT_ROMFS_FROM, which a launcher sets. It runs when that folder's.longshot_romfsstamp is this .nup's: its name and size, the check a folder's own unpack is already reused on. Then:link_or_copy(copied where the disk can't link);.longshot_romfs_checked, the heads list, the damaged files' note), sostand_in_damaged_filesopens nothing;--romfs.A folder from another .nup, one without game files, or the build's own folder is passed over, and the .nup is read as before. A folder's own earlier unpack of the same .nup still comes first.
build:
refuse_dump_links()reads each entry's attributes from its folder's listing (os.scandir) instead of an lstat per file: 36-49 s down to 1.3-1.5 s on the romfs's ~186,000 files. A folder it can't list now stops the build with a plain message. Before,os.walkpassed over it, and its files went missing from the game folder without a word.build:
link_or_copy()makes each folder once. It lets a link that's already there (from a retry) fail withFileExistsErrorinstead of looking each file up first; a failed link still falls back to the staged copy. A romfs's top-level files go todstitself:dst\.in a\\?\long path isn't tidied away, so they failed to link.docs:
docs/development.md, theLONGSHOT_ROMFS_FROMrow.tests:
tests/test_make_game_cache.pychecks:The shared set-up is
NupCacheHarness.The launcher side, which picks the folder and sets
LONGSHOT_ROMFS_FROM, is a separate PR on LongshotLauncher: MagicBeanDev/LongshotLauncher#58. Without it this does nothing: a build that isn't given the folder unpacks the .nup as before, and a launcher that sets the variable for an older builder gets the old behaviour.Flags and settings
LONGSHOT_ROMFS_FROM=<folder>/--romfs-from <folder>--nup: another game folder unpacked from the same .nup; its game files are hard-linked in instead of reading the .nup againMeasurements
main:
cf605c85(2026-11-14)this PR: feat/romfs-from @
bcf64463Rebased since onto main
0caf0618(now7f96ae5f): the numbers still hold. #357 and #358 changed the lift (nx2/chunks.py) and the prompts (nx2/prompt_assets.py), not the romfs step, and this PR's code is the same; only a row of the switch table moved.Hardware: Ryzen 5 5600, 16 GB, NVMe, Windows 11, NTFS
Scene: none (the build). The OoT .nup (25.62 GB, 186,583 files in v131072's romfs), building a second game folder from it.
Method: a second game folder's game files, main and this PR alternated (main / PR / main / PR), each into an empty folder on the same NVMe drive as the first:
make_game.pyruns for a .nup it hasn't unpacked in that folder,nx2/nup.py <file.nup> --extract;romfs_from_build()from a game folder already built from that .nup.Wall time, files counted, and for the PR, every file's link count plus 188 files sampled as the same file as the source's.
How to reproduce: build a game folder from the .nup, then
python -I nx2/make_game.py --nup <file.nup> --out <second folder> --romfs-from <first folder>.cf605c85(unpacks the .nup)bcf64463(links from the first folder)refuse_dump_linkson that romfs (2026-11-13)NTFS takes the links one at a time, so threads don't help: 0.48 ms a link on 1 thread, 0.63 ms on 16.
cf605c85bcf64463tests/test_make_game_cache.py--romfs-from)tests/run_all.py --jobs 4Testing
Tested:
tests/test_make_game_cache.py, andpython -I -B tests/run_all.py --jobs 4.Not tested:
Needs other testers:
--romfs-from, expecting its romfs as hard links (stat -c %habove 1) and the game starting from it (T-FWPE).Screenshots
Nothing on screen.
Risk and rollback
.partfile and swapped in withos.replace, which breaks the link rather than writing through it. A player who edits a romfs file by hand in one folder would change the other too;docs/development.mdsays so.Checklist
For everyone
type(scope): description).main.python -I -B tests/run_all.pyrun and passing (any failure that also fails onmainis named above).third_party/untouched (a new folder with aPROVENANCE.mdinstead).If applicable
Assisted-by:/Co-Authored-By:trailers.mainand passing here.tests/that reads changed source updated in this PR, not weakened.WIP: feat(build): a new version's game files hard-linked from another game folder of the same .nupto feat(build): a new version's game files hard-linked from another game folder of the same .nupbcf64463d27f96ae5f5aRebased onto main
0caf0618(#357 and #358 merged). The only conflict was the switch table in docs/development.md: both rows kept. The code is unchanged; test_make_game_cache (25 tests, 1 skipped for symbolic links), test_build_times, test_lift_cleanup_error and test_kept_translation pass on the new base.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.