49 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 f469c5a774 Raise the version to 0.2.8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:17:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1d6baac9d2 Merge pull request #233: Add colour profiles and brightness, contrast, saturation and gamma for the picture (Lucas-blip-png)
With TOTK_COLOR_* away from their defaults, present_image runs one compute pass (vk_color.c) that writes the game
picture's crop, adjusted, into an RGBA16F image, which FSR 1 or the blit then scales. At the defaults nothing is
recorded and the present takes the same source, format and rectangle as before. The Screen tab gets a Colour group
(profile and four sliders) after the field-of-view slider of #248; cfg.c and totk.ini.example keep both settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:15:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b768567f8d Merge pull request #248: Add a field-of-view setting for 1.4.3 (Lucas-blip-png)
TOTK_FOV (70..140 percent, Screen tab, live) multiplies the camera's vertical view angle through the new patch set fov
at the two camera routines the aspect set patches; page +60 holds the factor. At the default 100 the set is off and the
camera runs the game's own instructions. Both fov sites and the rest of the 1.4.3 table pass patches.check against the
1.4.3 main. 1.2.1 and 1.1.1 have no fov sites and log that the setting does nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:50 +03:00
lunar-meandClaude Opus 5.5 bdad89e0e4 Say "an update file other than the recorded one" in the installer guide (#247)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 db4d04a6e5 Merge pull request #269: Trust the narrowed shader walk when it finds the version's shaders (shenjackyuanjie)
The shader list covers several versions, so most of its rows are absent from any one game and the old test fell back to
reading the whole RomFS on every install: 23,825 of 41,414 rows on 1.2.1, 23,370 on 1.4.3. The fallback now runs only
when the narrowed walk finds next to nothing. On 1.2.1 and 1.4.3 the narrowed walk finds the same shaders as the full
one (27,576 and 28,667, none only in the full walk) in 2 s instead of 51 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:14:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 407c6915b5 Merge pull request #220: Support the 1.1.1 update as a third game version (shenjackyuanjie)
1.1.1 (update 131072) gets its boot constants, expected hashes, the 60 fps
RomFS overlay, its own code patch table, the scene module slot, loader probe
values and shader census lookup, and the setup accepts it. 1.2.1 and 1.4.3
keep every value: the runtime branches on RT_V111, and an install without a
recorded version still falls back to 1.2.1.

1.1.1 has no aspect patch set; outputs other than 16:9 are scaled to the
window on that version.

Fixes #173

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:03:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f408f26363 Merge task/savecommit: no save or backup for a Commit that changed nothing, a short swap retry (#260, #263)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:03:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d7266301d2 Skip the save and backup of a Commit that changed no byte, and retry the swap briefly
At start the game rewrites its save files with the same bytes and commits
twice; each of those commits made a new committed save and a backup, so one
start pushed every older backup out of save-backup. A Commit whose copy is
the newest save file for file (all hard links, same names) now only retries
a waiting swap.

Renaming the committed save away is retried for 200 ms instead of 2 s: a
failure no longer loses the commit, the next Commit or start retries, and the
game's next change to its save data waits for the commit thread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:00:01 +03:00
shenjackanddeepseek-v4.1-flash-1017 55c7411de0 Give the loader probe and the shader census the 1.1.1 values
Three places still assumed the two older versions:

- loader.c compared a 1.1.1 load against 1.2.1's expectations, so --probe-loader failed every time:
  main has 10,668 constructors against 1.2.1's 10,673, and its relocation kinds and totals differ.
  1.1.1 has its own table; subsdk0 and sdk match 1.2.1 down to every count.
- gfx_shader_probe.c looked for build/v121 and build/v143 only, so a census built for 1.1.1 was never
  found and --probe-shader failed on a checkout that has only that version. It looks for the running
  version first, then the other two.
- layout.py put 1.1.1 first in SUPPORTED while steps.py took SUPPORTED[0] as the version of an install
  made before versions were recorded, and such an install is 1.2.1: updating one picked 1.1.1's hashes
  and invalidated its extraction. The tuple keeps 1.2.1 first and the fallback names 1.2.1 outright.

--probe-loader now reports 0 failures on a 1.1.1 install.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:40 +08:00
shenjackanddeepseek-v4.1-flash-1017 1ea8813bed Read the scene module of 1.1.1 from its own slot
frame_rate.c finds the player through a global pointer in main's data segment: 1.4.3 keeps it at
main+0x3add238, 1.2.1 at main+0x4728538, and 1.1.1 was borrowing 1.2.1's address, which points at
something else in that build.

The 1.1.1 slot is main+0x4630a68. It was measured from the build itself: one function there walks
the whole chain this code walks (module+0x1e8 scene, the 14-component check at +0x50, component 13
at +0x58, the 0x70 link stride, the name compare) and 181 scene accesses read the same slot, which
makes it the scene module and not one of the single-instance pointers next to it.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:39 +08:00
shenjackanddeepseek-v4.1-flash-1017 6b10489e59 Let the setup take the 1.1.1 update
The update's hashes, the patch table and the boot constants are in place; the setup itself still refused the
version. SUPPORTED lists it now, so a player with that update can build.

The labels and messages that named the two supported updates name this one as well, so a player whose update is
1.1.1 is no longer asked for a file they do not have.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:39 +08:00
shenjackanddeepseek-v4.1-flash-1017 6d27cf8202 Add the 1.1 code patch table with its spin waits, hair and cloth sites
The 60 fps frame step and ratio, the render resolution, the slow-motion check and the actor animation clock, at the
addresses this build has them: each site was located again by instruction-window matching and its words checked against
the 1.1.1 main. The spin-wait back-edges are the ones that only another thread can end, and the last five sites are the
two inlined clocks of the animation update with the shared call they hand their seconds to.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:38 +08:00
shenjackanddeepseek-v4.1-flash-1017 93eeb8b29f Record the expected hashes of the 1.1 update
The ExeFS digests, the merged RomFS size and hash and the six shared fonts, so extract.py can tell this update from any
other. No rewritten_sha256: that field names MacNativeGames' NSO rewriter output, this dump never went through it, and
nothing reads it.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:38 +08:00
shenjackanddeepseek-v4.1-flash-1017 d8a211af7f Write the 60 fps RomFS overlay for 1.1
Its Common archive carries the same version number as 1.2.1, so make_romfs_patches.py takes v111 and reads
Common.Product.110, moving the quick-menu background captures to animation frame 0 where the world still draws.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:37 +08:00
shenjackanddeepseek-v4.1-flash-1017 2b16578f69 Boot the 1.1 game: its init_array count, checkpoint addresses and version name
The build knows the player's update as v111: DISPLAY_VERSIONS maps it for the version line of the settings menu, and
boot.c carries its two checkpoint return addresses (0xeb8c14 for SetFocusHandlingMode, 0xebcdec for the first
nvnBootstrapLoader call) beside the 1.2.1 and 1.4.3 ones, with init_array counts of 10668, 45 and 116 for main, subsdk0
and sdk. The build guide lists v111 among the versions every step takes.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:37 +08:00
shenjackanddeepseek-v4.1-flash-1017 157e619084 Trust the narrowed shader walk when it finds the version's shaders
The fallback read the whole RomFS whenever the shader list named shaders this version does not have. The list covers
several versions, and 1.1.1 holds 17,587 of its 41,414 rows, so every 1.1.1 install read all 304,469 files again:
23827 of the 41414 listed variants are not among the 27576 shaders found, and the narrowed walk costs 2.8 s against
70.6 s for the whole RomFS.

What says the walk fell short is a walk that found next to nothing, which is when the shader directories have moved.
Counting absent list rows does not: for 1.1.1 most of them are absent because the game does not have them.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 01:49:32 +08:00
Valiantsin DzerakhandClaude Opus 5.5 47485da7d9 Merge pull request #262: Linux fixes (wdiasjunior)
Shader preparation reads MacNativeGames' whole shader list history,
release downloads resume from a .part file, appimagetool and the type-2
runtime come from release tags, and the preparation summary counts the
vertex and fragment variants it skips with the graphics pipeline library
off.

The gettid cache in runtime/platform/linux.c conflicted with the same fix
already on dev (8dd7e20, #236); dev's version is kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:45:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1ba0424774 Merge task/savecommit: a save commit that cannot replace the committed save waits in save-tx instead of being lost (#260, #263)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:35:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 65fd631fea Keep a save commit that cannot replace the committed save instead of dropping it
Windows refuses to rename a directory while any file under it is open, so a
virus scanner, Explorer's preview or a sync tool holding a file of
save/<uid> made every Commit of a session fail after its 2 s of retries.
The game had its Commit return 0 and showed the new slots from the working
copy, and the next start deleted that copy: hours of play were gone (#260,
#263, log line "cannot move the committed save (error 5)").

A Commit now renames its finished staging copy to save-tx/ready-<n> before it
touches the committed save. That rename is the commit point. When the swap
into save/<uid> fails, ready-<n> stays the newest save: later working copies
and the next start are made from it, and the next Commit or start retries the
swap. The failure log names the programs holding files in the committed save
(Restart Manager) or says the folder refuses the rename (no DELETE access).
--probe-fs holds a committed file open across two Commits and restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:20:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8a4ab178ec Raise the version to 0.2.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:04:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8041ded696 Merge task/crashdiag: menu without the core gate, system font check, abort reports, command buffer diagnostics (#212, #176, #94)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e84c61f16d Say why a command recording buffer could not grow
A player's game ended with "nvn: command recording buffer of 1048576 bytes" during a dive into the Depths (#94):
realloc refused 1 MiB with commit to spare. That leaves a buffer pointer that is no block of ours, from a command
buffer object the game overwrote or initialized again without Finalize. The fatal line now gives the object, its
magic, the old buffer and its size, the bytes used and asked, and Initialize logs (eight times at most) when it
replaces an object that was not finalized.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e451ce6a0d Report abort and invalid C runtime parameters as crashes on Windows
abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function raise no exception: the
C runtime ends the process with 0xc0000409 and the log stops without a FATAL line. A SIGABRT handler and an
invalid-parameter handler now send both to the crash hook with the host return addresses in totk.exe, and the
report lists them as totk+offset frames after the guest registers. Checked with win32.c in a test program: assert,
abort and fclose(NULL) each give one report whose innermost frame is the failing call.

lifted_function no longer names the nearest lifted function for an address above the lifted code, so a host frame
of the runtime is not reported as game code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:57:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 637b98dea7 Check a system font before the menu's font atlas takes it
The menu merges the Japanese, Korean and Chinese language names from a system font. Dear ImGui asserts on a font
stb_truetype cannot parse, and the assert ends the process with 0xc0000409 (exit code 127 in a shell) and no line in
the log, a few seconds after the window opens: the report of an AMD laptop on Windows 11 Home Single Language (#176)
matches that, and a test build of our ImGui with a damaged font exits the same way. A cut-off font passes the parse
and faults later, when the glyphs are made, because stb_truetype reads tables without bounds checks.

fonts_up now runs each system font through a private copy of stb_truetype whose asserts only mark the font bad, after
checking that every table of its directory lies inside the file, and leaves out a font that fails with a log line.
--probe-window checks that the test takes the embedded Roboto and refuses a cut-off copy, one without a character
map and one with an unknown tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:56:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a554a7c13c Take the virtual core gate out of the settings menu
The row was labelled "Virtual core gate" with the text "Lets only one thread run per virtual core, as on the
console", but switching it on wrote free_cores = 1, which lifts the gate. Without the gate guest threads run in
parallel on one virtual core, and TotK keeps its per-core data without locks: an Arc B580 player who turned it on
crashed at every start with "CallCommands: command handle ... is not a live recording" (#212). free_cores stays an
ini key for experiments; the log now says when it is set, and totk.ini.example warns that the game can crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:56:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c346c7fde2 Merge task/camlabel: write back GPU renders into textures that share their page (#13, #63, #118, #252)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:55:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 de3ed42a4c Write back GPU renders into textures that share their page when the texture's own bytes are unchanged (#13, #63, #118, #252)
The page write tracking dropped the render of any texture smaller than a page whenever the CPU wrote another
part of that page. The Purah Pad camera renders the ID, screen rect and depth of what is under the reticle into
4x4 targets in two slots and reads them a frame later. On 1.4.3 one slot's rect target shares its page with data
the CPU writes every frame, so the game read a zero rect every other frame and the target's name label blinked; on
1.2.1 the targets of both slots were dropped and nothing was recognised. A texture that fills no page now keeps a
copy of its storage bytes from the GPU write, and a page write counts only when those bytes changed.
TOTK_WB_EXACT=0 restores the old decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:55:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 da6cd4009e List the bundled libXft and libXrender in the third-party notices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:05:30 +03:00
Valiantsin Dzerakh 14cc5f1d9b Bundle libXft and libXrender with the Xft Tk so the installer opens on hosts without them 2026-10-31 18:56:40 +03:00
Valiantsin Dzerakh e4eecef0f7 Pick the installer log font next to the UI font 2026-10-31 18:52:38 +03:00
Valiantsin Dzerakh ec34382f0a Use the platform UI font for the idle step rows in the installer 2026-10-31 18:52:38 +03:00
Valiantsin Dzerakh f8d8a4c553 Merge pull request #230: Restore Russian text in the Linux installer with Xft (lcd1232) 2026-10-31 18:52:09 +03:00
Wellington Dias 9865e7ab9a Count the vertex and fragment variants preparation skips with the graphics pipeline library off, and say so in its summary instead of reporting them as built. 2026-10-31 12:48:36 -03:00
Valiantsin Dzerakh e34909b40d Merge pull request #229: Find the shaders by their directories instead of reading the whole RomFS (shenjackyuanjie) 2026-10-31 18:48:09 +03:00
Wellington Dias 02d79bd378 Read a thread's id once on Linux, where gettid is a system call and every recursive mutex lock asked for it. 2026-10-31 12:41:49 -03:00
Wellington Dias 42e5c98395 Pin appimagetool 1.9.1 and the type-2 runtime 20251108 from their release tags, so a new upload under continuous no longer stops the AppImage build. 2026-10-31 12:41:49 -03:00
Valiantsin DzerakhandClaude Opus 5.5 e38eaa8224 Turn the recorder thread back on by default on AMD GPUs
The AMD device losses with the recorder (#5, #40) stopped after objects
were kept until their submission completed and the recorder began ending
its own parts; RDNA 1 and 2 cards in #5 ran with vk_recorder on and off.
vk_recorder = 0 still turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:41:38 +03:00
Wellington Dias 522c69e951 Download a cached release file again when its hash no longer matches its pin, in the same run, instead of stopping. 2026-10-31 12:40:55 -03:00
Wellington Dias e8f56106de Read the MacNativeGames pin in build_xlate.py from a fetch line with the history word too. 2026-10-31 12:39:41 -03:00
Wellington Dias 8f6664d758 Download the release's pinned files into a .part file that a rerun resumes, and delete a file whose hash is wrong, so a cut download no longer fails every later run. 2026-10-31 12:38:33 -03:00
Wellington Dias ec7bb5db0a Fetch MacNativeGames with its history and point origin/HEAD at each pinned commit, and stop shader_list.py on a shallow clone, so shader preparation reads the whole list instead of failing or quietly preparing a fraction of it. 2026-10-31 12:38:09 -03:00
Valiantsin Dzerakh bf7c507e4c Merge branch 'task/tidcache' into dev 2026-10-31 18:26:01 +03:00
8dd7e2041e Cache the thread id per thread on Linux (#236)
plat_thread_id made a gettid syscall on every call, and plat_rmutex_lock
calls it on every acquire: about 685,000 syscalls a second in the open
world on 1.4.3. A thread's id never changes and the runtime starts child
processes with posix_spawn only, so the id now comes from a __thread cache
after the first call. Windows is unchanged: GetCurrentThreadId reads the TEB.

Found, measured and fixed by H.A. Forster in issue #236 (RTX 2060 laptop:
36.3 to 46.0 fps with replay_async). SchulzCode confirmed it on an RTX 2070
Super with a 7800X3D: 98 to 120 fps at game resolution, 76 to 90 fps at
render_scale 150.

Co-Authored-By: H.A. Forster <154697759+h-a-forster@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:10:26 +03:00
Lucas-blip-png 459d414c29 Add a field-of-view setting for 1.4.3
The camera update and setup routines that the aspect set patches load the camera's vertical view angle a few words
before the ratio and pass it to sead's perspective projection. The new set fov loads the angle as before and multiplies
it by a factor the runtime writes to patch page +60, so TOTK_FOV (70..140 percent, menu Screen tab, live) widens or
narrows the view. The game's own zooms (bow, scope) scale with it; 100 switches the set off.

Checked on 1.4.3 at 1920x1080 at 70, 100, 130 and 140: the view narrows and widens around Link, the HUD is unchanged,
no objects appear or vanish at the edges, no validation errors. 1.2.1 has no fov sites yet (its original words are
unverified here); the setting logs that and does nothing there.
2026-10-31 11:08:06 -04:00
f482217350 Step the 1.2.1 equipment-switch cooldown until it is no longer positive (#18)
1.2.1 has the same mismatch as 1.4.3 at the same player state field: the
update steps +0x109c only while it is above FLT_EPSILON and the equipment
gate blocks changes while it is above 0. The realtime set now compares with
0 in the update. Ported from the 1.4.3 patch by JieML1100 in issue #18.

Co-Authored-By: JieML1100 <183249798+JieML1100@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:00:48 +03:00
a80c78f08d Step the 1.4.3 equipment-switch cooldown until it is no longer positive (#18)
The player update steps the countdown at player state +0x109c only while it
is above FLT_EPSILON, while the equipment gate blocks weapon, shield and bow
changes while it is above 0. With the real-time step a rounding remainder in
(0, FLT_EPSILON] stays there and locks equipment after a Fuse until a load.
The realtime set now steps the countdown while it is above 0. Root cause and
patch by JieML1100 in issue #18.

Co-Authored-By: JieML1100 <183249798+JieML1100@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:59:40 +03:00
Lucas-blip-pngandSTS78721 c0567191f0 Add colour profiles and brightness, contrast, saturation and gamma for the picture
Issue #136. With TOTK_COLOR_* away from their defaults, present_image runs one compute pass (vk_color.c, shaders/color_grade.comp) that writes the game picture's crop, adjusted, into an RGBA16F image, and FSR 1 or the blit scale that one; at the defaults nothing runs. The Screen tab gets a Colour group: a profile (Game's own, Vivid, Clarity, Custom) that sets the four values, and the four sliders. The adjustment, its order, ranges and presets are STS78721's patch from the issue, reworked onto dev: it now applies with FSR 1 scaling as well as the blit, its menu texts come from the catalog in English and Russian, and the patch's unused accessors and build_game.py change are left out.

Co-authored-by: STS78721 <134802927+STS78721@users.noreply.github.com>
2026-10-31 10:02:16 -04:00
shenjack 6813b60ed9 Find the shaders by their directories instead of reading the whole RomFS
inventory() read every RomFS file into memory and decompressed it before deciding whether the file can hold a
shader. On 1.1.1 that is 304,469 entries and 15.86 GiB, and the shaders live in six directories: 1,996 entries,
147 MiB, 0.9% of the bytes. The walk visits those directories and reads everything else only when the shader list
names keys it did not find there.

Measured on one install: 66.50 s -> 3.35 s for the same 27,576 shaders, key for key.

pack_shader_cache.py also rewrote its pack after every prepare although there was nothing to fold in: 2.6 s of
copying 1.37 GiB over the same entries, and a file system that then releases the old pack's blocks. It returns at
once when the cache holds no loose files.
2026-10-31 21:42:04 +08:00
lcd1232 d0e27b95c4 Restore Cyrillic rendering in the Linux installer with Xft 2026-10-31 17:40:54 +04:00
53 changed files with 2112 additions and 165 deletions

No files matched your search

+2
View File
@@ -64,6 +64,8 @@ These tools run on the developer's or player's machine during the build. They ar
| Component | Source | License | Where in the release |
|---|---|---|---|
| Tcl/Tk 9.0.4, rebuilt with Xft for the Linux installer | https://github.com/tcltk/tcl and https://github.com/tcltk/tk, tag `core-9-0-4`; archives pinned by SHA-256 in `tools/make_release_linux.py` | Tcl/Tk licence (BSD-style) | `python/lib/`; licence texts in `licenses/tcl-license.terms` and `licenses/tk-license.terms` |
| libXft 2 and libXrender 1, the sniper SDK sysroot's shared libraries, bundled beside the Xft Tk of the Linux installer; their Debian `copyright` files go into `licenses/` | https://repo.steampowered.com/steamrt-images-sniper/snapshots/3.0.20261021.266025/ | HPND (MIT-style) | `python/lib/`; `licenses/libXft-copyright.txt`, `licenses/libXrender-copyright.txt` |
| CPython 3.14.9 embeddable package, with `tkinter` and Tcl/Tk 9.0.4 taken from the python.org 3.14.9 installer's component MSI (unpacked, never run); includes `vcruntime140*.dll` (Microsoft, redistributable with Python), OpenSSL, libffi, SQLite, zlib, libtommath | https://www.python.org/downloads/release/python-3149/ | PSF-2.0 (Tcl/Tk: Tcl/Tk licence, BSD-style) | `python/` |
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), cffi 2.1.1, pycparser 3.11 | PyPI binary wheels, pins in `tools/requirements-installer.txt` | BSD-3-Clause; lz4 BSD-3-Clause; cryptography Apache-2.0 OR BSD-3-Clause; cffi MIT; pycparser BSD-3-Clause | `python/Lib/site-packages/` |
| llvm-mingw 20261020 (LLVM/Clang/LLD 23, mingw-w64, libc++, libunwind, compiler-rt), x86_64 subset | https://github.com/mstorsjo/llvm-mingw/releases/tag/20261020 | Apache-2.0 with LLVM exception; mingw-w64 ZPL-2.1/BSD/public domain; libc++ and libunwind Apache-2.0 with LLVM exception and MIT/UIUC; winpthreads MIT | `llvm/` |
+1 -1
View File
@@ -1 +1 @@
0.2.1
0.2.8
+2 -1
View File
@@ -52,7 +52,8 @@ hactool reads `-k <prod.keys>`; MSYS2 shells set `HOME`, which hides its default
## Build
From the repository root, in PowerShell. Every step that depends on the game version takes it as `<ver>`: `v143` for
1.4.3 or `v121` for 1.2.1. The tools default to `v121` when `--version` is left out. Saves load on either version.
1.4.3, `v121` for 1.2.1 or `v111` for 1.1.1. The tools default to `v121` when `--version` is left out. Saves load on any
of them.
1. Python environments (once). `.venv` runs the lifter and the build; `.venv-py314` runs the extraction scripts, which
need the stdlib `compression.zstd` of Python 3.14:
+3 -2
View File
@@ -34,7 +34,7 @@ The player runs a newer installer on the same folder. It reads `totk-install.jso
`profile/` and `totk.ini` after checking their hashes, rebuilds `totk.exe` and re-prepares the shaders. A newer
release that changes nothing game-derived skips the rebuild.
Given another update file than the recorded one (1.2.1, then 1.4.3), the installer extracts and builds again from it and
Given an update file other than the recorded one (1.2.1, then 1.4.3), the installer extracts and builds again from it and
keeps `profile/`. Saves carry over both ways: 1.4.3 converts a 1.2.1 save on load, and 1.2.1 loads the saves 1.4.3 wrote.
## Setup steps
@@ -113,7 +113,8 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
`tools/make_release_linux.py` builds the same setup for Linux and packs it as `TotK-Setup-x86_64.AppImage` (the static
type-2 runtime: no libfuse2 needed on the host). The player makes it executable and starts it; on a Steam Deck, from
Desktop Mode. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
Desktop Mode. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt with
Xft against the sniper sysroot so Russian text uses system TrueType fonts. The setup builds the game into the folder they pick, as on Windows, and the game is `totk` there with
`libSDL3.so.0` beside it. The folder inside the AppImage:
```
+1 -1
View File
@@ -46,7 +46,7 @@ def cancel(signum, frame):
def main(argv=None):
ap = Parser(prog='python -m installer', description=__doc__.splitlines()[0])
ap.add_argument('--base', type=Path, help='base game, XCI or NSP')
ap.add_argument('--update', type=Path, help='1.2.1 or 1.4.3 update NSP')
ap.add_argument('--update', type=Path, help='the update NSP: 1.1.1, 1.2.1 or 1.4.3')
ap.add_argument('--keys', type=Path, help='prod.keys')
ap.add_argument('--lang', help='game language, a Switch language code (en-US, en-GB, ja, de, ru, ...)')
ap.add_argument('--dest', type=Path, required=True, help='install folder')
+6 -4
View File
@@ -66,8 +66,10 @@ class App:
self.icon = tk.PhotoImage(file=str(png))
root.iconphoto(True, self.icon)
root.protocol('WM_DELETE_WINDOW', self.on_close)
self.ui_font = 'Segoe UI' if os.name == 'nt' else 'DejaVu Sans'
self.mono_font = 'Consolas' if os.name == 'nt' else 'DejaVu Sans Mono'
style = ttk.Style(root)
style.configure('Title.TLabel', font=('Segoe UI', 15, 'bold'))
style.configure('Title.TLabel', font=(self.ui_font, 15, 'bold'))
style.configure('Notice.TLabel', background='#fff3cd', foreground='#664d03', padding=8)
style.configure('Err.TLabel', foreground='#c01c28')
style.configure('Version.TLabel', foreground='#808080')
@@ -219,7 +221,7 @@ class App:
self.cancel_button.pack(side='right')
self.log_toggle = self.button(bar, 'ui.show_log', self.toggle_log)
self.log_toggle.pack(side='left')
self.run_header = ttk.Label(f, font=('Segoe UI', 11, 'bold'))
self.run_header = ttk.Label(f, font=(self.ui_font, 11, 'bold'))
self.run_header.pack(anchor='w')
self.step_rows = {}
self.steps_box = ttk.Frame(f)
@@ -233,7 +235,7 @@ class App:
self.warn_label = ttk.Label(f, wraplength=620, justify='left', foreground='#9a6700')
self.warn_label.pack(anchor='w')
self.log_frame = ttk.Frame(f)
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=('Consolas', 9))
self.log = tk.Text(self.log_frame, height=10, wrap='word', state='disabled', font=(self.mono_font, 9))
scroll = ttk.Scrollbar(self.log_frame, command=self.log.yview)
self.log.configure(yscrollcommand=scroll.set)
scroll.pack(side='right', fill='y')
@@ -378,7 +380,7 @@ class App:
glyph.configure(text=GLYPH[state], foreground=COLOR[state])
name.configure(text=self.cat('step.' + s) + (f' ({self.cat("ui.state_" + state)})'
if state in (SKIPPED, FAILED) else ''),
font=('Segoe UI', 10, 'bold') if state == RUNNING else ('Segoe UI', 10))
font=(self.ui_font, 10, 'bold') if state == RUNNING else (self.ui_font, 10))
mode = m.mode or 'install'
self.run_header.configure(text=self.cat('ui.running_' + mode if mode in ('install', 'resume', 'update')
else 'ui.running_install'))
+1 -1
View File
@@ -36,7 +36,7 @@ from pathlib import Path
WIN = os.name == 'nt'
EXE = '.exe' if WIN else ''
UCRT = Path(r'C:\msys64\ucrt64\bin')
SUPPORTED = ('1.2.1', '1.4.3') # updates the installer builds; the check identifies the player's by hash
SUPPORTED = ('1.2.1', '1.4.3', '1.1.1') # updates the installer builds; the check identifies the player's by hash
def version_tag(version):
+3 -3
View File
@@ -168,9 +168,9 @@ def raise_if_damaged(error, name, path):
def game_version(ctx):
"""The update this install is built from: what the check identified, else the recorded one (an install made before
1.4.3 support records none: 1.2.1)."""
return ctx.state.get('version', SUPPORTED[0])
"""The update this install is built from: what the check identified, else the recorded one. An install made before
1.4.3 support records no version, and that install is 1.2.1: name it here rather than leaning on SUPPORTED's order."""
return ctx.state.get('version', '1.2.1')
def expected_update(ctx):
+5 -5
View File
@@ -48,7 +48,7 @@
"check.vulkan_missing": "No Vulkan driver is installed. Install the latest driver for your graphics card.",
"check.vulkan_no_device": "Vulkan finds no graphics card. Install the latest driver for your graphics card.",
"check.vulkan_too_old": "The game needs a graphics card with Vulkan 1.3; found: {devices}. Update the graphics driver.",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.2.1 or 1.4.3 update and prod.keys.",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.1 or 1.2.1 or 1.4.3 update and prod.keys.",
"check.input_missing": "File not found ({input}): {path}",
"check.keys_unreadable": "The keys file cannot be read. Choose a prod.keys file.",
"check.keys_no_header_key": "The keys file has no header_key. Dump prod.keys from your console again.",
@@ -60,7 +60,7 @@
"check.ticket_missing": "The {input} file lacks the ticket its content needs.",
"check.base_not_totk": "The base game file is not The Legend of Zelda: Tears of the Kingdom ({detail}).",
"check.update_not_nsp": "The update must be an NSP file.",
"check.update_is_base": "The update file holds a base game. Choose the 1.2.1 or 1.4.3 update NSP.",
"check.update_is_base": "The update file holds a base game. Choose the 1.1.1 or 1.2.1 or 1.4.3 update NSP.",
"check.update_not_totk": "The update file is not a Tears of the Kingdom update ({detail}).",
"check.exefs_failed": "The game code of the {input} file failed its checks; see {log}.",
"check.input_damaged": "The {input} file is damaged or incomplete: {path} ({detail}). Copy that file again from its source, or dump it again, then run the setup again.",
@@ -92,13 +92,13 @@
"ui.notice": "The folder this setup builds contains the game. It is for your own use only; do not share or upload it.",
"ui.keys_private": "The keys file is read in place. Its contents are never shown, copied or stored.",
"ui.base": "Base game (NSP or XCI)",
"ui.update": "Update 1.2.1 or 1.4.3 (NSP)",
"ui.update": "Update 1.1.1 or 1.2.1 or 1.4.3 (NSP)",
"ui.keys": "prod.keys",
"ui.game_language": "Game language",
"ui.dest": "Install folder",
"ui.browse": "Browse...",
"ui.pick_game": "Choose the base game file",
"ui.pick_update": "Choose the 1.2.1 or 1.4.3 update file",
"ui.pick_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file",
"ui.pick_keys": "Choose prod.keys",
"ui.pick_dest": "Choose the install folder",
"ui.type_game": "Game files (NSP, XCI)",
@@ -116,7 +116,7 @@
"ui.update_mode": "This folder holds an earlier installation. The setup continues or updates it; the game files are asked for only if they are needed again.",
"ui.need_dest": "Choose the install folder.",
"ui.need_base": "Choose the base game file.",
"ui.need_update": "Choose the 1.2.1 or 1.4.3 update file.",
"ui.need_update": "Choose the 1.1.1 or 1.2.1 or 1.4.3 update file.",
"ui.need_keys": "Choose the prod.keys file.",
"ui.need_lang": "Choose the game language.",
"ui.missing_base": "The base game file does not exist.",
+5 -5
View File
@@ -48,7 +48,7 @@
"check.vulkan_missing": "Драйвер Vulkan не установлен. Установите свежий драйвер видеокарты.",
"check.vulkan_no_device": "Vulkan не находит видеокарту. Установите свежий драйвер видеокарты.",
"check.vulkan_too_old": "Игре нужна видеокарта с Vulkan 1.3; найдено: {devices}. Обновите драйвер видеокарты.",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.2.1 или 1.4.3 и prod.keys.",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.1 или 1.2.1 или 1.4.3 и prod.keys.",
"check.input_missing": "Файл не найден ({input}): {path}",
"check.keys_unreadable": "Не удаётся прочитать файл ключей. Выберите файл prod.keys.",
"check.keys_no_header_key": "В файле ключей нет header_key. Снова получите prod.keys со своей консоли.",
@@ -60,7 +60,7 @@
"check.ticket_missing": "В файле «{input}» нет тикета, который нужен его содержимому.",
"check.base_not_totk": "Файл базовой игры — не The Legend of Zelda: Tears of the Kingdom ({detail}).",
"check.update_not_nsp": "Обновление должно быть файлом NSP.",
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.2.1 или 1.4.3.",
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.1 или 1.2.1 или 1.4.3.",
"check.update_not_totk": "Файл обновления — не обновление Tears of the Kingdom ({detail}).",
"check.exefs_failed": "Код игры из файла «{input}» не прошёл проверку; подробности в {log}.",
"check.input_damaged": "Файл «{input}» повреждён или скопирован не полностью: {path} ({detail}). Скопируйте этот файл заново из источника или сделайте дамп ещё раз, затем снова запустите установку.",
@@ -92,13 +92,13 @@
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
"ui.base": "Базовая игра (NSP или XCI)",
"ui.update": "Обновление 1.2.1 или 1.4.3 (NSP)",
"ui.update": "Обновление 1.1.1 или 1.2.1 или 1.4.3 (NSP)",
"ui.keys": "prod.keys",
"ui.game_language": "Язык игры",
"ui.dest": "Папка установки",
"ui.browse": "Обзор...",
"ui.pick_game": "Выберите файл базовой игры",
"ui.pick_update": "Выберите файл обновления 1.2.1 или 1.4.3",
"ui.pick_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3",
"ui.pick_keys": "Выберите prod.keys",
"ui.pick_dest": "Выберите папку установки",
"ui.type_game": "Файлы игры (NSP, XCI)",
@@ -116,7 +116,7 @@
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
"ui.need_dest": "Выберите папку установки.",
"ui.need_base": "Выберите файл базовой игры.",
"ui.need_update": "Выберите файл обновления 1.2.1 или 1.4.3.",
"ui.need_update": "Выберите файл обновления 1.1.1 или 1.2.1 или 1.4.3.",
"ui.need_keys": "Выберите файл prod.keys.",
"ui.need_lang": "Выберите язык игры.",
"ui.missing_base": "Файл базовой игры не найден.",
+957
View File
@@ -0,0 +1,957 @@
# Code patches for TotK 1.1.1 main (recomp/patches.py). The sites come from MacNativeGames
# games/totk/runtime/game_patches.c (MIT, Copyright (c) 2026 MacNativeGames contributors) at upstream ebe621d, through
# its 1.2.1 addresses in game_version.h (2db612b); each one was located again in 1.1.1 by instruction-window matching
# and its original words checked against this build's main. The patch page holds two floats the runtime writes at every
# present (frame_rate.c): +0 the 30 Hz ratio (seconds * 30), +4 the frame step in seconds.
set fps60 TOTK_FPS60 1
set realtime TOTK_REALTIME 1
set cloth TOTK_CLOTH_REALTIME 1
set bounce TOTK_HIGHFPS_FIX 1
set output TOTK_PATCH_OUTPUT 0
set scene TOTK_PATCH_SCENE 0
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
site fps60 main 90e978 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
site fps60 main 90e97c 1a89b10a 1a9fa52a # clamp: csinc w10, w9, wzr, ge (desired = max(desired, 1))
site fps60 main 90e9a0 b9401a8a d503201f # lower trigger: nop the stored-candidate load
site fps60 main 90ea30 6b08015f 7100055f # raise path: cmp w10, #1
site fps60 main 90ea40 1a88c1ac 1a9fc5ac # raise path: csinc w12, w13, wzr, gt (next lower candidate floors at 1)
site fps60 main 90ea94 b9400288 52800028 # lower path: mov w8, #1 (minimum interval 1)
site fps60 main 90eab0 2a0a03f5 1a9fa555 # lower path: csinc w21, w10, wzr, ge (result never below 1)
# Real-time step: the VFR step (main+0x7ede6c, 1.0.0 0x7a83e4) and the global ratio after the controller call
# (main+0x8f2394, 1.0.0 0x8f68b8) read ratio and seconds from the patch page instead of interval * 0.5 and interval / 60.
site realtime main 833e4c 1e220020 @page:x9 # scvtf s0, w1 -> adrp x9, page
site realtime main 833e50 52a84e08 bd400120 # mov w8, #60.0 -> ldr s0, [x9] (ratio)
site realtime main 833e54 1e270101 bd400521 # fmov s1, w8 -> ldr s1, [x9, #4] (seconds)
site realtime main 833e58 1e2c1002 d503201f # fmov s2, #0.5 -> nop
site realtime main 833e64 1e211801 d503201f # fdiv s1, s0, s1 -> nop
site realtime main 833e68 1e220800 d503201f # fmul s0, s0, s2 -> nop
site realtime main 90e5d0 bd425e60 @page:x9 # ldr s0, [x19, #0x25c] -> adrp x9, page
site realtime main 90e5d4 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (global ratio)
site realtime main 90e5e4 5e21d800 d503201f # scvtf s0, s0 -> nop
site realtime main 90e5e8 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (1.1.1
# main+0x8ecb40, 1.2.1 main+0x89822c) scales s2 (seconds) and s3 (ratio) by the measured ratio. Upstream's
# veneer bodies, inlined at the site.
site cloth main 9efc20 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
site cloth main 9efc1c bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
site cloth main 9f0bcc 1e200900 f81f0fe9 @page:x9 bd400520 1e200900 f84107e9 # fmul s0, s8, s0
site cloth main 9f0ba8 bd0117e8 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f091f bd0137ff 3dc007ff f94003e9 910083ff # str s8, [sp, #0x114]
site cloth main a05a04 1e210801 f81f0fe9 @page:x9 bd400521 1e210801 f84107e9 # fmul s1, s0, s1
site cloth main a05a08 bd0137e0 d10083ff f90003e9 3d8007ff @page:x9 bd40013f 1e3f081f bd0157ff 3dc007ff f94003e9 910083ff # str s0, [sp, #0x134]
site cloth main 9f044c 97fbf1bd @page:x8 bd400104 1e240842 1e240863 97fbf1bd # bl 0x8ecb40
# Trampolines (upstream 27c3b43): the slow-motion check compares time object +0x28 (30 / (ratio * time scale)) with
# 100; compare +0x28 * +0x38 (30 / time scale) instead, the same value at 30 Hz and independent of the frame rate.
site bounce main bee7e0 bd402900 bd402900 bd403901 1e210800 # ldr s0, [x8, #0x28]; ldr s1, [x8, #0x38]; fmul s0, s0, s1
# Output size (upstream games/totk/runtime/game_patches.c patch_output_res; runtime/resolution.c switches the set on).
# The swapchain size nnMain writes before NvInitializeGraphics comes from page +48 ({w, h} as one 64-bit value), and
# the output size the renderer lays out its canvas, crop and UI in (main+0xcc3f00, read from the graphics parameters
# through main+0xcc4160) from page +32; the renderer itself goes to page +40.
site output main eb8cfc d280f008 @page:x8 # mov x8, #0x780 -> adrp x8, page
site output main eb8d00 f2c08708 f9401908 # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #48]
site output main cea5c8 aa1403e0 @page:x21 # mov x0, x20 -> adrp x21, page
site output main cea5cc 94000081 f90016b3 # bl 0xcc4160 -> str x19, [x21, #40]
site output main cea5d0 f9400015 f94012b5 # ldr x21, [x0] -> ldr x21, [x21, #32]
# Scene render size (upstream patch_render_res): the renderer's +0xcc4 / +0xcc8, which size every scene render target,
# take two floats from page +16 instead of the graphics parameters' docked 1600x900.
site scene main cea5e0 5e21d900 @page:x9 # scvtf s0, s8 -> adrp x9, page
site scene main cea5e8 bd0cc660 2d420520 # str s0, [x19, #0xcc4] -> ldp s0, s1, [x9, #16]
site scene main cea5ec 5e21d921 bd0cc660 # scvtf s1, s9 -> str s0, [x19, #0xcc4]
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v111/exefs --patches recomp/patches-v111.txt: loops that only
# another thread can end; their back-edges pass rt_spin_gate (runtime/kernel.c), TOTK_SPIN=0 keeps the core.
# spin waits found by tools/spin_waits.py on the 1.1.x modules
# main: 869 spin waits
spin main ac600 ac620 ac630 # 13 instructions; loads
spin main ac680 ac6a8 ac6b8 # 15 instructions; loads
spin main ac6f0 ac718 ac728 # 15 instructions; loads
spin main e8544 e854c # 3 instructions; loads
spin main 2457f0 245804 # 6 instructions; loads
spin main 644bbc 644bd0 # 6 instructions; loads
spin main 6456ac 6456c0 # 6 instructions; loads
spin main 6459d4 6459e8 # 6 instructions; loads
spin main 645d58 645d6c # 6 instructions; loads
spin main 6460c4 6460d8 # 6 instructions; loads
spin main 64649c 6464b0 # 6 instructions; loads
spin main 646740 646754 # 6 instructions; loads
spin main 6467d4 6467e8 # 6 instructions; loads
spin main 646e6c 646e80 # 6 instructions; loads
spin main 647080 647094 # 6 instructions; loads
spin main 6484f4 648510 # 8 instructions; loads
spin main 6485a4 6485c0 # 8 instructions; loads
spin main 649fb0 649fc4 # 6 instructions; loads
spin main 64a064 64a078 # 6 instructions; loads
spin main 64a3d0 64a3e4 # 6 instructions; loads
spin main 64afac 64afc8 # 8 instructions; loads
spin main 64b404 64b418 # 6 instructions; loads
spin main 64b980 64b994 # 6 instructions; loads
spin main 64c68c 64c6a0 # 6 instructions; loads
spin main 64c978 64c98c # 6 instructions; loads
spin main 64ca28 64ca3c # 6 instructions; loads
spin main 64d378 64d38c # 6 instructions; loads
spin main 64d8d8 64d8ec # 6 instructions; loads
spin main 64db90 64dba4 # 6 instructions; loads
spin main 64de18 64de2c # 6 instructions; loads
spin main 64de40 64de54 # 6 instructions; loads
spin main 64de68 64de7c # 6 instructions; loads
spin main 64e6c8 64e6dc # 6 instructions; loads
spin main 64e6f0 64e704 # 6 instructions; loads
spin main 64e8ac 64e8c0 # 6 instructions; loads
spin main 64e9a4 64e9b8 # 6 instructions; loads
spin main 64fb50 64fb64 # 6 instructions; loads
spin main 652eac 652ec0 # 6 instructions; loads
spin main 652ed4 652ee8 # 6 instructions; loads
spin main 652efc 652f10 # 6 instructions; loads
spin main 654590 6545a4 # 6 instructions; loads
spin main 654c5c 654c70 # 6 instructions; loads
spin main 654cfc 654d10 # 6 instructions; loads
spin main 655124 655138 # 6 instructions; loads
spin main 655c14 655c28 # 6 instructions; loads
spin main 656f80 656f94 # 6 instructions; loads
spin main 658e60 658e74 # 6 instructions; loads
spin main 658ebc 658ed0 # 6 instructions; loads
spin main 659b28 659b3c # 6 instructions; loads
spin main 659b50 659b64 # 6 instructions; loads
spin main 65c9e0 65c9f4 # 6 instructions; loads
spin main 65ca08 65ca1c # 6 instructions; loads
spin main 65ca30 65ca44 # 6 instructions; loads
spin main 65ca58 65ca6c # 6 instructions; loads
spin main 65ca80 65ca94 # 6 instructions; loads
spin main 65caa8 65cabc # 6 instructions; loads
spin main 65cad0 65cae4 # 6 instructions; loads
spin main 65caf8 65cb0c # 6 instructions; loads
spin main 65cb20 65cb34 # 6 instructions; loads
spin main 65cb48 65cb5c # 6 instructions; loads
spin main 65cb70 65cb84 # 6 instructions; loads
spin main 65cb98 65cbac # 6 instructions; loads
spin main 65cbc0 65cbd4 # 6 instructions; loads
spin main 65d4b0 65d4c4 # 6 instructions; loads
spin main 65d4d8 65d4ec # 6 instructions; loads
spin main 65d500 65d514 # 6 instructions; loads
spin main 65d528 65d53c # 6 instructions; loads
spin main 65da54 65da68 # 6 instructions; loads
spin main 65de00 65de14 # 6 instructions; loads
spin main 65e200 65e214 # 6 instructions; loads
spin main 65e6ec 65e700 # 6 instructions; loads
spin main 65ecbc 65ecd0 # 6 instructions; loads
spin main 65ece4 65ecf8 # 6 instructions; loads
spin main 663588 66359c # 6 instructions; loads
spin main 663b0c 663b20 # 6 instructions; loads
spin main 663da0 663db4 # 6 instructions; loads
spin main 663dec 663e00 # 6 instructions; loads
spin main 6645a4 6645b8 # 6 instructions; loads
spin main 6645cc 6645e0 # 6 instructions; loads
spin main 666530 666544 # 6 instructions; loads
spin main 666570 666584 # 6 instructions; loads
spin main 6671d4 6671e8 # 6 instructions; loads
spin main 6671fc 667210 # 6 instructions; loads
spin main 6676c0 6676d4 # 6 instructions; loads
spin main 667cfc 667d10 # 6 instructions; loads
spin main 66f34c 66f360 # 6 instructions; loads
spin main 6708f8 67090c # 6 instructions; loads
spin main 670920 670934 # 6 instructions; loads
spin main 670948 67095c # 6 instructions; loads
spin main 673f34 673f48 # 6 instructions; loads
spin main 67556c 675580 # 6 instructions; loads
spin main 676954 676968 # 6 instructions; loads
spin main 676994 6769a8 # 6 instructions; loads
spin main 676e6c 676e80 # 6 instructions; loads
spin main 677148 67715c # 6 instructions; loads
spin main 6771c8 6771dc # 6 instructions; loads
spin main 677484 677498 # 6 instructions; loads
spin main 677df0 677e04 # 6 instructions; loads
spin main 6792a8 6792bc # 6 instructions; loads
spin main 67a984 67a998 # 6 instructions; loads
spin main 698ef8 698f0c # 6 instructions; loads
spin main 6b6d8c 6b6da0 # 6 instructions; loads
spin main 6b922c 6b9240 # 6 instructions; loads
spin main 6c5408 6c541c # 6 instructions; loads
spin main 6c5614 6c5628 # 6 instructions; loads
spin main 6ccce0 6cccf4 # 6 instructions; loads
spin main 6ccd98 6ccdac # 6 instructions; loads
spin main 6d3c9c 6d3cb0 # 6 instructions; loads
spin main 6d3ff8 6d400c # 6 instructions; loads
spin main 6d61cc 6d61e0 # 6 instructions; loads
spin main 6d6210 6d6224 # 6 instructions; loads
spin main 6d6254 6d6268 # 6 instructions; loads
spin main 6d6298 6d62ac # 6 instructions; loads
spin main 6d75c4 6d75d8 # 6 instructions; loads
spin main 6d79dc 6d79f0 # 6 instructions; loads
spin main 6d9398 6d93ac # 6 instructions; loads
spin main 6dd8d0 6dd8e4 # 6 instructions; loads
spin main 6de150 6de164 # 6 instructions; loads
spin main 6e073c 6e0750 # 6 instructions; loads
spin main 6e0a94 6e0aa8 # 6 instructions; loads
spin main 6e4eb8 6e4ecc # 6 instructions; loads
spin main 6eb590 6eb5a4 # 6 instructions; loads
spin main 6eccac 6eccc0 # 6 instructions; loads
spin main 6ed9b4 6ed9c8 # 6 instructions; loads
spin main 6ee69c 6ee6b0 # 6 instructions; loads
spin main 6f94c0 6f94d4 # 6 instructions; loads
spin main 6f9738 6f974c # 6 instructions; loads
spin main 6fa324 6fa338 # 6 instructions; loads
spin main 70d844 70d858 # 6 instructions; loads
spin main 70dc74 70dc88 # 6 instructions; loads
spin main 710a70 710a84 # 6 instructions; loads
spin main 711848 71185c # 6 instructions; loads
spin main 7125a4 7125b8 # 6 instructions; loads
spin main 716474 716488 # 6 instructions; loads
spin main 719a30 719a44 # 6 instructions; loads
spin main 72cd78 72cd8c # 6 instructions; loads
spin main 72f66c 72f680 # 6 instructions; loads
spin main 737684 737698 # 6 instructions; loads
spin main 73e0dc 73e0f0 # 6 instructions; loads
spin main 73ed14 73ed28 # 6 instructions; loads
spin main 783520 783534 # 6 instructions; loads
spin main 797924 797938 # 6 instructions; loads
spin main 79794c 797960 # 6 instructions; loads
spin main 797c08 797c1c # 6 instructions; loads
spin main 797c64 797c78 # 6 instructions; loads
spin main 798ff4 799008 # 6 instructions; loads
spin main 79cbec 79cc00 # 6 instructions; loads
spin main 7ae134 7ae148 # 6 instructions; loads
spin main 7ae388 7ae39c # 6 instructions; loads
spin main 7c292c 7c2940 # 6 instructions; loads
spin main 7d618c 7d61a0 # 6 instructions; loads
spin main 7d904c 7d9060 # 6 instructions; loads
spin main 7e11c0 7e11d4 # 6 instructions; loads
spin main 7e1c9c 7e1cb0 # 6 instructions; loads
spin main 7e24a0 7e24b4 # 6 instructions; loads
spin main 7ed89c 7ed8b0 # 6 instructions; loads
spin main 7eef28 7eef3c # 6 instructions; loads
spin main 7f515c 7f5170 # 6 instructions; loads
spin main 801468 80147c # 6 instructions; loads
spin main 801490 8014a4 # 6 instructions; loads
spin main 8014b8 8014cc # 6 instructions; loads
spin main 80a0a4 80a0b8 # 6 instructions; loads
spin main 811500 811514 # 6 instructions; loads
spin main 816c40 816c54 # 6 instructions; loads
spin main 819450 819464 # 6 instructions; loads
spin main 85df6c 85e068 # 4 instructions; loads
spin main 863220 863234 # 6 instructions; loads
spin main 86352c 863540 # 6 instructions; loads
spin main 863a80 863a94 # 6 instructions; loads
spin main 863b94 863ba8 # 6 instructions; loads
spin main 86543c 865450 # 6 instructions; loads
spin main 865464 865478 # 6 instructions; loads
spin main 87e564 87e578 # 6 instructions; loads
spin main 87e58c 87e5a0 # 6 instructions; loads
spin main 880d44 880d58 # 6 instructions; loads
spin main 8810f4 881108 # 6 instructions; loads
spin main 8814c8 8814dc # 6 instructions; loads
spin main 88189c 8818b0 # 6 instructions; loads
spin main 887ce8 887cfc # 6 instructions; loads
spin main 887f50 887f64 # 6 instructions; loads
spin main 8884ec 888500 # 6 instructions; loads
spin main 888618 88862c # 6 instructions; loads
spin main 888748 88875c # 6 instructions; loads
spin main 8888e0 8888f4 # 6 instructions; loads
spin main 89183c 891850 # 6 instructions; loads
spin main 892ccc 892cec # 9 instructions; loads
spin main 89540c 89541c # 5 instructions; loads
spin main 895668 895694 # 12 instructions; loads
spin main 8956a8 8956b8 # 5 instructions; loads
spin main 8da7dc 8da7e4 # 3 instructions; loads
spin main 8da7ec 8da7f0 # 2 instructions; loads
spin main 8da858 8da860 # 3 instructions; loads
spin main 8fb190 8fb1a4 # 6 instructions; loads
spin main 9045bc 9045d0 # 6 instructions; loads
spin main 9045e4 9045f8 # 6 instructions; loads
spin main 90460c 904620 # 6 instructions; loads
spin main 904634 904648 # 6 instructions; loads
spin main 905b20 905b34 # 6 instructions; loads
spin main 9062f0 906304 # 6 instructions; loads
spin main 91019c 9101b0 # 6 instructions; loads
spin main 910720 910734 # 6 instructions; loads
spin main 9363cc 9363e0 # 6 instructions; loads
spin main 93655c 936570 # 6 instructions; loads
spin main 93819c 9381b0 # 6 instructions; loads
spin main 940060 94007c # 8 instructions; loads
spin main 94012c 940148 # 8 instructions; loads
spin main 97864c 978660 # 6 instructions; loads
spin main 99aa8c 99aabc # 13 instructions; loads
spin main 99aad0 99aae0 # 5 instructions; loads
spin main 9a5fd4 9a6004 # 13 instructions; loads
spin main 9a6018 9a6028 # 5 instructions; loads
spin main 9b2700 9b2710 # 5 instructions; loads
spin main 9f395c 9f3970 # 6 instructions; loads
spin main 9f39e0 9f39f4 # 6 instructions; loads
spin main 9f3b2c 9f3b40 # 6 instructions; loads
spin main 9f3bf8 9f3c0c # 6 instructions; loads
spin main 9f6f5c 9f6f70 # 6 instructions; loads
spin main a11908 a1191c # 6 instructions; loads
spin main a1257c a12590 # 6 instructions; loads
spin main a14d6c a14d80 # 6 instructions; loads
spin main a1b0a4 a1b0b8 # 6 instructions; loads
spin main a1b0ec a1b100 # 6 instructions; loads
spin main a1c3dc a1c3f0 # 6 instructions; loads
spin main a2b268 a2b27c # 6 instructions; loads
spin main a2cab8 a2cacc # 6 instructions; loads
spin main a2f228 a2f23c # 6 instructions; loads
spin main a8e920 a8e934 # 6 instructions; loads
spin main aa2c20 aa2c34 # 6 instructions; loads
spin main aa5108 aa511c # 6 instructions; loads
spin main aa9f94 aa9fa8 # 6 instructions; loads
spin main aaa674 aaa688 # 6 instructions; loads
spin main aad564 aad578 # 6 instructions; loads
spin main ab245c ab24b4 # 7 instructions; loads
spin main ac8874 ac88fc # 35 instructions; loads
spin main af4040 af4054 # 6 instructions; loads
spin main af4068 af407c # 6 instructions; loads
spin main af4090 af40a4 # 6 instructions; loads
spin main af40cc af40e0 # 6 instructions; loads
spin main af40f4 af4108 # 6 instructions; loads
spin main af411c af4130 # 6 instructions; loads
spin main b47a74 b47a88 # 6 instructions; loads
spin main b4e080 b4e094 # 6 instructions; loads
spin main b529f4 b52a08 # 6 instructions; loads
spin main b52a7c b52a90 # 6 instructions; loads
spin main b52cfc b52d10 # 6 instructions; loads
spin main b52dd0 b52de4 # 6 instructions; loads
spin main b53458 b5346c # 6 instructions; loads
spin main b535a8 b535bc # 6 instructions; loads
spin main b54320 b54334 # 6 instructions; loads
spin main b545fc b54610 # 6 instructions; loads
spin main b5662c b56640 # 6 instructions; loads
spin main b56654 b56668 # 6 instructions; loads
spin main b71800 b71814 # 6 instructions; loads
spin main b71828 b7183c # 6 instructions; loads
spin main b7b82c b7b840 # 6 instructions; loads
spin main b80970 b80984 # 6 instructions; loads
spin main b86fe8 b86ffc # 6 instructions; loads
spin main b8702c b87040 # 6 instructions; loads
spin main b87070 b87084 # 6 instructions; loads
spin main b8775c b87770 # 6 instructions; loads
spin main b97d6c b97d80 # 6 instructions; loads
spin main b97ed4 b97ee8 # 6 instructions; loads
spin main b9c628 b9c63c # 6 instructions; loads
spin main b9c6c8 b9c6dc # 6 instructions; loads
spin main b9c6f0 b9c704 # 6 instructions; loads
spin main b9c7b8 b9c7cc # 6 instructions; loads
spin main ba1db4 ba1dc8 # 6 instructions; loads
spin main ba72e8 ba72fc # 6 instructions; loads
spin main bb1070 bb1084 # 6 instructions; loads
spin main bb1098 bb10ac # 6 instructions; loads
spin main bb6c38 bb6c4c # 6 instructions; loads
spin main bb7fa0 bb7fb4 # 6 instructions; loads
spin main bd27c8 bd27d8 # 5 instructions; loads
spin main bdd678 bdd68c # 6 instructions; loads
spin main bf2ccc bf2ce0 # 6 instructions; loads
spin main bf2e24 bf2e38 # 6 instructions; loads
spin main bf32a0 bf32b4 # 6 instructions; loads
spin main bf3730 bf3744 # 6 instructions; loads
spin main c0a554 c0a568 # 6 instructions; loads
spin main c0a6b4 c0a6c8 # 6 instructions; loads
spin main c0a850 c0a864 # 6 instructions; loads
spin main c0d758 c0d76c # 6 instructions; loads
spin main c0d8b0 c0d8c4 # 6 instructions; loads
spin main c0dd3c c0dd50 # 6 instructions; loads
spin main c15ffc c16010 # 6 instructions; loads
spin main c1665c c16670 # 6 instructions; loads
spin main c2315c c23170 # 6 instructions; loads
spin main c2374c c23760 # 6 instructions; loads
spin main c23998 c239ac # 6 instructions; loads
spin main c2d128 c2d13c # 6 instructions; loads
spin main c3a9cc c3a9e0 # 6 instructions; loads
spin main c5ab4c c5ab60 # 6 instructions; loads
spin main c70388 c7039c # 6 instructions; loads
spin main c703b0 c703c4 # 6 instructions; loads
spin main c94f70 c94f84 # 6 instructions; loads
spin main c95a00 c95a14 # 6 instructions; loads
spin main c96764 c96778 # 6 instructions; loads
spin main c96948 c9695c # 6 instructions; loads
spin main c96ce4 c96cf8 # 6 instructions; loads
spin main c96d54 c96d68 # 6 instructions; loads
spin main c97400 c97414 # 6 instructions; loads
spin main c97ca0 c97cb4 # 6 instructions; loads
spin main c99ba0 c99bb4 # 6 instructions; loads
spin main c99c90 c99ca4 # 6 instructions; loads
spin main c9abac c9abc0 # 6 instructions; loads
spin main c9ac8c c9aca0 # 6 instructions; loads
spin main c9b2e0 c9b2f4 # 6 instructions; loads
spin main c9d9f4 c9da08 # 6 instructions; loads
spin main c9db3c c9db50 # 6 instructions; loads
spin main c9df14 c9df30 # 8 instructions; loads
spin main ccaaa8 ccaabc # 6 instructions; loads
spin main cd38a4 cd38b8 # 6 instructions; loads
spin main cda7c4 cda7d8 # 6 instructions; loads
spin main cdb388 cdb39c # 6 instructions; loads
spin main cdb3cc cdb3e0 # 6 instructions; loads
spin main cdbab4 cdbac8 # 6 instructions; loads
spin main cdbaf8 cdbb0c # 6 instructions; loads
spin main cdbce4 cdbcf8 # 6 instructions; loads
spin main cdce0c cdce20 # 6 instructions; loads
spin main cecedc cecef0 # 6 instructions; loads
spin main d0fbb4 d0fbc8 # 6 instructions; loads
spin main d1acd0 d1ace4 # 6 instructions; loads
spin main d1dc54 d1dc60 d1dcac d1dcc4 # 29 instructions; loads
spin main d2273c d22750 # 6 instructions; loads
spin main d231bc d231d0 # 6 instructions; loads
spin main d23398 d233ac # 6 instructions; loads
spin main d2e810 d2e824 # 6 instructions; loads
spin main d3d8dc d3d8f0 # 6 instructions; loads
spin main d4c3a8 d4c3bc # 6 instructions; loads
spin main d4ffc4 d4ffd8 # 6 instructions; loads
spin main d50a78 d50a8c # 6 instructions; loads
spin main d50b04 d50b18 # 6 instructions; loads
spin main d50bfc d50c10 # 6 instructions; loads
spin main d50e60 d50e74 # 6 instructions; loads
spin main d50e88 d50e9c # 6 instructions; loads
spin main d50ebc d50ed0 # 6 instructions; loads
spin main d510b8 d510cc # 6 instructions; loads
spin main d51afc d51b18 # 8 instructions; loads
spin main d52478 d5248c # 6 instructions; loads
spin main d527f4 d52808 # 6 instructions; loads
spin main d5283c d52850 # 6 instructions; loads
spin main d52864 d52878 # 6 instructions; loads
spin main d5288c d528a0 # 6 instructions; loads
spin main d528f8 d5290c # 6 instructions; loads
spin main d52920 d52934 # 6 instructions; loads
spin main d529a8 d529bc # 6 instructions; loads
spin main d52ab0 d52ac4 # 6 instructions; loads
spin main d91344 d91358 # 6 instructions; loads
spin main d91970 d91984 # 6 instructions; loads
spin main d91cc0 d91cd4 # 6 instructions; loads
spin main d91dc0 d91dd4 # 6 instructions; loads
spin main d92280 d92294 # 6 instructions; loads
spin main d923cc d923e0 # 6 instructions; loads
spin main d93600 d93614 # 6 instructions; loads
spin main d94100 d94114 # 6 instructions; loads
spin main d9412c d94140 # 6 instructions; loads
spin main d99098 d990ac # 6 instructions; loads
spin main d9c260 d9c298 # 8 instructions; loads
spin main d9c2c8 d9c300 # 8 instructions; loads
spin main d9fe34 d9fe48 # 6 instructions; loads
spin main da1eb4 da1ec8 # 6 instructions; loads
spin main da1edc da1ef0 # 6 instructions; loads
spin main da7010 da7024 # 6 instructions; loads
spin main da70f8 da710c # 6 instructions; loads
spin main dade1c dade30 # 6 instructions; loads
spin main db58dc db58f0 # 6 instructions; loads
spin main dbb0e4 dbb0f8 # 6 instructions; loads
spin main dbca30 dbca4c # 8 instructions; loads
spin main dbfc20 dbfc34 # 6 instructions; loads
spin main dbff00 dbff14 # 6 instructions; loads
spin main dc78fc dc7910 # 6 instructions; loads
spin main dcaa88 dcaa9c # 6 instructions; loads
spin main dcb2ac dcb2c0 # 6 instructions; loads
spin main dcb9c8 dcb9dc # 6 instructions; loads
spin main dd4a28 dd4a3c # 6 instructions; loads
spin main dd6414 dd6428 # 6 instructions; loads
spin main dd7498 dd74ac # 6 instructions; loads
spin main dd78b4 dd78c8 # 6 instructions; loads
spin main ddf338 ddf34c # 6 instructions; loads
spin main de7324 de7338 # 6 instructions; loads
spin main deb218 deb22c # 6 instructions; loads
spin main df249c df24b0 # 6 instructions; loads
spin main df2930 df2944 # 6 instructions; loads
spin main dfab0c dfab20 # 6 instructions; loads
spin main e1a248 e1a25c # 6 instructions; loads
spin main e27b10 e27b24 # 6 instructions; loads
spin main e28428 e2843c # 6 instructions; loads
spin main e38cac e38cc0 # 6 instructions; loads
spin main e38f90 e38fa4 # 6 instructions; loads
spin main e39274 e39288 # 6 instructions; loads
spin main e39558 e3956c # 6 instructions; loads
spin main e40b1c e40b30 # 6 instructions; loads
spin main e41478 e4148c # 6 instructions; loads
spin main e450bc e450d0 # 6 instructions; loads
spin main e45400 e45414 # 6 instructions; loads
spin main e51aa0 e51ab4 # 6 instructions; loads
spin main e51c08 e51c1c # 6 instructions; loads
spin main e51cf4 e51d08 # 6 instructions; loads
spin main e8e42c e8e440 # 6 instructions; loads
spin main e90628 e9063c # 6 instructions; loads
spin main e930d8 e930ec # 6 instructions; loads
spin main e94c64 e94c78 # 6 instructions; loads
spin main e9557c e95590 # 6 instructions; loads
spin main e966e4 e966f8 # 6 instructions; loads
spin main e9711c e97130 # 6 instructions; loads
spin main e97164 e97178 # 6 instructions; loads
spin main e9c134 e9c148 # 6 instructions; loads
spin main ea4c28 ea4c3c # 6 instructions; loads
spin main ea4c78 ea4c8c # 6 instructions; loads
spin main ea4cc4 ea4cd8 # 6 instructions; loads
spin main ea4d10 ea4d24 # 6 instructions; loads
spin main eb07cc eb07d4 # 3 instructions; loads
spin main eb07dc eb07e0 # 2 instructions; loads
spin main eb0808 eb0810 # 3 instructions; loads
spin main eb1548 eb155c # 6 instructions; loads
spin main eb4240 eb4254 # 6 instructions; loads
spin main eb5aa8 eb5abc # 6 instructions; loads
spin main eb637c eb6390 # 6 instructions; loads
spin main eb7954 eb7968 # 6 instructions; loads
spin main ebf63c ebf650 # 6 instructions; loads
spin main ebfb5c ebfb70 # 6 instructions; loads
spin main ebfb98 ebfbac # 6 instructions; loads
spin main ebfd50 ebfd64 # 6 instructions; loads
spin main ebffb4 ebffc8 # 6 instructions; loads
spin main eca534 eca548 # 6 instructions; loads
spin main eca6f8 eca70c # 6 instructions; loads
spin main ecb088 ecb09c # 6 instructions; loads
spin main ed26e8 ed26fc # 6 instructions; loads
spin main ed293c ed2950 # 6 instructions; loads
spin main ed2ab0 ed2ac4 # 6 instructions; loads
spin main ed2c24 ed2c38 # 6 instructions; loads
spin main ed2ed0 ed2ee4 # 6 instructions; loads
spin main ed3348 ed335c # 6 instructions; loads
spin main ed351c ed3530 # 6 instructions; loads
spin main ed3cf4 ed3d08 # 6 instructions; loads
spin main ed42e0 ed42f4 # 6 instructions; loads
spin main ed430c ed4320 # 6 instructions; loads
spin main ed5294 ed52a8 # 6 instructions; loads
spin main ed5658 ed566c # 6 instructions; loads
spin main ed5680 ed5694 # 6 instructions; loads
spin main ed79e8 ed79fc # 6 instructions; loads
spin main ed7a28 ed7a3c # 6 instructions; loads
spin main ed7a50 ed7a64 # 6 instructions; loads
spin main ed7a78 ed7a8c # 6 instructions; loads
spin main ed80ac ed80c0 # 6 instructions; loads
spin main ed80d4 ed80e8 # 6 instructions; loads
spin main ed934c ed9360 # 6 instructions; loads
spin main ed94a0 ed94b4 # 6 instructions; loads
spin main ed9a50 ed9a64 # 6 instructions; loads
spin main ede7fc ede810 # 6 instructions; loads
spin main ee0ce4 ee0cf8 # 6 instructions; loads
spin main eebd28 eebd3c # 6 instructions; loads
spin main ef9ed4 ef9ee8 # 6 instructions; loads
spin main efa7a4 efa7b8 # 6 instructions; loads
spin main efa7d4 efa7e8 # 6 instructions; loads
spin main efad94 efada8 # 6 instructions; loads
spin main efadc4 efadd8 # 6 instructions; loads
spin main efdac8 efdadc # 6 instructions; loads
spin main f01fa4 f01fb8 # 6 instructions; loads
spin main f022e4 f022f8 # 6 instructions; loads
spin main f02394 f023a8 # 6 instructions; loads
spin main f02434 f02448 # 6 instructions; loads
spin main f024e4 f024f8 # 6 instructions; loads
spin main f3c87c f3c890 # 6 instructions; loads
spin main f3d54c f3d560 # 6 instructions; loads
spin main f3dba8 f3dbbc # 6 instructions; loads
spin main f3dbd4 f3dbe8 # 6 instructions; loads
spin main f547a4 f547b8 # 6 instructions; loads
spin main f54d60 f54d74 # 6 instructions; loads
spin main f57494 f574a8 # 6 instructions; loads
spin main f66ef8 f66f0c # 6 instructions; loads
spin main f66f20 f66f34 # 6 instructions; loads
spin main f67170 f67184 # 6 instructions; loads
spin main f671fc f67210 # 6 instructions; loads
spin main f67224 f67238 # 6 instructions; loads
spin main f67254 f67268 # 6 instructions; loads
spin main f684f0 f68504 # 6 instructions; loads
spin main f6a1ec f6a200 # 6 instructions; loads
spin main f6a3f8 f6a40c # 6 instructions; loads
spin main f6afc4 f6afd8 # 6 instructions; loads
spin main f712a4 f712b8 # 6 instructions; loads
spin main f71400 f71414 # 6 instructions; loads
spin main f7155c f71570 # 6 instructions; loads
spin main f716b8 f716cc # 6 instructions; loads
spin main f729dc f729f0 # 6 instructions; loads
spin main f73084 f73098 # 6 instructions; loads
spin main f73148 f7315c # 6 instructions; loads
spin main f76574 f76588 # 6 instructions; loads
spin main f7c8ec f7c900 # 6 instructions; loads
spin main f83bd0 f83be4 # 6 instructions; loads
spin main f870f0 f87104 # 6 instructions; loads
spin main f94eac f94ec0 # 6 instructions; loads
spin main fa6580 fa6594 # 6 instructions; loads
spin main fa65a8 fa65bc # 6 instructions; loads
spin main fabb50 fabb64 # 6 instructions; loads
spin main fabe74 fabe88 # 6 instructions; loads
spin main fac8a0 fac8b4 # 6 instructions; loads
spin main fac8c4 fac8d8 # 6 instructions; loads
spin main face58 face6c # 6 instructions; loads
spin main fc0a28 fc0a3c # 6 instructions; loads
spin main fc0c68 fc0c7c # 6 instructions; loads
spin main fc0e8c fc0ea0 # 6 instructions; loads
spin main fc1054 fc1068 # 6 instructions; loads
spin main fc1178 fc118c # 6 instructions; loads
spin main fcb18c fcb1a8 # 8 instructions; loads
spin main fccfe8 fccffc # 6 instructions; loads
spin main fcd554 fcd640 # 4 instructions; loads
spin main fcdc5c fcdce4 # 4 instructions; loads
spin main fd2d30 fd2d44 # 6 instructions; loads
spin main fd6250 fd6264 # 6 instructions; loads
spin main fdf818 fdf82c # 6 instructions; loads
spin main fdfb30 fdfb44 # 6 instructions; loads
spin main fe0fa8 fe0fbc # 6 instructions; loads
spin main fe1ad8 fe1aec # 6 instructions; loads
spin main fea3a0 fea3b4 # 6 instructions; loads
spin main fead08 fead1c # 6 instructions; loads
spin main feb168 feb17c # 6 instructions; loads
spin main feb1d0 feb1e4 # 6 instructions; loads
spin main ff2af8 ff2b0c # 6 instructions; loads
spin main ff5680 ff5694 # 6 instructions; loads
spin main ff58e0 ff58f4 # 6 instructions; loads
spin main ff9660 ff9674 # 6 instructions; loads
spin main ffacec ffad00 # 6 instructions; loads
spin main 1001500 1001514 # 6 instructions; loads
spin main 1002c28 1002c3c # 6 instructions; loads
spin main 1010b3c 1010b50 # 6 instructions; loads
spin main 1010ef4 1010f08 # 6 instructions; loads
spin main 1011244 1011258 # 6 instructions; loads
spin main 1011544 1011558 # 6 instructions; loads
spin main 1011a9c 1011ab0 # 6 instructions; loads
spin main 1011e34 1011e48 # 6 instructions; loads
spin main 10121b4 10121c8 # 6 instructions; loads
spin main 1012228 101223c # 6 instructions; loads
spin main 1013150 1013164 # 6 instructions; loads
spin main 101349c 10134b0 # 6 instructions; loads
spin main 1013920 1013934 # 6 instructions; loads
spin main 1013b6c 1013b80 # 6 instructions; loads
spin main 1014950 1014964 # 6 instructions; loads
spin main 10149e8 10149fc # 6 instructions; loads
spin main 1015bac 1015bc0 # 6 instructions; loads
spin main 10160c0 10160d4 # 6 instructions; loads
spin main 10166f8 101670c # 6 instructions; loads
spin main 10167b4 10167c8 # 6 instructions; loads
spin main 1016870 1016884 # 6 instructions; loads
spin main 101692c 1016940 # 6 instructions; loads
spin main 10170f0 1017104 # 6 instructions; loads
spin main 10191dc 10191f0 # 6 instructions; loads
spin main 101948c 10194a0 # 6 instructions; loads
spin main 1019778 101978c # 6 instructions; loads
spin main 1019a2c 1019a40 # 6 instructions; loads
spin main 1019cf0 1019d04 # 6 instructions; loads
spin main 1019ff4 101a008 # 6 instructions; loads
spin main 101a054 101a068 # 6 instructions; loads
spin main 101b594 101b5b0 # 8 instructions; loads
spin main 101c7b4 101c7c8 # 6 instructions; loads
spin main 101c7fc 101c810 # 6 instructions; loads
spin main 101c844 101c858 # 6 instructions; loads
spin main 101c88c 101c8a0 # 6 instructions; loads
spin main 101c8d4 101c8e8 # 6 instructions; loads
spin main 1023190 10231a4 # 6 instructions; loads
spin main 10461ac 10461c0 # 6 instructions; loads
spin main 1047278 104728c # 6 instructions; loads
spin main 1050a8c 1050aa0 # 6 instructions; loads
spin main 1055074 10550bc # 3 instructions; loads
spin main 1056bd4 1056be8 # 6 instructions; loads
spin main 106037c 1060390 # 6 instructions; loads
spin main 1066bf4 1066c08 # 6 instructions; loads
spin main 106bc38 106bc4c # 6 instructions; loads
spin main 106be80 106be94 # 6 instructions; loads
spin main 1070220 1070234 # 6 instructions; loads
spin main 1071264 1071278 # 6 instructions; loads
spin main 1071788 107179c # 6 instructions; loads
spin main 1073910 1073924 # 6 instructions; loads
spin main 10815e0 10815f4 # 6 instructions; loads
spin main 108cb28 108cb3c # 6 instructions; loads
spin main 1095678 109568c # 6 instructions; loads
spin main 10956c0 10956d4 # 6 instructions; loads
spin main 109f134 109f148 # 6 instructions; loads
spin main 10bc820 10bc834 # 6 instructions; loads
spin main 10be4c8 10be4dc # 6 instructions; loads
spin main 10be4f0 10be504 # 6 instructions; loads
spin main 10c2d4c 10c2d60 # 6 instructions; loads
spin main 10c3748 10c375c # 6 instructions; loads
spin main 10d5ef8 10d5f0c # 6 instructions; loads
spin main 10da208 10da21c # 6 instructions; loads
spin main 10e3edc 10e3ef0 # 6 instructions; loads
spin main 10e5ee4 10e5ef8 # 6 instructions; loads
spin main 10ec860 10ec874 # 6 instructions; loads
spin main 10ec8dc 10ec8f0 # 6 instructions; loads
spin main 10f1db4 10f1dc8 # 6 instructions; loads
spin main 10ff3b4 10ff3c8 # 6 instructions; loads
spin main 10ff788 10ff79c # 6 instructions; loads
spin main 10ffef0 10fff04 # 6 instructions; loads
spin main 1100500 1100514 # 6 instructions; loads
spin main 1103e80 1103e94 # 6 instructions; loads
spin main 110ca34 110cb68 # 4 instructions; loads
spin main 110cab8 110cb4c # 4 instructions; loads
spin main 1117420 1117434 # 6 instructions; loads
spin main 1117448 111745c # 6 instructions; loads
spin main 111bf5c 111bf70 # 6 instructions; loads
spin main 111c1bc 111c1d0 # 6 instructions; loads
spin main 111c918 111c92c # 6 instructions; loads
spin main 112aef0 112af04 # 6 instructions; loads
spin main 1143e28 1143e3c # 6 instructions; loads
spin main 1145110 1145124 # 6 instructions; loads
spin main 1145138 114514c # 6 instructions; loads
spin main 1149244 1149258 # 6 instructions; loads
spin main 114b868 114b87c # 6 instructions; loads
spin main 114f850 114f864 # 6 instructions; loads
spin main 114fcdc 114fcf0 # 6 instructions; loads
spin main 1150cbc 1150cd0 # 6 instructions; loads
spin main 115aa38 115aa4c # 6 instructions; loads
spin main 117d7c8 117d7dc # 6 instructions; loads
spin main 118a000 118a014 # 6 instructions; loads
spin main 118a3d8 118a3ec # 6 instructions; loads
spin main 118f65c 118f670 # 6 instructions; loads
spin main 1193d30 1193d44 # 6 instructions; loads
spin main 1198130 1198144 # 6 instructions; loads
spin main 1198158 119816c # 6 instructions; loads
spin main 1198e48 1198e5c # 6 instructions; loads
spin main 119c414 119c428 # 6 instructions; loads
spin main 11a0778 11a078c # 6 instructions; loads
spin main 11a0fcc 11a0fe0 # 6 instructions; loads
spin main 11a5884 11a5898 # 6 instructions; loads
spin main 11bbd80 11bbd94 # 6 instructions; loads
spin main 11bd154 11bd168 # 6 instructions; loads
spin main 11bd17c 11bd190 # 6 instructions; loads
spin main 11e5fc8 11e5fdc # 6 instructions; loads
spin main 11e5ff0 11e6004 # 6 instructions; loads
spin main 11ea5b8 11ea5cc # 6 instructions; loads
spin main 11eccf8 11ecd0c # 6 instructions; loads
spin main 11f50d8 11f50ec # 6 instructions; loads
spin main 11f5624 11f5638 # 6 instructions; loads
spin main 12010e8 12010fc # 6 instructions; loads
spin main 120ecc0 120ecd4 # 6 instructions; loads
spin main 12171d8 12171ec # 6 instructions; loads
spin main 1217cd8 1217cec # 6 instructions; loads
spin main 1219124 1219138 # 6 instructions; loads
spin main 1231ca8 1231cbc # 6 instructions; loads
spin main 1231e58 1231e6c # 6 instructions; loads
spin main 1234784 1234798 # 6 instructions; loads
spin main 1234d14 1234d28 # 6 instructions; loads
spin main 1236974 1236988 # 6 instructions; loads
spin main 1236b1c 1236b30 # 6 instructions; loads
spin main 12479cc 12479e0 # 6 instructions; loads
spin main 1261af8 1261b0c # 6 instructions; loads
spin main 1261f20 1261f34 # 6 instructions; loads
spin main 1262200 1262214 # 6 instructions; loads
spin main 1268448 126845c # 6 instructions; loads
spin main 1283a80 1283a94 # 6 instructions; loads
spin main 128ba28 128ba3c # 6 instructions; loads
spin main 128ba90 128baa4 # 6 instructions; loads
spin main 128bc40 128bc54 # 6 instructions; loads
spin main 128bc6c 128bc80 # 6 instructions; loads
spin main 128bc98 128bcac # 6 instructions; loads
spin main 128bcc4 128bcd8 # 6 instructions; loads
spin main 128c468 128c47c # 6 instructions; loads
spin main 128c490 128c4a4 # 6 instructions; loads
spin main 12924ec 1292500 # 6 instructions; loads
spin main 1292e4c 1292e60 # 6 instructions; loads
spin main 1292eb4 1292ec8 # 6 instructions; loads
spin main 129458c 12945a0 # 6 instructions; loads
spin main 12966e0 12966f4 # 6 instructions; loads
spin main 129e62c 129e640 # 6 instructions; loads
spin main 129f954 129f968 # 6 instructions; loads
spin main 12a61b0 12a61c4 # 6 instructions; loads
spin main 12ab4cc 12ab4e0 # 6 instructions; loads
spin main 12ab4f4 12ab508 # 6 instructions; loads
spin main 12c1228 12c123c # 6 instructions; loads
spin main 12c1250 12c1264 # 6 instructions; loads
spin main 12ca068 12ca07c # 6 instructions; loads
spin main 12d040c 12d0420 # 6 instructions; loads
spin main 12e3820 12e3834 # 6 instructions; loads
spin main 12e8e80 12e8e94 # 6 instructions; loads
spin main 12e9640 12e9654 # 6 instructions; loads
spin main 1309bc8 1309bdc # 6 instructions; loads
spin main 1314ed8 1314eec # 6 instructions; loads
spin main 131553c 1315550 # 6 instructions; loads
spin main 1322ccc 1322dac # 11 instructions; loads
spin main 13277f4 1327808 # 6 instructions; loads
spin main 1327ab8 1327acc # 6 instructions; loads
spin main 132a1e0 132a1f4 # 6 instructions; loads
spin main 132b0a8 132b0bc # 6 instructions; loads
spin main 132b87c 132b890 # 6 instructions; loads
spin main 1332fc0 1332fd4 # 6 instructions; loads
spin main 1363450 1363464 # 6 instructions; loads
spin main 13feca8 13fecbc # 6 instructions; loads
spin main 13fee40 13fee54 # 6 instructions; loads
spin main 157c12c 157c140 # 6 instructions; loads
spin main 157dfe0 157dff4 # 6 instructions; loads
spin main 1585a20 1585a34 # 6 instructions; loads
spin main 1585ee0 1585ef4 # 6 instructions; loads
spin main 158c4c8 158c4dc # 6 instructions; loads
spin main 1599a14 1599a28 # 6 instructions; loads
spin main 159aed0 159aee4 # 6 instructions; loads
spin main 159aef8 159af0c # 6 instructions; loads
spin main 15a2a20 15a2a34 # 6 instructions; loads
spin main 15a3228 15a323c # 6 instructions; loads
spin main 15a34e8 15a34fc # 6 instructions; loads
spin main 15a3510 15a3524 # 6 instructions; loads
spin main 15a3f44 15a3f58 # 6 instructions; loads
spin main 15ae678 15ae68c # 6 instructions; loads
spin main 15b8d78 15b8d8c # 6 instructions; loads
spin main 15b9218 15b922c # 6 instructions; loads
spin main 15b9320 15b9334 # 6 instructions; loads
spin main 15b9638 15b964c # 6 instructions; loads
spin main 15c6c9c 15c6cb0 # 6 instructions; loads
spin main 15d5380 15d5394 # 6 instructions; loads
spin main 15e8a0c 15e8a20 # 6 instructions; loads
spin main 15e9ad4 15e9ae8 # 6 instructions; loads
spin main 15e9b7c 15e9b90 # 6 instructions; loads
spin main 15ef1c4 15ef1d8 # 6 instructions; loads
spin main 15ef304 15ef318 # 6 instructions; loads
spin main 15ef32c 15ef340 # 6 instructions; loads
spin main 1607238 160724c # 6 instructions; loads
spin main 160779c 16077b0 # 6 instructions; loads
spin main 16173e0 16173f4 # 6 instructions; loads
spin main 1623748 162375c # 6 instructions; loads
spin main 1627bdc 1627bf0 # 6 instructions; loads
spin main 1627c2c 1627c40 # 6 instructions; loads
spin main 1628eb8 1628ecc # 6 instructions; loads
spin main 16298a8 16298bc # 6 instructions; loads
spin main 16340f8 163410c # 6 instructions; loads
spin main 1636748 163675c # 6 instructions; loads
spin main 1636ed4 1636ee8 # 6 instructions; loads
spin main 163b7f0 163b804 # 6 instructions; loads
spin main 1648354 1648368 # 6 instructions; loads
spin main 1651478 165148c # 6 instructions; loads
spin main 1653730 1653744 # 6 instructions; loads
spin main 1656c5c 1656c70 # 6 instructions; loads
spin main 16596b0 16596b8 # 3 instructions; loads
spin main 16596c0 16596c4 # 2 instructions; loads
spin main 16596f8 1659700 # 3 instructions; loads
spin main 165cb68 165cb7c # 6 instructions; loads
spin main 1677368 167737c # 6 instructions; loads
spin main 16dc224 16dc238 # 6 instructions; loads
spin main 173d61c 173d630 # 6 instructions; loads
spin main 1747354 1747368 # 6 instructions; loads
spin main 1747918 174792c # 6 instructions; loads
spin main 17a0a20 17a0a34 # 6 instructions; loads
spin main 1932774 1932788 # 6 instructions; loads
spin main 1933ea8 1933ebc # 6 instructions; loads
spin main 1962e88 1962e90 # 3 instructions; loads
spin main 1962e98 1962e9c # 2 instructions; loads
spin main 1962f20 1962f28 # 3 instructions; loads
spin main 196300c 1963014 # 3 instructions; loads
spin main 196301c 1963020 # 2 instructions; loads
spin main 19630c8 19630d0 # 3 instructions; loads
spin main 1975850 1975858 # 3 instructions; loads
spin main 1975860 1975864 # 2 instructions; loads
spin main 1975888 1975890 # 3 instructions; loads
spin main 19758c0 19758c8 # 3 instructions; loads
spin main 19758d0 19758d4 # 2 instructions; loads
spin main 19758fc 1975904 # 3 instructions; loads
spin main 19e5048 19e505c # 6 instructions; loads
spin main 1a8b8b4 1a8b8c4 # 5 instructions; loads
spin main 1b65f9c 1b65fb0 # 6 instructions; loads
spin main 1b66058 1b6606c # 6 instructions; loads
spin main 1b66114 1b66128 # 6 instructions; loads
spin main 1b66714 1b66728 # 6 instructions; loads
spin main 1b667d0 1b667e4 # 6 instructions; loads
spin main 1b6688c 1b668a0 # 6 instructions; loads
spin main 1dff370 1dff37c # 4 instructions; loads
spin main 21e6508 21e6530 # 4 instructions; loads
spin main 222a3e0 222a3fc # 8 instructions; loads
spin main 22322d0 22322d8 # 3 instructions; loads
spin main 2232358 2232360 # 3 instructions; loads
spin main 22323e8 2232410 # 3 instructions; loads
spin main 223931c 223932c # 5 instructions; loads
spin main 225dc10 225dc18 # 3 instructions; loads
spin main 225dc20 225dc24 # 2 instructions; loads
spin main 225dcac 225dcb4 # 3 instructions; loads
spin main 225dd38 225dd40 # 3 instructions; loads
spin main 225dd48 225dd4c # 2 instructions; loads
spin main 225dd64 225dd6c # 3 instructions; loads
spin main 22b72dc 22b72f0 # 6 instructions; loads
spin main 22b7440 22b7454 # 6 instructions; loads
spin main 22c60ac 22c60d0 # 4 instructions; loads
spin main 22c619c 22c61a8 # 4 instructions; loads
spin main 233ce40 233ce54 # 6 instructions; loads
spin main 240ccb0 240ccb8 # 3 instructions; loads
spin main 240ccc0 240ccc4 # 2 instructions; loads
spin main 240cd40 240cd48 # 3 instructions; loads
spin main 240d57c 240d584 # 3 instructions; loads
spin main 240d58c 240d590 # 2 instructions; loads
spin main 240d5fc 240d604 # 3 instructions; loads
spin main 240da9c 240daa4 # 3 instructions; loads
spin main 240daac 240dab0 # 2 instructions; loads
spin main 240db3c 240db44 # 3 instructions; loads
spin main 240dc80 240dc88 # 3 instructions; loads
spin main 240dc90 240dc94 # 2 instructions; loads
spin main 240dce8 240dcf0 # 3 instructions; loads
spin main 240dd34 240dd3c # 3 instructions; loads
spin main 240dd44 240dd48 # 2 instructions; loads
spin main 240ddfc 240de04 # 3 instructions; loads
spin main 240de0c 240de10 # 2 instructions; loads
spin main 240de50 240de58 # 3 instructions; loads
spin main 240de80 240de88 # 3 instructions; loads
spin main 240df74 240df7c # 3 instructions; loads
spin main 240df84 240df88 # 2 instructions; loads
spin main 240e068 240e070 # 3 instructions; loads
spin main 240e0fc 240e104 # 3 instructions; loads
spin main 240e10c 240e110 # 2 instructions; loads
spin main 240e1c4 240e1cc # 3 instructions; loads
spin main 240e1e4 240e1e8 # 2 instructions; loads
spin main 240e288 240e290 # 3 instructions; loads
spin main 240e2b8 240e2c0 # 3 instructions; loads
spin main 240e300 240e308 # 3 instructions; loads
spin main 240e310 240e314 # 2 instructions; loads
spin main 240e338 240e340 # 3 instructions; loads
spin main 240e384 240e38c # 3 instructions; loads
spin main 240e394 240e398 # 2 instructions; loads
spin main 240e3c0 240e3c8 # 3 instructions; loads
spin main 240e91c 240e924 # 3 instructions; loads
spin main 240e92c 240e930 # 2 instructions; loads
spin main 240e954 240e95c # 3 instructions; loads
spin main 240f088 240f090 # 3 instructions; loads
spin main 240f098 240f09c # 2 instructions; loads
spin main 240f0c0 240f0c8 # 3 instructions; loads
spin main 2410984 241098c # 3 instructions; loads
spin main 2410994 2410998 # 2 instructions; loads
spin main 2410a18 2410a20 # 3 instructions; loads
spin main 2410a40 2410a48 # 3 instructions; loads
spin main 2410a50 2410a54 # 2 instructions; loads
spin main 2410ad4 2410adc # 3 instructions; loads
spin main 2410e70 2410e78 # 3 instructions; loads
spin main 2410e80 2410e84 # 2 instructions; loads
spin main 2410ed4 2410edc # 3 instructions; loads
spin main 2410f1c 2410f24 # 3 instructions; loads
spin main 2410f2c 2410f30 # 2 instructions; loads
spin main 2410fcc 2410fd4 # 3 instructions; loads
spin main 241102c 2411034 # 3 instructions; loads
spin main 241103c 2411040 # 2 instructions; loads
spin main 24110c4 24110cc # 3 instructions; loads
spin main 2413d28 2413d30 # 3 instructions; loads
spin main 2413d38 2413d3c # 2 instructions; loads
spin main 2413dc8 2413dd0 # 3 instructions; loads
spin main 2413e04 2413e0c # 3 instructions; loads
spin main 2413e14 2413e18 # 2 instructions; loads
spin main 2413e84 2413e8c # 3 instructions; loads
spin main 2416358 2416360 # 3 instructions; loads
spin main 2416368 241636c # 2 instructions; loads
spin main 2416388 2416390 # 3 instructions; loads
spin main 2462470 2462484 # 6 instructions; loads
spin main 2462498 24624ac # 6 instructions; loads
spin main 24651f4 2465208 # 6 instructions; loads
spin main 2465688 246569c # 6 instructions; loads
spin main 2474d20 2474d34 # 6 instructions; loads
spin main 2475154 2475168 # 6 instructions; loads
spin main 2479134 2479148 # 6 instructions; loads
spin main 2479b7c 2479b90 # 6 instructions; loads
spin main 247a378 247a38c # 6 instructions; loads
spin main 2493050 2493064 # 6 instructions; loads
spin main 2886500 2886514 # 6 instructions; loads
spin main 28865bc 28865d0 # 6 instructions; loads
spin main 2886678 288668c # 6 instructions; loads
spin main 2887484 2887498 # 6 instructions; loads
spin main 2887540 2887554 # 6 instructions; loads
spin main 28875fc 2887610 # 6 instructions; loads
spin main 28876b8 28876cc # 6 instructions; loads
spin main 2887774 2887788 # 6 instructions; loads
spin main 2887830 2887844 # 6 instructions; loads
spin main 28878ec 2887900 # 6 instructions; loads
spin main 28879a8 28879bc # 6 instructions; loads
spin main 2887e70 2887e84 # 6 instructions; loads
spin main 2887f2c 2887f40 # 6 instructions; loads
spin main 2a3e540 2a3e548 # 3 instructions; loads
spin main 2a3e550 2a3e554 # 2 instructions; loads
spin main 2a3e58c 2a3e594 # 3 instructions; loads
# subsdk0: 8 spin waits
spin subsdk0 2090 20a4 # 6 instructions; loads
spin subsdk0 278b4 278c8 # 6 instructions; loads
spin subsdk0 bcf28 bcfd0 bcfd8 # 14 instructions; loads
spin subsdk0 bd0d0 bd178 bd180 # 14 instructions; loads
spin subsdk0 c03a0 c03b4 # 6 instructions; loads
spin subsdk0 1bbefc 1bbf08 # 4 instructions; loads
spin subsdk0 1bd838 1bd84c # 6 instructions; loads
spin subsdk0 1c0c68 1c0c74 # 4 instructions; loads
# sdk: 5 spin waits
spin sdk f5218 f5350 # 14 instructions; loads
spin sdk 23a590 23a5d8 # 19 instructions; loads
spin sdk 23a658 23a6a0 # 19 instructions; loads
spin sdk 23a7fc 23a918 # 19 instructions; loads
spin sdk 3d3250 3d3270 # 9 instructions; loads
+6
View File
@@ -32,6 +32,12 @@ site realtime main 8f2398 1e2c1001 bd400120 # fmov s1, #0.5 -> ldr s0, [x9] (g
site realtime main 8f23a8 5e21d800 d503201f # scvtf s0, s0 -> nop
site realtime main 8f23ac 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Equipment-switch cooldown (issue #18, found by JieML1100 on 1.4.3): the player update (main+0x852d24) steps the
# countdown at player state +0x109c through the timer step (main+0xec98cc) only while it is above FLT_EPSILON, but the
# equipment gate (main+0xcae6f8) blocks weapon, shield and bow changes while it is above 0. A measured ratio can leave
# a remainder in (0, FLT_EPSILON] that is never stepped again; stepping while above 0 takes it below 0.
site realtime main 852d28 1e270101 1e2703e1 # fmov s1, w8 (FLT_EPSILON) -> fmov s1, wzr
# Hair and cloth (upstream 290ec9f, 8f8022f): the actor animation update builds a second clock {seconds = factor / 30,
# ratio = factor} per rendered frame. Three inlined places: at the fmul, seconds = measured seconds * factor; at the
# store of the factor into the ratio slot, measured ratio * factor. The call of the shared update (main+0x89822c)
+16
View File
@@ -32,6 +32,13 @@ site realtime main 2daa70 1e201820 d503201f # fdiv s0, s1, s0 -> nop
site realtime main 2daa74 1e2c1001 d503201f # fmov s1, #0.5 -> nop
site realtime main 2daa78 1e210800 d503201f # fmul s0, s0, s1 -> nop
# Equipment-switch cooldown (issue #18, found by JieML1100): the player update (main+0x2b0368) steps the countdown at
# player state +0x109c through the timer step (main+0x1b0834, value += ratio * rate) only while it is above FLT_EPSILON,
# but the equipment gate (main+0x31cc7c) blocks weapon, shield and bow changes while it is above 0. Whole 30 Hz steps
# reach exactly 0; a measured ratio leaves a rounding remainder, and one in (0, FLT_EPSILON] is never stepped again and
# keeps equipment locked until a load. Stepping while above 0 takes the remainder below 0 like any other last step.
site realtime main 2b036c 0f016681 0f000401 # movi v1.2s, #0x34, lsl #24 (FLT_EPSILON) -> movi v1.2s, #0
# Hair and cloth: the actor animation update builds a second clock {seconds = factor / 30, ratio = factor} per
# rendered frame. 1.4.3 inlines it in two actor paths, where the factor was just stored to the ratio slot: the
# replacement stores measured ratio * factor there and computes measured seconds * factor. Two other callers share the
@@ -85,6 +92,15 @@ site aspect main b43460 bd406ac0 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
site aspect main 1ff04f4 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 253fbb0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# Field of view (runtime/resolution.c res_fov_live writes TOTK_FOV / 100 to page +60 and switches the set). The same two
# camera routines as the aspect set load the camera's vertical view angle (radians) a few words before the ratio and
# hand it to sead's perspective projection (+0xa0, then sin / cos / tan of half of it at +0xa4..+0xac). The replacement
# loads the angle as before and multiplies it by the factor on the page; s8 holds the factor because the next
# instruction (fmul s8, s0, s1) overwrites it. Zooms the game makes itself (the bow, the scope) scale with it.
set fov TOTK_FOV_PATCH 0
site fov main 628c3c bd413e60 f81f0fea @page:x10 bd413e60 bd403d48 1e280800 f84107ea # ldr s0, [x19, #0x13c] (camera update)
site fov main b4342c bd4066c0 f81f0fea @page:x10 bd4066c0 bd403d48 1e280800 f84107ea # ldr s0, [x22, #0x64] (camera setup)
# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c):
# every call of ZSTD_decompressMultiFrame (main+0x3a1f0: dctx, dst, capacity, src, size, dict, dict size, ddict) and of
# its wrapper ZSTD_decompress_usingDDict (main+0x3a1e0, which tail-branches to it) goes to a host function on the patch
+2 -1
View File
@@ -13,7 +13,8 @@ the generator), each <replacement> an instruction word (hex) or `@page:x<n>`, wh
encoded for the site's address. The patch page is the first 4 KiB page after main's image (layout.py leaves the rest
of the 2 MiB-aligned module slot free); the runtime commits it and owns its contents. Words in use, by byte offset:
+0..15 the frame-rate step (runtime/frame_rate.c), +16..55 the output, swapchain and scene sizes
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect). A new
(runtime/resolution.c res_init), +56..59 the camera's aspect ratio (runtime/resolution.c res_init, set aspect), +60..63
the field-of-view factor (runtime/resolution.c res_fov_live, set fov). A new
set takes words outside these.
The replacement words are lifted at the site's address in place of the original instruction, and the generated C reads
+14 -12
View File
@@ -3,11 +3,12 @@
// da35b83 (MIT, Copyright (c) 2026 Peiikai).
// The boot driver: the guest main thread runs the game's own nn::init::Start
// with three host callbacks, the second of which runs DT_INIT of main, subsdk0 and sdk; Start then calls nnMain.
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call:
// SetFocusHandlingMode called from main+0xe7a954 (1.4.3: +0xbdb48c) in nnMain, every module's constructor flag
// reads 2
// nvnBootstrapLoader called from main+0xe7eb2c (1.4.3: +0xbe0360) in the graphics bring-up; logs the run
// statistics, exits 0
// --boot checks two checkpoints on the way and ends at the first nvnBootstrapLoader call, at the return address of
// each in main (1.1.1, 1.2.1, 1.4.3):
// SetFocusHandlingMode main+0xeb8c14 (1.2.1: +0xe7a954, 1.4.3: +0xbdb48c) in nnMain, every module's constructor
// flag reads 2
// nvnBootstrapLoader main+0xebcdec (1.2.1: +0xe7eb2c, 1.4.3: +0xbe0360) in the graphics bring-up; logs the
// run statistics, exits 0
// With --seconds S the run continues past that call; rt_frame_presented ends it once the world clock passed S.
#include <stdio.h>
#include <stdlib.h>
@@ -22,18 +23,19 @@
#include "platform/platform.h"
#define SYM_INIT_START "_ZN2nn4init5StartEmmPFvvES2_S2_"
// main: return addresses of nnMain's SetFocusHandlingMode call (1.2.1 from this port's analysis of main; 1.4.3 from
// MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : 0xe7eb2cull)
// main: return addresses of nnMain's SetFocusHandlingMode call (1.1.1 and 1.2.1 from this port's analysis of main;
// 1.4.3 from MacNativeGames' scripts/verify_boot.py) and of the first nvnBootstrapLoader call, per game version.
#define CHECKPOINT_RET (RT_V143 ? 0xbdb48cull : RT_V111 ? 0xeb8c14ull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V111 ? 0xebcdecull : 0xe7eb2cull)
#define CTOR_DONE 2 // a module's constructor flag after its DT_INIT ran the init_array
// The init_array sizes (1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check expects
// the same.
// The init_array sizes (1.1.1 and 1.2.1: counted in the modules; 1.4.3: DT_INIT_ARRAYSZ); MacNativeGames' boot check
// expects the same. The three tables carry the same rows, so cb_module_inits walks them by the size of one.
typedef struct { const char *name; uint64_t entries; } InitCount;
static const InitCount inits_111[] = {{"main", 10668}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_121[] = {{"main", 10673}, {"subsdk0", 45}, {"sdk", 116}};
static const InitCount inits_143[] = {{"main", 10717}, {"subsdk0", 45}, {"sdk", 83}};
#define expected_inits (RT_V143 ? inits_143 : inits_121)
#define expected_inits (RT_V143 ? inits_143 : RT_V111 ? inits_111 : inits_121)
static volatile int32_t inits_done, checkpoint_seen;
uint64_t rt_guest_start_ns;
+13 -7
View File
@@ -35,9 +35,9 @@ static const char *const known[] = {
"BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS",
"BIND_PLUS", "BIND_R", "BIND_RCLICK", "BIND_RIGHT", "BIND_RSTICK_DOWN", "BIND_RSTICK_LEFT", "BIND_RSTICK_RIGHT", "BIND_RSTICK_UP",
"BIND_SKIP", "BIND_STATS", "BIND_UP", "BIND_X", "BIND_Y",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DEVICE_PROFILE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA", "COLOR_PRESET", "COLOR_SATURATION", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DEVICE_PROFILE", "DUMP", "DUMP_BUF", "DUMP_DIR", "DUMP_LOG",
"EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "FLIGHT_SPIKE_MS",
"FLIGHT_THUMBS", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
"FLIGHT_THUMBS", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "GPL", "GPL_DRAW_BUILD", "GPU",
"GPU_INDEX", "GPU_NAME", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
@@ -50,7 +50,7 @@ static const char *const known[] = {
"SHADER_MEMO", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR", "SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
"WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS", "ZSTD_CHECK", "ZSTD_NATIVE",
};
// The totk.ini section a key cfg_set may write goes to when the file lacks it (totk.ini.example's sections).
@@ -59,7 +59,9 @@ static const struct { const char *key, *section; } sections[] = {
{"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"},
{"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"},
{"GPU_INDEX", "display"}, {"OUTPUT_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"},
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
{"COLOR_GAMMA", "display"},
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"},
@@ -80,7 +82,7 @@ static const struct { const char *key, *section; } sections[] = {
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
};
// The settings the menu shows: the value in effect when the key is unset, and what the key accepts. The only place a
@@ -95,7 +97,11 @@ static const CfgMeta metas[] = {
{"PERF_OVERLAY", "off", "off|fps|full", LIVE},
{"OUTPUT_RES", "1920x1080", NULL, RESTART}, {"RENDER_SCALE", "game", "game|50|67|75|100|125|150|200", RESTART},
{"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
// colour adjustment (vk_color.c); the preset is the menu's, the four values are what the present uses
{"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE},
{"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE},
{"COLOR_GAMMA", "1", "0.8..1.2", LIVE},
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
// Game
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
@@ -131,7 +137,7 @@ static const CfgMeta metas[] = {
{"VK_DYN_A2C", "", "0|1", RESTART}, {"VK_SUBGROUP_PIN", "1", "0|1", RESTART},
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART},
{"WB_FENCE_LIMIT", "65536", "0..1073741824", RESTART}, {"WB_EXACT", "1", "0|1", RESTART},
{"INPUT_RECORD", "", NULL, RESTART},
};
+71
View File
@@ -0,0 +1,71 @@
// Generated by tools/build_shaders.py from runtime/shaders/color_grade.comp; do not edit.
// source sha256 c93d6ac75667bdee5442d7e94fbf0bd6e7fe7250caa98974424aead10f554106
#pragma once
#include <stdint.h>
static const uint32_t color_grade_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x00000073, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000000d, 0x00060010, 0x00000004,
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000000d, 0x0000000b, 0x0000001c,
0x00030047, 0x00000015, 0x00000002, 0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000,
0x00050048, 0x00000015, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000015, 0x00000002,
0x00000023, 0x00000010, 0x00040047, 0x00000029, 0x00000021, 0x00000000, 0x00040047, 0x00000029,
0x00000022, 0x00000000, 0x00030047, 0x0000005d, 0x00000019, 0x00040047, 0x0000005d, 0x00000021,
0x00000001, 0x00040047, 0x0000005d, 0x00000022, 0x00000000, 0x00040047, 0x0000006a, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00040015, 0x00000006,
0x00000020, 0x00000001, 0x00040017, 0x00000007, 0x00000006, 0x00000002, 0x00040015, 0x0000000a,
0x00000020, 0x00000000, 0x00040017, 0x0000000b, 0x0000000a, 0x00000003, 0x00040020, 0x0000000c,
0x00000001, 0x0000000b, 0x0004003b, 0x0000000c, 0x0000000d, 0x00000001, 0x00040017, 0x0000000e,
0x0000000a, 0x00000002, 0x00030016, 0x00000013, 0x00000020, 0x00040017, 0x00000014, 0x00000013,
0x00000004, 0x0005001e, 0x00000015, 0x00000007, 0x00000007, 0x00000014, 0x00040020, 0x00000016,
0x00000009, 0x00000015, 0x0004003b, 0x00000016, 0x00000017, 0x00000009, 0x0004002b, 0x00000006,
0x00000018, 0x00000001, 0x00040020, 0x00000019, 0x00000009, 0x00000007, 0x00020014, 0x0000001c,
0x00040017, 0x0000001d, 0x0000001c, 0x00000002, 0x00040017, 0x00000023, 0x00000013, 0x00000003,
0x00090019, 0x00000026, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
0x00000000, 0x0003001b, 0x00000027, 0x00000026, 0x00040020, 0x00000028, 0x00000000, 0x00000027,
0x0004003b, 0x00000028, 0x00000029, 0x00000000, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
0x0004002b, 0x00000006, 0x00000033, 0x00000002, 0x0004002b, 0x0000000a, 0x00000034, 0x00000000,
0x00040020, 0x00000035, 0x00000009, 0x00000013, 0x0004002b, 0x00000013, 0x0000003c, 0x3f000000,
0x0004002b, 0x0000000a, 0x0000003f, 0x00000001, 0x0004002b, 0x00000013, 0x00000046, 0x3e59b3d0,
0x0004002b, 0x00000013, 0x00000047, 0x3f371759, 0x0004002b, 0x00000013, 0x00000048, 0x3d93dd98,
0x0006002c, 0x00000023, 0x00000049, 0x00000046, 0x00000047, 0x00000048, 0x0004002b, 0x0000000a,
0x0000004d, 0x00000002, 0x0004002b, 0x00000013, 0x00000053, 0x00000000, 0x0006002c, 0x00000023,
0x00000054, 0x00000053, 0x00000053, 0x00000053, 0x0004002b, 0x0000000a, 0x00000056, 0x00000003,
0x00090019, 0x0000005b, 0x00000013, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
0x00000002, 0x00040020, 0x0000005c, 0x00000000, 0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d,
0x00000000, 0x0004002b, 0x00000013, 0x00000061, 0x3f800000, 0x0004002b, 0x0000000a, 0x00000069,
0x00000008, 0x0006002c, 0x0000000b, 0x0000006a, 0x00000069, 0x00000069, 0x0000003f, 0x0006002c,
0x00000023, 0x00000071, 0x0000003c, 0x0000003c, 0x0000003c, 0x0006002c, 0x00000023, 0x00000072,
0x00000061, 0x00000061, 0x00000061, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x000300f7, 0x0000006b, 0x00000000, 0x000300fb, 0x00000034, 0x0000006c,
0x000200f8, 0x0000006c, 0x0004003d, 0x0000000b, 0x0000000f, 0x0000000d, 0x0007004f, 0x0000000e,
0x00000010, 0x0000000f, 0x0000000f, 0x00000000, 0x00000001, 0x0004007c, 0x00000007, 0x00000011,
0x00000010, 0x00050041, 0x00000019, 0x0000001a, 0x00000017, 0x00000018, 0x0004003d, 0x00000007,
0x0000001b, 0x0000001a, 0x000500af, 0x0000001d, 0x0000001e, 0x00000011, 0x0000001b, 0x0004009a,
0x0000001c, 0x0000001f, 0x0000001e, 0x000300f7, 0x00000021, 0x00000000, 0x000400fa, 0x0000001f,
0x00000020, 0x00000021, 0x000200f8, 0x00000020, 0x000200f9, 0x0000006b, 0x000200f8, 0x00000021,
0x0004003d, 0x00000027, 0x0000002a, 0x00000029, 0x00050041, 0x00000019, 0x0000002d, 0x00000017,
0x0000002c, 0x0004003d, 0x00000007, 0x0000002e, 0x0000002d, 0x00050080, 0x00000007, 0x0000002f,
0x00000011, 0x0000002e, 0x00040064, 0x00000026, 0x00000030, 0x0000002a, 0x0007005f, 0x00000014,
0x00000031, 0x00000030, 0x0000002f, 0x00000002, 0x0000002c, 0x0008004f, 0x00000023, 0x00000032,
0x00000031, 0x00000031, 0x00000000, 0x00000001, 0x00000002, 0x00060041, 0x00000035, 0x00000036,
0x00000017, 0x00000033, 0x00000034, 0x0004003d, 0x00000013, 0x00000037, 0x00000036, 0x00060050,
0x00000023, 0x00000039, 0x00000037, 0x00000037, 0x00000037, 0x00050081, 0x00000023, 0x0000003a,
0x00000032, 0x00000039, 0x00050083, 0x00000023, 0x0000003e, 0x0000003a, 0x00000071, 0x00060041,
0x00000035, 0x00000040, 0x00000017, 0x00000033, 0x0000003f, 0x0004003d, 0x00000013, 0x00000041,
0x00000040, 0x0005008e, 0x00000023, 0x00000042, 0x0000003e, 0x00000041, 0x00050081, 0x00000023,
0x00000044, 0x00000042, 0x00000071, 0x00050094, 0x00000013, 0x0000004a, 0x00000044, 0x00000049,
0x00060050, 0x00000023, 0x0000004b, 0x0000004a, 0x0000004a, 0x0000004a, 0x00060041, 0x00000035,
0x0000004e, 0x00000017, 0x00000033, 0x0000004d, 0x0004003d, 0x00000013, 0x0000004f, 0x0000004e,
0x00060050, 0x00000023, 0x00000050, 0x0000004f, 0x0000004f, 0x0000004f, 0x0008000c, 0x00000023,
0x00000051, 0x00000001, 0x0000002e, 0x0000004b, 0x00000044, 0x00000050, 0x0007000c, 0x00000023,
0x00000055, 0x00000001, 0x00000028, 0x00000051, 0x00000054, 0x00060041, 0x00000035, 0x00000057,
0x00000017, 0x00000033, 0x00000056, 0x0004003d, 0x00000013, 0x00000058, 0x00000057, 0x00060050,
0x00000023, 0x00000059, 0x00000058, 0x00000058, 0x00000058, 0x0007000c, 0x00000023, 0x0000005a,
0x00000001, 0x0000001a, 0x00000055, 0x00000059, 0x0004003d, 0x0000005b, 0x0000005e, 0x0000005d,
0x0008000c, 0x00000023, 0x00000064, 0x00000001, 0x0000002b, 0x0000005a, 0x00000054, 0x00000072,
0x00050051, 0x00000013, 0x00000065, 0x00000064, 0x00000000, 0x00050051, 0x00000013, 0x00000066,
0x00000064, 0x00000001, 0x00050051, 0x00000013, 0x00000067, 0x00000064, 0x00000002, 0x00070050,
0x00000014, 0x00000068, 0x00000065, 0x00000066, 0x00000067, 0x00000061, 0x00040063, 0x0000005e,
0x00000011, 0x00000068, 0x000200f9, 0x0000006b, 0x000200f8, 0x0000006b, 0x000100fd, 0x00010038,
};
+7 -3
View File
@@ -161,10 +161,14 @@ static uint64_t read_ptr(uint64_t a) {
uint64_t p = 0;
return read_guest(a, &p, 8) ? p : 0;
}
// 1.2.1: [main+0x4728538] (1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the resident actors; the one
// named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h TOTK_GAME_V121/V143).
// 1.2.1: [main+0x4728538] (1.1.1: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13 lists the
// resident actors; the one named "Player" has its position at +0x2b4 (MacNativeGames world_survey.c, game_version.h
// TOTK_GAME_V121/V143). The 1.1.1 slot was measured from its own dereference chain: 181 scene accesses in that build
// read it, and one of them reproduces this walk (the 14-component check, component 13, the 0x70 stride and the name
// compare) instruction for instruction.
static int player_pos(float xyz[3]) {
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : 0x4728538)), scene = module ? read_ptr(module + 0x1e8) : 0;
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V111 ? 0x4630a68 : 0x4728538)),
scene = module ? read_ptr(module + 0x1e8) : 0;
int32_t count = 0;
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
+185 -46
View File
@@ -146,31 +146,36 @@ static int remove_tree(const char *path) {
}
// ---- save transactions
// The save mount's root is a working copy, made from the committed save when the first save mount appears. Commit
// copies it to a staging directory and swaps that in for the committed save:
// The save mount's root is a working copy, made from the newest save when the first save mount appears. Commit copies
// it to a staging directory, which a rename makes a finished commit, and swaps that in for the committed save:
//
// <profile>/save/<uid>/ committed save: what the next start loads
// <profile>/save/<uid>/ committed save: what the next start loads when no finished commit waits
// <profile>/save-tx/work/ working copy, the save mount's root; deleted at the last Unmount and at start
// <profile>/save-tx/new/ Commit's staging copy of the working copy
// <profile>/save-tx/ready-<n>/ a finished commit not swapped in yet: the newest save
// <profile>/save-tx/old/ the committed save a Commit replaced, on its way to save-backup
//
// Commit: (1) copy work to new and flush every file (FlushFileBuffers) and directory, a file equal to the committed
// one as a hard link to it; (2) rename save/<uid> to old;
// (3) rename new to save/<uid>, the commit point; (4) move old to save-backup/<UTC time> and keep the newest three.
// Both renames are durable (plat_fs_rename). The game's Commit returns 0 once steps (1) to (4) are
// queued; a thread runs them (95-135 ms on the game thread before, in the 1.4.3 fight save) while every change to the
// working copy waits in save_touched, and Unmount and the next Commit wait too. A crash in that time loses this Commit
// only, and a failed one is logged instead of returned. save/<uid> is renamed away only after new is complete
// on disk, so the recovery rule at start (save_recover) is:
// save/<uid> present it is the committed save (the old one if the crash came before (3), the new one after);
// new, if present, is an unfinished copy and is deleted (rollback);
// save/<uid> absent the crash came between (2) and (3) and new is complete: rename it to save/<uid> (roll
// forward); with no new either, old goes back to save/<uid>.
// Commit: (1) copy work to new and flush every file (FlushFileBuffers) and directory, a file equal to the newest
// save's as a hard link to it; (2) rename new to ready-<n>, the commit point, and delete the ready-<n> it supersedes;
// (3) rename save/<uid> to old; (4) rename ready-<n> to save/<uid>; (5) move old to save-backup/<UTC time> and keep
// the newest three. Every rename is durable (plat_fs_rename). Windows refuses (3) while any file under save/<uid> is
// open, in this or another process (a virus scanner, Explorer's preview, a sync tool), and a folder an elevated run
// made can refuse it too. ready-<n> then stays the newest save: later working copies are made from it, and the next
// Commit and the next start try (3) to (5) again. The game's Commit returns 0 once the steps are queued; a thread runs
// them (95-135 ms on the game thread before, in the 1.4.3 fight save) while every change to the working copy waits in
// save_touched, and Unmount and the next Commit wait too. A crash in that time loses this Commit only, and a failed
// one is logged instead of returned. The recovery rule at start (save_recover):
// new present an unfinished copy: deleted (rollback). An older build renamed save/<uid> away before its
// finished copy: with save/<uid> and ready-<n> absent, new is that copy and becomes save/<uid>;
// ready-<n> present the highest n is the newest save and (3) to (5) run (roll forward); the others are deleted;
// save/<uid> absent with no ready-<n> either, old goes back to save/<uid>.
// Then old, if present, goes to save-backup, and work (changes no Commit took) is deleted.
#define SAVE_BACKUPS 3
#define COPY_CHUNK (1u << 20)
static char tx_root[1100], backup_root[1100], new_root[1100], old_root[1100];
static char ready_root[1100]; // the newest finished commit not swapped in, empty when none
static unsigned ready_seq; // the highest ready-<n> this run made or found
static PlatMutex tx_lock = PLAT_MUTEX_INIT; // taken before lock, never inside it
static int save_mounts; // M_SAVE mounts: the working copy exists while this is above 0
@@ -183,10 +188,11 @@ static int crashed(void) {
static int exists(const char *p) { return plat_fs_kind(p) != PLAT_FS_MISSING; }
// Rename a directory, durably; retried for up to 2 s while another process (a virus scanner, the indexer) holds a
// file in it. 0 on success.
static int move_dir(const char *from, const char *to) {
for (int i = 0; i < 40; i++) {
// Rename a directory, durably; retried every 50 ms, tries times in all, while another process (a virus scanner, the
// indexer) holds a file in it. 0 on success.
#define MOVE_TRIES 40 // 2 s
static int move_dir(const char *from, const char *to, int tries) {
for (int i = 0; i < tries; i++) {
int e = plat_fs_rename(from, to, 1);
if (!e) return 0;
if (e != PLAT_FS_BUSY) return -1;
@@ -274,6 +280,44 @@ static int copy_tree(const char *src, const char *dst, const char *base, int dur
return bad;
}
static int listed_cmp(const void *a, const void *b) {
return strcmp(((const Listed *)a)->name, ((const Listed *)b)->name);
}
// Whether paths a and b are one file (hard links of it).
static int same_inode(const char *a, const char *b) {
PlatFile *h[2] = {plat_fs_open(a, 0, NULL), plat_fs_open(b, 0, NULL)};
PlatFileId ia, ib;
int same = h[0] && h[1] && plat_file_id(h[0], &ia) && plat_file_id(h[1], &ib) && ia.volume == ib.volume &&
ia.file_id == ib.file_id;
for (int i = 0; i < 2; i++)
if (h[i]) plat_file_close(h[i]);
return same;
}
// Whether tree a holds the names and kinds of tree b and each file of it as a hard link: what copy_tree makes from a
// working copy whose every file equals base's.
static int same_tree(const char *a, const char *b) {
Listing la, lb;
int ea = list_dir(a, &la), eb = list_dir(b, &lb);
int same = !ea && !eb && la.n == lb.n;
if (same) {
qsort(la.e, (size_t)la.n, sizeof *la.e, listed_cmp);
qsort(lb.e, (size_t)lb.n, sizeof *lb.e, listed_cmp);
}
for (int i = 0; same && i < la.n; i++) {
same = !strcmp(la.e[i].name, lb.e[i].name) && la.e[i].kind == lb.e[i].kind && la.e[i].kind != PLAT_FS_LINK;
if (!same) break;
char *pa = join(a, la.e[i].name), *pb = join(b, lb.e[i].name);
same = la.e[i].kind == PLAT_FS_DIR ? same_tree(pa, pb) : same_inode(pa, pb);
free(pa);
free(pb);
}
listing_free(la.e, la.n);
listing_free(lb.e, lb.n);
return same;
}
static int name_cmp(const void *a, const void *b) { return strcmp(a, b); }
// The backups' folder names (digits first), sorted: oldest first, as rotate_backup names them in creation order.
@@ -312,7 +356,7 @@ static void rotate_backup(void) {
while (strcmp(name, newest) <= 0 && k < 100000);
}
char *dst = join(backup_root, name);
if (move_dir(old_root, dst)) {
if (move_dir(old_root, dst, MOVE_TRIES)) {
rt_log("fs: cannot move the replaced save to save-backup; deleted");
remove_tree(old_root);
}
@@ -325,22 +369,82 @@ static void rotate_backup(void) {
}
}
// The newest save: the finished commit waiting to be swapped in, else the committed save.
static const char *newest_save(void) { return *ready_root ? ready_root : save_root; }
// Steps (3) to (5): swap the finished commit ready_root in for the committed save. 0 on success; on failure
// ready_root stays the newest save. (3) is tried for 200 ms only: the commit is safe in ready_root, the next Commit
// or start tries again, and the game's next change to its save data waits for this one.
#define SWAP_TRIES 4
static int swap_in(void) {
if (!*ready_root) return 0;
rotate_backup(); // an old left by a failed (5): (3) needs its name
if (exists(save_root) && move_dir(save_root, old_root, SWAP_TRIES)) {
char why[512];
plat_fs_busy_reason(save_root, why, sizeof why);
rt_log("fs: save commit: the committed save cannot be replaced (%s); the commit waits in %s and the next "
"Commit or start retries", *why ? why : "a file in it is in use", ready_root);
return -1;
}
if (crashed()) return -1;
if (move_dir(ready_root, save_root, MOVE_TRIES)) {
rt_log("fs: save commit: cannot move the finished commit to the committed save; it waits in %s", ready_root);
move_dir(old_root, save_root, MOVE_TRIES); // else save/<uid> stays absent and the next start rolls forward
return -1;
}
*ready_root = 0;
if (crashed()) return -1;
rotate_backup();
return 0;
}
// The number n of a "ready-<n>" name, 0 for any other name.
static unsigned ready_number(const char *name) {
char *end;
if (strncmp(name, "ready-", 6) || name[6] < '0' || name[6] > '9') return 0;
unsigned long n = strtoul(name + 6, &end, 10);
return *end || n > 0xffffffffu ? 0 : (unsigned)n;
}
// Startup: the newest finished commit into ready_root; the older ones, which it supersedes, are deleted.
static void ready_scan(void) {
Listing l;
list_dir(tx_root, &l);
*ready_root = 0, ready_seq = 0;
for (int i = 0; i < l.n; i++)
if (l.e[i].kind == PLAT_FS_DIR && ready_number(l.e[i].name) > ready_seq) ready_seq = ready_number(l.e[i].name);
for (int i = 0; i < l.n; i++) {
unsigned n = l.e[i].kind == PLAT_FS_DIR ? ready_number(l.e[i].name) : 0;
char *p = n ? join(tx_root, l.e[i].name) : NULL;
if (n == ready_seq && p) snprintf(ready_root, sizeof ready_root, "%s", p);
else if (p) remove_tree(p);
free(p);
}
listing_free(l.e, l.n);
}
// Startup: finish or roll back an interrupted Commit and drop uncommitted changes (the rule above).
static void save_recover(void) {
const char *did = "nothing to recover";
ready_scan();
int has_save = exists(save_root);
if (!has_save && exists(new_root)) {
if (move_dir(new_root, save_root)) rt_fatal("fs: cannot complete an interrupted save commit");
if (exists(new_root) && !has_save && !*ready_root) {
if (move_dir(new_root, save_root, MOVE_TRIES)) rt_fatal("fs: cannot complete an interrupted save commit");
did = "completed an interrupted commit";
} else if (!has_save && exists(old_root)) {
if (move_dir(old_root, save_root)) rt_fatal("fs: cannot roll back an interrupted save commit");
did = "rolled back an interrupted commit";
has_save = 1;
} else if (exists(new_root)) {
if (remove_tree(new_root)) rt_fatal("fs: cannot delete an unfinished save commit");
did = "rolled back an unfinished commit";
}
if (!has_save && !*ready_root && exists(old_root)) {
if (move_dir(old_root, save_root, MOVE_TRIES)) rt_fatal("fs: cannot roll back an interrupted save commit");
did = "rolled back an interrupted commit";
} else if (exists(old_root))
did = "moved the save an interrupted commit replaced to save-backup";
rotate_backup();
if (*ready_root)
did = swap_in() ? "a finished commit still waits in save-tx; the game loads it from there"
: "completed a finished commit";
if (exists(work_root)) {
if (remove_tree(work_root)) rt_fatal("fs: cannot delete the old save working copy");
if (!strcmp(did, "nothing to recover"))
@@ -349,11 +453,11 @@ static void save_recover(void) {
rt_log("host save recovery: %s", did);
}
// The first save mount: a fresh working copy of the committed save.
// The first save mount: a fresh working copy of the newest save.
static void work_create(void) {
int n = 0, linked = 0;
uint64_t bytes = 0;
if (remove_tree(work_root) || copy_tree(save_root, work_root, NULL, 0, &n, &linked, &bytes))
if (remove_tree(work_root) || copy_tree(newest_save(), work_root, NULL, 0, &n, &linked, &bytes))
rt_fatal("fs: cannot make the save working copy");
rt_log("host save working copy: %d files, %.1f MiB", n, bytes / 1048576.0);
}
@@ -364,38 +468,45 @@ static void work_discard(void) {
if (remove_tree(work_root)) rt_log("fs: save working copy not fully deleted (a file still open?)");
}
// Commit of the save mount, tx_lock held. Steps (1) to (4) above; every failure before (3) leaves the committed save.
// Commit of the save mount, tx_lock held. Steps (1) to (5) above; a failure before (2) leaves the newest save as it
// was, one after it leaves the commit waiting in ready_root.
static uint32_t save_commit_locked(void) {
uint64_t t0 = plat_now_ns(), bytes = 0;
int nfiles = 0, linked = 0;
tx_step = tx_dead = 0;
rotate_backup(); // an old left by a failed (4)
const char *base = exists(save_root) ? save_root : NULL;
const char *base = exists(newest_save()) ? newest_save() : NULL;
if (remove_tree(new_root) || copy_tree(work_root, new_root, base, 1, &nfiles, &linked, &bytes)) {
if (tx_dead) return FS_R_INVALID_ARGUMENT;
rt_log("fs: save commit: copying the working copy failed; committed save unchanged");
rt_log("fs: save commit: copying the working copy failed; the newest save is unchanged");
remove_tree(new_root);
return FS_R_INVALID_ARGUMENT;
}
if (crashed()) return FS_R_INVALID_ARGUMENT;
if (exists(save_root) && move_dir(save_root, old_root)) {
rt_log("fs: save commit: cannot move the committed save; committed save unchanged");
if (base && linked == nfiles && same_tree(new_root, base)) { // the game rewrote equal bytes: no save, no backup
remove_tree(new_root);
plat_xchg32(&save_dirty, 0);
int waits = swap_in();
if (tx_dead) return FS_R_INVALID_ARGUMENT;
rt_log("fs commit save: %d files, none changed, in %.0f ms%s", nfiles, (plat_now_ns() - t0) / 1e6,
waits ? "; the newest save waits in save-tx" : "");
return 0;
}
char ready[1100];
snprintf(ready, sizeof ready, "%s/ready-%010u", tx_root, ready_seq + 1);
if (move_dir(new_root, ready, MOVE_TRIES)) {
rt_log("fs: save commit: cannot finish the staging copy; the newest save is unchanged");
remove_tree(new_root);
return FS_R_INVALID_ARGUMENT;
}
if (crashed()) return FS_R_INVALID_ARGUMENT;
if (move_dir(new_root, save_root)) {
if (move_dir(old_root, save_root))
rt_fatal("fs: save commit: cannot swap in the new save; the next start completes it");
rt_log("fs: save commit: cannot swap in the new save; committed save unchanged");
remove_tree(new_root);
return FS_R_INVALID_ARGUMENT;
}
if (crashed()) return FS_R_INVALID_ARGUMENT;
rotate_backup();
ready_seq++;
if (*ready_root) remove_tree(ready_root); // superseded: the working copy it was made from held all of it
snprintf(ready_root, sizeof ready_root, "%s", ready);
plat_xchg32(&save_dirty, 0);
rt_log("fs commit save: %d files, %d changed (%.1f MiB written), %d unchanged linked, in %.0f ms", nfiles,
nfiles - linked, bytes / 1048576.0, linked, (plat_now_ns() - t0) / 1e6);
if (crashed()) return FS_R_INVALID_ARGUMENT;
int waits = swap_in();
if (tx_dead) return FS_R_INVALID_ARGUMENT;
rt_log("fs commit save: %d files, %d changed (%.1f MiB written), %d unchanged linked, in %.0f ms%s", nfiles,
nfiles - linked, bytes / 1048576.0, linked, (plat_now_ns() - t0) / 1e6, waits ? "; waits in save-tx" : "");
return 0;
}
@@ -1401,7 +1512,8 @@ static void probe_save_tx(const uint64_t *uid) {
else mixed++;
CHECK(ok && (va == vb) && (va == committed), "crash at commit step %d: tx_a %d, tx_b %d (old %d, new %d)", k,
va, vb, committed, next);
CHECK(!exists(new_root) && !exists(old_root), "crash at commit step %d: staging left after recovery", k);
CHECK(!exists(new_root) && !exists(old_root) && !*ready_root, "crash at commit step %d: staging left after "
"recovery", k);
}
CHECK(steps >= 4 && olds && news && !mixed, "crash points: %d (%d old, %d new, %d mixed)", steps, olds, news,
mixed);
@@ -1416,6 +1528,13 @@ static void probe_save_tx(const uint64_t *uid) {
tx_links_committed("tx_a.sav"), tx_links_committed("tx_b.sav"));
committed = gen;
// a Commit that changed no byte (the game rewrites its files at start) makes no new save and no backup
static char backups_before[64][48], backups_after[64][48];
int nbefore = backup_names(backups_before);
CHECK(tx_write(a, committed) && tx_commit() == 0 && tx_read_committed("tx_a.sav") == committed &&
backup_names(backups_after) == nbefore && !memcmp(backups_before, backups_after, sizeof backups_before),
"a Commit of unchanged bytes made a backup");
// the background commit: a write right after Commit waits for it and stays out of it; an Unmount right after
// Commit waits for it and keeps it
int queued = ++gen;
@@ -1428,6 +1547,26 @@ static void probe_save_tx(const uint64_t *uid) {
"an Unmount right after Commit lost it");
committed = gen;
// a file of the committed save held open by another program (a virus scanner, Explorer's preview, a sync tool):
// Windows refuses to rename a directory with an open file under it, so the committed save cannot be swapped out.
// The game and a restart still see every Commit, and the first start after the file is closed swaps them in.
char *held_path = join(save_root, "tx_b.sav");
PlatFile *held = plat_fs_open(held_path, 0, NULL);
free(held_path);
int pending = ++gen;
CHECK(held && tx_write(a, pending) && tx_commit() == 0, "Commit with a committed file held open");
probe_restart(uid);
CHECK(tx_read(a) == pending, "a Commit made while a committed file was held open, after a restart: tx_a %d, "
"expected %d", tx_read(a), pending);
pending = ++gen;
CHECK(tx_write(a, pending) && tx_commit() == 0 && tx_read(a) == pending, "a second Commit while held open");
if (held) plat_file_close(held);
probe_restart(uid);
CHECK(tx_read(a) == pending && tx_read_committed("tx_a.sav") == pending && tx_read_committed("tx_b.sav") == kept,
"the held-open Commits after the file was closed and a restart: tx_a %d, on disk %d, expected %d",
tx_read(a), tx_read_committed("tx_a.sav"), pending);
committed = pending;
// the read-only mount shows the committed save and refuses changes
CHECK(tx_write(a, 99) && RES(S_MOUNT_SAVE_RO, U("ro"), TOTK_APPLICATION_ID, U(uid)) == 0 &&
tx_read("ro:/tx_a.sav") == committed && RES(S_OPEN_FILE, U(&h), U("ro:/tx_a.sav"), FS_OPEN_WRITE) ==
+14 -5
View File
@@ -214,6 +214,7 @@ static void window_settings_live(void) {
unsigned v = cfg_version();
if (started && v == seen) return;
seen = v;
res_fov_live();
const char *e = cfg_value("TOTK_VK_PRESENT");
if (!started || strncmp(e, present, sizeof present - 1)) {
snprintf(present, sizeof present, "%s", e);
@@ -595,20 +596,27 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT);
}
// Colour adjustment (TOTK_COLOR_*, vk_color.c): an adjusted copy of the crop takes the game image's place below
VkImage graded = general ? vk_color_record(cb, slot, src, fmt, rect) : VK_NULL_HANDLE;
const int grect[4] = {0, 0, cw, ch};
VkImage psrc = graded ? graded : src;
VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt;
const int *prect = graded ? grect : rect;
// FSR (TOTK_UPSCALER) when the picture's rectangle is larger than the crop; else the bilinear blit
int fsr = general && vk_fsr_record(cb, slot, src, fmt, w, h, rect, dst, to);
int fsr = general && vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to);
g_scaler = fsr ? 2 : cw != to[2] || ch != to[3];
if (!fsr) {
if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT);
VkImageBlit region = {
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{cx, cy, 0}, {cx + cw, cy + ch, 1}},
.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}},
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}};
vkCmdBlitImage(cb, src, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region, VK_FILTER_LINEAR);
if (general) // later submissions that write the source wait for the blit's read
sync_barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0);
}
// last: the image's layout, and the stage and access of its last write, as each step leaves it
@@ -719,6 +727,7 @@ void gfx_window_release_vk(void) {
g_flip_on = 0; // its blit is done (device idle); the swapchain goes away unpresented
menu_vk_release();
vk_fsr_release();
vk_color_release();
free_capture();
free_thumbs();
for (int i = 0; i < FRAMES; i++) {
+5 -3
View File
@@ -50,11 +50,13 @@ static const char *str_field(const char *from, const char *to, const char *name,
static int hexval(char c) { return c <= '9' ? c - '0' : (c | 32) - 'a' + 10; }
// The shader census the probes translate (tools/shader_census.py --version <ver>): build/<ver>/shaders in the nearest
// parent of totk.exe that has one with a jobs.json, the running game version's first, then the other's (any census
// parent of totk.exe that has one with a jobs.json, the running game version's first, then the others' (any census
// serves a probe). 1 with that folder in dir.
int probe_census_dir(char *dir, size_t n) {
const char *const versions[2] = {RT_V143 ? "v143" : "v121", RT_V143 ? "v121" : "v143"};
for (int v = 0; v < 2; v++) {
const char *const versions[3] = {RT_V111 ? "v111" : RT_V143 ? "v143" : "v121",
RT_V111 ? "v121" : RT_V143 ? "v121" : "v143",
RT_V111 ? "v143" : RT_V143 ? "v111" : "v111"};
for (int v = 0; v < 3; v++) {
char exe[PLAT_PATH_MAX * 2];
int len = plat_exe_path(exe, sizeof exe);
if (!len || len >= sizeof exe) return 0;
+1
View File
@@ -131,6 +131,7 @@ static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out
}
int probe_window(void) {
if (!menu_font_check()) return fail("the menu's font check takes a damaged font or refuses a good one");
gfx_window_start();
VkCtx *c = vk_ctx();
if (!c) return fail("no Vulkan device");
+3 -1
View File
@@ -184,8 +184,9 @@ typedef struct { const char *file, *sha256; } RtFileHash;
extern const RtFileHash rt_exefs_sha256[]; // tools/data/expected-<version>.json "exefs_sha256"
extern const int rt_exefs_sha256_count;
extern const char rt_default_exefs[], rt_default_romfs[], rt_default_profile[];
extern const int rt_game_version; // the game build: 121 (1.2.1) or 143 (1.4.3)
extern const int rt_game_version; // the game build: 111 (1.1.1), 121 (1.2.1) or 143 (1.4.3)
extern const char rt_port_version[]; // VERSION at the build's root: 0.1.0, 0.1.0+973c626 in a release
#define RT_V111 (rt_game_version == 111) // 1.1.1: its own boot constants and data slots
#define RT_V143 (rt_game_version >= 143)
// ---- kernel (kernel.c)
@@ -253,6 +254,7 @@ void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene
int res_is_16x9(unsigned w, unsigned h); // within 1% of 16:9 (the test res_init and romfs.c share)
int res_wants_wide_overlay(const unsigned out[2]); // not 16:9 on a game version with the camera aspect patch
void res_wanted(unsigned out[2]); // the output size TOTK_OUTPUT_RES asks for
void res_fov_live(void); // TOTK_FOV to the camera (set fov); on a settings change
// ---- code patches (patches.c) and the frame step (frame_rate.c)
void rt_patches_init(void); // after ldr_load: switches from the environment, the patch
// page committed; logs the sets
+3
View File
@@ -306,6 +306,9 @@ static void ap_init(void);
static int cores_enabled(void) {
if (g_cores_on < 0) {
g_cores_on = !cfg_bool("TOTK_FREE_CORES"); // "0" keeps the gate, like unset
if (!g_cores_on)
rt_log("kernel: virtual core gate off (TOTK_FREE_CORES): guest threads run without the limit of one per "
"core that the game's per-core data needs; for experiments only, the game can fail");
diag_init();
ap_init();
}
+8 -3
View File
@@ -552,8 +552,8 @@ uint64_t ldr_plt_stub(const char *module, const char *name) {
}
// ---- --probe-loader
// Expected values, measured on the 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
// MacNativeGames' port notes give for that version.
// Expected values, measured on the 1.1.1, 1.2.1 and 1.4.3 modules; the 1.4.3 relocation totals match the ones
// MacNativeGames' port notes give for that version. 1.1.1 shares subsdk0 and sdk with 1.2.1 down to every count.
typedef struct {
const char *name;
uint64_t kinds[K_COUNT], total, sym_import, sym_own;
@@ -564,12 +564,17 @@ static const Expect expect_121[] = {
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_111[] = {
{"main", {508604, 1412, 32, 639}, 510687, 2083, 0, 673, {0, 16, 645}, 12, 0x30, 10668},
{"subsdk0", {1623, 12820, 1030, 3924}, 19397, 6576, 11198, 667, {9, 0, 654}, 4, 0x30, 45},
{"sdk", {10917, 13976, 1207, 8932}, 35032, 16, 24099, 13, {8, 0, 0}, 5, 0x30, 116},
};
static const Expect expect_143[] = {
{"main", {499078, 1393, 28, 650}, 501149, 2071, 0, 680, {0, 16, 656}, 8, 0x10b4f10, 10717},
{"subsdk0", {3424, 10927, 905, 3817}, 19073, 6557, 9092, 663, {9, 0, 654}, 0, 0x20, 45},
{"sdk", {15013, 10135, 1079, 7832}, 34059, 10, 19036, 8, {8, 0, 0}, 0, 0x400, 83},
};
#define expect (RT_V143 ? expect_143 : expect_121)
#define expect (RT_V143 ? expect_143 : RT_V111 ? expect_111 : expect_121)
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
+11 -1
View File
@@ -183,7 +183,8 @@ static int by_host(const void *a, const void *b) {
return x < y ? -1 : x > y;
}
// The lifted function that contains a host code address, by the nearest function start below it.
// The lifted function that contains a host code address, by the nearest function start below it; 0 outside the lifted
// code.
static uint64_t lifted_function(uintptr_t rip) {
if (!fn_rev) {
static RtModule mods[MAX_MODULES];
@@ -197,6 +198,9 @@ static uint64_t lifted_function(uintptr_t rip) {
fn_rev[fn_rev_n++] = (FnRev){(uintptr_t)mods[i].fns[k], mods[i].addrs[k]};
qsort(fn_rev, fn_rev_n, sizeof *fn_rev, by_host);
}
// The lifted code is one range with the host code outside it (kernel.c ap_init checks that); an address above the
// last function start is taken for host code, which costs only the last function's body.
if (!fn_rev_n || rip > fn_rev[fn_rev_n - 1].host) return 0;
uint64_t lo = 0, hi = fn_rev_n;
while (lo < hi) {
uint64_t mid = (lo + hi) / 2;
@@ -220,6 +224,12 @@ static void crash_handler(const PlatCrash *x) {
rt_log_raw("\n", 1);
uint64_t f = lifted_function(rip);
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
for (int i = 0; i < x->nframes; i++) { // abort and invalid parameter: where the host code called it
uint64_t g = lifted_function(x->frames[i]);
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - (rip - x->offset)));
if (g) rt_logf(" in lifted function f_%llx", (unsigned long long)g);
rt_log_raw("\n", 1);
}
Cpu *c = kern_cpu();
if (c) {
for (int i = 0; i < 31; i++)
+12 -1
View File
@@ -82,7 +82,12 @@ static void cmd_reserve(cmd_t *o, size_t need) {
if (!cap) cap = 64 << 10;
while (used + need + OP_RESERVE > cap) cap *= 2;
uint8_t *b = realloc(o->seg, cap);
if (!b) rt_fatal("nvn: command recording buffer of %zu bytes", cap);
// A block this size fails only without commit (the fatal report states it) or when seg is no block of ours: a
// command buffer object the game overwrote or copied shows a wrong magic or more used than its old size.
if (!b)
rt_fatal("nvn: command recording buffer of %zu bytes (command buffer %p, magic %#x, buffer %p of %zu bytes, %zu "
"used, %zu asked)", cap, (void *)o, o->magic, (void *)o->seg, o->seg ? (size_t)(o->end - o->seg) : 0,
used, need);
o->seg = b;
o->cur = b + used;
o->end = b + cap;
@@ -312,6 +317,12 @@ static void release_copies(uintptr_t lo, uintptr_t hi) {
// at ~0 / 0 so NVN 55 inline code never finds room and calls the API (nvn_obj.h cmd_t)
NVN_API(nvnCommandBufferInitialize) {
cmd_t *o = cmd_of(c);
// Initialize over an object that was never finalized drops its recording buffer (seg lives in host memory). Logged
// only: the old bytes may be a copy whose buffer another object owns, so freeing it here is not safe.
static uint32_t reinits;
if (o->magic == CMD_MAGIC && o->seg && __atomic_fetch_add(&reinits, 1, __ATOMIC_RELAXED) < 8)
rt_log("nvn: Initialize over the command buffer %p, which was not finalized; its %zu-byte recording buffer %p "
"is dropped", (void *)o, (size_t)(o->end - o->seg), (void *)o->seg);
memset(o, 0, sizeof *o);
o->magic = CMD_MAGIC;
o->dev = arg_p(c, 1);
+14 -1
View File
@@ -1094,6 +1094,12 @@ void plat_fs_sync_dir(const char *path) {
}
}
// Open files never stop a rename here.
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
(void)dir;
if (cap) *out = 0;
}
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
DIR *d = opendir(dir);
if (!d) return fs_err(errno);
@@ -1172,7 +1178,14 @@ size_t plat_getenv(const char *name, char *out, size_t cap) {
return n;
}
uint32_t plat_thread_id(void) { return (uint32_t)syscall(SYS_gettid); }
// Cached per thread: plat_rmutex_lock asks on every acquire, hundreds of thousands of times a second in play, and
// gettid is a full syscall on Linux (Windows reads GetCurrentThreadId from the TEB). A thread's id never changes, and
// the runtime starts child processes with posix_spawn only, so no forked copy inherits a stale id.
uint32_t plat_thread_id(void) {
static __thread uint32_t tid;
if (!tid) tid = (uint32_t)syscall(SYS_gettid);
return tid;
}
// The first set variable of names ("ru_RU.UTF-8") as a BCP-47 tag ("ru-RU") into tag; "" for C and POSIX.
static void locale_tag(const char *const *names, int n, char *tag, size_t cap) {
+9 -1
View File
@@ -203,13 +203,17 @@ _Noreturn void plat_reexec(void);
// A crash in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
// overflow) goes to fn, which reports it and must end the process. Faults in other modules (drivers) are left to the
// OS. The write-fault hook above runs first.
// OS. The write-fault hook above runs first. On Windows abort() (a failed assert, std::terminate) and an invalid
// parameter to a C runtime function go there too, with the host frames in this executable: the C runtime ends the
// process for both with 0xc0000409 and no exception.
typedef struct {
const char *what; // "access violation", "illegal instruction", ...
uint32_t code; // the Windows exception code or the signal number
uintptr_t rip, offset; // the faulting instruction, and its offset in the executable
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
uint64_t addr; // its address when access >= 0
int nframes; // abort and invalid parameter: return addresses in this executable, innermost first
uintptr_t frames[16]; // (rip is frames[0]); 0 for a fault, whose rip is exact
} PlatCrash;
void plat_crash_hook(void (*fn)(const PlatCrash *c));
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
@@ -303,6 +307,10 @@ int plat_fs_rename(const char *from, const char *to, int durable);
int plat_fs_link(const char *existing, const char *path); // a hard link
int plat_fs_links(const char *path); // the file's hard link count, 0 when unknown
void plat_fs_sync_dir(const char *path); // a directory's entries to the disk, as far as the FS allows
// Why dir could not be renamed, for the log: into out "no permission to rename it" when this process may not delete
// it, else the programs holding a file under it ("explorer.exe 1234, ..."), else an empty string. Windows refuses to
// rename a directory with any file open under it; elsewhere out is always empty.
void plat_fs_busy_reason(const char *dir, char *out, size_t cap);
// cb(name, kind, size, ctx) for each entry of dir but "." and ".."; cb returns 0 to stop. The listing's error.
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx);
// An existing file for reading and, with write, writing; *err (may be NULL) gets the reason on failure.
+118
View File
@@ -7,8 +7,10 @@
#include <pdh.h>
#include <pdhmsg.h>
#include <psapi.h>
#include <restartmanager.h>
#include <shellapi.h>
#include <timeapi.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
@@ -582,9 +584,37 @@ static LONG CALLBACK crash_veh(EXCEPTION_POINTERS *ep) {
return EXCEPTION_CONTINUE_SEARCH;
}
// abort() and an invalid parameter to a C runtime function raise no exception: the C runtime ends the process with
// 0xc0000409 after calling the SIGABRT or invalid-parameter handler. The report names the host frames in this
// executable; the innermost is the caller of abort (assert) or of the C runtime function. Skipped frames: this function
// and the handler that called it.
__attribute__((noinline)) static void crash_from_runtime(const char *what, uint32_t code) {
PlatCrash c = {what, code, 0, 0, -1, 0};
void *f[62];
USHORT n = RtlCaptureStackBackTrace(2, 62, f, NULL);
MODULEINFO mi;
if (GetModuleInformation(GetCurrentProcess(), GetModuleHandleW(NULL), &mi, sizeof mi)) {
uintptr_t base = (uintptr_t)mi.lpBaseOfDll;
for (USHORT i = 0; i < n && c.nframes < (int)(sizeof c.frames / sizeof *c.frames); i++)
if ((uintptr_t)f[i] - base < mi.SizeOfImage) c.frames[c.nframes++] = (uintptr_t)f[i];
if (c.nframes) c.rip = c.frames[0], c.offset = c.rip - base;
}
crash_fn(&c);
}
static void abort_signal(int sig) { crash_from_runtime("abort", (uint32_t)sig); }
static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wchar_t *file, unsigned line,
uintptr_t reserved) {
(void)expr, (void)func, (void)file, (void)line, (void)reserved; // NULL and 0 in the release C runtime
crash_from_runtime("invalid parameter to a C runtime function", 0xc0000417u); // STATUS_INVALID_CRUNTIME_PARAMETER
}
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
crash_fn = fn;
AddVectoredExceptionHandler(0, crash_veh);
signal(SIGABRT, abort_signal);
_set_invalid_parameter_handler(invalid_parameter);
}
// A fresh process gets a fresh layout: Windows places DLLs, heaps and thread stacks at random addresses. The parent
@@ -846,6 +876,94 @@ void plat_fs_sync_dir(const char *path) {
}
}
// Restart Manager (rstrtmgr.dll, loaded on first use) names the processes that hold a file open.
typedef DWORD(WINAPI *RmStartSession_t)(DWORD *, DWORD, WCHAR *);
typedef DWORD(WINAPI *RmRegisterResources_t)(DWORD, UINT, LPCWSTR *, UINT, RM_UNIQUE_PROCESS *, UINT, LPCWSTR *);
typedef DWORD(WINAPI *RmGetList_t)(DWORD, UINT *, UINT *, RM_PROCESS_INFO *, LPDWORD);
typedef DWORD(WINAPI *RmEndSession_t)(DWORD);
static struct {
INIT_ONCE once;
RmStartSession_t start;
RmRegisterResources_t add;
RmGetList_t list;
RmEndSession_t end;
} rm = {INIT_ONCE_STATIC_INIT};
static BOOL CALLBACK rm_load(INIT_ONCE *o, void *p, void **c) {
(void)o, (void)p, (void)c;
HMODULE l = plat_lib_open("rstrtmgr.dll");
if (!l) return TRUE;
rm.add = (RmRegisterResources_t)(void *)GetProcAddress(l, "RmRegisterResources");
rm.list = (RmGetList_t)(void *)GetProcAddress(l, "RmGetList");
rm.end = (RmEndSession_t)(void *)GetProcAddress(l, "RmEndSession");
RmStartSession_t start = (RmStartSession_t)(void *)GetProcAddress(l, "RmStartSession");
if (rm.add && rm.list && rm.end) rm.start = start; // start set: all of them are there
return TRUE;
}
// The files under dir (links not followed), at most BUSY_FILES of them, each a malloc'd path.
#define BUSY_FILES 256
typedef struct { LPCWSTR p[BUSY_FILES]; UINT n; } BusyFiles;
static void busy_files(const wchar_t *dir, BusyFiles *f) {
wchar_t pat[WPATH + 4];
swprintf(pat, WPATH + 4, L"%ls\\*", dir);
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(pat, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
DWORD a = fd.dwFileAttributes;
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..") || (a & FILE_ATTRIBUTE_REPARSE_POINT)) continue;
size_t n = wcslen(dir) + wcslen(fd.cFileName) + 2;
wchar_t *p = n <= WPATH ? malloc(n * sizeof *p) : NULL;
if (!p) continue;
swprintf(p, n, L"%ls\\%ls", dir, fd.cFileName);
if (a & FILE_ATTRIBUTE_DIRECTORY) busy_files(p, f), free(p);
else f->p[f->n++] = p;
} while (f->n < BUSY_FILES && FindNextFileW(h, &fd));
FindClose(h);
}
void plat_fs_busy_reason(const char *dir, char *out, size_t cap) {
if (!cap) return;
*out = 0;
wchar_t w[WPATH];
if (!widen(dir, w, WPATH)) return;
for (wchar_t *q = w; *q; q++)
if (*q == L'/') *q = L'\\';
// a rename needs DELETE access to the directory: a folder made by an elevated run can refuse it
HANDLE h = CreateFileW(w, DELETE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h == INVALID_HANDLE_VALUE) {
if (GetLastError() == ERROR_ACCESS_DENIED) snprintf(out, cap, "no permission to rename it");
return;
}
CloseHandle(h);
InitOnceExecuteOnce(&rm.once, rm_load, NULL, NULL);
DWORD session;
WCHAR key[CCH_RM_SESSION_KEY + 1] = {0};
if (!rm.start || rm.start(&session, 0, key) != ERROR_SUCCESS) return;
BusyFiles f = {.n = 0};
busy_files(w, &f);
RM_PROCESS_INFO info[8];
UINT need = 0, n = 8;
DWORD reasons = 0, e = f.n ? rm.add(session, f.n, f.p, 0, NULL, 0, NULL) : ERROR_SUCCESS;
if (f.n && e == ERROR_SUCCESS) e = rm.list(session, &need, &n, info, &reasons);
else n = 0;
rm.end(session);
for (UINT i = 0; i < f.n; i++) free((void *)f.p[i]);
if (e == ERROR_MORE_DATA) snprintf(out, cap, "%u programs hold files in it", need); // info is not filled then
if (e != ERROR_SUCCESS) return;
size_t at = 0;
for (UINT i = 0; i < n && at < cap; i++) { // the program's display name ("Windows Explorer") and process id
char name[CCH_RM_MAX_APP_NAME + 1];
if (!WideCharToMultiByte(CP_UTF8, 0, info[i].strAppName, -1, name, sizeof name, NULL, NULL)) strcpy(name, "?");
int k = snprintf(out + at, cap - at, "%s%s %lu", i ? ", " : "", name, info[i].Process.dwProcessId);
if (k < 0) break;
at += (size_t)k;
}
}
int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t size, void *ctx), void *ctx) {
wchar_t wd[WPATH], pat[WPATH + 4];
if (!widen(dir, wd, WPATH)) return PLAT_FS_ERROR;
+25 -1
View File
@@ -12,7 +12,8 @@
//
// Patch page: +16 the scene size {w, h} as floats, +32 the output size and +48 the swapchain size as {w, h} in one
// 64-bit word, +40 the renderer (1.2.1) or display configuration (1.4.3) the patched code stores there.
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect).
// +56 the camera's aspect ratio as a float when the output is not 16:9 (set aspect), +60 the field-of-view factor
// (set fov, res_fov_live).
// 1.4.3 scales the scene it reads by output width / 1920 (a 2560x1440 output renders its 1600x900 scene at 2128x1200;
// MacNativeGames measured the same), so its page holds the wanted size divided by that factor and the game's 5/6 needs
// no scene patch. 1.2.1 renders the size it reads (1600x900 on a 2560x1440 output): its page holds the wanted size, and
@@ -253,6 +254,29 @@ int res_wants_wide_overlay(const unsigned out[2]) {
return !res_is_16x9(out[0], out[1]) && rt_patch_set("aspect") >= 0;
}
// Field of view (TOTK_FOV, percent of the game's own, 70..140, live): the camera's vertical view angle times the
// factor on page +60 (recomp/patches-v143.txt, set fov). The horizontal angle follows through the aspect ratio. 100
// switches the set off, so the camera code is the game's own. Runs on a settings change (gfx_present.c
// window_settings_live), the first time at the first present.
#define FOV_MIN 70
#define FOV_MAX 140
void res_fov_live(void) {
static int set = -2, shown = 100;
if (set == -2) set = rt_patch_set("fov");
int pct = cfg_int("TOTK_FOV");
pct = pct < FOV_MIN ? FOV_MIN : pct > FOV_MAX ? FOV_MAX : pct;
if (pct == shown) return;
shown = pct;
if (set < 0) {
rt_log("resolution: this game version has no field-of-view patch; TOTK_FOV=%d does nothing", pct);
return;
}
float k = pct / 100.0f;
memcpy((uint8_t *)rt_patch_page() + 60, &k, sizeof k); // before the switch: a site that sees it on reads k
rt_patch_switch(set, pct != 100);
rt_log("resolution: field of view %d%% of the game's", pct);
}
void res_wanted(unsigned out[2]) {
decide();
out[0] = R.ow, out[1] = R.oh;
+23
View File
@@ -0,0 +1,23 @@
#version 450
// Colour adjustment on the present (runtime/vk_color.c): the game's picture, crop rectangle at offset, into an RGBA16F
// image the FSR 1 pass or the blit then scales to the window. Brightness, contrast around mid-grey, saturation against
// Rec. 709 luma, then a power curve: the order and ranges of STS78721's colour profiles (issue #136).
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D src;
layout(binding = 1, rgba16f) uniform writeonly image2D dst;
layout(push_constant) uniform P {
ivec2 offset; // the crop rectangle's corner in src
ivec2 size; // its size, the size of dst
vec4 adj; // brightness (added), contrast, saturation, gamma (exponent)
} pc;
void main() {
ivec2 p = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(p, pc.size))) return;
vec3 c = texelFetch(src, p + pc.offset, 0).rgb;
c += pc.adj.x;
c = (c - 0.5) * pc.adj.y + 0.5;
c = mix(vec3(dot(c, vec3(0.2126, 0.7152, 0.0722))), c, pc.adj.z);
c = pow(max(c, vec3(0.0)), vec3(pc.adj.w));
imageStore(dst, p, vec4(clamp(c, 0.0, 1.0), 1.0));
}
+86 -1
View File
@@ -11,6 +11,7 @@
// 575ad88e02f9a89a9a885d90039d48af33c982f4, MIT, Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors,
// Copyright (c) 2026 Peiikai); the pages are rows of option_rows.cpp.
#include <atomic>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -19,6 +20,16 @@
#include "imgui.h"
#include "imgui_internal.h" // SetFocusID, NavCursorVisible
#include "backends/imgui_impl_vulkan.h"
// A private copy of stb_truetype, the parser Dear ImGui builds its font atlas with, to check a system font before the
// atlas gets it (font_usable). Its asserts only mark the font bad here.
static int g_font_bad;
#define STBTT_STATIC
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTT_assert(x) ((x) ? (void)0 : (void)(g_font_bad = 1))
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
#include "imstb_truetype.h"
#pragma clang diagnostic pop
#include "ui/binds_page.h"
#include "ui/option_rows.h"
@@ -262,6 +273,43 @@ static const struct { const char *lang; UiTextId name; } kCjk[] = {
{"zh-Hant", UI_MENU_LANGNAME_ZH_HANT},
};
static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
// 1 when Dear ImGui can take this font file: more than the 100 bytes AddFontFromMemoryTTF asserts on, every table of its
// directory inside the file (stb_truetype reads tables without bounds checks, so a cut-off file faults when the glyphs
// are made), and stb_truetype parses it. ImGui asserts on a font it cannot parse, and an assert ends the process with
// 0xc0000409 and no line in the log.
static int font_usable(const uint8_t *d, uint64_t n) {
if (!d || n <= 100 || n > INT_MAX) return 0;
int off = stbtt_GetFontOffsetForIndex(d, 0);
if (off < 0 || (uint64_t)off + 12 > n) return 0;
uint32_t tables = (uint32_t)d[off + 4] << 8 | d[off + 5];
if ((uint64_t)off + 12 + 16ull * tables > n) return 0;
for (uint32_t i = 0; i < tables; i++) {
const uint8_t *r = d + off + 12 + 16 * i;
if ((uint64_t)be32(r + 8) + be32(r + 12) > n) return 0;
}
stbtt_fontinfo fi;
g_font_bad = 0;
return stbtt_InitFont(&fi, d, off) && !g_font_bad;
}
extern "C" int menu_font_check(void) {
uint64_t n = sizeof roboto_ttf;
uint8_t *bad = (uint8_t *)malloc(n);
if (!bad) return 0;
memcpy(bad, roboto_ttf, n);
uint32_t tables = (uint32_t)bad[4] << 8 | bad[5];
for (uint32_t i = 0; i < tables; i++)
if (!memcmp(bad + 12 + 16 * i, "cmap", 4)) memset(bad + 12 + 16 * i, 0, 4); // no character map
int ok = font_usable(roboto_ttf, n) && !font_usable(roboto_ttf, 4096) && !font_usable(roboto_ttf, 100) &&
!font_usable(bad, n);
memset(bad, 0, 4); // an unknown font tag
ok = ok && !font_usable(bad, n);
free(bad);
return ok;
}
static int fonts_up(float scale) {
enum { N = sizeof kCjk / sizeof *kCjk };
ImFontAtlas *fa = ImGui::GetIO().Fonts;
@@ -276,6 +324,13 @@ static int fonts_up(float scale) {
char path[512];
uint64_t size = 0;
if (!plat_font_path(kCjk[i].lang, path, sizeof path) || !(data[i] = plat_file_read(path, &size))) continue;
if (!font_usable(data[i], size)) {
rt_log("menu: system font %s left out: the font parser cannot read it (%llu bytes)", path,
(unsigned long long)size);
free(data[i]);
data[i] = NULL;
continue;
}
merged++;
ImFontGlyphRangesBuilder b;
b.AddText(ui_text_in("en", kCjk[i].name));
@@ -527,6 +582,35 @@ static const char *present_note(void) {
}
// ---- the tabs
// Colour adjustment (vk_color.c): a profile writes the four values (STS78721's presets, issue #136); moving one of them
// makes the profile Custom.
static void rows_color(void) {
static const struct { const char *name, *b, *c, *s, *g; } presets[] = {
{"default", "0", "100", "100", "1"}, {"vivid", "0", "115", "135", "0.95"}, {"clarity", "2", "110", "115", "1"}};
static const UiTextId names[] = {UI_MENU_COLOR_DEFAULT, UI_MENU_COLOR_VIVID, UI_MENU_COLOR_CLARITY,
UI_MENU_COLOR_CUSTOM};
opt_group(UI_MENU_COLOR);
if (opt_choice("TOTK_COLOR_PRESET", UI_MENU_COLOR_PRESET, names, 4, UI_MENU_COLOR_PRESET_DESC)) {
const char *p = cfg_value("TOTK_COLOR_PRESET");
for (const auto &e : presets)
if (!strcmp(p, e.name)) {
cfg_set("TOTK_COLOR_BRIGHTNESS", e.b), cfg_set("TOTK_COLOR_CONTRAST", e.c);
cfg_set("TOTK_COLOR_SATURATION", e.s), cfg_set("TOTK_COLOR_GAMMA", e.g);
cfg_save();
}
}
bool moved = opt_slider("TOTK_COLOR_BRIGHTNESS", UI_MENU_COLOR_BRIGHTNESS, -30, 30, UI_MENU_COLOR_BRIGHTNESS_DESC, 1,
"%+.0f %%");
moved |= opt_slider("TOTK_COLOR_CONTRAST", UI_MENU_COLOR_CONTRAST, 70, 150, UI_MENU_COLOR_CONTRAST_DESC, 5, "%.0f %%");
moved |= opt_slider("TOTK_COLOR_SATURATION", UI_MENU_COLOR_SATURATION, 50, 200, UI_MENU_COLOR_SATURATION_DESC, 5,
"%.0f %%");
moved |= opt_slider("TOTK_COLOR_GAMMA", UI_MENU_COLOR_GAMMA, 0.8f, 1.2f, UI_MENU_COLOR_GAMMA_DESC, 0.02f, "%.2f");
if (moved && strcmp(cfg_value("TOTK_COLOR_PRESET"), "custom")) {
cfg_set("TOTK_COLOR_PRESET", "custom");
cfg_save();
}
}
static void tab_screen(float scale) {
opt_begin("##screen", scale);
opt_toggle("TOTK_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC);
@@ -544,6 +628,8 @@ static void tab_screen(float scale) {
opt_slider("TOTK_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
static const UiTextId aspect[] = {UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH};
opt_choice("TOTK_ASPECT", UI_MENU_ASPECT, aspect, 2, UI_MENU_ASPECT_DESC);
opt_slider("TOTK_FOV", UI_MENU_FOV, 70, 140, UI_MENU_FOV_DESC, 5, "%.0f %%");
rows_color();
opt_end();
}
@@ -667,7 +753,6 @@ static void tab_advanced(float scale) {
static const UiTextId preempt[] = {UI_MENU_PREEMPT_OFF, UI_MENU_PREEMPT_ON, UI_MENU_PREEMPT_INHERIT}; // 0|1|2
opt_choice("TOTK_ASYNC_PREEMPT", UI_MENU_ASYNC_PREEMPT, preempt, 3, UI_MENU_ASYNC_PREEMPT_DESC);
opt_toggle("TOTK_REPLAY_ASYNC", UI_MENU_REPLAY_ASYNC, UI_MENU_REPLAY_ASYNC_DESC);
opt_toggle("TOTK_FREE_CORES", UI_MENU_FREE_CORES, UI_MENU_FREE_CORES_DESC);
opt_toggle("TOTK_TIMER_1MS", UI_MENU_TIMER_1MS, UI_MENU_TIMER_1MS_DESC);
opt_group(UI_MENU_GROUP_SHADERS);
+3
View File
@@ -53,6 +53,9 @@ int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot);
// Records the frame menu_overlay_frame laid out into cb, which is inside dynamic rendering on the image.
void menu_overlay_draw(VkCommandBuffer cb);
void menu_vk_release(void); // before the device goes away (gfx_window_release_vk)
// The check the font atlas runs on a system font, on the embedded font and damaged copies of it: 1 when it takes the
// whole font and refuses a cut-off one, one without a character map and one with an unknown tag (--probe-window).
int menu_font_check(void);
// ---- what the menu asks of the platform side (gfx_window_win.c, gfx_window_sdl.c)
void gfx_window_apply_settings(void); // TOTK_FULLSCREEN or TOTK_WINDOW_SIZE changed: the window follows
+35 -4
View File
@@ -67,8 +67,25 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_ASPECT_DESC] = "When the window's shape differs from the game resolution's (a 16:9 picture on a 21:9 "
"screen): keep the picture's shape with black bars, or stretch it over the window.",
[UI_MENU_ASPECT_FIT] = "Black bars", [UI_MENU_ASPECT_STRETCH] = "Stretch",
[UI_MENU_FOV] = "Field of view",
[UI_MENU_FOV_DESC] = "How much of the world the camera shows, in percent of the game's own. Game version 1.4.3 only.",
[UI_MENU_SHARPNESS] = "FSR sharpness",
[UI_MENU_SHARPNESS_DESC] = "Sharpening after FSR scaling; 0 turns it off.",
[UI_MENU_COLOR] = "Colour",
[UI_MENU_COLOR_PRESET] = "Colour profile",
[UI_MENU_COLOR_PRESET_DESC] = "Sets the four values below. Vivid: more contrast and saturation, shadows opened a "
"little (made for OLED screens). Clarity: a little more contrast, saturation and "
"brightness. Moving a value makes the profile Custom. The HUD changes with the picture; "
"this menu does not.",
[UI_MENU_COLOR_DEFAULT] = "Game's own", [UI_MENU_COLOR_VIVID] = "Vivid (OLED)", [UI_MENU_COLOR_CLARITY] = "Clarity",
[UI_MENU_COLOR_CUSTOM] = "Custom",
[UI_MENU_COLOR_BRIGHTNESS] = "Brightness", [UI_MENU_COLOR_BRIGHTNESS_DESC] = "Added to every colour; 0 leaves it.",
[UI_MENU_COLOR_CONTRAST] = "Contrast",
[UI_MENU_COLOR_CONTRAST_DESC] = "Spreads colours away from mid-grey (above 100 %) or towards it; 100 % leaves them.",
[UI_MENU_COLOR_SATURATION] = "Saturation",
[UI_MENU_COLOR_SATURATION_DESC] = "Colour strength against grey; 100 % leaves it.",
[UI_MENU_COLOR_GAMMA] = "Gamma",
[UI_MENU_COLOR_GAMMA_DESC] = "Below 1 brightens dark and middle tones, above 1 darkens them; 1 leaves them.",
[UI_MENU_FPS_LIMIT] = "Frame rate limit",
[UI_MENU_FPS_LIMIT_DESC] = "The most frames per second the game draws. Above 60 is experimental and needs Game speed "
@@ -178,8 +195,6 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_PREEMPT_OFF] = "Off", [UI_MENU_PREEMPT_ON] = "On", [UI_MENU_PREEMPT_INHERIT] = "Inheritance only",
[UI_MENU_REPLAY_ASYNC] = "GPU frame prep on its own thread",
[UI_MENU_REPLAY_ASYNC_DESC] = "Prepares frames for the GPU on a separate thread instead of the game thread. Adds up to one frame of latency.",
[UI_MENU_FREE_CORES] = "Virtual core gate",
[UI_MENU_FREE_CORES_DESC] = "Lets only one thread run per virtual core, as on the console.",
[UI_MENU_TIMER_1MS] = "1 ms Windows timer",
[UI_MENU_TIMER_1MS_DESC] = "Raises the system timer resolution to 1 ms for steadier frame pacing.",
[UI_MENU_PIPE_WAIT] = "Waiting draw handling",
@@ -312,8 +327,26 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_ASPECT_DESC] = "Когда форма окна отличается от разрешения игры (картинка 16:9 на экране 21:9): сохранить "
"форму картинки с чёрными полосами или растянуть её на всё окно.",
[UI_MENU_ASPECT_FIT] = "Чёрные полосы", [UI_MENU_ASPECT_STRETCH] = "Растянуть",
[UI_MENU_FOV] = "Поле зрения",
[UI_MENU_FOV_DESC] = "Сколько мира показывает камера, в процентах от игрового. Только для версии игры 1.4.3.",
[UI_MENU_SHARPNESS] = "Резкость FSR",
[UI_MENU_SHARPNESS_DESC] = "Повышение резкости после масштабирования FSR; 0 выключает его.",
[UI_MENU_COLOR] = "Цвет",
[UI_MENU_COLOR_PRESET] = "Цветовой профиль",
[UI_MENU_COLOR_PRESET_DESC] = "Задаёт четыре значения ниже. «Яркий»: больше контраста и насыщенности, тени немного "
"светлее (для OLED-экранов). «Чёткость»: немного больше контраста, насыщенности и "
"яркости. Изменение любого значения делает профиль «Своим». Интерфейс игры меняется "
"вместе с картинкой, это меню — нет.",
[UI_MENU_COLOR_DEFAULT] = "Как в игре", [UI_MENU_COLOR_VIVID] = "Яркий (OLED)", [UI_MENU_COLOR_CLARITY] = "Чёткость",
[UI_MENU_COLOR_CUSTOM] = "Свой",
[UI_MENU_COLOR_BRIGHTNESS] = "Яркость", [UI_MENU_COLOR_BRIGHTNESS_DESC] = "Прибавляется к каждому цвету; 0 — без изменений.",
[UI_MENU_COLOR_CONTRAST] = "Контраст",
[UI_MENU_COLOR_CONTRAST_DESC] = "Отдаляет цвета от среднего серого (выше 100 %) или приближает к нему; 100 % — без "
"изменений.",
[UI_MENU_COLOR_SATURATION] = "Насыщенность",
[UI_MENU_COLOR_SATURATION_DESC] = "Сила цвета относительно серого; 100 % — без изменений.",
[UI_MENU_COLOR_GAMMA] = "Гамма",
[UI_MENU_COLOR_GAMMA_DESC] = "Ниже 1 осветляет тёмные и средние тона, выше 1 затемняет их; 1 — без изменений.",
[UI_MENU_FPS_LIMIT] = "Ограничение частоты кадров",
[UI_MENU_FPS_LIMIT_DESC] = "Наибольшее число кадров в секунду. Выше 60 — экспериментальный режим, нужна «Скорость "
@@ -427,8 +460,6 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_PREEMPT_OFF] = "Выкл.", [UI_MENU_PREEMPT_ON] = "Вкл.", [UI_MENU_PREEMPT_INHERIT] = "Только наследование",
[UI_MENU_REPLAY_ASYNC] = "Подготовка кадра для GPU в отдельном потоке",
[UI_MENU_REPLAY_ASYNC_DESC] = "Кадр для GPU готовится в отдельном потоке, а не в потоке игры. Добавляет до одного кадра задержки.",
[UI_MENU_FREE_CORES] = "Ограничение виртуальных ядер",
[UI_MENU_FREE_CORES_DESC] = "На каждом виртуальном ядре работает один поток, как на консоли.",
[UI_MENU_TIMER_1MS] = "Таймер Windows 1 мс",
[UI_MENU_TIMER_1MS_DESC] = "Повышает точность системного таймера до 1 мс для ровного темпа кадров.",
[UI_MENU_PIPE_WAIT] = "Обработка ожидающих отрисовок",
+6 -1
View File
@@ -36,7 +36,13 @@ typedef enum {
UI_MENU_RENDER_SCALE, UI_MENU_RENDER_SCALE_DESC, UI_MENU_RENDER_SCALE_GAME,
UI_MENU_UPSCALER, UI_MENU_UPSCALER_DESC, UI_MENU_UPSCALER_FSR, UI_MENU_UPSCALER_BILINEAR,
UI_MENU_ASPECT, UI_MENU_ASPECT_DESC, UI_MENU_ASPECT_FIT, UI_MENU_ASPECT_STRETCH,
UI_MENU_FOV, UI_MENU_FOV_DESC,
UI_MENU_SHARPNESS, UI_MENU_SHARPNESS_DESC,
UI_MENU_COLOR, // group heading over the colour rows
UI_MENU_COLOR_PRESET, UI_MENU_COLOR_PRESET_DESC, UI_MENU_COLOR_DEFAULT, UI_MENU_COLOR_VIVID, UI_MENU_COLOR_CLARITY,
UI_MENU_COLOR_CUSTOM,
UI_MENU_COLOR_BRIGHTNESS, UI_MENU_COLOR_BRIGHTNESS_DESC, UI_MENU_COLOR_CONTRAST, UI_MENU_COLOR_CONTRAST_DESC,
UI_MENU_COLOR_SATURATION, UI_MENU_COLOR_SATURATION_DESC, UI_MENU_COLOR_GAMMA, UI_MENU_COLOR_GAMMA_DESC,
// Game
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
@@ -108,7 +114,6 @@ typedef enum {
UI_MENU_ASYNC_PREEMPT, UI_MENU_ASYNC_PREEMPT_DESC, UI_MENU_PREEMPT_OFF, UI_MENU_PREEMPT_ON,
UI_MENU_PREEMPT_INHERIT,
UI_MENU_REPLAY_ASYNC, UI_MENU_REPLAY_ASYNC_DESC,
UI_MENU_FREE_CORES, UI_MENU_FREE_CORES_DESC,
UI_MENU_TIMER_1MS, UI_MENU_TIMER_1MS_DESC,
UI_MENU_PIPE_WAIT, UI_MENU_PIPE_WAIT_DESC, UI_MENU_PIPE_WAIT_DEFAULT, UI_MENU_PIPE_WAIT_DRAW,
UI_MENU_PIPE_SKIP, UI_MENU_PIPE_SKIP_DESC, UI_MENU_PIPE_SKIP_STRICT, UI_MENU_PIPE_SKIP_ANY,
+5
View File
@@ -102,6 +102,11 @@ void vk_present_blit(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int
int vk_fsr_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int sw, int sh, const int crop[4],
VkImage dst, const int out[4]);
void vk_fsr_release(void); // before the device goes away
// Colour adjustment on the present (vk_color.c): with TOTK_COLOR_* away from their defaults, records a pass that writes
// src's crop rectangle {x, y, w, h}, adjusted, into an RGBA16F image of that size in GENERAL and returns it (the caller
// scales that one instead, crop {0, 0, w, h}); VK_NULL_HANDLE = nothing recorded. src is in GENERAL, fmt its format.
VkImage vk_color_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]);
void vk_color_release(void); // before the device goes away
void vk_present_flip(void);
// Every submission to ctx->gfx_queue (and xfer_queue when shared) takes this lock; vk_present_image and the swapchain
// rebuild do. Recursive.
+188
View File
@@ -0,0 +1,188 @@
// Colour adjustment on the present (vk_color.h): brightness, contrast, saturation and gamma of the game's picture, after
// the game and before the FSR 1 pass or the blit scale it to the window. Settings TOTK_COLOR_BRIGHTNESS (-30..30, %
// added), TOTK_COLOR_CONTRAST and TOTK_COLOR_SATURATION (%, 100 = unchanged) and TOTK_COLOR_GAMMA (exponent, 1 =
// unchanged), read when the settings change; the menu's colour profiles only write these four. At the default values
// nothing is recorded and the present is what it was. Design and preset values: STS78721's colour profiles (issue #136).
//
// One compute pass (shaders/color_grade.comp) reads the crop rectangle of the game's image and writes an RGBA16F image
// of its size, which present_image then hands to FSR or the blit in place of the game's image. The HUD is part of that
// picture and is adjusted with it; the in-game menu is drawn later, straight into the swapchain image, and is not.
// The source view of slot s is destroyed when slot s records again (present_image has waited for that slot's fence);
// the output image is shared by all slots, and each frame's first barrier orders it after the previous frame's reads.
#include "vk.h"
#include <string.h>
#include "cfg.h"
#include "color_grade_spv.h"
#include "host.h"
#define SLOTS 8
static struct {
int tried, ok;
PFN_vkCmdPushDescriptorSetKHR push;
VkDescriptorSetLayout dsl;
VkPipelineLayout layout;
VkPipeline pipe;
VkSampler sampler;
VkImage img; // the adjusted picture, RGBA16F at the crop size
VmaAllocation alloc;
VkImageView view;
uint32_t w, h;
VkImageView src_view[SLOTS]; // the source view each slot's last frame read
unsigned seen; // cfg_version the values below were read at
float adj[4]; // brightness (added), contrast, saturation, gamma
} C = {.seen = ~0u};
static int init(VkCtx *c) {
C.tried = 1;
C.push = (PFN_vkCmdPushDescriptorSetKHR)vkGetDeviceProcAddr(c->device, "vkCmdPushDescriptorSetKHR");
VkDescriptorSetLayoutBinding b[2] = {
{.binding = 0, .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT},
{.binding = 1, .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .descriptorCount = 1,
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT}};
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
.bindingCount = 2, .pBindings = b};
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 32};
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
.pSetLayouts = &C.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_NEAREST,
.minFilter = VK_FILTER_NEAREST, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, .maxLod = 0};
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof color_grade_spv,
.pCode = color_grade_spv};
VkShaderModule m = VK_NULL_HANDLE;
if (!C.push || vkCreateDescriptorSetLayout(c->device, &dci, NULL, &C.dsl) != VK_SUCCESS ||
vkCreatePipelineLayout(c->device, &li, NULL, &C.layout) != VK_SUCCESS ||
vkCreateSampler(c->device, &si, NULL, &C.sampler) != VK_SUCCESS ||
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS) {
rt_log("vk: colour adjustment off: its pipeline could not be created");
return 0;
}
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
.layout = C.layout};
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &C.pipe);
vkDestroyShaderModule(c->device, m, NULL);
if (r != VK_SUCCESS) {
rt_log("vk: colour adjustment off: its pipeline could not be created");
return 0;
}
return C.ok = 1;
}
static void free_image(VkCtx *c) {
if (C.view) vkDestroyImageView(c->device, C.view, NULL);
if (C.img) vmaDestroyImage(c->vma, C.img, C.alloc);
C.view = VK_NULL_HANDLE, C.img = VK_NULL_HANDLE, C.w = C.h = 0;
}
static int ensure_image(VkCtx *c, uint32_t w, uint32_t h) {
if (C.img && C.w == w && C.h == h) return 1;
// another crop size (a resolution change): earlier presents may still read the old image
vk_queue_lock();
vkDeviceWaitIdle(c->device);
vk_queue_unlock();
free_image(c);
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D,
.format = VK_FORMAT_R16G16B16A16_SFLOAT, .extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &C.img, &C.alloc, NULL) != VK_SUCCESS) {
C.img = VK_NULL_HANDLE;
return 0;
}
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = C.img,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = VK_FORMAT_R16G16B16A16_SFLOAT,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (vkCreateImageView(c->device, &vi, NULL, &C.view) != VK_SUCCESS) {
C.view = VK_NULL_HANDLE;
free_image(c);
return 0;
}
C.w = w, C.h = h;
return 1;
}
// The four values from the settings, re-read when they change. 1 when any differs from "unchanged".
static int adjusting(void) {
unsigned v = cfg_version();
if (v != C.seen) {
C.seen = v;
C.adj[0] = (float)cfg_double("TOTK_COLOR_BRIGHTNESS") / 100.0f;
C.adj[1] = (float)cfg_double("TOTK_COLOR_CONTRAST") / 100.0f;
C.adj[2] = (float)cfg_double("TOTK_COLOR_SATURATION") / 100.0f;
C.adj[3] = (float)cfg_double("TOTK_COLOR_GAMMA");
}
return C.adj[0] != 0.0f || C.adj[1] != 1.0f || C.adj[2] != 1.0f || C.adj[3] != 1.0f;
}
static void barrier(VkCommandBuffer cb, VkImage img, VkImageLayout from, VkPipelineStageFlags2 ss, VkAccessFlags2 sa,
VkPipelineStageFlags2 ds, VkAccessFlags2 da) {
VkImageMemoryBarrier2 b = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask = ss, .srcAccessMask = sa,
.dstStageMask = ds, .dstAccessMask = da, .oldLayout = from,
.newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = img,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
VkDependencyInfo d = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1,
.pImageMemoryBarriers = &b};
vkCmdPipelineBarrier2(cb, &d);
}
VkImage vk_color_record(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, const int crop[4]) {
if (!adjusting()) return VK_NULL_HANDLE;
VkCtx *c = vk_ctx();
if (!C.tried) init(c);
if (!C.ok || !ensure_image(c, (uint32_t)crop[2], (uint32_t)crop[3])) return VK_NULL_HANDLE;
slot %= SLOTS;
if (C.src_view[slot]) vkDestroyImageView(c->device, C.src_view[slot], NULL);
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = src,
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = fmt,
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
if (vkCreateImageView(c->device, &vi, NULL, &C.src_view[slot]) != VK_SUCCESS) {
C.src_view[slot] = VK_NULL_HANDLE;
return VK_NULL_HANDLE;
}
const VkPipelineStageFlags2 CS = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, ALL = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT;
// the game's writes to src are visible; last frame's reads of the output are done (UNDEFINED: its old content goes)
barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, ALL, VK_ACCESS_2_MEMORY_WRITE_BIT, CS, VK_ACCESS_2_SHADER_SAMPLED_READ_BIT);
barrier(cb, C.img, VK_IMAGE_LAYOUT_UNDEFINED, ALL, 0, CS, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT);
VkDescriptorImageInfo ii = {C.sampler, C.src_view[slot], VK_IMAGE_LAYOUT_GENERAL},
oi = {VK_NULL_HANDLE, C.view, VK_IMAGE_LAYOUT_GENERAL};
VkWriteDescriptorSet w[2] = {
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &ii},
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &oi}};
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.pipe);
rec_push(C.push, cb, VK_PIPELINE_BIND_POINT_COMPUTE, C.layout, 0, 2, w);
struct { int32_t offset[2], size[2]; float adj[4]; } pc = {{crop[0], crop[1]}, {crop[2], crop[3]},
{C.adj[0], C.adj[1], C.adj[2], C.adj[3]}};
vkCmdPushConstants(cb, C.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
vkCmdDispatch(cb, ((uint32_t)crop[2] + 7) / 8, ((uint32_t)crop[3] + 7) / 8, 1);
// later submissions that write src wait for this read; FSR and the blit read the output
barrier(cb, src, VK_IMAGE_LAYOUT_GENERAL, CS, 0, ALL, 0);
barrier(cb, C.img, VK_IMAGE_LAYOUT_GENERAL, CS, VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, ALL,
VK_ACCESS_2_MEMORY_READ_BIT);
return C.img;
}
void vk_color_release(void) {
VkCtx *c = vk_ctx();
for (int i = 0; i < SLOTS; i++)
if (C.src_view[i]) vkDestroyImageView(c->device, C.src_view[i], NULL), C.src_view[i] = VK_NULL_HANDLE;
free_image(c);
if (C.pipe) vkDestroyPipeline(c->device, C.pipe, NULL);
if (C.sampler) vkDestroySampler(c->device, C.sampler, NULL);
if (C.layout) vkDestroyPipelineLayout(c->device, C.layout, NULL);
if (C.dsl) vkDestroyDescriptorSetLayout(c->device, C.dsl, NULL);
C.pipe = VK_NULL_HANDLE, C.sampler = VK_NULL_HANDLE, C.layout = VK_NULL_HANDLE, C.dsl = VK_NULL_HANDLE;
C.tried = C.ok = 0;
}
+24 -9
View File
@@ -2568,7 +2568,8 @@ static struct {
PlatThread *th[LB_MAX_THREADS];
uint8_t clamp, poly, minus_w, samples; // library state (GKey fields)
double t0, ms[3]; // ms: build time per kind (vertex, fragment, compute)
uint64_t built[3], have[3], fail[3], warmed[3], noshader, done, cs_draw; // warmed: driver cache only
// warmed: driver cache only; nolib: vertex and fragment variants skipped with the graphics pipeline library off
uint64_t built[3], have[3], fail[3], warmed[3], noshader, nolib, done, cs_draw;
double warm_ms[3];
volatile int32_t kept; // libraries entered in libs[] so far
int32_t keep_n;
@@ -2734,14 +2735,21 @@ static void lb_library(const GfxShader *sh, int vertex, int keep) {
lb_count(stat, !pipe ? 2 : have || got != pipe ? 1 : 0, ms);
}
// A vertex or fragment variant with the graphics pipeline library off: no stage library to build ahead.
static void lb_skip_lib(void) {
plat_mutex_lock(&LB.stat);
LB.nolib++;
plat_mutex_unlock(&LB.stat);
}
static void lb_report(const char *how) {
float mem_used, mem_total, mem_proc;
plat_mem_info(&mem_used, &mem_total, &mem_proc);
plat_mutex_lock_shared(&LB.stat);
rt_log("vk_pipeline: stage library prebuild %s: %llu of %zu variants in %.0f ms; kept: vertex %llu built (avg %.1f ms), "
"%llu there; fragment %llu built (avg %.1f ms), %llu there; compute %llu built (avg %.1f ms), %llu there; driver "
"cache only: vertex %llu (avg %.1f ms), fragment %llu (avg %.1f ms), compute %llu (avg %.1f ms); %llu failed, %llu not in the shader cache; "
"process working set %.0f MB", how, (unsigned long long)LB.done, LB.n,
"cache only: vertex %llu (avg %.1f ms), fragment %llu (avg %.1f ms), compute %llu (avg %.1f ms); %llu failed, %llu not in the shader cache, "
"%llu vertex/fragment skipped (graphics pipeline library off); process working set %.0f MB", how, (unsigned long long)LB.done, LB.n,
now_ms() - LB.t0, (unsigned long long)LB.built[0], LB.built[0] ? LB.ms[0] / LB.built[0] : 0.0,
(unsigned long long)LB.have[0], (unsigned long long)LB.built[1], LB.built[1] ? LB.ms[1] / LB.built[1] : 0.0,
(unsigned long long)LB.have[1], (unsigned long long)LB.built[2], LB.built[2] ? LB.ms[2] / LB.built[2] : 0.0,
@@ -2749,7 +2757,7 @@ static void lb_report(const char *how) {
(unsigned long long)LB.warmed[1], LB.warmed[1] ? LB.warm_ms[1] / LB.warmed[1] : 0.0,
(unsigned long long)LB.warmed[2], LB.warmed[2] ? LB.warm_ms[2] / LB.warmed[2] : 0.0,
(unsigned long long)(LB.fail[0] + LB.fail[1] + LB.fail[2]),
(unsigned long long)LB.noshader, (double)mem_proc);
(unsigned long long)LB.noshader, (unsigned long long)LB.nolib, (double)mem_proc);
plat_mutex_unlock_shared(&LB.stat);
}
@@ -2769,8 +2777,9 @@ static void lb_worker(void *arg) {
plat_mutex_unlock(&LB.stat);
} else if (gfx_shader_stage(sh) == GFX_STAGE_CS) {
lb_compute(sh);
} else if (P.gpl && (gfx_shader_stage(sh) == GFX_STAGE_VS || gfx_shader_stage(sh) == GFX_STAGE_FS)) {
lb_library(sh, gfx_shader_stage(sh) == GFX_STAGE_VS, 1);
} else if (gfx_shader_stage(sh) == GFX_STAGE_VS || gfx_shader_stage(sh) == GFX_STAGE_FS) {
if (P.gpl) lb_library(sh, gfx_shader_stage(sh) == GFX_STAGE_VS, 1);
else lb_skip_lib();
}
plat_mutex_lock(&LB.stat);
LB.done++;
@@ -2861,8 +2870,9 @@ static void lb_warm_worker(void *arg) {
VkPipeline pipe = l ? compute_create(sh, l) : VK_NULL_HANDLE;
if (pipe) vkDestroyPipeline(P.device, pipe, NULL);
lb_count(2, pipe ? 3 : 2, now_ms() - t0);
} else if (P.gpl && (st == GFX_STAGE_VS || st == GFX_STAGE_FS)) {
lb_library(sh, st == GFX_STAGE_VS, 0);
} else if (st == GFX_STAGE_VS || st == GFX_STAGE_FS) {
if (P.gpl) lb_library(sh, st == GFX_STAGE_VS, 0);
else lb_skip_lib();
}
if (owned) gfx_shader_release_private(sh);
plat_mutex_lock(&LB.stat);
@@ -2894,7 +2904,12 @@ static void lb_warm_prepared(const char *dir) {
if (!k) lb_warm_worker(NULL);
for (int i = 0; i < k; i++) plat_thread_join(t[i]);
lb_report("preparation done");
printf("prepare-shaders: %zu variants built into the driver cache on %d threads in %.0f ms\n", n, k, now_ms() - LB.t0);
// with gpl off only compute variants are built: say so, rather than count the skipped ones as built
if (LB.nolib)
printf("prepare-shaders: %zu variants on %d threads in %.0f ms, %llu vertex/fragment variants skipped (graphics "
"pipeline library off, they build at their first draw)\n", n, k, now_ms() - LB.t0, (unsigned long long)LB.nolib);
else
printf("prepare-shaders: %zu variants built into the driver cache on %d threads in %.0f ms\n", n, k, now_ms() - LB.t0);
free(LB.e), LB.e = NULL, LB.n = 0, LB.next = 0, LB.done = 0;
}
+4 -8
View File
@@ -127,14 +127,10 @@ void rec_drain(void) {
void rec_begin(VkCommandBuffer cb) {
static int mode = -1;
if (mode < 0) {
// Default off on AMD GPUs: with the recorder their drivers lost the device within the first frames or at menu
// opens, 1 boot in 3 on an RX 9070 XT, and TOTK_VK_RECORDER=0 ended it for three users (issue #5); under RADV
// the kernel saw an illegal opcode at the end of a command buffer (issue #40). Letting the recorder end its own
// parts (rec_end_cb) fixed the white scene on an RX 6700 XT; the default stays off until more AMD cards run it.
const char *set = cfg_get("TOTK_VK_RECORDER");
mode = rt_knob("VK_RECORDER", vk_caps.amd ? "0" : "1");
if (!mode && vk_caps.amd && !(set && *set))
rt_log("vk_rec: no recorder thread on AMD GPUs (TOTK_VK_RECORDER=1 turns it on)");
// Default on for every GPU. AMD drivers once lost the device with the recorder (issues #5 and #40); after
// vk_mem_note_recording kept objects alive until their submission completed and the recorder began ending
// its own parts (rec_end_cb), RDNA 1 and 2 cards ran with it on and off without a loss or a white scene.
mode = rt_knob("VK_RECORDER", "1");
if (mode) {
plat_event_init(&wake_ev, 0, 0);
rec_prio = plat_thread_priority_get();
+29 -2
View File
@@ -55,6 +55,7 @@ typedef struct Tex {
uint64_t wr_fence; // any entry: newest submission recorded to write its image (render, copy, clear,
// storage binding); ReadTexels waits for it instead of draining the replay queue
uint8_t listed; // in owned[]
uint8_t *snap; // a texture that fills no page: its storage bytes when the GPU write was recorded
uint32_t wt_n; // WriteTexels into the storage since the last upload
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
} Tex;
@@ -507,6 +508,7 @@ static void destroy_tex(Tex *e) {
if (e->released) nreleased--, rel_unlink(e);
vk_exec_image_gone(e->pub.image);
vk_defer_image(e->pub.image, e->alloc);
free(e->snap);
free(e);
}
@@ -727,12 +729,33 @@ static void upload(Tex *e) {
perf_end(PERF_TEX, p0);
}
// Whether e's storage fills no 4 KiB page of its own, so the write tracking can only see writes to pages it shares.
static int fills_no_page(const Tex *e) {
return ((e->pub.addr + e->t.L.size) & ~0xfffull) <= ((e->pub.addr + 0xfff) & ~0xfffull);
}
// A texture that fills no page keeps a copy of its storage bytes from when a GPU write to it was recorded
// (vk_texture_gpu_written), up to SNAP_MAX bytes; wb_exact (TOTK_WB_EXACT, default on) enables it.
enum { SNAP_MAX = 4096 };
static int wb_exact = -1;
static volatile int32_t n_kept;
// vk_mem_changed over the pages wholly inside e's storage (all of its pages when it fills none): a neighbour that shares
// the first or last page does not count as a write to e.
// the first or last page does not count as a write to e. A texture that fills no page shares all of its pages, so
// there a page write counts only when e's own bytes differ from the copy taken at the GPU write. TotK's Purah Pad
// camera renders the ID, screen rect and depth of what is under the reticle into 4x4 targets of 512 bytes, two
// slots used in turn, and the CPU reads them a frame later. One slot's rect target shares its page with data the CPU
// writes every frame: without the copy its renders were never written back, the game read a zero rect every other
// frame and the target's name label blinked. TOTK_WB_EXACT=0 restores the page-only decision.
static int storage_rewritten(const Tex *e, uint32_t since, uint32_t *now) {
uint64_t lo = (e->pub.addr + 0xfff) & ~0xfffull, hi = (e->pub.addr + e->t.L.size) & ~0xfffull;
if (hi <= lo) lo = e->pub.addr, hi = e->pub.addr + e->t.L.size;
return vk_mem_changed(lo, hi - lo, since, now);
int ch = vk_mem_changed(lo, hi - lo, since, now);
if (ch != 1 || !since || !e->snap || memcmp(e->snap, (const void *)(uintptr_t)e->pub.addr, e->t.L.size)) return ch;
log_limited(&n_kept, "write-back of %#llx kept: fmt %#x %ux%u, %llu bytes; a neighbour wrote its page",
(unsigned long long)e->pub.addr, e->t.d.format, e->t.d.width, e->t.d.height,
(unsigned long long)e->t.L.size);
return 0;
}
// A mirrored texture is uploaded on first use and again when its storage pages carry a newer write generation.
@@ -947,6 +970,10 @@ void vk_texture_gpu_written(const tex_t *t) {
if (e->pub.mirrored) {
e->gpu_dirty = 1;
storage_rewritten(e, 0, &e->gpu_gen); // CPU writes up to here are older than this render
if (wb_exact < 0) wb_exact = cfg_bool("TOTK_WB_EXACT");
if (wb_exact && !e->snap && e->t.L.size <= SNAP_MAX && fills_no_page(e) && !(e->snap = malloc(SNAP_MAX)))
rt_fatal("vk: out of memory for a texture's storage copy");
if (e->snap) memcpy(e->snap, (const void *)(uintptr_t)e->pub.addr, e->t.L.size);
e->gpu_seq = ++gpu_writes;
e->gpu_fence = vk_exec_recording_fence();
wb_cfg_init();
+1 -1
View File
@@ -150,7 +150,7 @@ WINDRES = UCRT / 'windres.exe' # runtime/totk.rc: the manifest of totk.exe
TOOLCHAIN_BIN = None # llvm-mingw bin directory, set by main() for --toolchain
DEPS = None # the deps directory of an llvm-mingw build
# tools/data/expected-<display version>.json for each build version name.
DISPLAY_VERSIONS = {'v0': '1.0.0', 'v112': '1.1.2', 'v121': '1.2.1', 'v143': '1.4.3'}
DISPLAY_VERSIONS = {'v0': '1.0.0', 'v111': '1.1.1', 'v112': '1.1.2', 'v121': '1.2.1', 'v143': '1.4.3'}
GiB = 1 << 30
# Peak commit of one compile, 1.2.1 at -O1 (2026-10-20): 120-180 MiB per ~3 MiB chunk, 568 MiB for main_table.c
+1 -1
View File
@@ -51,7 +51,7 @@ def mng_pin() -> str:
"""The MacNativeGames commit tools/fetch_refs.sh pins."""
for line in (ROOT / "tools" / "fetch_refs.sh").read_text(encoding="utf-8").splitlines():
f = line.split()
if len(f) == 4 and f[:2] == ["fetch", "MacNativeGames"]:
if len(f) >= 4 and f[:2] == ["fetch", "MacNativeGames"]: # a fifth word asks for the history
return f[3]
sys.exit("tools/fetch_refs.sh pins no MacNativeGames commit")
+25
View File
@@ -0,0 +1,25 @@
{
"_comment": "Written locally for the player's own 1.1.x update (version value 0x20000, seen as 1.1.1). RomFS hashes filled in after the first merge; no game data here. No rewritten_sha256: this dump was not run through MacNativeGames' NSO rewriter, so the three digests of the other versions have no reference here and none of the tools read them.",
"title_id": "0100F2C0115B6000",
"version": 131072,
"display_version": "1.1.1",
"exefs_sha256": {
"main": "1664b8cf664524d90ca64deb3ce41709266457df966dcb499485267554eb8cec",
"main.npdm": "fa3f60922406ab9e2488c2b8d77b12e9869b435033fd8c27c543c347bc185587",
"rtld": "28317605926caa40a5cbee3e3cb290c65783b4d9f8c3697b43ca41ab2866a44d",
"sdk": "91573d4444d0dcd07080db78fece2b098b76080d77cdb2e1d301a2a41d7c4eea",
"subsdk0": "9d04c98997977e99c12a48a2dbfd3064b8da96b5152e0119064957a43f6c3f99"
},
"romfs": {
"size": 17051891304,
"sha256": "02ed5792f2f6ccff563c035d4a5408a49c1e941760785e8321b42c98e96dbe9b"
},
"shared_fonts_sha256": {
"standard": "823913c9d80807979381e327846466f561f31ba1842cea8f7fedc5ad7d45d2ad",
"chinese-simplified": "a04eb490283d8393b2c49c52e315132461d790ac73996cb43ac869ce72003938",
"chinese-simplified-extension": "f5784e7a553fef9a05b3b7dba94e77e79162ace9db8dafc715ace0be1fd9b9ff",
"chinese-traditional": "9d59b74e59a2dc92296d6bebad7cce791ba94688bd436304aaae174a58bd7457",
"korean": "5a4e694782ead5ddcbc0a226aab24362e2d641ca33bac7bdf8cda36ff157c9e8",
"nintendo-extension": "f9f62e025c9bbe0e9ae25a54ba5011202b1e3a3f3c33b2d876c40aff6174af68"
}
}
+14 -4
View File
@@ -6,23 +6,33 @@ set -eu
cd "$(dirname "$0")/.."
mkdir -p refs
# fetch <name> <url> <commit> [history]: a shallow checkout of the commit, or with "history" the commit and all its
# ancestors (tools/shader_list.py walks the log of MacNativeGames' shader list). origin/HEAD names the commit either
# way, so tools can read the pinned revision as origin/HEAD.
fetch() {
name=$1 url=$2 commit=$3
name=$1 url=$2 commit=$3 history=${4:-}
dir=refs/$name
if [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$commit" ]; then
if [ "$(git -C "$dir" rev-parse HEAD 2>/dev/null)" = "$commit" ] &&
[ "$(git -C "$dir" rev-parse -q --verify refs/remotes/origin/HEAD 2>/dev/null)" = "$commit" ] &&
{ [ -z "$history" ] || [ "$(git -C "$dir" rev-parse --is-shallow-repository)" = false ]; }; then
echo "$name: up to date"
return
fi
rm -rf "$dir"
git init -q "$dir"
git -C "$dir" remote add origin "$url"
git -C "$dir" fetch -q --depth 1 origin "$commit"
if [ -n "$history" ]; then
git -C "$dir" fetch -q origin "$commit"
else
git -C "$dir" fetch -q --depth 1 origin "$commit"
fi
git -C "$dir" checkout -q FETCH_HEAD
git -C "$dir" update-ref refs/remotes/origin/HEAD "$commit"
echo "$name: $commit"
}
fetch LASRecomp https://github.com/peiikai/LASRecomp.git da35b83a9611d046a9a681aae668d7085954d37f
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git b75be8cfa704df3701e86130c8ced268e79d6b11
fetch MacNativeGames https://github.com/ly05010419/MacNativeGames.git b75be8cfa704df3701e86130c8ced268e79d6b11 history
fetch d2r-mobile https://github.com/jhaizhou-ops/d2r-mobile.git 53e7c5bb9ca3ea53e2160005be8fbe920cd9cb53
fetch nxnative https://github.com/seedds/nxnative.git d5b380b3a80495609c2e751bb994911a638935ad
fetch SwitchRecomp https://github.com/bgyss/SwitchRecomp.git 0f99e1c5c1e78fdc8e01e58a0f8dc8622e82976b
+26 -5
View File
@@ -38,6 +38,7 @@ import subprocess
import sys
import tarfile
import time
import urllib.error
import urllib.request
import zipfile
from pathlib import Path
@@ -122,15 +123,35 @@ def fetch(name):
url, want = PINS[name]
DL.mkdir(parents=True, exist_ok=True)
path = DL / Path(url.split('?')[0]).name.replace('%7E', '~')
if name in ('tcl-linux-src', 'tk-linux-src'):
path = DL / f'{name}-9.0.4.tar.gz' # upstream archives have identical basenames
if name == 'libvpx-src':
path = DL / 'libvpx-1.17.0.tar.gz'
if not path.is_file():
print(f'download {url}')
with urllib.request.urlopen(url) as r, open(path, 'wb') as f:
shutil.copyfileobj(r, f)
if path.is_file():
if sha256_file(path) == want:
return path
print(f'{path}: not the pinned file (a cut download or an older pin); downloading it again')
path.unlink()
# into <name>.part, renamed once complete: a cut connection leaves a .part that the next run resumes, never a
# short file under the final name
part = path.with_name(path.name + '.part')
have = part.stat().st_size if part.is_file() else 0
print(f'download {url}' + (f' (resuming at {have} bytes)' if have else ''))
req = urllib.request.Request(url, headers={'Range': f'bytes={have}-'} if have else {})
try:
with urllib.request.urlopen(req) as r:
if have and r.status != 206: # the server ignored the range: start over
have = 0
with open(part, 'ab' if have else 'wb') as f:
shutil.copyfileobj(r, f)
except urllib.error.HTTPError as e:
if e.code != 416: # 416: the .part already holds the whole file
raise
part.replace(path)
got = sha256_file(path)
if got != want:
sys.exit(f'{path}: sha256 {got}, pinned {want}')
path.unlink() # a .part resumed from another upload, or the URL now serves another file: start clean next run
sys.exit(f'{path}: sha256 {got}, pinned {want}; deleted, run again to download it anew')
return path
+73 -11
View File
@@ -61,6 +61,10 @@ PINS = {
'python-linux': ('https://github.com/astral-sh/python-build-standalone/releases/download/20261026/'
'cpython-3.14.10%2B20261026-x86_64-unknown-linux-gnu-install_only.tar.gz',
'1b38024924eb7f4cf0cab45d7b871d1f1991354190fccd2625ac7529a43786e2'),
'tcl-linux-src': ('https://github.com/tcltk/tcl/archive/refs/tags/core-9-0-4.tar.gz',
'8c01bac92a9a8ce271b63d3aa28f09cde2c6762d6719ca671cdc8aee25391234'),
'tk-linux-src': ('https://github.com/tcltk/tk/archive/refs/tags/core-9-0-4.tar.gz',
'ab9bd89fcff2e1248b7a9c097035f817fa0725285028c95686fdb92db65bc148'),
'ninja-linux': ('https://github.com/ninja-build/ninja/releases/download/v1.13.2/ninja-linux.zip',
'5749cbc4e668273514150a80e387a957f933c6ed3f5f11e03fb30955e2bbead6'),
'zstd-src': ('https://github.com/facebook/zstd/releases/download/v1.5.7/zstd-1.5.7.tar.gz',
@@ -68,12 +72,12 @@ PINS = {
'vulkan-headers-src': ('https://github.com/KhronosGroup/Vulkan-Headers/archive/refs/tags/v1.4.363.tar.gz',
'cbaf687d3c59b9666fe080e7f8396b6b4f4d344a768ee6b337c59852f4526b68'),
'vma-header': build_game.VMA_FETCH,
# appimagetool and the runtime are published only under the moving "continuous" tag: pinned by hash, so a new
# upload stops the build until the pins are updated on purpose
'appimagetool': ('https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage',
'ac7fd56feade6002c2f57db39587113275d351fce911a5fbb29ea84cd0666cab'),
'appimage-runtime': ('https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-x86_64',
'617aa77314d57c8b8e4fae9c3f0b35af7b30d5529cb69e98f64f92510ed98a46'),
# appimagetool and the type-2 runtime from release tags, not "continuous", whose uploads change under the same
# URL and would stop the build at the hash check
'appimagetool': ('https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage',
'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0'),
'appimage-runtime': ('https://github.com/AppImage/type2-runtime/releases/download/20251108/runtime-x86_64',
'2fca8b443c92510f1483a883f60061ad09b46b978b2631c807cd873a47ec260d'),
}
mr.PINS.update(PINS) # one cache (toolchain/release-dl) and one fetch for both releases
@@ -81,10 +85,11 @@ mr.PINS.update(PINS) # one cache (toolchain/release-dl) and one fetch for both
LLVM_MEMBERS = [f'bin/clang-{LLVM_MAJOR}', 'bin/clang', 'bin/clang++', 'bin/lld', 'bin/ld.lld', 'bin/llvm-ar',
'bin/llvm-ranlib', 'bin/llvm-objcopy', 'bin/llvm-strip', f'lib/clang/{LLVM_MAJOR}/include']
# The parts of the sniper SDK a C/C++ compile and link for x86_64 can read.
SNIPER_MEMBERS = ['usr/include', 'usr/lib/x86_64-linux-gnu', 'usr/lib/gcc/x86_64-linux-gnu',
SNIPER_MEMBERS = ['usr/include', 'usr/share/pkgconfig', 'usr/lib/x86_64-linux-gnu', 'usr/lib/gcc/x86_64-linux-gnu',
'lib/x86_64-linux-gnu', 'lib64', 'usr/share/doc/libsdl3-0', 'usr/share/doc/libopus0',
'usr/share/doc/libvpx6', 'usr/share/doc/libc6', 'usr/share/doc/gcc-10-base',
'usr/share/doc/libvulkan1']
'usr/share/doc/libvulkan1',
'usr/share/doc/libxft2', 'usr/share/doc/libxrender1']
# glibc's link libraries and their scripts (lib<name>.so, .a, _nonshared.a), and the GCC runtime pieces clang links.
GLIBC_LIBS = ['c', 'm', 'mvec', 'dl', 'pthread', 'rt', 'util']
GCC_LIBS = {'libgcc.a', 'libgcc_eh.a', 'libgcc_s.so', 'libstdc++.a', 'libsupc++.a'}
@@ -139,8 +144,11 @@ def full_sysroot():
"""The sniper SDK's x86_64 compile and link files in toolchain/release-work/sniper, absolute symlinks made relative
(the SDK points some at /lib and /usr/lib, which outside its container are the host's)."""
root = WORK / 'sniper'
if (root / '.relinked').is_file():
if (root / '.relinked').is_file() and (root / 'usr/share/doc/libxft2/copyright').is_file():
return root
# Older cached SDK extractions omitted the font libraries' architecture-independent pkg-config files and the
# libXft and libXrender copyright files.
(root / '.unpacked').unlink(missing_ok=True)
tar_extract('sniper-sdk', root, SNIPER_MEMBERS)
for p in root.rglob('*'):
if p.is_symlink() and os.readlink(p).startswith('/'):
@@ -309,7 +317,50 @@ def build_sysroot(out, sysroot, files):
# ---- python/ ---------------------------------------------------------------------------------------------------------
def build_python(out):
def build_tk_xft(out, llvm, sysroot):
"""Replace standalone Python's no-Xft Tcl/Tk with the same version using system TrueType fonts (#227)."""
prefix = WORK / 'tcltk-xft'
env = dict(os.environ, CC=sysroot_cc(llvm, sysroot),
PKG_CONFIG_SYSROOT_DIR=str(sysroot),
PKG_CONFIG_LIBDIR=os.pathsep.join(str(sysroot / p) for p in
('usr/lib/x86_64-linux-gnu/pkgconfig', 'usr/share/pkgconfig')),
PKG_CONFIG_PATH='')
sources = {}
for name in ('tcl', 'tk'):
src = mr.unpack(f'{name}-linux-src') / f'{name}-core-9-0-4'
sources[name] = src
build = src / 'unix'
options = [f'--prefix={prefix}', '--enable-shared', '--disable-zipfs', '--disable-rpath']
if name == 'tk':
options += [f'--with-tcl={prefix / "lib"}', '--enable-xft', '--disable-libcups', '--disable-xss']
run([build / 'configure', *options], cwd=build, env=env)
run(['make', f'-j{os.cpu_count() or 4}'], cwd=build, env=env)
run(['make', 'install'], cwd=build, env=env)
lib = prefix / 'lib'
# A successful GUI selftest alone did not catch missing glyphs. Reject a no-Xft build before packaging it.
symbols = subprocess.run(['nm', '-D', str(lib / 'libtcl9tk9.0.so')],
capture_output=True, text=True, check=True).stdout
if not re.search(r'\bU Xft', symbols):
sys.exit('Tcl/Tk build has no Xft font rendering')
dst = out / 'python' / 'lib'
for name in ('libtcl9.0.so', 'libtcl9tk9.0.so'):
shutil.copy2(lib / name, dst / name)
# libXft is not on every desktop (the old Tk did not need it) and without it _tkinter fails to import, so the
# window would not open at all. python's RPATH ($ORIGIN/../lib) finds these beside the Tk library.
sys_lib = sysroot / 'usr' / 'lib' / 'x86_64-linux-gnu'
for pattern in ('libXft.so.2*', 'libXrender.so.1*'):
for f in sys_lib.glob(pattern):
shutil.copy2(f, dst / f.name, follow_symlinks=False)
for name in ('tcl9.0', 'tk9.0'):
shutil.rmtree(dst / name)
shutil.copytree(lib / name, dst / name, symlinks=True)
licenses = out / 'licenses'
licenses.mkdir(exist_ok=True)
for name, src in sources.items():
shutil.copy2(src / 'license.terms', licenses / f'{name}-license.terms')
def build_python(out, llvm, sysroot):
src = mr.unpack('python-linux') / 'python'
py = out / 'python'
shutil.copytree(src, py, symlinks=True,
@@ -319,6 +370,7 @@ def build_python(out):
run([exe, '-m', 'pip', 'install', '--no-compile', '--only-binary', ':all:', '-r',
ROOT / 'tools' / 'requirements-installer.txt'], env=env, stdout=subprocess.DEVNULL)
run([exe, '-m', 'pip', 'uninstall', '-y', 'pip'], stdout=subprocess.DEVNULL) # the setup installs nothing
build_tk_xft(out, llvm, sysroot)
strip(py, out / 'llvm' / 'bin' / 'llvm-strip')
site = next(py.glob('lib/python3.*/site-packages'))
# The release folder, tools/ and recomp/ on sys.path, as the Windows python's ._pth does: `python -m installer` and
@@ -346,6 +398,8 @@ def collect_licenses(out, sysroot):
(doc / 'libc6' / 'copyright', 'glibc-copyright.txt'),
(doc / 'gcc-10-base' / 'copyright', 'libstdc++-copyright.txt'),
(doc / 'libvulkan1' / 'copyright', 'vulkan-loader-copyright.txt'),
(doc / 'libxft2' / 'copyright', 'libXft-copyright.txt'),
(doc / 'libxrender1' / 'copyright', 'libXrender-copyright.txt'),
(WORK / 'vulkan-headers' / 'LICENSE.md', 'vulkan-headers-LICENSE.txt'),
(WORK / 'zstd' / 'LICENSE', 'zstd-LICENSE.txt'),
(out / 'python' / 'lib' / f'python{sys_version(out)}' / 'LICENSE.txt', 'python-LICENSE.txt'),
@@ -370,6 +424,8 @@ APPRUN = '''#!/bin/sh
# The AppImage entry: the setup window, run by the bundled Python from the read-only mount (no bytecode written there).
HERE="$(dirname "$(readlink -f "$0")")"
export PYTHONDONTWRITEBYTECODE=1
export TCL_LIBRARY="$HERE/python/lib/tcl9.0"
export TK_LIBRARY="$HERE/python/lib/tk9.0"
exec "$HERE/python/bin/python3" -m installer.gui "$@"
'''
@@ -452,6 +508,12 @@ def check_bundle(out):
f'{r.stderr[-2000:]}')
print(f'bundle check: the installer ran the host check from the folder and stopped at the game files '
f'(exit {r.returncode})')
run([py, '-c', 'import tkinter as tk; from tkinter import font; '
'root = tk.Tk(); root.withdraw(); '
'fonts = [font.nametofont("TkDefaultFont"), font.Font(family="DejaVu Sans"), '
'font.Font(family="DejaVu Sans Mono")]; '
'missing = [(f.actual("family"), c) for f in fonts for c in "УстановкаЁёЙй" if f.measure(c) <= 0]; '
'root.destroy(); assert not missing, f"Cyrillic glyphs missing: {missing}"'], cwd='/', env=env)
r = subprocess.run([str(py), '-m', 'installer.gui', '--selftest', '--root', str(out)], cwd='/', env=env,
capture_output=True, text=True, timeout=300)
if r.returncode:
@@ -511,7 +573,7 @@ def main():
build_bin(out, llvm, sysroot)
rtlib = runtime_lib(out, llvm, sysroot)
build_sysroot(out, sysroot, used_sysroot_files(out, llvm, sysroot, rtlib))
build_python(out)
build_python(out, llvm, sysroot)
mr.build_sources(out)
collect_licenses(out, sysroot)
build_entry(out)
+4 -4
View File
@@ -1,6 +1,6 @@
# Adapted from MacNativeGames games/totk/scripts/make_romfs_patches.py @ ebe621d (MIT, Copyright (c) 2026
# MacNativeGames contributors).
"""Build the 60 fps RomFS overlay for TotK 1.2.1 or 1.4.3 (runtime/romfs.c, TOTK_ROMFS_PATCHES).
"""Build the 60 fps RomFS overlay for TotK 1.1.1, 1.2.1 or 1.4.3 (runtime/romfs.c, TOTK_ROMFS_PATCHES).
UI/LayoutArchive/Common.Product.<110 or 140>.Nin_NX_NVN.blarc.zs: the weapon / shield / material quick menus capture the game
picture for their background when the panes C_CaptureBG_00, C_CaptureClearBG_00 and P_CaptureClearBG_00 become
@@ -8,7 +8,7 @@ visible at animation frame 1.0 (anim/ShortCutBG_00_In.bflan), AutoBuilder the sa
(anim/AutoBuilder_00_In.bflan). UI animations advance 0.5 frames per game frame at 60 fps, so the capture lands after
the world stopped drawing and the background can be black. The visibility keys move to frame 0.0: the live world is
captured at once (upstream checked weapon, shield, material and ability wheel at 60 fps, and 30 fps).
The game-over flow fix of the same upstream script is 1.0.0 only (1.2.1 and 1.4.3 run DmF_SY_GameOver.110) and is
The game-over flow fix of the same upstream script is 1.0.0 only (1.1.1, 1.2.1 and 1.4.3 run DmF_SY_GameOver.110) and is
left out.
--aspect <w>x<h> (a game resolution of another shape than 16:9, such as 3440x1440 or 1280x800) also lays the UI out
@@ -44,7 +44,7 @@ from pathlib import Path
root = Path(__file__).resolve().parents[1]
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument('--version', choices=('v121', 'v143'), default='v121')
ap.add_argument('--version', choices=('v111', 'v121', 'v143'), default='v121')
ap.add_argument('--romfs', type=Path, help='default build/extracted/<version>/romfs')
ap.add_argument('--out', type=Path,
help='default build/<version>/romfs-patches (romfs-patches-xbox with --xbox-prompts)')
@@ -309,7 +309,7 @@ def sarc_replace(data, new):
return bytes(out)
COMMON = {'v121': '110', 'v143': '140'} # the version in the name of each game's Common archive
COMMON = {'v111': '110', 'v121': '110', 'v143': '140'} # the version in the name of each game's Common archive
ARCHIVE = f'UI/LayoutArchive/Common.Product.{COMMON[args.version]}.Nin_NX_NVN.blarc.zs'
MEMBERS = {
'anim/ShortCutBG_00_In.bflan': ('C_CaptureBG_00', 'C_CaptureClearBG_00', 'P_CaptureClearBG_00'),
+5
View File
@@ -79,6 +79,11 @@ def pack(cache, keep=False):
out = cache / PACK
packed = read_pack(out)
loose = loose_files(cache)
if not loose:
# Nothing to fold in: every file of the cache is already in the pack, so writing it again copies the same
# bytes over the same entries (1.37 GiB here, 2.6 s, and a file system that then has the old pack's blocks to
# release as well). The installer runs this after every prepare.
return len(packed), 0, 0, out.stat().st_size if out.is_file() else 0
keys = sorted(set(packed) | set(loose))
tmp = out.with_name(PACK + '.tmp')
index, written, skipped, offset = [], [], 0, 16 + 48 * len(keys)
+9 -2
View File
@@ -140,10 +140,17 @@ def main():
t0 = time.monotonic()
variants, prio = parse_list(read_list(a.list))
extracted = ROOT / 'build' / 'extracted' / a.version
shaders, _ = inventory(a.romfs or extracted / 'romfs', a.exefs or extracted / 'exefs')
listed = len(variants)
shaders, _ = inventory(a.romfs or extracted / 'romfs', a.exefs or extracted / 'exefs', everything=False)
# A missing list row says nothing about the walk: the list covers several versions, and 1.1.1 holds 17,587 of its
# 41,414 rows, so most rows are absent because the game does not have them, not because the walk missed them. A walk
# that found next to nothing does say something: the shader directories have moved.
if len(shaders) < max(64, listed // 20):
print(f'only {len(shaders)} shaders found of the {listed} listed variants: reading every RomFS file instead',
flush=True)
shaders, _ = inventory(a.romfs or extracted / 'romfs', a.exefs or extracted / 'exefs')
t1 = time.monotonic()
progs, recs, seen, skipped = {}, [], set(), {'not in this version': 0, 'stage': 0}
listed = len(variants)
variants += [(key, DEFAULT) for key in sorted(shaders)]
for key, deps in variants:
s = shaders.get(key)
+9 -1
View File
@@ -172,7 +172,13 @@ def startup_package(name):
name.startswith('Shader/') and name.endswith('.zs'))
def inventory(romfs, exefs):
# The RomFS directories that can hold shaders: the agl and gsys packages, the effects, terrain, the UI layout archives
# and the game's own shader packs. Every shader of 1.1.1 (27,576) comes from these six; they are 1,996 entries and
# 147 MiB, 0.9% of a RomFS whose every file the walk below used to read and decompress first (65 s against 2.8 s).
SHADER_DIRS = ('Shader/', 'Effect/', 'Lib/agl/', 'Lib/Terrain/', 'Lib/gsys/', 'UI/LayoutArchive/')
def inventory(romfs, exefs, everything=True):
"""Every unique shader: key -> dict(stage, control, code, source, origin, startup). As in MNG, a shader is a startup
shader when it comes from an agl package, a game module, or a startup_package() RomFS file; the others come from
effect and UI layout resources, which MNG translates only for bind states players have met. Also returns the number
@@ -228,6 +234,8 @@ def inventory(romfs, exefs):
pos += max(size, 8)
for name in sorted(entries):
if not everything and not name.startswith(SHADER_DIRS):
continue
raw = read(name)
if raw[:4] not in (ZSTD, b'SARC', b'FSHA', b'BNSH', b'BAHS') and b'BNSH\0\0\0\0' not in raw:
continue
+4
View File
@@ -8,6 +8,7 @@ our play meets: at 1340bee the list alone has 134 rows of 1.4.3 shaders, its his
25,238. A row no draw asks for costs one translation. No copy of the list goes into the repository.
"""
import subprocess
import sys
PATH = 'games/totk/assets/shader_list.tsv'
@@ -18,6 +19,9 @@ def git(repo, *args):
def history(repo, rev='origin/HEAD'):
"""rev's list, then each V row of an older revision that no newer one has, newest first."""
if git(repo, 'rev-parse', '--is-shallow-repository').strip() == 'true':
# a shallow clone has rev's list only: preparing from it skips most variants and shows only as stutter in play
sys.exit(f'{repo} is a shallow clone without the shader list\'s history; run tools/fetch_refs.sh')
lines = git(repo, 'show', f'{rev}:{PATH}').splitlines()
seen = set(lines)
for h in git(repo, 'log', '--format=%H', rev, '--', PATH).split()[1:]:
+15 -2
View File
@@ -26,7 +26,8 @@
# async_preempt = 1
# Replay NVN submissions on their own thread, at most one frame behind the game (1 = on). Adds up to a frame of latency.
# replay_async = 0
# 1 lifts the virtual core gate: guest threads run without the console's limit of one thread per core.
# 1 lifts the virtual core gate: guest threads run without the console's limit of one thread per core. For experiments
# only: the game keeps per-core data without locks and can crash.
# free_cores = 0
# Windows timer resolution of 1 ms for steadier frame pacing (0 = the Windows default).
# timer_1ms = 1
@@ -88,7 +89,7 @@
# Vulkan.
# Vulkan validation layer: checks every call and slows the game down a lot. Needs the layer installed (Vulkan SDK).
# vk_validation = 0
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on, except on AMD GPUs.
# A thread of its own makes the Vulkan calls of each frame, 0 or 1. Default: on.
# vk_recorder = 1
# Dynamic vertex input, 0 or 1. Default: on, except on Intel's older integrated GPU drivers (Vulkan 1.3.215 and older).
# vk_dyn_vertex = 1
@@ -140,6 +141,18 @@
# window is no longer stretched as it used to be; stretch fills the window. Changes while the game runs take effect at
# once.
# aspect = fit
# Field of view, in percent of the game's own: 70 to 140, 100 (the default) leaves the camera as it is. Higher shows
# more of the world around Link. Game version 1.4.3 only. Changes while the game runs take effect at once.
# fov = 100
# Colour adjustment of the picture, HUD included, before it is scaled to the window: brightness (-30 to 30, added in %),
# contrast and saturation (70 to 150 and 50 to 200 %), gamma (0.8 to 1.2, below 1 brightens dark tones). At 0, 100,
# 100 and 1 nothing runs. color_preset is the menu's profile (default, vivid, clarity, custom), which sets the four.
# Changes while the game runs take effect at once.
# color_preset = default
# color_brightness = 0
# color_contrast = 100
# color_saturation = 100
# color_gamma = 1
[language]
# Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale.