344 Commits
Author SHA1 Message Date
Valiantsin DzerakhandClaude Opus 5.5 0f65b4ee07 Show the game and port versions in the Discord status
The details line read "Update 1.4.3"; it now reads "Game 1.4.3 · Recomp 0.3.7".
The port version drops its +commit suffix there, the art's hover text keeps
the full one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:56:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1ec1e21d77 Use the project's own Discord application for Rich Presence
The default app id pointed at the application the feature was developed
against, owned by the pull request's author. The new one belongs to the
project, with the ring icon uploaded as the totk_512x512 art asset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:49:51 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f0fe1e56e3 Merge task/surround51: 5.1 surround output (#438), stereo by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:36:18 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1e378dd177 Keep stereo as the default speaker output
With audio_channels = auto as the default, a player whose Windows device reports six or more channels gets 5.1
without asking for it. USB headsets with virtual 7.1 and HDMI receivers or TVs set to 5.1 or 7.1 report that too,
though many of their owners listen in stereo. Stereo as the default keeps today's sound for everyone; 5.1 and auto
stay in the Sound tab and in totk.ini.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:35:52 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a732c16ce6 Merge pull request #438: 5.1 surround output (Specht2010)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:35:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 46a3b64895 Keep the console at start: until the game window opens it shows the shader prewarm
d2fbe0a freed the console as soon as the log file was open, but the game window only opens after the shader
prewarm. After an update the prewarm translates every shader again (about 5 minutes on a 24-thread CPU), and the
player saw neither a console nor a window all that time. The console release and show_console are taken back; the
window is still raised at start (#521).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:31:19 +03:00
Valiantsin Dzerakh c4a360e50d Up version 0.3.7 2026-11-03 20:27:14 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4f38e0e14b Ask for the save and input recording in the crash template itself
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:17:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 69e7bef868 Merge dev into task/discord
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:11:51 +03:00
Valiantsin DzerakhandClaude Opus 5.5 13e9ffec38 Merge pull request #455: Discord Rich Presence, off by default (avrahambenaram)
Conflicts in cfg.c (the key list) and main.c (the usage line) resolved by keeping dev's lines and adding the
Discord keys and --probe-discord.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:11:12 +03:00
Valiantsin Dzerakh 85123016c2 Merge task/smallfixes: game languages in the F1 menu (#580), versions and lows in the overlay (#587, #588), no console and a raised window at start (#208, #521)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	runtime/cfg.c
2026-11-03 20:10:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d2fbe0a2b1 Close the game's own console and bring its window to the front (#208, #521)
A game started from a frontend (Playnite, RetroBat, ES-DE) or a shortcut got a console beside it, and its window
opened behind other windows, out of reach without a mouse. Once the log file is open, the console is freed when no
other process shares it; a terminal launch keeps its output, and show_console = 1 keeps the console. The window is
raised at start, with AttachThreadInput when Windows refuses the foreground. rt_fatal writes only to the log, and a
console owned by the game closed with it anyway, so no error text is lost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:09:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3422952e85 Show the versions, 1% low and 0.1% low in the full performance overlay (#587, #588)
The panel opens with the port version and the game update, so a screenshot in a bug report carries both. Under RAM,
after a gap, two rows on the same grid: FPS with the 1% and 0.1% lows, and the frame times. perfmon.c keeps a
histogram of frame times in 0.25 ms bins, one slice per second over the last ten seconds, and reads the lows from it
twice a second.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:09:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 beb93276eb Offer every game language the build ships in the F1 menu (#580)
The game language row listed eleven languages. 1.4.3 also ships Brazilian Portuguese (Mals USpt), and every build
ships British English, Canadian French and Latin American Spanish (EUen, USfr, USes), which ov_language already
accepted. ui_text_game_langs lists them per game build: pt-BR from 1.4.3 on, since 1.2.1 has no USpt. A row selects
only its own code, so en-GB and es-419 no longer show as English and Spanish.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 20:09:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1640b85530 Merge task/dlss: DLSS pull requests #566, #594 and #534
#566 (mictony6), #594 (DizzyMan1337) and #534 (SchulzCode), tested one by one by itaskaev2 on an RTX 4080 SUPER with 1.4.3. One camera-motion implementation (#594's), one dlss_preset key, translator patch 0012 of #594 renumbered 0015. #594's scene patch, render-size hook, snapshot copy and per-object tables run only when DLSS can run: a 1.4.3 run without DLSS and one with dlaa = 1 but no NGX log the game's own scene size, 0 validation errors and 0 shader failures. selftest and the kernel, vkexec, nvn-tex and shader probes pass; 29587 shader variants prepared, 0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:59:42 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5024daf40d Merge pull request #534: DLSS / DLAA on Linux, a DLSS model choice, the real frame time and a character-trail mask (SchulzCode)
Merged on top of #566 and #594, by hand in vk_dlaa.c:
- One dlss_preset key: default keeps #594's model per mode (K for DLAA, Quality and Balanced, M for Performance, L
  for Ultra Performance); k, j or any other letter a..m takes that model in every mode, live, recreating the features
  as #534 does. The menu texts say so, with pt-BR and zh-Hans added.
- The mask pass stays on #594's camera motion (the identity and the CPU-made reprojection in dlaa_mv.comp's two
  blocks) and adds #534's outputs; it runs before the per-object pass. Where the reactive mask test
  (TOTK_DLSS_REACTIVE) ran, DLSS gets that mask instead of the characters'.
- #566 dropped the scaler_skipped/scaler_missed report, so #534's reset of it goes too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:46:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 13b602f28f Keep #594's DLSS work off for players without DLSS
The scene patch, the heap sized for the largest DLSS scene and the function-entry hook that catches the game's render
size setup were on for every 1.4.3 player; they are on now only when DLSS can run (dlaa = 1, nvngx_dlss.dll there),
and the hook gives up after 1800 presents if the setup is never seen. Everyone else keeps the game's own scene size
and dynamic resolution, as before #594.

The uniform snapshot's second copy in host memory and the per-object motion's last-frame blocks are made only while
DLSS is available: without it nothing reads them, and the snapshot goes into the device copy in one pass as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:39:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 80349df1a6 Merge pull request #594: per-object motion vectors for DLSS, Ultra Performance, live DLSS modes and per-mode presets (DizzyMan1337)
Merged on top of #566. The camera motion is #594's: its blocks in camb also feed the per-object pass, and
dlaa_mv.comp stays as on main, fed the identity and the reprojection made on the CPU in double. #566's
vk_usnap_host goes, vk_usnap_bytes serves the same. #566's scaling-draw swap, camera rune guard and output-size
scaling pass stay in vk_dlaa.c. Translator patch 0012 becomes 0015 after dev's 0012-0014.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:27:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6dd08db962 Merge pull request #566: DLSS's picture reaches the screen, camera motion in double precision, the camera rune's half-size pass leaves DLSS armed (mictony6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 19:23:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8f1ac244b5 Merge task/amiibofix: amiibo UID and ModelInfo in the Switch's layout (#579, #541; from #519 and #582)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:39:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3a556c5048 Give the game an amiibo's UID and model info in the Switch's layout
TagInfo took the UID as dump bytes 0..6, so the check byte BCC0 (byte 3) replaced the UID's last byte. ModelInfo had
the series and the figure type swapped, because the dump offsets were misread (0x57 is the figure type, 0x5A the
series), and the model number in the tag's big-endian order, while the game reads it little-endian. The game tells
figures of one character apart by that number, so scans gave the wrong rewards. The years of the common and register
info dates go little-endian too. --probe-services checks TagInfo and ModelInfo of a Link (Archer) dump.

The same fixes are in pull requests #519 (genres) and #582 (Gelomban6), which agree on the ModelInfo layout; their
event and feature changes are not part of this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:39:50 +03:00
Valiantsin DzerakhandClaude Opus 5.5 42d46c1324 Merge task/hdrhue: pull request #442, a highlight hue correction for the HDR tonemap (Specht2010)
Bright saturated colours such as fire move towards white in HDR as the game's SDR clamp makes them, by hdr_hue
(0..100, default 80, live). Only the HDR tonemap changes; hdr_hue = 0 is the old picture. On top: the menu text in
Portuguese and Chinese. Checked: SPIR-V header built from this source and valid, probes, HDR shots at 0 and 80.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:29:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4df329d266 Translate the HDR highlight hue setting into Portuguese and Chinese
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 16:29:20 +03:00
Valiantsin Dzerakh 18160ef379 Merge branch 'pr/442' into task/hdrhue 2026-11-03 16:18:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e3111ac012 Merge task/prwave8: community pull requests #527 and the setup part of #489
#527: the setup takes NSZ and XCZ game files and unpacks them itself (tools/ncz.py), every NCA checked against its
hash. #489: the setup downloads NVIDIA's nvngx_dlss.dll for RTX GPUs (pinned tag, size and SHA-256); its runtime part
came in with prwave6. On top: a cut download warns instead of failing the setup, no warning without an RTX GPU, and
the file dialog keeps "All files".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:37:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e8b8860abe Warn on a cut DLSS download and stay quiet without an RTX GPU
A connection cut mid-file raises http.client.IncompleteRead, which is not an OSError, so it failed the whole setup at
the DLSS step instead of warning. The download box is ticked by default, so every player without an RTX GPU got a
warning for a download they never chose; that case is a log line now. The game file dialog keeps its "All files"
filter: the check tells an NSZ or XCZ by its content, not its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:37:11 +03:00
itaskaev2andClaude Opus 5.5 aa46bbbceb List NVIDIA DLSS and its download by the setup in the README
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:36:07 +03:00
itaskaev2andClaude Opus 5.5 444e6f5b51 Download NVIDIA DLSS in the setup for RTX GPUs
dlaa and the DLSS modes need nvngx_dlss.dll next to totk.exe, which the release does not ship, so most players
never had DLSS. A new setup step after prepare downloads NVIDIA's own copy from the DLSS SDK repository
(github.com/NVIDIA/DLSS, tag v310.9.1, NVIDIA RTX SDK license) and checks its size and SHA-256.

The window has a tick box for it, on by default and named with its source and licence; the CLI takes --dlss. The
step runs on Windows with an NVIDIA RTX GPU (the host check's Vulkan devices), keeps a DLSS library the player
already put there, and on a failed download warns and lets the game run without DLSS; the next setup run tries
again. Texts in English, Russian, Brazilian Portuguese and Simplified Chinese, docs/installer.md step 7, tests in
tests/installer_test.py (DlssTest, with a file: URL).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 15:36:07 +03:00
Valiantsin Dzerakh c8ec6c2fe2 Merge branch 'pr/527' into task/prwave8 2026-11-03 15:35:50 +03:00
DizzyMan1337andClaude Opus 5.5 ad3ecd8f3c Per-object motion vectors for DLSS, camera motion in double precision, Ultra Performance and DLSS presets.
Solid scene draws whose position reads an object's own uniform blocks (bones c5, place c7, c6) are drawn a second time
with a vertex shader variant (translator patch 0012) that computes last frame's position from last frame's blocks; the
game writes those into two copies that take turns, and the uniform snapshot keeps both (vk_mvprev.c). The draws are
listed during the replay and recorded through the recorder thread with repeated state skipped. Blocks whose last-frame
copies measured wrong (c9 and c11 of walls and cliffs) keep this frame's; positions without an object block (terrain,
grass) keep the camera's motion, which measured as good.

The camera motion is reprojected in double precision on the CPU: the float inverse made noise on far surfaces and a
still camera that DLSS followed into a wobble. DLSS gets Ultra Performance (a third of the output, also in the F1 menu)
and NVIDIA's recommended preset per mode (K, M for Performance, L for Ultra Performance; TOTK_DLSS_PRESET overrides).

Test switches: TOTK_MV_CHECK measures the motion vectors and DLSS's output against the picture, TOTK_DLAA_TIMES times
DLSS and the motion passes on the GPU. A reactive mask pass for transparents is included but off (TOTK_DLSS_REACTIVE):
its mask covers sky and fog too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:31:58 +08:00
DizzyMan1337andClaude Opus 5.5 2950132ad8 DLAA: a jitter sequence as long as the upscaling ratio asks for
NVIDIA's DLSS guide asks for 8 x (output / scene)^2 jitter phases: 18 in Quality, 24 in Balanced, 32 in
Performance. The sequence was 16 phases in every mode; it keeps 16 as the least (DLAA).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:31:49 +08:00
DizzyMan1337andClaude Opus 5.5 f071264d61 DLSS modes switch while the game runs
1.4.3 sets its render size once, at start (main+0x7000e67028 calls
main+0x585bd0(renderer, force)). The function-entry hook catches that call's
renderer, and a TOTK_DLSS change writes the new scene size to the patch page and
has the next nvnQueuePresentTexture call the setup again with force set, on the
game's thread between frames, with the guest registers kept. The heap is sized
for the largest scene a mode asks for. The DLSS texture bias is part of the
sampler cache key and follows the mode and the dlaa switch. The game's scaling
pass is told from the in-place FXAA by its missing depth attachment, not by the
sizes, which change a frame ahead of the drawing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 17:31:49 +08:00
Valiantsin DzerakhandClaude Opus 5.5 fbc938f2a5 Merge task/astclowvram: ASTC to BC3 by default on GPUs with less than 5 GiB (#87)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:00:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1b48c8dc2c Encode decoded ASTC to BC3 by default on GPUs with less than 5 GiB of video memory
A GPU without ASTC gets the textures decoded to RGBA8, four times the memory of BC3, and on a 4 GiB card
that fills video memory: a GTX 1650 went from 10-23 to 3-12 ms of GPU time a frame with astc_bc = 1
(#87). BC3 loses some precision (normal maps), so it stays off where memory is not short. astc_bc unset
now turns it on when the largest device-local heap is under 5 GiB and logs the choice; 0 and 1 still
decide. The menu row becomes Auto / Off / On, and the description says what Auto does in all four
languages.

On an RX 9070 nothing changes. The Radeon iGPU on Windows reports its shared 10.5 GiB as device-local,
so it keeps RGBA8 as well. All probes pass, probe-astc and probe-cfg included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 12:00:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f4a884650f Merge task/replayasync: the replay runs on its own thread by default (#88)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:48:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 83d2302b6d Replay NVN submissions on their own thread by default
replay_async was off because no timing on our machines showed a gain. Where the game waits for the GPU
it does: a GTX 1650 in the Depths went from 450-686 frames over 50 ms a minute to 10-13, and the 1% low
from 10-12 to 13-24 fps (#88). An RTX 5090 ran slower with it off (#459). Where the replay or the game
threads bound the frame it is neutral (machines A and B, the Steam Deck in #308), and no crash report
ties to it. MacNativeGames has its translation thread on by default for TotK. It adds up to a frame of
latency; replay_async = 0 turns it off.

The fight scene on an RX 9070 with it on: 60.0 fps mean, 1% low 56.3 in the last window, no frame over
50 ms, 0 validation errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:48:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c1c89590be Merge task/prwave7: community pull requests #504, #539, #532, #584 and #564
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:13:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5bc508ce9b Merge task/gplrdna: graphics pipeline libraries back on for AMD's RDNA 1/2 driver (#381)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:13:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 422e3ba50b Turn graphics pipeline libraries back on for AMD's RDNA 1/2 driver
GPL was turned off on that driver branch because its device losses at boot (#5) came with a cold
pipeline cache. Their cause turned out to be GPU writes past 3D BC images, fixed hours later by the
3D BC stand-ins in vk_exec.c. With GPL off every new pipeline is built whole on the draw's path: an
RX 6600 log in #381 has frames of 0.5-1.2 s spent in pipeline builds.

On a Radeon iGPU (RDNA 2, driver 26.10.1, the RX 6600 users' driver) the 0.3.4 release with GPL on
booted from a cold cache and ran the fight scene: 921 fast links, none failed, no device loss, 0
validation errors. Pipeline waits in play dropped from 1381 ms (longest 225 ms) with GPL off to 481 ms
(longest 26 ms). TOTK_GPL=0 still turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:11:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4587cb5f61 Merge pull request #564: Keep HDR highlights through the game's FSR 1 upscale (ProSep)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:00:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a919c0ff4f Merge pull request #584: Lay the UI out for 3:2 game resolutions (Wintego)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:00:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4d7c0d0077 Merge pull request #532: Keep pooled speech balloons in 16:9 proportions on wide and tall game resolutions (TakeyaSaito)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:00:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 05ada38d69 Merge pull request #539: Runtime text renders as ?: add cheat names and mod folder names to the font atlas glyph ranges (fsfksjfjse)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:00:05 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2678749942 Merge pull request #504: Log a warning when memory = import is used on a discrete GPU (ExpertKT)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 11:00:04 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0dc1837e21 Merge task/issuetemplates: a Performance issue template and per-area steps for the logs to attach
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:50:13 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cd204fb075 Issue templates: a Performance template and per-area steps for the logs and files to attach
Performance gets its own template: kind of problem, machine, place, overlay numbers, and the steps for a log with
the performance trace and flight recorder dumps. Other bug says what to attach per area: trace = audio in totk.ini for
the audio underrun lines, an input recording and the save for repeatable bugs, the installer's install log. Every bug
template asks for the last version that worked and the mods in use, and takes Copy system info as a multi-line field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:50:13 +03:00
Valiantsin DzerakhandClaude Opus 5.5 85049eb1a7 Merge task/xlateub: the shader translator gives Maxwell's results for bitfields past bit 31, SHF by 0 and unwritten registers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:23:47 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c0e66fe4c1 Shader translator: Maxwell results where SPIR-V leaves bitfields, funnel shifts and unwritten registers undefined
- BFE / BFI (patch 0012): a field past bit 31 is cut there, one that starts past it extracts 0 (signed: bit 31
  repeated) and inserts nothing, as PTX bfe / bfi. Clamped only where the field is not a constant inside the word.
  1.4.3's Depths terrain vertex shaders (42 variants, 134 extracts) compute the position as ((2y + x) * 8) & 0xff,
  up to 248; AMD's bitfield instructions take it modulo 32. UltraTears saw the Depths walls and ground missing on
  RADV (Steam Deck, RX 570).
- SHF by 0 (patch 0013) gives the unshifted word instead of shifting the other one by 32. Every register SHF of
  1.4.3 pairs its word with 0, so 1.4.3 results do not change.
- Function-local variables start at 0 (patch 0014): a register read on a path that never wrote it is no longer an
  undefined value Mesa may fold.

All 12,454 1.4.3 shaders translate and pass spirv-val. RX 9070, Depths save: the same picture before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 10:19:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 efeea51319 Merge task/foreigncrash: crashes outside totk.exe get a FATAL line in the log (#506, #531, #488, #415)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:54:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 869edd45cf Report crashes outside totk.exe instead of ending without a log line
A fault in a GPU driver, the C runtime or another DLL, or a call through a null pointer, ended the process with
nothing in totk.log: crash_veh reports only faults whose rip is in this executable (#506, #531, #488, #415).

Windows: an unhandled-exception filter reports every exception no handler took. It runs only after every handler
declined, so exceptions a driver raises and catches itself never reach it, and it costs nothing until a crash. The
report names the module (from its export directory, no loader lock) and the return addresses in totk.exe on the
faulting thread's stack, unwound from the faulting context.

Linux: fault_handler passes faults in libraries (Mesa, SDL) and at address 0 to the crash hook as well, with the
library name from dladdr.

--probe-crash-call (strlen in ucrtbase.dll reads address 8) and --probe-crash-null crash on purpose. Before: exit
0xc0000005 and no FATAL line. After: "FATAL: access violation ... (ucrtbase.dll+0xf2b61)" with host frames
probe_crash and main, exit 71.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 09:50:27 +03:00
Wintego 4e5637277e Lay the UI out for 3:2 game resolutions
The settings menu offers 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000 (the 3:2 screens of Surface and
Framework laptops and tablets), and the setup builds romfs-patches-<w>x<h> for each, so the HUD moves to the top and
bottom edges as it does at 16:10 instead of being stretched.
2026-11-03 09:16:51 +03:00
ProSepandClaude Opus 5.5 3d9c80e2bd Rewrite the FSR output clamps as max(x, 0) and log a missed match
clamp(x, 0.0, x) is undefined for negative x in GLSL.std.450, so the output clamps of the game's FSR pass now become
FMax(x, 0.0), one word shorter, with the rest of the module moved down. When the FSR shader is recognised but none of
its output clamps is found (a translator that stores the colour another way), the log now says the highlights are
still clipped instead of staying silent. The comment also notes that the game takes this pass at 1:1 too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 03:21:31 +01:00
Michael Anthony BitoonandClaude Opus 5.5 27df2d85d9 Keep DLSS on the scene's light target when the camera rune draws a half-size pass after the lighting
The camera view draws a half-size RGBA16F target with a half-size D32F depth after the lighting (852x480 under a
1704x960 scene). It matched the light target pattern, so the DLAA point re-armed on it and DLSS scaled it into the
scene's own light target, which left every object black. An armed pair is no longer replaced by a smaller one, and
the scaling pass DLSS writes into must have the output size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 08:40:13 +08:00
Michael Anthony Bitoon 5b23d6671b Make DLSS's picture reach the screen and stop the whole image wobbling with DLSS and DLAA
With a DLSS mode the game's own scaling draw covered DLSS's picture with the jittered scene. At a 1280x720 scene and
1920x1080 output the scaling is done by the game's tonemap, not by the FXAA shader vk_dlaa_skip_draw looks for, so no
hash list could catch it. From DLSS's evaluation to the frame's end, passes larger than the light target now sample
DLSS's output in its place (vk_dlaa_swap_img, applied in vk_exec_draw.c tex_info), whichever shader scales.

The motion-vector shader inverted the view-projection in 32-bit floats. With a still camera (identical matrices each
frame) the rounding still gave a few hundredths of a pixel of motion that changed with every jitter step; DLSS
followed it and the whole picture wobbled. mv_pass now makes last frame's view-projection times the inverse of this
frame's in double precision from the uniform snapshot (vk_usnap_host), and the shader only applies it.

Measured on 1.4.3 indoors, still camera, mean per-pixel change between consecutive frames on static areas:
DLSS quality 0.18 -> 0.06, DLAA 0.14 -> 0.05. A camera pan at 10 px per frame shows no ghosting.
2026-11-03 08:40:13 +08:00
ProSepandClaude Opus 5.5 7f1dd27788 Keep HDR highlights through the game's FSR 1 upscale
Some frames the game upscales its tonemapped scene with its own FSR 1 pass instead of the plain upscale, and that
shader clamps each channel to 0..1, so with TOTK_HDR=1 every highlight above the HUD's white is cut back to it: around
the sun the glare turns into a flat grey-white patch that stays until the camera moves. vk_hdr.c fsr_unclamp finds the
shader by FSR's two reciprocal constants and drops only the clamps whose result goes to its output, leaving the
filter's own weight clamps alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-03 01:16:00 +01:00
fsfksjfjse 19ffd3a2da Add mod folder names to the font atlas too
A mod's folder name is drawn on the Mods tab (opt_text_row, menu.cpp) and as the Cheats tab's group heading (cheats_tab.cpp), and it comes from disk at runtime like a cheat name, so its glyphs were never in the atlas either.

Both sources are added in the same place in fonts_up(), so one pass covers the whole menu.
2026-11-03 03:55:25 +08:00
fsfksjfjse 5826231870 Add cheat names to the font atlas glyph ranges 2026-11-03 03:39:34 +08:00
Valiantsin DzerakhandClaude Opus 5.5 378971a513 Merge task/menuunblock: the game's input comes back 1 s after the menu closes with something still held (#462, #494)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:34:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d81249ff29 Let the game's input through 1 s after the menu closes even when something still reads as held
After the in-game menu or the text entry closes, input_poll kept the game's input blocked until one poll saw no
button and all four stick axes at 0. An input that never reads as released (a button another program holds down, a
key whose release went to another window, a stick resting outside its dead zone) kept the game deaf until restart
(#462, #494). Closing with Minus+Plus hid this: that poll zeroes the pad before the gate looks. Closing with Esc did
not. The wait now ends after 1 s with input held, and the log names what was held.

Repro: field save, a script holding the right stick at 3000, Esc opens and closes the menu, left-stick pulses after.
Before: 0.00 m/s for the 30 s after the close. After: Link walks from the second poll window on; the log line reads
"input: the menu closed 1 s ago and input is still held (buttons 0, sticks 0 0 3000 0)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:34:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f8a33200ac Merge task/v110state: installs recorded as update 1.1.1 update as 1.1.0 (#332)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:17:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 14ed048dd3 Read an install recorded as update 1.1.1 as 1.1.0 when the setup updates it
Releases before 0.3.1 recorded the 0x20000 update as 1.1.1. After the rename (#371) nothing mapped the recorded
name, so an update of such an install asked make_romfs_patches.py for --version v111 and failed at the end (#332);
in a fresh setup folder it would not find expected-v1.1.1.json at all. migrate_state maps the old name before the
plan and gives the extract record the key of the renamed expected file; the full check of kept output still hashes
the extraction against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:17:31 +03:00
SchulzCode 1d49f64f34 Reduce DLSS trails behind moving characters with a bias mask
The motion vectors follow the camera only, so a character, animal or held item that moves on its own drags a faint
copy of itself through DLSS's history. TotK's G-buffer target 0 carries an object class in green: above 64/255 for
characters, creatures and held items (Link 253, others 125, held items 160), 0 for terrain and 32 for foliage, checked
in the field, in Goron City and on horseback. The motion-vector pass now also writes DLSS's bias mask
(DLSS.Input.Bias.Current.Color.Mask) where such an object is now or was the frame before, read through the motion
vector from the previous frame's class mask, so the history that holds the moving object is weighed down there.
dlss_char_mask (0, 35, 65 or 100 percent, default 65; F1 > Screen > "DLSS character trails", live) sets the strength.
A 640x360 pass of the G-buffer's shape, seen while loading, no longer replaces the scene's class buffer in a frame.
The status line also prints after 300 frames, with the frames that had the mask.
2026-11-02 19:21:56 +01:00
SchulzCode bd54e701a8 Choose the DLSS model and pass DLSS the real frame time
dlss_preset (default, k or j; F1 > Screen > "DLSS model", live) sets the render preset hints when a DLSS feature is
created. default leaves the choice to NGX (preset K for DLAA, Quality and Balanced in the 310 releases); j is the
transformer model NVIDIA describes as slightly less ghosting at the cost of more flicker. A change recreates the
features at the next DLAA run. FrameTimeDeltaInMsec now carries the time since the previous evaluation instead of a
fixed 16.67 ms. The menu and totk.ini.example name the DLSS model file of the platform in use.
2026-11-02 19:21:56 +01:00
SchulzCode 1e908362ea Load NGX on Linux for DLSS / DLAA
vk_dlaa.c reached the NGX core only through Windows: the registry, LoadLibraryA and GetModuleFileNameW. On Linux the
NVIDIA driver installs the core as libnvidia-ngx.so.1 with the same Vulkan entry points; it is opened with plat_lib_open.
The parameter object's vtable follows the Itanium C++ ABI there, each overload group in declaration order rather than
MSVC's reverse order, so the slot enum has a Linux variant; vtable_ok confirms it at start. NGX gets the executable's
folder as a 32-bit wchar_t string, converted from UTF-8 without the C locale, and loads libnvidia-ngx-dlss.so.<version>
from there. The Windows path is unchanged.

resolution.c sized the scene for a DLSS mode only on Windows, where it looks for nvngx_dlss.dll; on Linux it now looks
for libnvidia-ngx-dlss.so.<version> next to totk, so quality, balanced and performance work there too. The no-op
stubs vk_dlaa.c had for other platforms go, as Linux now builds the real code.
2026-11-02 19:21:56 +01:00
Takeya SaitoandClaude Opus 5.5 4cbc7bbb70 Keep pooled speech balloons in 16:9 proportions on wide and tall game resolutions
The marker fix gave the root's scale of BalloonMessage_00 to its part pane Pa_BalloonMessage_00. The game pools that
part, cloning it into Pa_BalloonMessage_%02d at run time, and the clones don't keep the template pane's scale, so on
32:9 NPC speech balloons drew twice as wide as they should.

The scale now goes inside the part layout instead, to the top panes of PaBalloonMessage_00 (C_CaptureBase_00 and
N_InOut_00, their offset scaled with them), which every clone is built from. Its animations only key alpha, visibility
and N_Loop_00's y. The part pane keeps scale 1. Same on the y axis for 16:10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:10:48 +00:00
itaskaev2andClaude Opus 5.5 afe3404434 Accept NSZ and XCZ game files in the setup
The check step tells a compressed file by its content (an NSP or XCI holding NCZ files), whatever its name, and
unpacks it into the temp folder with tools/ncz.py before the extraction reads it. Every NCA comes back byte for byte
and is checked against the hash in its name. The file dialog has one filter for all game files, and the form says
that compressed files are unpacked automatically, in every setup language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 18:03:27 +01:00
ExpertKT f75405ee51 Log a warning when memory = import is used on a discrete GPU 2026-11-03 00:31:53 +08:00
Avraham ab32d00cc9 Merge remote-tracking branch 'origin/dev' into discord-rich-presence 2026-11-02 13:11:39 -03:00
Specht2010andClaude Opus 5.5 5d254e462c Merge dev into the 5.1 surround output
The device sink loop now writes every output channel; upstream's TOTK_AUDIO_DUMP gets the front pair before clipping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 11:44:25 -04:00
Valiantsin Dzerakh c38e0ca13d Up version 0.3.4 2026-11-02 18:01:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0a4679b64c Merge task/amiibodir: the Amiibo tab picks its folder with the host's dialog and holds the reader on searching across the game's detection restarts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:44:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 69104be9b8 Hold the amiibo reader on searching across the game's detection restarts
While TotK waits for a tag it stops and starts detection several times a second, tens with the menu open: the Amiibo
tab's reader line flipped between ready and searching and the log took two lines per restart. The tab now shows
searching for a second after a stop (amiibo_get_reader_state; the game still reads amiibo_get_device_state), and the
log names the first three restarts, then a count every 200th.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:43:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4e90c61c52 Choose the amiibo folder with the host's folder dialog
The Amiibo tab's folder field took no typing: the menu never passes typed characters to Dear ImGui, so "Open" only
re-read "./amiibo". A missing folder said so at the bottom of the right pane, below its scroll.

"Open" now shows the shell's folder dialog on Windows (IFileOpenDialog on a thread of its own, owned by the game
window) and SDL_ShowOpenFolderDialog on the SDL window thread elsewhere; the tab shows the folder as text and names a
missing one inside the empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:36:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 275a752bec Merge task/prwave6: community pull requests #501, #495, #485, #486 and #446, and the runtime part of #489
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:21:42 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3ee347b248 Size the scene by a DLSS mode only where DLSS can run, and log whether fxaa = 0 found the game's FXAA
The DLSS modes already apply only with dlaa on (3de0839), so the DLL check no longer looks at dlaa, and off Windows,
where vk_dlaa.c is no-op stubs, a DLSS mode no longer shrinks the scene. fxaa = 0 names the FXAA shader it skipped,
or reports a minute of frames without it, so a translator change that moves FXAA_FS shows on any GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:11:14 +03:00
itaskaev2andClaude Opus 5.5 9263989625 Skip the game's scaling draw over DLSS's picture again
With a DLSS mode (a scene smaller than the output) DLSS writes the scaled picture into the scaling pass's target and
vk_dlaa_skip_draw skips the game's own scaling draw, known by its fragment shader's SPIR-V hash. Translator patch
0011 (the shadow filter spread) changed that hash, so the draw ran again and covered DLSS's picture with the scaled
jittered scene: the whole picture shook by a fraction of a pixel each frame, most visible on lit flat surfaces in
shrines. The FXAA switch (fxaa = 0) stopped working for the same reason.

Add the hash of the current translator, and log once when DLSS scaled a frame whose scaling draw was not found, so
the next translator change that moves the hash shows in the log.

Measured on 1.4.3, Ukouh Shrine, output 3200x1800, dlss = quality, mean absolute change between consecutive frames
with a still camera: 1.40 before, 0.07 after (0.054 with dlaa = 0). Outdoors on stone: 4.29 before, 1.08 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:04:07 +03:00
itaskaev2andClaude Opus 5.5 438aa41747 Keep the scene at render_scale when DLSS cannot run
A DLSS mode (quality, balanced, performance) shrank the scene to 67, 58 or 50 % of the output and sharpened the
material textures' mip bias to match, even when nvngx_dlss.dll was missing and NGX reported DLSS as not available.
The game's plain scaling then stretched the smaller scene, and the negative bias made textures shimmer in motion.

With dlaa = 1 the scene size now waits on nvngx_dlss.dll next to totk.exe (checked before the device exists, as
the size is decided then) and follows render_scale without it. The mip bias applies only when NGX came up with
DLSS. dlaa = 0 keeps the documented size with the game's scaling, without the bias.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:04:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 55ba4d312d Say in totk.ini.example that variable refresh removes the frame limit's judder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:03:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5bd11f5d5c Keep the 4/5-frame gap between device-paced audio frames to Linux
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:03:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8c53e6205f Merge pull request #446: Pace audio frames apart (fixes progressive sound loss) and raise the Linux prefill default (lowlevel-larry)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:03:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0b4b4f8036 Merge pull request #486: Warn in totk.ini.example that memory = import is much slower on a discrete GPU (ExpertKT)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:02:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c6fa3091a5 Merge pull request #485: Say in totk.ini.example that the frame limit should divide the monitor's refresh rate (ExpertKT)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:02:49 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8741363ae5 Merge pull request #495: Keep world markers over their target at game resolutions of another shape than 16:9 (TakeyaSaito)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:02:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 329acf81d2 Merge pull request #501: Keep a texture's pool flags instead of reading its pool object later (lowlevel-larry)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 17:02:31 +03:00
lowlevel-larryandClaude Opus 5.5 3675286747 Keep a texture's pool flags instead of reading its pool object later
tex_mirrored (vk_texture.c) read t->pool->flags whenever the back end created the host entry of a texture, which can
happen long after nvnTextureInitialize. A game may finalize a memory pool and free the memory the NVNmemoryPool object
lived in while textures from it are still looked up (on a level change, for instance); reading the freed object then
faults. The NVN driver keeps no pointer to the pool either. tex_t now records the pool's flags at Initialize, in the
padding after layout[8], so NVNtexture's size is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 10:40:51 -03:00
lowlevel-larryandClaude Opus 5.5 ea72b01524 Pace audio frames apart and raise the Linux prefill default
Device-paced frames rendered back to back after each period pull (1024
samples on PipeWire, 4-5 frames), before the game's audio thread could
clear the render event or fill the AUX return ring. Frames now stay at
least 4/5 of a frame apart, as the DSP renders one frame every 5 ms on
hardware. This fixes AUX clicks at frame seams (#426) and the progressive
sound loss of #131 (overlapping render notifications 52.7% -> 0.1%).

Linux prefill default raised to 60 ms: at 20 ms PipeWire underran
thousands of times within minutes in busy scenes.

The reverb ring (#421) and advance_voice stepping parts of this change
landed in dev separately (d22a19c, b44caca).

Fixes #131

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:47:35 -03:00
Takeya SaitoandClaude Opus 5.5 4933725112 Keep world markers over their target at game resolutions of another shape than 16:9
The wide and tall UI overlays scale each layout's root pane on the stretched axis. The lock-on arrow, the name label
of the item or character in front of Link, enemy life bars and notices, and speech balloons are moved in code to the
target's projected position on a pane under that root, so the position was scaled as well: on 32:9 these markers sat
at half their distance from the middle of the screen, short of what they point at.

Those four layouts keep their root at scale 1 and give the scale to the panes under it that the code does not move and
no animation scales, so the markers land on their target and keep their 16:9 proportions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:46:20 +00:00
Valiantsin DzerakhandClaude Opus 5.5 49c19b20d0 Merge task/recrace: the replay's deferrals wait for the submission it records; TOTK_REC_CHECK and TOTK_REC_DELAY_US debug the recorder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:03:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 328589abbe Add TOTK_REC_CHECK and TOTK_REC_DELAY_US to find objects the recorder still names
TOTK_REC_CHECK=1 searches the packets of the part being recorded for each image, view and buffer vk_memory.c defers or
destroys. A deferral stamped below the part's value, or a destruction of an object a packet names, is logged with the
packet op, both stamps and the GPU's value, and counted by kind and op in a summary at exit. put() zeroes each packet's
room while the check is on: packers reserve room for optional arrays they may leave unwritten, and those words held
handles from the ring's previous lap. TOTK_REC_DELAY_US=N makes the recorder wait N us before each batch, as on a slow
machine. rec_begin takes the part's value for the check. Both are off by default; off, the recorder pays one test per
batch and per packet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 15:02:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 90a3b07762 Stamp an object the replay defers with the submission it records
vk_memory.c defer stamped every deferred image, view and buffer with the last submitted timeline value. An object the
replay lets go of while it records a submission can be named by that submission too: vk_texture.c slot_resolve hands a
draw a released entry, and entry_of destroys released entries at that address later in the same submission. The
completion thread then destroyed the object once the GPU reached the earlier value, before the submission naming it
ran, and with the recorder thread behind, before the recorder made the call. The stamp is now the later of the last
submitted value and vk_exec_recording_fence, as LAS Recomp and Savage Wind Recomp stamp their deferrals
(runtime/nvn/vulkan/textures.c defer). TOTK_DEFER_STAMP=0 brings back the old stamp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:59:49 +03:00
ExpertKT 6f3d553a23 Warn in totk.ini.example that memory = import is much slower on a discrete GPU 2026-11-02 19:43:17 +08:00
ExpertKT aed3e83cc2 Say in totk.ini.example that the frame limit should divide the monitor's refresh rate 2026-11-02 19:41:07 +08:00
Valiantsin DzerakhandClaude Opus 5.5 98c75a6b9e Merge task/noavx: v2 builds save XMM registers with SSE moves in the preemption stub and refuse instructions above x86-64-v2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:08:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e603095ffa Refuse instructions above x86-64-v2 in the runtime objects of a v2 build
-march=x86-64-v2 does not reach inline assembly, target attributes or intrinsics, which is how kern_ap_stub's YMM
saves got into libtotkrt-v2.a (issue #454). tools/check_isa.py disassembles objects and lists every function with
VEX/EVEX, BMI1/2, LZCNT, MOVBE, ADX, SHA, AES, PCLMUL or RDRAND/RDSEED code or YMM/ZMM/mask registers, except
functions in its DISPATCHED list (sha256.c blocks_ni, behind __builtin_cpu_supports). build_game.py --cpu v2 runs it
over the runtime objects after compiling and before linking or archiving, so make_release.py's v2 library build fails
too. Objects unchanged since the last clean scan are skipped; a full scan of the runtime takes about a second. The
release ships the script, and the installer's runtime fingerprint includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:06:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7310481885 Save XMM registers with SSE moves in kern_ap_stub on builds without AVX
The asynchronous preemption stub saved and restored YMM0-15 with vmovdqa and ran vzeroupper in inline asm, which
-march=x86-64-v2 does not reach. The v2 runtime library therefore carried VEX code, and a CPU without AVX stopped
with an illegal instruction the first time a core owner was interrupted, a few seconds into boot (issue #454,
Pentium Gold G6400, totk+0xe7a2b50 = kern_ap_stub+0x20). Without __AVX__ the stub now saves XMM0-15 with movdqa into
the same slots; v2 game code keeps nothing in the upper YMM halves. The v3 stub is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:05:35 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d15150b4a8 Merge task/passcrumbs: the device-lost report names the pass or dispatch the GPU was running
AMD buffer marks at every replay pass and dispatch, on by default (fight2 off/on x2: 89.45 vs 89.6 fps, no new slow
frames); NVIDIA diagnostic checkpoints with TOTK_PASS_CRUMBS=1, compiled but not run here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 13:47:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 710b7f5dd0 The device-lost report names the pass or dispatch the GPU was running
The replay marks the start and the end of every render pass and dispatch
(vk_exec.c vk_exec_pass_begin and vk_exec_pass_end, vk_exec_draw.c
op_dispatch and op_dispatch_ind), outside the pass so no pass is broken.
With VK_AMD_buffer_marker the GPU writes the last started and finished mark
into words 2 and 3 of the breadcrumb buffer; with
VK_NV_device_diagnostic_checkpoints the same points are checkpoints, read
with vkGetQueueCheckpointData2NV after the loss. A ring of 4096 records maps
a mark to its submission, index, target formats and size, draw count and the
input ids of the first and last draw's shaders. vk_device_fault_report logs
both marks with their records. The calls go through the recorder thread.

TOTK_PASS_CRUMBS (menu: GPU pass marks): unset, AMD buffer marks only; 1,
also NVIDIA checkpoints, whose extension is enabled only then; 0, none.
The design follows Savage Wind Recomp 3fae40c; the code is ours.

--probe-vkexec gets a pass marks part: pass, dispatch, pass in one
submission; the marks read back after the fence name the last pass and the
report text says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:50:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3e87747031 Merge task/refsfix: correctness fixes found in the updated references
Zero-byte NVN pools get no Vulkan record, WaitSynchronization keeps a pending cancel for the next blocking wait,
SignalToAddress subtracts 2 when it wakes all waiters, and the 1.2.1 texture loader widens pool offsets unsigned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:22:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 53d3f8ea2e The 1.2.1 texture loader widens a texture's pool offset unsigned
The loader keeps a texture's place in its pool as a 32-bit word (main+0xad4294) and widens it with a sign extend for
nvnTextureBuilderSetStorage's offset (main+0xad43f0) and for SetPackagedTextureData's source (main+0xad440c). Past
2 GiB the offset turns negative and the storage lands 4 GiB below the pool (MacNativeGames ca311e2d, seen with a 4K
texture pack's heap table in a 3.58 GiB pool). The shipped heap table keeps every pool far below 2 GiB, but
TOTK_APP_POOL raised with such a pack's heap table (RomFS mods replace it) can reach it. The new patch set pooloff
(TOTK_POOL_OFFSET_PATCH, on by default) widens unsigned: the same value below 2 GiB, right up to 4 GiB.

1.4.3 needs no patch: both of its loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits (str x25 / mov
x2, x25 / add x1, x25, x8). gen.py accepts both original words, and the lifted C at the sites reads the offset as
uint32_t with the set on and as int32_t with it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:20:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0ab16c21b5 SignalToAddress modify by waiting count subtracts 2 when it wakes all waiters
SignalAndModifyByWaitingCountIfEqual with count <= 0 and at least one waiter stored value - 1; Horizon stores
value - 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree).
The word was already updated with compare-and-swap, as LAS Recomp 1144aea made theirs; the count > 0 values were
right.

--probe-kernel arbiter part expected the old value: it now wants 4 for value 6 with one waiter and count -1, and
4 for two waiters and count 0. Before: 5 for both. After: ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:16:06 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1f824a6581 WaitSynchronization leaves a pending cancel to the next wait that would block
Ours took a pending CancelSynchronization first, before the signalled objects and before the timeout-0 return, so a
poll or a wait on a signalled object used it up and the blocking wait it was meant for ran on. Horizon checks a
signalled object, then timeout 0, then the cancel (mesosphere KSynchronizationObject::Wait; LAS Recomp 1144aea moved
the poll the same way): both keep it pending. A cancel that wakes a blocked wait still ends it cancelled:
CancelSynchronization now wakes it with R_CANCELLED, which the loop tells apart from a signal's -1.

--probe-kernel events part: a cancel sent before a poll and before a wait on a signalled event; each is followed by a
blocking wait with a 500 ms timeout. Before: the poll and the signalled wait returned 0xec01 (cancelled) and the
blocking waits timed out (0xea01). After: timeout, success, then cancelled both times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:15:54 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b6ab660b00 A zero-byte memory pool initialises as an empty pool in the Vulkan back end
vk_pool_init gave a size-0 pool with storage a record: the host import of 0 bytes failed (the driver refuses the
memory, VK_ERROR_INVALID_EXTERNAL_HANDLE) and the record went into the pool index as the empty range [mem, mem]. A
lookup at that address then found the empty pool before the pool that starts there and reported it unbacked. TotK
makes such pools now and then (MacNativeGames d3407e29, 1.2.1). Such a pool now gets no record, as a GPU_NO_ACCESS
pool; vk_pool_fini finds none and does nothing. Removal by key already kept a neighbour's range safe at Finalize.

--probe-vkexec gains an empty pool part: a zero-byte pool at the start of another, lookups there before and after
its Finalize. It failed before ("a lookup at the start of a pool finds the empty pool there").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:14:10 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d554165cf0 Merge task/pr333: pull request #333, round-to-odd float FMA in double for CPUs without FMA3 (deanzx06)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:50:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 5eb0a19ee5 Pin the upstream references at their 2026-11-02 heads
LASRecomp, MacNativeGames, d2r-mobile, nxnative and SavageWindRecomp moved; SwitchRecomp and deko3d did not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:49:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9998e31644 Merge pull request #333: runtime: vectorized AVX round-to-odd FMA fallback for x86-64-v2 (deanzx06)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:42:02 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9e17766bad Merge task/prwave5: community pull requests #465, #384, #416, #393 and #468
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:37:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 45df231f37 Merge pull request #468: Read the gradients of a TXD per coordinate so terrain picks the right mip level (mictony6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:31:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 caf88c88a4 Merge pull request #393: Group the stick rows of the Controls tab under an "Analog sticks" heading and fix their "default" mark (wdiasjunior)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:31:43 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ab912a3071 Merge pull request #416: Offer 256 as a sun shadow map size (wdiasjunior)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:31:31 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e0e1ecfdbb Merge pull request #384: Fix installer catalog test encoding on Windows (FaserDocs)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:31:28 +03:00
Valiantsin DzerakhandClaude Opus 5.5 86d75ad282 Merge pull request #465: Keep the replay's transient buffers and the zero buffer in host-coherent memory (MierzwaMatheus)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 08:31:26 +03:00
MierzwaMatheus f4f9d6dd53 Keep the replay's transient buffers and the zero buffer in host-coherent memory
The CPU fills these chunks and the zero buffer and never flushes them. On Intel integrated GPUs (ANV on Meteor
Lake) the host-cached memory type that VMA picks for random host access is not coherent, so the GPU read what
an earlier submission had left there. A lighting fragment shader got another draw's UpdateUniformBuffer payload
as its step count (0x3f800000 instead of 96 or 128), looped about a billion times and lost the device; other
stale payloads drew the black frames. Requiring HOST_COHERENT keeps the same memory types on GPUs where host
memory is already coherent.
2026-11-02 01:22:13 -03:00
Michael Anthony Bitoon 984c9bc7c3 Read the gradients of a TXD per coordinate so terrain picks the right mip level
TotK's terrain shaders fill the gradient registers of a texture-with-derivatives instruction per coordinate (du/dx, du/dy, dv/dx, dv/dy), while the translator takes the first two registers as dPdx and the last two as dPdy. The mip level then came from the wrong derivatives and was too sharp on terrain faces turned across the screen, so patches that followed the faces crawled when the camera moved.

After a shader loads, every two-dimensional gradient pair has its inner components swapped by two new composite constructs, so the shader cache is untouched. TOTK_TXD_TRANSPOSE=0 turns it off. The finding is row 2.13 of docs/maxwell.md.
2026-11-02 11:59:29 +08:00
Avraham 664ee03759 Add Discord Rich Presence, off by default.
A host thread speaks Discord's documented IPC protocol to a running client
through platform.h's new plat_ipc_* (a named pipe on Windows, a Unix socket
in $DISCORD_IPC_PATH, $XDG_RUNTIME_DIR, $TMPDIR or /tmp on Linux) and shows
the game update and the elapsed play time as the status of a configurable
application. It never holds a virtual core, picks up settings changes within
half a second of the menu writing them, re-sends the activity once a minute
as its keep-alive, answers the client's Ping frames and reconnects 15 s
after a break; Discord clears the status when the process ends.

The Menu tab of the settings menu switches discord_rpc, and discord_app_id
and discord_image in totk.ini name the application and its art asset (a
player can register their own). The default app id is the one this was
developed against; the maintainer may want to register a project-owned one
and swap it. --probe-discord runs the exchange without the game: it caught
the opcode numbering (Handshake is 0, not 1) against a live client.

Tested on 1.4.3, Linux: probe passed end to end against Vesktop's arRPC
bridge, an invalid app id falls back to the default, selftest clean.
2026-11-01 22:12:58 -03:00
Valiantsin DzerakhandClaude Opus 5.5 7d9a45855d Merge task/dlssoff: no DLSS scene size while DLSS/DLAA is off (#435)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:36:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3de08398bf Size the scene by the DLSS mode only while DLSS/DLAA is on
With dlaa = 0 and dlss = quality left from the menu, the scene still rendered at 67 % and the plain upscaler
stretched it, so the picture looked blurrier with DLSS switched off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:36:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 25f616e853 Merge task/audiopace: advance a voice by the samples it rendered, and an env-gated sink dump (#426)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:30:13 +03:00
Valiantsin DzerakhandClaude Sonnet 5.5 b44caca695 Count a voice's consumed samples with the same accumulation the mixer steps by, so a frame never skips a source sample (#426)
advance_voice computed fraction + seconds * rate * pitch while voice_render adds rate * pitch / 48000 per output sample. The two rounded apart on most frames at 32 kHz and 44.1 kHz, so the voice jumped one sample ahead of what was rendered. audio_voice_consumed now runs the mixer's own loop.

TOTK_AUDIO_DUMP=<path> writes the sink mix before the clamp and logs how many frames were rendered under 1 ms after the previous one; a one-time log line reports the first enabled AUX effect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 03:29:33 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1ce4cf8583 Merge task/reverbring: one ring length for the cursor and the taps of both reverbs' delay lines (#421)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:17:27 +03:00
Valiantsin DzerakhandClaude Sonnet 5.5 d22a19c91d Give the reverb delay lines one ring length for cursor and taps so no tap reads an unwritten slot (#421)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-02 03:16:58 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dd33a75ec4 Merge task/amiibocc: accept amiibo dumps by their F1 10 FF EE capability container (#441, #410)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:10:24 +03:00
Valiantsin DzerakhandClaude Opus 5.5 eb5e6e698e Accept amiibo dumps by the amiibo capability container F1 10 FF EE
The loader asked for E1, the container byte of a plain NDEF tag, so it refused every real amiibo dump as
"Not an amiibo dump". --probe-services now loads a dump with an amiibo header and refuses the same file with E1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 03:10:24 +03:00
deanzx06 ec9029def4 runtime: vectorized AVX round-to-odd FMA emulation for non-FMA/AVX-only CPUs 2026-11-02 05:17:58 +07:00
Specht2010andClaude Opus 5.5 a6b56288d5 Add a highlight hue correction to the HDR tonemap
The game's SDR clamp turns bright saturated colours towards white: a torch flame clips red, then green, and reads
yellow-white, the colour its emitted light was tuned to. The extended HDR curve keeps the channel ratios, so the same
flame stays deep orange next to the yellow light it casts. hdr_hue (0 to 100, default 80, live) moves colours from the
picture's white upwards towards the game's own SDR result at the HDR luminance, as RenoDX's TotK mod does. It rides in
the support buffer's HDR vector (z), unused until now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:11:25 -04:00
Specht2010andClaude Opus 5.5 963901cc12 Add 5.1 surround output
The mixer folded the game's 5.1 device sink down to stereo and IAudioDevice told the game it had two channels.
audio_channels (auto, stereo, 5.1; Sound tab, Speakers) now opens the SDL stream with six channels in the device sink's
order (FL FR C LFE RL RR) and reports six to the game, which then mixes for 5.1. auto picks 5.1 when the default
playback device has six or more channels (a 5.1 or 7.1 speaker setup in Windows) and stereo otherwise; stereo keeps
the existing downmix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:50:30 -04:00
Valiantsin DzerakhandClaude Opus 5.5 a4ce826f91 Merge task/headless: stop a Linux setup without a display at the check step and say plainly that zh/ko need the system fonts (#338)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:29:55 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b3f569a448 Wrap the display check's condition over two lines
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:29:54 +03:00
Valiantsin Dzerakh 2470097f67 Fail the Linux setup in the check step when the plan prepares shaders and no display is set, and say in the font warning that the game will not start without the Switch system fonts 2026-11-01 23:29:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0851638064 Merge task/liftretry: retry the lift with half the workers after a MemoryError and take the count from TOTK_LIFT_JOBS (#240)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:28:03 +03:00
Valiantsin DzerakhandClaude Sonnet 5.5 526e9f32bd Retry the lift's emit pool with half the workers after a MemoryError, and read the worker count from TOTK_LIFT_JOBS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 23:27:46 +03:00
Valiantsin DzerakhandClaude Opus 5.5 475db6e27a Merge task/savecopy: delete read-only folders and cloud placeholders of an old save working copy and name the failing path (#374)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:55:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6d31b157a0 Ask FindFirstFileW for the reparse tag only when the path is a reparse point
plat_fs_kind served every path through FindFirstFileW, which fails on a path with a trailing separator and reads
wildcards. GetFileAttributesW decides again, as before; only reparse points need the find data's tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:55:27 +03:00
Valiantsin DzerakhandClaude Sonnet 5.5 d3f403ecf6 Delete read-only folders and cloud placeholders of an old save working copy, retry for 2 s, and name the failing path in the fatal
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-01 22:54:20 +03:00
Wellington Dias 1722b5da2c Offer 256 as a sun shadow map size for integrated GPUs short of memory bandwidth 2026-11-01 16:21:54 -03:00
Valiantsin DzerakhandClaude Opus 5.5 6c2595275d Merge task/amiibo: bind an nfp system event again after the game destroyed it (#398)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:21:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7c1cd42aa5 Bind a system event again after the guest destroyed it
The game destroys the nfp activate and deactivate events and attaches the same SystemEventType again before each
detection. kern_bind_system_event kept the first writable handle per guest address and returned early, so the second
Attach left the event destroyed and the game's next TryWaitSystemEvent aborted (issue #398, equipping the Amiibo
power). A kept handle now counts only while the SDK's state byte of the event is nonzero; otherwise the event is
created again and its entry takes the new handle, and a signal to a destroyed event is dropped. probe-services
destroys and attaches both nfp events again and checks that they work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 22:20:36 +03:00
Wellington Dias 88e044dcd3 Mark a per-stick dead zone row as default at the shared value, and unset it when stepped back onto that value. 2026-11-01 13:53:53 -03:00
Wellington Dias 2215c3fc2f Merge branch 'dev' into stick-deadzones
# Conflicts:
#	docs/controls.md
#	runtime/cfg.c
#	runtime/input.c
#	runtime/ui/menu.cpp
#	runtime/ui_text.c
#	runtime/ui_text.h
#	totk.ini.example
2026-11-01 13:42:21 -03:00
Valiantsin DzerakhandClaude Opus 5.5 2e95221d23 Add a player guide to mods and point the README and the tester guide to it
Mods were described only in one dense list in the tester guide. docs/mods.md walks a player through installing a mod,
its order and switches, TKMM merges, code patches and their rebuild, what renaming a mod folder does, the reasons the
Mods tab gives for an unused mod, and what to check when something looks wrong. docs/cheats.md gains a folder example
and the rename note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:34:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8eb48493c0 Rebuild the code cheats and ExeFS patches of a renamed mod folder
The lift key hashed each cheat and ExeFS patch file by its name and content only, but tools/cheats.py and
tools/exefs_patches.py name every patch set after "<mod folder>/<cheat or file>". After a mod folder was renamed,
totk.exe saw none of its code cheats in the build and started the rebuild, the setup found the lift current and skipped
it, and the cheats stayed "after the rebuild" for good. The key now takes each file's path under mods/ as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:28:32 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ab1dee5bdb Close the spawned child's descriptors only where glibc has posix_spawn_file_actions_addclosefrom_np
The Linux release builds against the Steam Runtime's glibc 2.31, which lacks that 2.34 call: plat_restart and
plat_spawn stopped the runtime library from compiling. A weak reference uses it when the running glibc has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:17:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2af42ccf0d Use plat_setenv and strcasecmp where the stick dead zone code called Windows-only functions
_putenv_s in cfg_probe.c and _stricmp in the menu's key binding row stopped the Linux runtime library from compiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:15:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2125ad6eea Extract the RomFS again when its index on disk cannot be read
A 0.2.8 install's romfs/index.json had two flipped bytes, and the update stopped with an unexpected JSONDecodeError
instead of extracting again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:08:05 +03:00
Valiantsin DzerakhandClaude Opus 5.5 454f236a0d Merge task/prwave4: community pull requests #358, #365, #375, #354, #246 and #371
#358 stops the Windows release on a translator older than its sources, #365 stages large CPU texture uploads in
parts, #375 names a busy totk_xlate.exe, #354 builds the runtime library without a lift, #246 adds noinline to the
Linux thread id cache dev already had, #371 calls the 0x20000 update 1.1.0. #331 is not merged: cacbdf2 fixed the same
ExitThread crash in dev.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:00:48 +03:00
FaserDocs 86a9ea6d53 Read installer sources once as UTF-8 in the catalog test 2026-11-01 15:54:57 +01:00
Valiantsin DzerakhandClaude Opus 5.5 a68106de5d Merge pull request #371: Call the 1.1.0 update 1.1.0, not 1.1.1 (shenjackyuanjie)
The Brazilian Portuguese and Simplified Chinese setup texts now name the 1.1.0 update beside 1.2.1 and 1.4.3, as the
English and Russian ones do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:51:58 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6e95e441e3 Merge pull request #246: Cache the thread id per thread on Linux (SchulzCode)
dev already caches the id; this keeps that and takes the PR's noinline, so a fiber resumed on another thread reads its
own thread's slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:51:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3e3d4ef42d Merge pull request #354: Build the runtime library without the generated chunk lists (shenjackyuanjie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:51:02 +03:00
Valiantsin DzerakhandClaude Opus 5.5 7a918c6c30 Merge pull request #375: Name a busy totk_xlate.exe in the prepare step instead of failing with an unexpected error (shenjackyuanjie)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:51:01 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cd01186519 Merge pull request #365: Stage large CPU texture uploads in parts of at most 32 MiB (itaskaev2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:51:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a6c2c1b2e1 Merge pull request #358: Stop the Windows release on a translator older than its sources (m3rcury02)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:50:59 +03:00
Valiantsin Dzerakh 7ec95eafbe Up version 0.3.1 2026-11-01 17:45:34 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a932c5015c Merge task/mods1 and task/modcheats: mods folder, cheats from mods, RESTBL merge, ExeFS patches, rebuild at start
The Mods tab lists the mods folder with switches and load order. Cheat files come from the mods (PR #51, no bundled
files); the resource size table of the enabled mods is merged at start; code cheats and ExeFS .ips/.pchtxt patches are
built in, and totk.exe starts the setup's rebuild window when an enabled mod brings ones the build lacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:42:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9b4d286bb3 Pick the setup window's language from one drop-down list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:40:23 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3668271182 Start the rebuild window and its setup run with a Python that finds the installer
The rebuild command puts the release folder on sys.path itself: totk.exe starts it in the install folder, and only the
Windows release Python's ._pth names that folder. In a repository the window and its setup run use the Python that
runs the setup, since the tools' embeddable Python there has neither tkinter nor the repository on its path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 17:40:22 +03:00
shenjackanddeepseek-v4.1-flash-1017 72dcdc0534 Report a busy totk_xlate.exe in the prepare step the way the compile step reports a busy game exe
The prepare step copies the shader translator with replace_file, which writes <name>.tmp and then
os.replace. A preparation that was stopped from outside leaves the totk_xlate.exe that totk.exe started
holding that file, and Windows refuses the replace with PermissionError [WinError 5]. The setup showed
that as an unexpected error and said nothing about what to close (issue #174). Catch the PermissionError
on both copies and name the file, as compile.exe_in_use already does for totk.exe.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 21:22:52 +08:00
shenjackanddeepseek-v4.1-flash-1017 a7b988b417 Name the 1.1.0 update 1.1.0 instead of 1.1.1
The update this port builds for has version value 0x20000, which is Tears of the Kingdom 1.1.0, not 1.1.1: the upstream expected-v1.1.2.json carries 0x40000 and the other expected files step 0x10000 per release, so 0x20000 is 1.1.0 and 0x30000 is 1.1.1. Pull request #220 introduced the wrong label in expected-v1.1.1.json.

Expected hashes, patch sites, boot constants and the RomFS layout are unchanged; only names change:
- tools/data/expected-v1.1.1.json -> expected-v1.1.0.json, display_version 1.1.0
- recomp/patches-v111.txt -> patches-v110.txt
- installer/layout.py SUPPORTED and tools/build_game.py DISPLAY_VERSIONS
- make_romfs_patches.py --version choices and its COMMON table key (the RomFS archive name stays Common.Product.110)
- runtime RT_V111 -> RT_V110, the inits_111 and expect_111 tables, the probe's census folder v110
- README, docs and the installer texts

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 21:08:13 +08:00
shenjack cc4572b244 Build the runtime library without the generated chunk lists
--make-runtime-lib compiles runtime/ alone, so it must not insist on a chunk manifest that only a lift writes: the
library is rebuilt by itself when the runtime sources change.
2026-11-01 20:29:42 +08:00
itaskaev2andClaude Opus 5.5 da9460457e Stage large CPU texture uploads in parts of at most 32 MiB
With astc = cpu a large ASTC array decodes to RGBA8 in one staging buffer: the
1024x1024 array of 121 layers with 11 levels takes 645 MiB. On an Intel UHD with
8 GB of shared memory that allocation failed (-2) and the game closed before the
title screen (#347).

upload now fills one staging buffer per part of at most 32 MiB (a single region
larger than that gets its own) and records one copy per part, all in the same
upload batch, which is submitted as before. The parts fit the staging pool's
64 MiB blocks. The GPU ASTC decoder still reads every region from one buffer.

Fixes #347.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:08:52 +01:00
Valiantsin DzerakhandClaude Opus 5.5 4310407226 Move the Mods and Cheats tabs after Advanced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:39:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c3dae879cb Build the mods' ExeFS patches into the game and switch them with their mod
A mod that patched the game code (.ips, .pchtxt) stayed off as a whole.
tools/exefs_patches.py now reads IPS, IPS32 and IPSwitch files from each
mod's exefs folder and from atmosphere/exefs_patches/<name>/. It keeps the
ones whose build id is main's and turns their writes into main's code into
one patch set per file. The writes go through the same rules as the code
cheats (tools/cheats.py convert). A file that writes main's data, or that the
rules cannot express, is unsupported as a whole. The setup's lift runs it
after cheats.py and writes build-cache/exefs.status.

runtime/mods.c checks each patch file against that status:

- a built file's set is switched on with its mod;
- a file the build lacks keeps the mod off and starts the rebuild at the next
  start, like a code cheat does;
- an unsupported file keeps the mod off, and the Mods tab shows the reason;
- a file for another version is counted and left out.

A mod patch on a site of one of the port's own sets turns that set off while
the mod is on, and the Mods tab names it. The Mods tab also gets the "Rebuild
the game now" row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:29:40 +03:00
Valiantsin DzerakhandClaude Opus 5.5 1bebcfd9ef Rebuild the game at start when an enabled mod has unbuilt code cheats
Code cheats need the game lifted and compiled with them. Running the setup
again did that, but it lifted and compiled everything and prepared the
shaders again.

- The setup keeps the generated C and the objects in its temp folder (about
  1.3 GB). gen.py rewrites only the chunks whose C changed, so Ninja
  recompiles only those. One new code cheat on 1.4.3 took a 47 s lift and
  19 s of compile and link on 24 cores.
- Prepare's key no longer follows the compile key, only the runtime, the
  translator and the game version, so a rebuild keeps the prepared shaders.
- The cleanup writes build-cache/rebuild.txt, the command that opens the
  setup window in a new --rebuild mode on the install folder. That mode runs
  at once and starts the game again when it succeeds.
- At start, runtime/cheats.c runs that command and exits when an enabled
  mod has code cheats the build lacks (plat_spawn, new for Windows and
  Linux). build-cache/rebuild.skip keeps a failed or cancelled rebuild from
  starting again for the same cheats. "Rebuild the game now" on the Cheats
  tab tries again, and [mods] mods_rebuild = off turns the rebuild off.
  Scripted runs never start it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:16:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 3dd7f846b8 Merge the resource size table of the enabled mods at start
The game sizes the buffer for a file from its entry in
System/Resource/ResourceSizeTable.*.rsizetable.zs, so a mod file that grew
could fail to load, and a file a mod added had no entry. With a player mod
enabled, romfs.c restbl_overlay now rebuilds the table at start from:

- the game's table;
- every table of this version that a mod (or the Xbox prompts folder) ships,
  where the larger entry wins;
- a mod's table for another version, for that mod's own files only;
- each served mod file: a grown file's entry rises by the growth times its
  type's factor, and an added file gets an estimate.

The factors come from the 1.4.3 table measured against its own files (most
types need the size plus at most 8 KiB; bfres 1.5x, bkres 2x, bgyml 3.85x).
The result is served from <profile>/mods/ and takes about 90 ms. A mod's
table no longer counts as a clash with the Xbox prompts. Game entries are
never lowered.

tests/mods_romfs_check.py checks the merged table and now passes probe-fs a
profile of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:05:41 +03:00
Valiantsin DzerakhandClaude Opus 5.5 ac69f39529 Read cheat files from the mods folder instead of shipping them
A cheat file is now part of a mod: mods/<mod>/cheats/<build id>.txt, or the
Atmosphere places inside the mod folder, the file name in any case. The port
ships no cheat files any more (the MyNXCheats files and their notice row are
gone).

- tools/cheats.py --mods builds the code cheats of every mod folder; a
  cheat's key hashes "<mod folder>/<name>", so two mods' cheats of one name
  keep their own switches.
- The setup reads the cheats from <install>/mods and writes the status to
  build-cache/cheats.status.
- runtime/cheats.c reads the files of the enabled mods the port can use, in
  the Mods tab order, and the status from build-cache.
- A mod with only cheats is usable. The Cheats tab groups the cheats by mod,
  and the Mods tab says how many cheats a mod brought.
- The cheat texts are in all four menu languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:05:31 +03:00
m3rcury02andClaude Opus 5.5 33c2a1ac63 Stop the Windows release on a translator older than its sources.
make_release.py copied build/xlate as it found it, so a translator change reached no Windows release until someone rebuilt it by hand, and nothing failed when they did not: 0.2.1 and 0.2.8 shipped a translator from before the gather bias while the runtime logged that the bias was on. The Linux release builds its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 16:03:41 +05:30
Valiantsin DzerakhandClaude Opus 5.5 5e584ea966 Merge pull request #51: Load Atmosphere cheat files and switch their cheats from a Cheats tab in the menu (conongnang)
Merged onto the mods folder (task/mods1); the Cheats tab sits after Mods. The cheats' menu text and the cheat file's
place are reworked for the mods folder in the commits that follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:31:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 acf2e10bc5 Mark a mod the port cannot use as not used under its switch
Its row said On like a working mod, and only the description pane told why nothing happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:17:48 +03:00
Valiantsin DzerakhandClaude Opus 5.5 fa59758f63 Add a Mods tab: the mods folder with switches and load order, and the port's own patches
Each mod is a row with its switch, the files it serves and why it is unused when it is; the top and left face buttons,
Ctrl+Up and Ctrl+Down or the drawn arrows move it. The real-time step, cloth and high-fps fix switches move here from
the Game tab under the port's own patches. The text is in the catalog in English, Russian, Brazilian Portuguese and
Simplified Chinese. The tester guide says where mods go and how TKMM's merged output fits in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:14:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 59c8d20fe3 Serve the mods folder as RomFS overlays: the higher mod wins a file, unusable mods are skipped
Player mod files are checked by identity, not hashed, and a path several overlays give is served by one: a higher mod
beats a lower one, any player mod beats the romfs_mods manifest and the port's own patches. A mod path that differs from
a RomFS path only in case is served under the RomFS spelling. Files named for another game version (.Product.<n>.,
missing from this RomFS) are counted and left out. A player mod that replaces a file of the Xbox prompts folder sends
the game back to the Switch prompts, as a romfs_mods manifest already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:14:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 b875e3efee Scan a mods folder next to totk.exe and keep its order and switches in totk.ini
One folder per mod as emulators take them: romfs, exefs and cheats folders, the Atmosphere layout and TKMM's output
(romfslite counts as romfs). A mod with its own code (subsdk or a replaced main) stays off as a whole, so does one with
ExeFS patches until a rebuild applies them; cheat files are listed, not loaded yet. The scan goes through the platform
layer, so Linux builds it. probe-mods covers the layouts and the order keys; probe-cfg fails on a key listed twice in
the defaults table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:14:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c46b4c2cad Merge pull request #353: Turn the recorder thread off by default on AMD GPUs again (eioo)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:54:16 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c7a7adcfe4 Merge task/prwave3: community pull requests #346, #175, #235 and #191, with #89 and #113 from task/prgfx
- #346: the setup extracts the Switch shared fonts from an optional firmware folder.
- #175: a dead zone per gamepad stick, with the resting deflection shown in Controls.
- #235: amiibo through an emulated NFC reader and an Amiibo tab, its text in the catalog.
- #191: Simplified Chinese in the setup window and launcher, beside Brazilian Portuguese.
- #89 and #113 (task/prgfx): sun shadow cascades, PCSS, contact shadows and DLSS/DLAA, all off
  or at the game's own values by default; fight2 at the defaults shows no frame time cost.

#51 (cheats) waits for part 3 of the mods design on task/cheats51; #259 repeats de3ed42.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:51:01 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9cb099a8fa Fit the Amiibo tab in the menu and stop it writing a folder on its own
The panes now leave the bottom row free (opt_bottom), as the option tabs do, so Continue and Quit
stay visible; the hold time label sits above its slider instead of being cut off beside it; the
Tap and Lift buttons take the frame height, so their text is not clipped. A missing amiibo folder
lists nothing and says so: the tab no longer swaps in the game folder and saves it to totk.ini
just by being opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:47:59 +03:00
Valiantsin DzerakhandClaude Opus 5.5 c3ec7a5312 Check the emulated NFC reader in probe-services
Since #235 the reader is there from the start: the availability event is set when it is attached,
and AttachActivateEvent and AttachDeactivateEvent bind their events instead of answering
device-not-found. The probe now attaches both and checks that the activate event stays clear
without an amiibo, so every nfp override is called again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:43:03 +03:00
Valiantsin DzerakhandClaude Opus 5.5 a40c507aa1 Name the stick dead zone notes stick_note, apart from the prompts note in the same function
The per-stick rows of #175 declared a second `note` in tab_controls, beside the button prompts note
dev added there, and runtime/ui/menu.cpp did not compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:36:10 +03:00
Valiantsin DzerakhandClaude Opus 5.5 8a2e52b929 Merge task/prgfx: sun shadow cascades, PCSS, contact shadows and DLSS/DLAA from #89 and #113
runtime/cfg.c: the known names take both sides, the per-stick dead zones beside the shadow and
DLSS switches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:35:42 +03:00
eioo d7357368e8 Turn the recorder thread off by default on AMD GPUs again
0.2.8 turned the recorder on for every GPU (e38eaa8). The AMD device losses
and hangs came back the same day: a black screen or VK_ERROR_DEVICE_LOST at
startup on RDNA 2, 3 and 4, under AMD's Windows driver and under RADV
(#277, #283), and vk_recorder = 0 still gets past it.

The submission recorder goes back to defaulting off on vendor 0x1002;
TOTK_VK_RECORDER=1 still forces it on for an A/B run, and the log says which
path was taken. The rec_end_cb line now prints only when a part really was
recorded by the recorder, so a log no longer suggests the setting was
bypassed (#315).
2026-11-01 11:31:17 +02:00
Valiantsin DzerakhandClaude Opus 5.5 c0007bd16d Merge pull request #191: Add Simplified Chinese to the setup window and the in-game menu (tuwary)
The menu side is dev's: it has Simplified Chinese since #243, glyphs included. Simplified Chinese
joins the setup window as a fourth language beside Brazilian Portuguese, in the language switch,
the Windows and locale pick and the launcher; zh-Hans.json gets the nineteen texts added since
(fonts step, CPU level, base game checks, Linux checks) and the {game_exe} placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:29:30 +03:00
Valiantsin DzerakhandClaude Opus 5.5 97e666ce2f Merge pull request #235: Add Amiibo support via nfp:user IPC emulation and ImGui picker (leonelalej)
The Amiibo page reads its text from the catalog (UI_MENU_AMIIBO_*, in all four menu languages)
instead of English literals, and its status line keeps an id and the file name so it follows a
menu language change. Folders show as name/ in the list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:18:45 +03:00
Valiantsin DzerakhandClaude Opus 5.5 87ccb22f51 Merge pull request #175: Give each gamepad stick its own dead zone and show where the stick rests (applezra)
The stick reading moved from input_win.c to input.c in dev, so the per-stick dead zone and
input_pad_stick_percent are applied there, under the platform mutex. The rows sit beside the
stick camera speed and walk key rows; pt-BR and zh-Hans get the per-stick texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:16:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4cece2c848 Merge pull request #346: Let the installer extract the Switch shared fonts from a firmware folder (fch869)
Conflicts with dev resolved by keeping both sides: the fonts step sits between check and extract,
beside the x86-64-v2 check, the pt-BR window language and the xbox-label patches. pt-BR gets the
thirteen firmware and fonts texts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 12:03:29 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0b84f48df2 Build vk_dlaa.c off Windows as no-op stubs
It includes windows.h and loads NGX as a Windows DLL, and build_game.py leaves only *_win.* files out of the Linux
build, which then stopped at it. Elsewhere DLAA, the DLSS scaling and the FXAA switch now do nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:42:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 90319e6c9a Load NGX only with dlaa on at start, and skip the FXAA shader lookup at the defaults
vk_dlaa_wanted answered 1 on every machine, so each start on an NVIDIA driver loaded the NGX core and enabled its
device extensions with DLAA off; dlaa now applies after a restart. vk_dlaa_skip_draw hashed the fragment shader of
every draw through an unlocked 8192-entry table, which 1.4.3's shader variants outgrow; with fxaa on and DLSS not
scaling there is nothing to skip, so it returns before the lookup. Also gives the DLSS rows their Brazilian
Portuguese and Simplified Chinese text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:41:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 f700c28cc4 Merge pull request #113: Add NVIDIA DLSS/DLAA and a toggle for the game's FXAA (DizzyMan1337)
The sampler keeps both the DLSS mip bias and pull request #321's anisotropic level; DLAA's pass hook runs after the
contact shadows'; the extension arrays grow for NGX's extensions beside HDR's and the calibrated timestamps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:40:27 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4ab1dc80f5 Merge task/shadows: pull request #89, shadow options (itaskaev2), with the follow-ups
Keeps pull request #321's anisotropic filtering, which #89 added a second time, and #267's shadow_res beside #89's
shadow_scale. The HDR parameters move from byte 1408 of the support buffer to 1440: translator patches 0010 and 0011
read pcss at 1408 and shadow_spread at 1424.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:39:38 +03:00
Valiantsin DzerakhandClaude Opus 5.5 75fd885e19 Merge task/prwave2: eight community pull requests merged by hand
#296 (x86-64-v2 build for CPUs without AVX2, v3 stays the default), #321 (anisotropic filtering, the game's level by
default), #138 (right-stick camera speed), #183 and #243 (Brazilian Portuguese and Simplified Chinese menus, with every
menu text and the regional Windows tags such as ru-RU still finding their menu), #211 (Button prompts setting and Xbox
icons), #281 (HDR10 output, off by default). Also fixes probe-cfg's range check for fractional ranges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:31:10 +03:00
Valiantsin DzerakhandClaude Opus 5.5 0b5a1ea677 Give the Brazilian Portuguese and Simplified Chinese menus every menu text
The two catalogs came from pull requests written before the shadow, filtering, HDR, prompts, camera speed and walk key
rows and the overlay's FSR note; probe-services checks every menu id in both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:30:36 +03:00
Valiantsin DzerakhandClaude Opus 5.5 62e7e910aa Read fractional range ends in the probe's default check
probe-cfg parsed the ends of a min..max range with strtol, so walk_scale's 0.2..0.9 became 0..0 and its default 0.51
failed the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:28:53 +03:00
Valiantsin DzerakhandClaude Opus 5.5 10a065f972 Drop the Brazilian Portuguese free cores menu text, whose row dev removed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:28:07 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dbed5771b9 Skip the HDR record lock on image deletion while HDR is off
vk_hdr_image_gone took rec_lock for every texture image the game destroys, also with TOTK_HDR=0, where no record
exists; the records start at the first HDR present, which sets vk_hdr_track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:15:50 +03:00
Valiantsin Dzerakh f30bbf048a Merge pull request #281: Add HDR10 output (ProSep)
# Conflicts:
#	THIRD_PARTY_NOTICES.md
#	runtime/cfg.c
#	runtime/vk.h
#	runtime/vk_device.c
2026-11-01 11:15:24 +03:00
Valiantsin Dzerakh 4524fed813 Merge pull request #211: Show Xbox button icons in the prompts, with a Button prompts setting that follows the pad layout (waxyfeet)
# Conflicts:
#	docs/installer.md
#	runtime/cfg.c
#	runtime/ui/menu.cpp
#	runtime/ui_text.h
2026-11-01 11:14:40 +03:00
Valiantsin Dzerakh d85450384d Merge pull request #243: Add a Simplified Chinese menu catalog (shenjackyuanjie)
# Conflicts:
#	runtime/cfg.c
#	runtime/ov_probe.c
#	runtime/ui/menu.cpp
#	runtime/ui_text.c
#	runtime/ui_text.h
#	totk.ini.example
2026-11-01 11:13:28 +03:00
Valiantsin Dzerakh 7a38fc6847 Merge pull request #183: Add a Brazilian Portuguese catalog for the settings menu and the overlay (Lucas-blip-png)
# Conflicts:
#	runtime/cfg.c
2026-11-01 11:12:23 +03:00
Valiantsin Dzerakh f95fb43b7f Merge pull request #138: Add a right-stick camera speed setting that scales the game's own turn rates (shin2344234)
# Conflicts:
#	runtime/cfg.c
#	runtime/input.c
#	runtime/ui_text.c
2026-11-01 11:11:59 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2652bbc552 Keep the game's anisotropic filtering level by default
16x costs texture bandwidth on every install, which modest GPUs and the Steam Deck feel; 2x to 16x stay a choice in
the menu and totk.ini.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:11:31 +03:00
Valiantsin Dzerakh 683a2fe61b Merge pull request #321: Add an anisotropic filtering setting (ProSep)
# Conflicts:
#	runtime/ui_text.c
#	runtime/ui_text.h
2026-11-01 11:11:17 +03:00
Valiantsin DzerakhandClaude Opus 5.5 2d4563dea1 Name the x86-64-v2 minimum in the Brazilian Portuguese CPU message
Pull request #296 renamed check.cpu_no_avx2 to check.cpu_too_old; the catalog of pull request #186 still had the old key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:10:49 +03:00
Valiantsin Dzerakh 57129898e8 Merge pull request #296: Run the game on x86-64-v2 CPUs without AVX2 (chen3feng)
# Conflicts:
#	README.md
2026-11-01 11:10:05 +03:00
Valiantsin DzerakhandClaude Opus 5.5 e1693cee8e Merge task/prwave1: eleven community pull requests that merge into dev cleanly
#272 #273 (texture writes and Wine), #247 #274 #284 (docs), #275 #278 (build and shader preparation), #267 (sun
shadow map size, the game's own by default), #239 (GPU idle split in the perf line), #197 (walk key), #186 (Brazilian
Portuguese installer catalog, with the three strings it lacked).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:07:22 +03:00
Valiantsin DzerakhandClaude Opus 5.5 770086957a Add the three installer strings the Brazilian Portuguese catalog lacked
check.base_is_update and check.base_combined came to dev after pull request #186 was written, warn.shaders_skipped
with pull request #278; tests/installer_gui_test.py wants every language to cover English.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:04:15 +03:00
Valiantsin DzerakhandClaude Opus 5.5 d07a71f0ca Keep the game's own sun shadow map size by default
A fixed 1024 map costs GPU time and texture heap on every install, also where the game would draw a smaller one; the
fixed sizes stay a choice in the menu and totk.ini.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 11:02:57 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9db9260a21 Drop the SHARPNESS and ASPECT section entries that pull request #267 listed a second time
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:55:59 +03:00
Valiantsin Dzerakh fb99b2bab1 Merge pull request #186: Add a Brazilian Portuguese catalog for the installer window and the launcher (Lucas-blip-png) 2026-11-01 10:55:12 +03:00
Valiantsin Dzerakh 4e84422e26 Merge pull request #197: Add a walk key that tilts the keyboard sticks part of the way (shenjackyuanjie) 2026-11-01 10:55:11 +03:00
Valiantsin Dzerakh 949b06b72e Merge pull request #239: Split the GPU's idle time by whether the next part was submitted yet (perf line) (lowlevel-larry) 2026-11-01 10:55:10 +03:00
Valiantsin Dzerakh f7e5639ab9 Merge pull request #278: Let shader preparation survive a driver crash (wdiasjunior) 2026-11-01 10:55:09 +03:00
Valiantsin Dzerakh 6701021594 Merge pull request #267: Fix the sun shadow map at one size (wdiasjunior) 2026-11-01 10:55:08 +03:00
Valiantsin Dzerakh e565b876df Merge pull request #275: Read the extracted game from the work folder in build_game.py (wdiasjunior) 2026-11-01 10:55:06 +03:00
Valiantsin Dzerakh 3a2f4081b1 Merge pull request #284: README: document Linux support (x-wav) 2026-11-01 10:55:05 +03:00
Valiantsin Dzerakh 1fa07afdd6 Merge pull request #274: Name 1.1.1 where the docs list the updates (shenjackyuanjie) 2026-11-01 10:55:04 +03:00
Valiantsin Dzerakh 76e684dfd2 Merge pull request #247: docs(installer): fix invalid comparative "another ... than" (lunar-me) 2026-11-01 10:55:03 +03:00
Valiantsin Dzerakh 8643d48d4f Merge pull request #273: Default shadow storage to on when running under Wine (koosc) 2026-11-01 10:55:02 +03:00
Valiantsin Dzerakh 0d1c54b4ac Merge pull request #272: Mark texture storage as written in nvnTextureFlushTexels (koosc) 2026-11-01 10:55:01 +03:00
turquopool 873ebb7755 Let the installer extract the Switch shared fonts from a firmware folder
The setup takes an optional --firmware directory (an unpacked firmware or an emulator's Contents/registered) and, for Chinese, Korean and Traditional Chinese, runs the vendored font extractor into <install>/shared-fonts. Without the folder it warns as before and the install still completes. The fonts step is keyed by the language and a cheap firmware fingerprint; the window gets a folder picker and help text, and the command line takes --firmware.
2026-11-01 15:22:14 +08:00
Valiantsin DzerakhandClaude Opus 5.5 edfad9c9bb Merge task/freecoresnote: free_cores in totk.ini works again, with a loud crash warning (#212)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:18:26 +03:00
Valiantsin DzerakhandClaude Opus 5.5 659a2fa75f Let totk.ini set free_cores again and say loudly that the game crashes with it
Reverts dafc35f: free_cores = 1 in totk.ini lifts the virtual core gate again, as in 0.2.8. The mode stays as an
experiment, and every place a player meets it says the game crashes: totk.ini.example, a WARNING line in the start log,
and a line under every crash report and FATAL while the gate is off (kern_free_cores_note), so a log from #212 names
the cause at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 10:10:09 +03:00
Wellington Dias cd0917e18d Add a dead zone slider per analog stick to the Controls tab, carrying an older stick_deadzone line over to both. 2026-11-01 02:08:15 -03:00
Valiantsin DzerakhandClaude Opus 5.5 57db3f8fe0 Merge task/fsrlabel: the overlay says when FSR is selected but the picture is shrunk (#219)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 06:01:19 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dd448482b9 Name the overlay scaling line 'bilinear (FSR only enlarges)' when FSR is selected and the image is larger than the window
FSR only enlarges (vk_fsr_record returns 0 when the picture is not larger than the crop), so a 3840x2160 image in a 1920x1080 window is shrunk by the bilinear blit and the line read 'bilinear' next to a menu showing FSR (#219).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 06:00:56 +03:00
Valiantsin DzerakhandClaude Opus 5.5 4e101d17f5 Merge pull request #271: return 0 from storage_rewritten when the texture range is not write-watched (koosc)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 05:56:43 +03:00
Chris Koos 45430366b1 Return 0 from storage_rewritten when write tracking is unavailable 2026-11-01 05:55:34 +03:00
conongnangandClaude Opus 5.5 bf40602179 Move the Cheats tab to its own file and leave menu.cpp's font ranges alone, to stay clear of other open PRs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 09:15:54 +07:00
6f8dc257be Add an anisotropic filtering setting
aniso (game, 2, 4, 8, 16; default 16) raises anisotropic filtering of the game's tiled, filtered, mipmapped colour
samplers, so ground and paths seen at a shallow angle stay sharp into the distance instead of smearing. Depth-compare,
nearest, unmipmapped and clamped samplers keep the game's level: clamped samplers read screen-sized buffers, where
anisotropic taps reach across an object's edge and leave a speckled fringe (the inventory's character preview). The wrap
test reads the game's own modes, so mirror-clamp samplers stay out on devices without mirror-clamp-to-edge, where they
map to mirrored repeat. Read at start, as samplers are cached.

Ported from the aniso half of itaskaev2's shadow and filtering commit and applezra's follow-up that limits it to tiling
samplers. On the Screen tab of the settings menu (English and Russian) and in totk.ini.example.

Co-Authored-By: itaskaev2 <165683571+itaskaev2@users.noreply.github.com>
Co-Authored-By: applezra <Ezra.s.swenson@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 03:15:05 +01:00
conongnangandClaude Opus 5.5 183519190a Bundle the cheat files for 1.2.1 and 1.4.3 and install them when the player has none
The setup keeps a player's own file. A bundled file the tools refuse costs its cheats, not the install.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:42:10 +07:00
conongnangandClaude Opus 5.5 a81b00930e Key the lift step on the files in cheats, so extracting main does not rebuild the game
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:15:20 +07:00
conongnangandClaude Opus 5.5 d8cbdf1067 Ship tools/cheats.py in the release, which runs it in the lift step
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:15:20 +07:00
conongnangandClaude Opus 5.5 23498a49ce Use the platform layer in cheats.c, so the Linux runtime builds it
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:15:20 +07:00
conongnangandClaude Opus 5.5 0bf9a09eb3 Merge upstream main into cheats
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 08:07:29 +07:00
Valiantsin DzerakhandClaude Opus 5.5 a2819a0bb4 Merge task/menucrash: the step floor also covers the Havok contact crash of #29 and #92 (comment)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:52:09 +03:00
Valiantsin DzerakhandClaude Opus 5.5 065428c81f Name the 1.4.3 contact-setup crash under the real-time step's floor
Issues #29 and #92 (0.1.1, 240 Hz monitors, fps_limit = display) crash in main+0x1292560 reading a Havok body's
shape. Forced steps between 1/252 and 1/230 s reproduce it in that call chain on the Depths load 3 of 3 times; steps
of 1/240 s and longer, or the same jitter through the floor, play. The comment now records it next to the #241 abort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:51:19 +03:00
CHEN Feng 34b4faee7c Run the game on x86-64-v2 CPUs without AVX2
-march=x86-64-v3 kept the port and the installer to CPUs with AVX2 and FMA. --cpu v2 compiles the generated code and the runtime for SSE4.2 instead. Without FMA3 the FP lowering calls fma/fmaf, so runtime/fma.c provides correctly rounded ones (musl, MIT) that honour MXCSR's rounding mode, FTZ and DAZ; the UCRT's software fallback rounds incorrectly by up to half an ulp. simd.h's fast paths keep their assumptions.

The installer's host check asks for x86-64-v2 and selects libtotkrt.a or libtotkrt-v2.a; both release scripts build and ship the pair. The differential tests compile fma.c as well, so a --cflags=-march=x86-64-v2 run tests it.

Tested on an i7-3770K (no AVX2, FMA or BMI) with an RTX 3060 Ti, game 1.4.3: the installer ran end to end, --selftest passed and play holds 40-60 fps at 1080p. fma and fmaf matched 152,288 exact vectors over two seeds, including ties, subnormals and every rounding mode, FTZ and DAZ combination. The v3 build is unchanged: fma.c is an empty translation unit there.
2026-11-01 07:09:13 +08:00
Valiantsin DzerakhandClaude Opus 5.5 191b5459d3 Merge task/basegame: name an update or a combined package chosen as the base game (#261)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:05:08 +03:00
Valiantsin DzerakhandClaude Opus 5.5 375acc3a71 Say that a base game file is an update or a combined package instead of claiming it is not Tears of the Kingdom
An update NSP chosen as the base game keeps the base title id on its Program NCA and carries the update id (0100F2C0115B6800) only in its Meta NCA, so program_nca rejected it with the generic base-game metadata message (#261). Check.open_input now names an update file or a file with several Meta NCAs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 02:04:47 +03:00
Valiantsin DzerakhandClaude Opus 5.5 6e29935772 Merge task/handcut: log the local wall time with each window focus change (#255, diagnostic)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:10:37 +03:00
Valiantsin DzerakhandClaude Opus 5.5 131c458d0e Log the local wall time with each window focus change
A GPU hang that takes the whole system down ends the log without an error (#255, #266, #270 end on a focus pair).
The time on the focus lines lets a report be lined up against the driver's entry in the Windows event log, to tell
whether an alt-tab or an overlay came before the hang or followed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 01:09:31 +03:00
Chris Koos d9d6264bb2 Default shadow storage to on when running under Wine 2026-10-31 15:02:30 -07:00
Chris Koos f3ce186887 Mark texture storage as written in nvnTextureFlushTexels
nvnTextureFlushTexels was a no-op stub. Call nvn_guest_written on CPU-visible texture storage so mirrored textures whose texels were flushed by the guest are stamped in the write-generation table for re-upload, matching nvnBufferFlushMappedRange.
2026-10-31 15:01:22 -07:00
Valiantsin DzerakhandClaude Opus 5.5 5208d88ba7 Merge task/ucrtabort: ExitThread jumps back without the C runtime's longjmp, which fast-fails under Control Flow Guard (#222, #176)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:54:11 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cacbdf2549 Leave a guest thread by our own register jump instead of the C runtime's longjmp
ExitThread went back to the thread start with longjmp on a buffer from a non-unwinding setjmp, whose frame field is 0.
When Control Flow Guard is on for the process, ucrtbase's longjmp checks the buffer first and ends the process with
fast fail 13 (0xc0000409) on a frame of 0: offset 0xb373c of ucrtbase 10.0.26100.9711, the offset in the event logs of
#222, #176 and #75. Exploit protection can force CFG on any program, and the first guest thread to exit, right after the
game's first six-axis read, killed the process with no line in the log; the abort and invalid-parameter handlers never
run for a fast fail.

plat_jmp_save and plat_jmp_go keep and restore the Win64 callee-saved registers, MXCSR and the x87 control word
themselves and validate nothing; Linux keeps setjmp and longjmp. Started with CFG forced on, the build before this
change ends with 0xc0000409 at the same log line and in --probe-kernel; this one reaches gameplay and passes the probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:53:17 +03:00
x-wav 88325105e4 README: document Linux support
#60 added Linux (AppImage) and Steam Deck support, but the README still only mentions Windows. This adds a Linux install section with the requirements from docs/installer.md, and updates the intro, Status and Planned.
2026-10-31 22:17:26 +01:00
Valiantsin DzerakhandClaude Opus 5.5 0164eca287 Merge task/cmdbuf: reuse finalized command recording buffers, wait out a failed allocation (#94)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:39:21 +03:00
Valiantsin DzerakhandClaude Opus 5.5 9d95cbdded Keep finalized command recording buffers for reuse and wait out a failed allocation
1.4.3 initializes and finalizes command buffers about 2000 times a second while a scene loads and several times a
frame in play. Finalize freed the recording buffer, so each new recording grew again from 64 KiB through up to five
reallocations; the two large per-frame recordings ended in fresh 1-2 MiB blocks, new commit twice a frame. On a
16 GB machine at render scale 200 one of those 1 MiB growths failed during a Depths load and the game ended with
"nvn: command recording buffer of 1048576 bytes" (#94).

Finalize now hands the buffer to a pool of 16 (largest kept, up to 4 MiB each) and the next recording takes the
largest one: a 60 s island run made 22 growths instead of about 110000. A failed recording buffer or command copy
allocation frees the pool and retries every 10 ms for up to 30 s before the fatal, so a short commit shortage (Windows
growing a system-managed page file) stalls the game instead of closing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:38:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 adbd61e379 Merge task/freecores: totk.ini's free_cores no longer lifts the virtual core gate (#212)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:20:20 +03:00
Valiantsin DzerakhandClaude Opus 5.5 dafc35fb35 Keep the virtual core gate on when totk.ini sets free_cores
Earlier settings menus wrote free_cores = 1 under the label "Virtual core gate", which promised the gate. Without it a
thread that holds no virtual core reports its ideal core to GetCurrentProcessorNumber, so the game's job workers all
report core 0 and run its per-core code at once. On 1.2.1 two of them add the same per-core NVN control memory, which
retires the command handle the other just recorded, and its CallCommands fails at start (#212); on 1.4.3 the game
corrupts a heap in main+0x82e00 or hangs at the title menu. A player with such a totk.ini crashed at every start and
the menu no longer shows the row.

free_cores is no longer a setting: only TOTK_FREE_CORES in the environment lifts the gate, and a free_cores value in
totk.ini is logged as ignored. The kernel probe asks the kernel whether the gate is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:19:34 +03:00
ProSepandClaude Opus 5.5 007786d487 Add HDR10 output
With hdr = 1 the game's tonemap shader is replaced by one that keeps highlights up to the display's peak, the present
reads the float picture the game copies into its 8-bit window texture, and a compute pass encodes it as PQ in Rec. 2020
for an HDR10 swapchain. Peak, paper white and HUD brightness are live settings in the menu and totk.ini. The approach
follows the RenoDX TotK mod by souperman9 and Musa; no code was copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 21:17:12 +01:00
Valiantsin DzerakhandClaude Opus 5.5 b5286ef054 Merge task/fpsdisplay: keep the game step at 1/240 s or longer, frame limit up to 240 (#241)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:00:00 +03:00
Valiantsin DzerakhandClaude Opus 5.5 cad5b0cc39 Keep the game step at 1/240 s or longer and stop the frame limit at 240
With a step under 1/240 s (ratio below 0.125) 1.4.3 aborts with Havok "Out of memory" from the physics pre-solve
buffer resize (main+0x1261630) within seconds of loading some saves, so a 280 Hz monitor with fps_limit = display
crashed on load (issue #241). frame_step_present floors the averaged step at 1/240 s, and the limit, including the
monitor's rate, stops at 240; a higher fps_limit now logs and gives 240 instead of 60. Limits of 240 and below step
exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 22:59:02 +03:00
Wellington Dias dbe8daf25c Record the shader variants the driver is building during preparation, skip them after a crash and build the rest again 2026-10-31 16:39:38 -03:00
Wellington Dias c05b083130 Read the extracted game from the work folder in build_game.py and stop when the RomFS is missing 2026-10-31 16:23:28 -03:00
Wellington Dias 1a07a8d46f Fix the sun shadow map at one size, so foliage shadows stop turning blocky and jumping.
Each frame the engine sizes its shadow cascades to the area they cover (main+0xcebdf4 in 1.2.1): from 960 down to about
400 near the ground, so leaf shadows turn coarse, and every change of size moves the shadow texels, so they jump instead
of sliding. The Switch does the same; the community mods force a fixed size. The shadow patch set loads the size from
patch page +92 just before the engine rounds it (main+0xcec22c in 1.2.1, 0x683604 in 1.4.3), and the menu's Shadow map size
(shadow_res in totk.ini) chooses it: game, 512, 768, 1024 (default), 1536 or 2048, with the texture heap grown above 960. A write
watch on the size field found the one writer; with the setting, every shadow map in both versions is the fixed size.
recomp/patches.py lists the patch page words in use.
2026-10-31 15:59:53 -03:00
shenjackanddeepseek-v4.1-flash-1017 73886c4de6 Name 1.1.1 where the docs list the updates
The installer takes the 1.1.1 update since #220, but the README, the installer guide and the tester guide still name 1.2.1
and 1.4.3 alone, so a player on 1.1.1 reads that their update is not supported.

The saves note stays with 1.2.1 and 1.4.3: carrying a save across those two is what has been tested.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:48:57 +08:00
shenjackanddeepseek-v4.1-flash-1017 4223adcb72 Check the Chinese menu catalog in probe-services
The probe asserts a zh-Hans row and the language setting that reaches it, so a catalog that loses its strings or a
language map that stops resolving fails the self test.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:34:52 +08:00
shenjackanddeepseek-v4.1-flash-1017 b94bacd6d5 Name zh-Hans in the MENU_LANG reference
The runtime accepts it; the ini comment and its example line now say so instead of listing en and ru alone.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:34:52 +08:00
shenjackanddeepseek-v4.1-flash-1017 371859c9c1 Merge the Chinese menu glyphs into the font atlas and offer the language
The atlas takes the glyphs of the four CJK rows (ja, ko, zh-Hans, zh-Hant), so MENU_LANG switches while the game runs
without rebuilding it, and the Menu tab lists Simplified Chinese next to English and Russian.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:34:52 +08:00
shenjackanddeepseek-v4.1-flash-1017 ad305912ef Add a Simplified Chinese catalog for the settings menu
Every label and row description in zh-Hans, the catalog rows of the software keyboard included. A menu id without a
Chinese entry falls back to English, so the menu never shows an empty row.

The row also covers the menu language: TOTK_MENU_LANG names a catalog language ("en", "ru", "zh-Hans") and an unset
setting follows the system UI language through the same code map the game language uses ("zh-CN" -> "zh-Hans"). A
language whose row has no menu text still shows English.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:34:52 +08:00
shenjackanddeepseek-v4.1-flash-1017 24c4a27e0b Keep the game's input blocked until the sticks are released
The menu and the software keyboard hold the game's input until everything they saw is released again, and that release
test counted buttons only. With the walk key a keyboard stick moves part way without setting a direction bit, so the
movement went through the moment the menu or the text entry closed; the four axes count as held now.

The walk key's conflict note reads the bindings from a snapshot rather than the live table that the polling thread
rebuilds, and it looks at every key of an action instead of the two its cells show, so a key bound third still names the
action it presses.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:33:39 +08:00
shenjackanddeepseek-v4.1-flash-1017 6e73cd813d Add a walk key that tilts the keyboard's sticks part of the way
A keyboard key is all or nothing, so the game only ever saw a full tilt from it and the keyboard could not walk. While
the key named by walk_key is held, the two keyboard stick samples are scaled to walk_scale, the same thing yuzu and eden
do with their modifier setting; the gamepad's axes and the mouse camera keep their own values.

0.51 rather than half: a sample of 32767 times 0.5 is 16383, one short of the 16384 the game's stick-direction bits
need, so half a tilt would leave the direction clear while the axis moved. No key is on by default, because Shift is B
in the default bindings. The Controls tab lists the keys players pick, steps the scale in hundredths and names the
action a key also presses.

Co-authored-by: deepseek-v4.1-flash-1017 <noreply@deepseek.com>
2026-11-01 02:33:39 +08:00
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
DizzyMan1337andClaude Opus 5.5 54337ce6a4 Let DLAA copy the camera block out of the uniform snapshot buffer
mv_pass copies the scene's camera block from the snapshot arena with vkCmdCopyBuffer, which needs
VK_BUFFER_USAGE_TRANSFER_SRC_BIT on the source (VUID-vkCmdCopyBuffer-srcBuffer-00118). Reported by
Lucas-blip-png in #228 and SchulzCode in #253.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:16:47 +08: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
SchulzCode 9e079622dc Merge main (0.2.1) into the DLSS branch
Conflicts: the known-key and section lists in cfg.c keep both sides; vk_backend.c, vk_device.c and vk_exec_draw.c include platform.h and vk_dlaa.h; create_instance takes the window's instance extensions from gfx_window_vk_extensions as on main, in a 12-entry array for NGX's.
2026-10-31 16:19:27 +01: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
lunar-me 0c143f6fe3 docs(installer): use "other than" instead of "another ... than" 2026-11-01 04:08:24 +13: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
SchulzCode ff5706c28b Cache the thread id per thread on Linux
plat_thread_id() made a gettid syscall on every call, about 700,000 times a second in the open world (plat_rmutex_lock
asks on every acquire). A thread's id never changes and the runtime does not fork, so the first call on each thread
keeps it in a thread-local. noinline keeps a caller from reusing the thread-local's address across a fiber switch,
since a fiber may resume on another thread. Fixes #236.
2026-10-31 16:03:42 +01: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
lowlevel-larryandClaude Opus 5.5 c79b5f6cf7 Split the GPU's idle time by whether the next part was submitted yet, with calibrated timestamps
The perf line's "bound" said cpu at 640x360 on a Ryzen 5 4600G (Vega 7, RADV) and also at 1600x900, where lowering the
resolution showed the GPU held the frame back; the GPU busy time alone cannot tell the two apart. With
VK_KHR/EXT_calibrated_timestamps the GPU timestamps of each part go on the CPU clock (QPC on Windows, CLOCK_MONOTONIC
elsewhere), each part records when it was handed to vkQueueSubmit2, and a third perf line splits the gap before each
timed part: submitted after the previous part ended (the GPU waited for the replay or the game) or already queued
(untimed uploads and presents, or a wait). It also gives the gaps over 0.5 ms, the longest one and the mean time from
submit to start.

On the 4600G at 640x360 (44 fps) it reads 11.3 ms idle a frame, 10.3 ms of it waiting for the next submission, in 3-4
gaps of up to 8 ms, with the replay at 16 ms a frame: the GPU starves behind the replay. Without the extension, or when
the driver refuses the domain pair, the line is left out. Only TOTK_TRACE=perf and the full overlay time the GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:42:51 -03:00
leonelalej 2bcfc2ef39 fix(runtime): clean amiibo platform portability, default path, and dump validation 2026-10-31 14:11:46 +00: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
Lucas-blip-png ff4d79c700 Merge remote-tracking branch 'origin/dev' into installer-ptbr
# Conflicts:
#	installer/guilogic.py
2026-10-31 09:42:07 -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
Lucas-blip-png 902ea50f14 Merge remote-tracking branch 'origin/dev' into ptbr-menu-catalog
# Conflicts:
#	runtime/ov_probe.c
2026-10-31 09:40:37 -04:00
Seth 24c4838560 Merge branch 'dev' of https://github.com/valentderah/totk-pc-recomp into stick-camera-speed 2026-10-31 07:22:27 -05:00
Deon FoucheandClaude Opus 5.5 0ad2db7a33 Merge dev into xbox-button-icons again for 1.2.1's camera aspect ratio and HUD layouts
romfs.c keeps dev's note that both game versions now take the layouts of other shapes, next to the Xbox prompts' part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 22:16:57 +10:00
Deon FoucheandClaude Opus 5.5 01d69bf6d1 Merge dev into xbox-button-icons, with the restart and the prompts on the Linux port's portable calls
The restart moves into gfx_present.c's gfx_window_closed, which the Win32 and SDL windows share, and linux.c gets
plat_restart and plat_restart_wait: posix_spawn of /proc/self/exe with no descriptor past stderr and the default
signals, and a wait on a pidfd. The prompts code takes plat_file_exists, plat_setenv, strcasecmp and strdup, and the
menu and the log say a game resolution of another shape than 16:9 (16:10 now as well as 21:9) keeps the Switch prompts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 22:14:22 +10:00
Seth 84e81d0d7f Merge branch 'dev' of https://github.com/valentderah/totk-pc-recomp into stick-camera-speed
# Conflicts:
#	runtime/cfg.c
#	runtime/input_win.c
#	runtime/romfs.c
#	runtime/ui/menu.cpp
#	runtime/ui_text.h
#	totk.ini.example
2026-10-31 07:08:11 -05:00
Valiantsin DzerakhandClaude Opus 5.5 6a04fe9222 Merge pull request #89: Shadow resolution, cascade range, soft and contact shadows, anisotropic filtering (itaskaev2)
Merged with applezra's follow-ups on the same branch: the sun shadow filter narrowed by the shadow map scale
(translator patch 0011), anisotropic filtering forced only on tiling samplers, and TOTK_DUMP_UBO with a vec4 count.

Conflicts with dev: cfg.c (names, display section, defaults next to aspect and device_profile), menu.cpp (the new rows
after the aspect row), vk_texture.c (the pool alias check takes scale == 1 and dev's bind_pool_memory size argument),
build_xlate.py (patches 0005-0011 are ours; the Linux note kept), totk.ini.example (both sections). The contact pass
release went into gfx_present.c, the shared half of the old gfx_window.c.

aniso defaults to game: the picture stays as before until a player raises it in the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:44:48 +03:00
Deon FoucheandClaude Opus 5.5 703e721bf8 Offer Restart now under the button prompts while they change after restart
The row opens a confirmation; Restart writes totk.ini, closes the game as Quit game does and starts totk.exe again
with the same command line and environment (plat_restart). The new process waits until the old one is gone before it
moves the log and opens the saves (plat_restart_wait), and the confirmation waits while the game writes a save.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
Deon FoucheandClaude Opus 5.5 19fb3a75c9 Find the resource size table by any ResourceSizeTable.*.rsizetable.zs name, as MacNativeGames does
The prompts then do not fail on a table named without Nin_NX_NVN; the test RomFS of 1.4.3 uses that form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
Deon FoucheandClaude Opus 5.5 0598032cc4 Document the Button prompts setting and what the Xbox prompts show with each layout
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
Deon FoucheandClaude Opus 5.5 4c17621045 Write both Xbox prompt folders in the setup, the release and the builds
The setup runs make_romfs_patches.py --prompts xbox and --prompts xbox-label and warns once when either fails, the
release bundles tools/button_icons.py and fontTools, and build_game.py and package_build.py copy both folders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
Deon FoucheandClaude Opus 5.5 50f7f5a86c Pick the button prompts with a setting of their own, matched to the pad layout
prompts = nintendo or xbox (the Button prompts row of the Controls tab, read at start) takes over from
pad_layout = xbox_prompts, which is read as pad_layout = nintendo with prompts = xbox. overlay_init loads
romfs-patches-xbox for the Nintendo layout and romfs-patches-xbox-label for xbox and auto (input_prompts_folder), loads
romfs-patches instead of stopping when the RomFS mods folder replaces one of their files, and the menu says why the
prompts in use differ from the setting. The settings probe checks the folder against the layout's button table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
Deon FoucheandClaude Opus 5.5 f7d563db05 Draw Xbox button icons into the prompts overlay with make_romfs_patches.py --prompts
tools/button_icons.py, adapted from MacNativeGames' make_button_icons.py, draws LB, RB, LT, RT, Menu, View and a d-pad
cross into the button font of every font archive and into the button guide's L and ZL images, with the letters cut out
in the game's own text fonts, and raises the resource size table for the archives that grow. --prompts xbox swaps the
face buttons as --xbox-prompts did (now its older name); --prompts xbox-label keeps their letters for the xbox and auto
pad layouts. The plain overlay is unchanged. tests/romfs_prompts_test.py runs both on a RomFS it builds itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:50:17 +10:00
tuwaryandClaude Opus 5.5 859ed28735 Add Simplified Chinese to the setup window and the in-game menu
The setup window gets installer/text/zh-Hans.json, a full copy of the English catalog, and a third entry in its
language switch. A Windows display language of zh-CN or zh-SG picks it; zh-TW, zh-HK and zh-MO write Traditional
Chinese and stay on English. A Chinese window still starts with English as the game language, because Chinese game
text needs the Switch shared fonts the installer does not extract. The launcher's message box and
make_release.py's launcher_text.h carry the Chinese strings too.

The menu and the performance overlay get a text_zh_hans table for every menu id, and TOTK_MENU_LANG takes zh-Hans.
Left unset, the menu follows the system UI language with the same Simplified Chinese regions. The font atlas merges
in every glyph of the menu language's text from the system font (Microsoft YaHei, else SimSun) and is built again
when the menu language changes. ov_probe checks that every menu id has Chinese text, that the printf formats keep
the English conversions, and how system language tags map to menu languages; installer_gui_test checks the new
catalog the same way as the Russian one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DcYzeu7eb2tXZMU4bRVcGh
2026-10-31 16:33:53 +08:00
Lucas-blip-png 1e32d605ce Add a Brazilian Portuguese catalog for the installer window and the launcher
The installer had English and Russian. installer/text/pt-BR.json covers every id with the same placeholders; the window offers PT-BR beside EN and RU and picks it when the Windows display language is Portuguese (Brazil), LANGID 0x0416; Portugal's Portuguese stays English. make_release.py writes LAUNCHER_*_PT_BR into launcher_text.h and the launcher shows it on the same LANGID. The catalog tests check every window language against English.
2026-10-31 04:01:30 -04:00
Lucas-blip-png 655253444a Shorten the pt-BR label of the pipeline skip row so its value fits beside it 2026-10-31 03:52:15 -04:00
DizzyMan1337andClaude Opus 5.5 2e8b4ff1b1 DLSS upscaling: quality, balanced and performance modes
TOTK_DLSS (dlaa|quality|balanced|performance, read at start) sets the scene to
67, 58 or 50 % of the output. DLSS then runs at the begin of the game's scaling
pass (the FXAA shader drawing the light target into the output-sized target,
before the HUD) and its result replaces that draw; the HUD is drawn afterwards
and never jittered or passed through DLSS. The jitter now ends where the scene
ends (the bloom), separately from where DLSS runs. Scaled modes add a mip bias
of log2(scale) to repeating, mipmapped material samplers.

With a scene smaller than the output, fxaa = 0 no longer skips the game's
scaling draw (it left the 3D picture out).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:33:03 +08:00
DizzyMan1337andClaude Opus 5.5 6d181cf327 Merge upstream v0.1.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:32:04 +08:00
Lucas-blip-png 3aa604a9db Add a Brazilian Portuguese catalog for the settings menu and the overlay
The menu had English and Russian. pt-BR gets its own row with every menu id, a Português (Brasil) entry in the menu language list (TOTK_MENU_LANG = pt-BR), and is picked automatically when the Windows display language is pt-BR. The game language pt-BR now maps to this row instead of Portugal's pt, so the software keyboard shows Brazilian text too; pt keeps its row and the English menu. probe_menu_text checks the row is complete, keeps its overlay formats and follows the setting.
2026-10-31 03:21:36 -04:00
conongnang 44105a95ff Merge branch 'valentderah:main' into cheats 2026-10-31 13:41:59 +07:00
applezraandClaude Opus 5.5 5dcac19982 Force anisotropic filtering only on samplers that tile
The aniso setting raised every filtered, mipmapped colour sampler to the chosen level. Some of those read screen-sized
buffers, among them the ones the inventory's character preview is drawn from. Their coordinates stretch steeply at the
model's edges, so the anisotropic taps reach across the edge and leave a speckled fringe around the character. The
level is now raised only for samplers that repeat on U and V, which the surface textures it is meant for do; clamped
samplers keep the game's level. Distant ground stays as sharp as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:56:47 -07:00
applezraandClaude Opus 5.5 c193b2aefa Give each gamepad stick its own dead zone and show where the stick rests
stick_deadzone sets one dead zone for both sticks, and finding the value that stops a drift means guessing. A worn
stick usually drifts on its own while the other one is fine. lstick_deadzone and rstick_deadzone (0 to 50 percent,
live) set one stick each and fall back to stick_deadzone while unset, so existing totk.ini files behave as before.
input_pad_deadzone in input_map.c picks the value; input_pad_stick and its scaling stay as they are.

The Controls tab shows Left and Right stick dead zone rows in place of the single one. With a gamepad connected each
row shows its stick's deflection now, before the dead zone ("stick now"), so a drifting stick's resting value can be
read off and the zone set just above it. opt_slider takes the note line and a fallback key for that: a row whose key is
unset shows the fallback's value, and a change sets the key. probe-cfg checks the fallback, the clamp and
input_stick_percent. The menu text is in English and Russian.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:58:22 -07:00
applezraandClaude Opus 5.5 45f24a72e3 Let TOTK_DUMP_UBO set how many vec4 of each uniform block are printed
The value is now a count (1 still means 16, capped at 4096), so blocks such as the deferred lighting shaders' camera
block, whose shadow cascade matrices sit at vec4 42 to 58, can be read from a frame dump.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:10:35 -07:00
applezraandClaude Opus 5.5 bc5eb0cb15 Narrow the sun shadow filter by the shadow map scale
The deferred lighting shaders soften the sun shadow with four compares around the receiver, offset by element 56 .xy of
their lighting block. The offset is in UV and the game sized it for its own 960x960 cascades, so with shadow_scale above
1 it spans that many more texels and the near cascade shows four separate copies of each edge. Translator patch 0011
multiplies those reads by the support buffer's new shadow_spread field in fragment shaders that sample a 2D array
shadow texture, leaving fp_c4 (the cascade matrices) alone. support_fill writes 1 / shadow_scale there, so the spacing
stays the game's in texels; at shadow_scale 1 the shaders behave as before. The support buffer grows to 1440 bytes and
build_xlate.py expects eleven patches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 21:10:27 -07:00
Seth 32fc1b35e1 Add a right-stick camera speed setting that scales the game's own turn rates
The game's five camera sensitivities (Options, Very Low to Very High) turn the camera at the rates
in the StickSensitivity array of PlayerCamera.game__component__PlayerCameraParam.bgyml, 0.8 to 2.4.
stick_sens (0.1 to 3, read at start) multiplies that array: camera_speed.c decompresses
Pack/Actor/PlayerCamera.pack.zs with the RomFS's pack.zsdic, scales the floats in place and
compresses it again with the same dictionary, and romfs.c serves the result from
<profile>/camera/. Scaling the stick's deflection instead could not raise the top speed, since a
full tilt cannot go further.

The mouse camera reaches the game as a right-stick deflection, so input_win.c mouse_camera divides
it by the same factor and mouse_sens alone keeps setting the mouse's speed.

romfs.c heapdef_overlay is split into read_served and serve_generated so both generated files
share the reading, writing and overlay bookkeeping.
2026-10-30 17:58:41 -05:00
TotK localandClaude Opus 5.5 1512d9ed65 Add NVIDIA DLAA and a switch for the game's FXAA
DLAA runs DLSS at the scene's own size on the R11G11B10F light target, right before the bloom chain's first pass,
so bloom, post and HUD read the anti-aliased picture. NGX is loaded from the driver's NGX core at run time; no NVIDIA
SDK code is compiled in, and nvngx_dlss.dll goes next to totk.exe. The scene's draws get a Halton(2,3) viewport
jitter that steps after each DLAA run; camera motion vectors come from the scene depth reprojected through this and
last frame's view-projection of the scene camera block (c4), read from the uniform snapshot. One DLSS feature per
scene size keeps dynamic resolution from stalling. dlaa and fxaa switch live from the settings menu; fxaa skips the
draw of the game's FXAA pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:34:55 +08:00
itaskaev2andClaude Opus 5.5 24571ec0b0 Add soft sun shadows (PCSS) through a translator patch
The deferred lighting shaders take the sun shadow as four explicit-LOD compares on the cascade array. Translator patch
0010 turns every explicit-LOD compare on a 2D array texture into a PCSS lookup when the support buffer's new pcss
field has x above 0: eight raw depth fetches within pcss.w find the blockers, (receiver - blocker) * pcss.x gives the
penumbra width clamped to pcss.y..pcss.z, and eight compares on a disc of that radius average to the result. With x at
0 the original single compare runs behind a branch. Radii are in UV, so they hold at any shadow_scale.

The support buffer grows to 1424 bytes; support_fill writes the field from pcss (percent of the default width, 0 to
200, default 0; TOTK_PCSS_K overrides the factor at 100 percent, default 0.5). build_xlate.py expects ten patches. The
setting is in totk.ini.example and on the Screen tab of the menu (English and Russian).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:00:23 +01:00
itaskaev2andClaude Opus 5.5 4af60604de Add screen-space contact shadows
TotK fills a full-resolution G-buffer with a B10G11R11 scene colour, an R32F linear depth and a D32F depth, lights it
in half resolution, and composites the lighting into the scene colour in one pass whose only target is that colour,
with a D16 class depth. vk_contact.c recognises both passes, and when the pass after the composite begins (or a
dispatch or copy comes first) runtime/shaders/contact_shadow.comp marches each pixel 16 steps toward the sun through
the linear depth and darkens the scene colour where nearby geometry blocks the ray: the small shadows at feet, under
stones and in grass that the shadow maps miss. Transparent surfaces, the sky and post-processing come after.

The projection, far plane and sun direction come from the fragment stage's 0x940-byte camera block (c4): c4[7].x,
c4[8].y, c4[14].y, and the first cascade's depth row c4[44].xyz, whose negation points toward the sun. Scene colour
images get STORAGE usage while the setting is on.

contact_shadows (percent strength, default 0) is in totk.ini.example and on the Screen tab of the menu (English and
Russian). TOTK_CONTACT_DEBUG=1 shows the shadow factor in grey instead of the frame. It costs about 0.45 ms per frame
at a 3840x2160 scene on an RTX 4080 SUPER.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:29:53 +01:00
itaskaev2andClaude Opus 5.5 386e70453b Add a shadow cascade range setting for 1.4.3
The sun's shadow cascades end at 20, 40, 80 and 160 m: the depth shadow parameter object (main+0x6fa3c0) registers
depth_shadow_near_0_1..3 and depth_shadow_far_0 as immediates, and the field's .bgsdw files do not override them. The
new cascade patch set reads the four distances from patch page +64..+76 instead, and shadow_range (percent, 50 to 400,
default 100) writes them there at start, so the visible steps where shadows turn softer move away from the player. It
spreads the shadow map over more ground, which shadow_scale pays back. Only patches-v143.txt has the set; on 1.2.1 the
setting logs that and does nothing.

The setting is in totk.ini.example and on the Screen tab of the menu (English and Russian).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 15:06:58 +01:00
itaskaev2andClaude Opus 5.5 bfb4933531 Add shadow map resolution and anisotropic filtering settings
TotK draws its sun cascades into a 960x960 DEPTH16 array of 4 layers, and its static shadows into 1440x1440 and
720x720 arrays of 2, whatever the output resolution, and shrinks the cascades under load. shadow_scale (1 to 4,
default 1) creates those images that many times larger on each side: passes into them scale the game's viewports,
scissors and support-buffer viewport size, and copies scale their regions, so the game is unaware of it. Images the
CPU writes or that alias pool memory keep their size. When the maps are enlarged, depth-compare samplers filter
linearly, because the game's few nearest taps are spaced for its own texels and leave hard, stepped edges otherwise.

aniso (game, 2, 4, 8, 16; default 16) raises anisotropic filtering of linear mipmapped colour samplers; depth-compare,
nearest and unmipmapped samplers keep the game's setting.

Both are in totk.ini.example and on the Screen tab of the settings menu (English and Russian).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 14:47:47 +01:00
conongnangandClaude Opus 5.5 3df82b8842 Load Atmosphere cheat files and switch their cheats from a Cheats tab in the menu
Players bring their own cheats\<build id>.txt next to totk.exe; none ship with the port. The setup turns cheats that
write main's code into patch sets (tools/cheats.py), cheats that write data run in a cheat VM after dmnt's every
presented frame (runtime/cheats.c), and the menu's Cheats tab switches each one, saved under [cheats] in totk.ini.

The patch table now takes several sets per site, first set in file order winning, and replacements that end in a
branch or ret, so cheats that patch the same instruction or end in a jump can be built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 10:28:37 +07:00
191 changed files with 22936 additions and 1365 deletions

No files matched your search

+38 -3
View File
@@ -23,10 +23,23 @@ body:
validations:
required: true
- type: input
id: last-good
attributes:
label: Last version that worked
description: If an earlier version did not crash at this point, which one.
- type: textarea
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
label: System info
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED.
render: text
validations:
required: true
- type: textarea
id: mods
attributes:
label: Mods and cheats
description: The folders in mods/ with their download links, or "none". Say whether it also crashes without them.
validations:
required: true
- type: dropdown
@@ -56,10 +69,32 @@ body:
label: What you did before the crash
validations:
required: true
- type: markdown
attributes:
value: |
### The log
- The log of the run that crashed is `logs\totk.log` in the folder of `totk.exe` (F1, Advanced tab, Open log folder). Copy it before you start the game again: the next start renames it to `totk.1.log`, and after five starts it is gone.
- Attach the whole file. Do not cut or edit it: the crash report is at the end, the GPU and driver lines are at the start.
- The log says `device lost` or `VK_ERROR_DEVICE_LOST` and the GPU is an NVIDIA one: set GPU pass marks to On (F1, Advanced tab, Diagnostics), restart the game, make it crash again and attach that log. The `vk: pass marks` lines then name what the GPU was drawing.
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe. Copy it before you start the game again, because the next start overwrites it.
description: Drag totk.log into this field.
validations:
required: true
- type: markdown
attributes:
value: |
### Save and input recording
When the crash comes back at one place in the game (an area, a shrine, a cutscene, a quest step), the save that gets there lets us crash it on our machine.
1. Zip the `profile` folder next to `totk.exe` and attach it. Say which slot you load, or that you use the autosave.
2. If it takes more than walking a few steps from the save: turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load the save, make it crash, and attach `input-record.txt` from the logs folder together with the `totk.log` of that run.
- type: textarea
id: save
attributes:
label: Save and input recording
description: Drag the zipped profile folder and input-record.txt into this field, when the crash comes back at one place.
+20 -5
View File
@@ -23,24 +23,37 @@ body:
validations:
required: true
- type: input
id: last-good
attributes:
label: Last version that worked
description: If an earlier version drew this correctly, which one.
- type: textarea
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
label: System info
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
render: text
validations:
required: true
- type: textarea
id: mods
attributes:
label: Mods and cheats
description: The folders in mods/ with their download links, or "none". Say whether the glitch also shows without them. Turn off ReShade and other overlays that draw into the game before you report.
validations:
required: true
- type: textarea
id: screenshot
attributes:
label: Screenshot or video
description: Drag the file into this field. Say where in the game it was taken.
description: Drag the file into this field. Say where in the game it was taken. Flicker needs a video.
validations:
required: true
- type: textarea
id: details
attributes:
label: What looks wrong and when
description: Does it show all the time, in one place, or after some time in play? Which settings did you change from the defaults?
description: Does it show all the time, in one place, or after some time in play? Does it go away after a restart, or when you change a setting?
validations:
required: true
- type: dropdown
@@ -56,4 +69,6 @@ body:
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe.
description: Drag logs\totk.log from the folder of totk.exe into this field, from the run that showed the glitch. Copy it before you start the game again.
validations:
required: true
-65
View File
@@ -1,65 +0,0 @@
name: Other bug
description: Sound, controls, performance, the settings menu, the installer, or game logic.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Audio
- Controls and input
- Performance and stutter
- Settings menu and overlay
- Installer
- Game logic and physics
- Something else
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: system
attributes:
label: GPU, driver and Windows version
description: Copy system info in the menu's Advanced tab puts them on the clipboard.
validations:
required: true
- type: dropdown
id: blocker
attributes:
label: Can you keep playing?
options:
- "Yes"
- "No, it stops the game or a quest"
validations:
required: true
- type: textarea
id: steps
attributes:
label: What happened and how to reproduce it
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: Attach logs\totk.log from the folder of totk.exe.
+108
View File
@@ -0,0 +1,108 @@
name: Performance
description: Low frame rate, freezes, hitches, stutter or long loading.
labels: [performance]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first.
Shaders the setup did not prepare are built the first time they are needed, so a short hitch the first time an area or effect shows is expected. Report it when it happens again in the same place.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this problem.
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3".
validations:
required: true
- type: input
id: last-good
attributes:
label: Last version that ran better
description: If an earlier version was faster in the same place, which one.
- type: textarea
id: system
attributes:
label: System info
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. It lists the settings you changed too. On a Steam Deck, add LCD or OLED.
render: text
validations:
required: true
- type: dropdown
id: machine
attributes:
label: Machine
options:
- Desktop PC
- Laptop, on mains power
- Laptop, on battery
- Steam Deck
- Other handheld PC
validations:
required: true
- type: textarea
id: mods
attributes:
label: Mods and cheats
description: The folders in mods/ with their download links, or "none".
validations:
required: true
- type: dropdown
id: kind
attributes:
label: What kind of problem?
options:
- The frame rate is low all the time
- Short freezes or hitches that come back in the same place
- The frame rate is fine, but motion is not smooth
- It gets slower the longer I play
- Loading takes long
validations:
required: true
- type: textarea
id: where
attributes:
label: Where and what you were doing
description: For example "Lookout Landing, walking to the tower", "the Depths below Hyrule Castle", "fighting a Talus in the Sky". How long into the session it started.
validations:
required: true
- type: textarea
id: overlay
attributes:
label: Performance overlay
description: Press F10 twice (or both stick clicks) for the full panel. Stay a minute in the place and write down the frame rate, the mean and the longest frame time, or attach a screenshot of the panel.
validations:
required: true
- type: markdown
attributes:
value: |
### Recording the log
1. In the settings menu (F1), Advanced tab, Diagnostics: turn on Performance log. For freezes and hitches, also turn on Flight recorder. Restart the game: both take effect at the next start.
2. Play at least three minutes in the place where it happens. The log gets a frame-time summary every 60 s, and a line for every frame over 50 ms that names what the frame waited for.
3. With the flight recorder on, press F8 right after a hitch. It also saves on its own after every frame longer than the freeze threshold (100 ms), at most once per 10 s.
4. Close the game with Quit game in the menu or the window's close button. Do not end it from Task Manager: the last summary is written when the game exits.
5. Copy `logs\totk.log` from the folder of `totk.exe` before you start the game again.
6. Each flight recorder save is a folder; the log line `flight: dump` names it. Zip the folders and attach them.
7. Turn both off again afterwards: they cost a little frame time.
- type: textarea
id: log
attributes:
label: Log
description: Drag totk.log into this field.
validations:
required: true
- type: textarea
id: flight
attributes:
label: Flight recorder saves
description: Drag the zipped folders into this field, for freezes and hitches.
+94
View File
@@ -0,0 +1,94 @@
name: Other bug
description: Sound, controls, the settings menu, the installer, or game logic.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Check the pinned issues at the top of the issue list first. Crashes, graphics glitches and performance have their own templates.
Do not attach or link game files, keys, firmware or builds. Issues that do are closed.
- type: checkboxes
id: duplicates
attributes:
label: Existing issues
options:
- label: I read the pinned issues and [searched open and closed issues](https://github.com/valentderah/totk-pc-recomp/issues?q=is%3Aissue) for this bug.
required: true
- type: dropdown
id: area
attributes:
label: Area
options:
- Audio
- Controls and input
- Settings menu and overlay
- Installer
- Game logic and physics
- Something else
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: The title line of the settings menu (F1), for example "TotK Recomp 0.1.0, game 1.4.3". For the installer, the version of TotK-Setup.
validations:
required: true
- type: input
id: last-good
attributes:
label: Last version that worked
description: If an earlier version did this correctly, which one.
- type: textarea
id: system
attributes:
label: System info
description: Paste what Copy system info (F1, Advanced tab) puts on the clipboard. On a Steam Deck, add LCD or OLED. For the installer, write your OS and the install disk.
render: text
validations:
required: true
- type: textarea
id: mods
attributes:
label: Mods and cheats
description: The folders in mods/ with their download links, or "none". Say whether it also happens without them.
validations:
required: true
- type: dropdown
id: blocker
attributes:
label: Can you keep playing?
options:
- "Yes"
- "No, it stops the game or a quest"
validations:
required: true
- type: textarea
id: steps
attributes:
label: What happened and how to reproduce it
description: What you expected, what happened instead, and the steps that lead there.
validations:
required: true
- type: markdown
attributes:
value: |
### What to attach for your area
Close the game with Quit game in the menu or the window's close button, then copy `logs\totk.log` from the folder of `totk.exe` before you start the game again.
**Audio.** Open `totk.ini` in the folder of `totk.exe` and add the line `trace = audio`; when the file already has `trace = perf` (Performance log on), change it to `trace = perf,audio`. Start the game, play at least two minutes where the sound goes wrong and close the game. The log then has an `audio:` line every 5 s with the buffer underruns. Name the output device and how it is connected (speakers, headphones, USB, Bluetooth, HDMI), and attach a short video with sound if you can. Remove the line afterwards.
**Controls and input.** Name the controller and how it is connected (USB, Bluetooth, Steam Input). Does it also happen with keyboard and mouse?
**Controls, game logic, anything you can trigger again.** Turn on Input recording (F1, Advanced tab, Diagnostics), restart the game, load a save, make the bug happen and close the game. Attach `input-record.txt` from the logs folder and a zip of the save you loaded: the `profile` folder next to `totk.exe`. The recording replays what you did, so we can see the bug on our machine.
**Installer.** Attach the newest `install-*.log` from `temp\logs` in the folder of `TotK-Setup.exe` (on Linux: `.setup-temp/logs` in the install folder), and the log file the error message names.
- type: textarea
id: log
attributes:
label: Log and other files
description: Drag totk.log and the files your area asks for into this field.
validations:
required: true
File renamed without changes.
+34 -11
View File
@@ -1,46 +1,69 @@
# TotK Recomp
**TotK Recomp** is a static recompilation of *The Legend of Zelda: Tears of the Kingdom* (Nintendo Switch, 2023) into a native Windows x86-64 application ([docs/architecture.md](docs/architecture.md)).
**TotK Recomp** is a static recompilation of *The Legend of Zelda: Tears of the Kingdom* (Nintendo Switch, 2023) into a native x86-64 application for Windows and Linux ([docs/architecture.md](docs/architecture.md)).
> [!IMPORTANT]
> **No game files are included.** You need your own legally obtained copy of *Tears of the Kingdom*, dumped from your own console: the base game and the 1.2.1 or 1.4.3 update. Instructions for obtaining those files will not be provided.
> **No game files are included.** You need your own legally obtained copy of *Tears of the Kingdom*, dumped from your own console: the base game and the 1.1.0, 1.2.1 or 1.4.3 update. Instructions for obtaining those files will not be provided.
>
> The game is built from your files on your machine, so the result contains the game. It is for your own use. Do not share it.
## Status
Early development. The game is playable on Windows; visual bugs and crashes are still expected.
Early development. The game is playable on Windows and Linux; visual bugs and crashes are still expected.
Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
## Features
- Native code, not emulation
- Game updates 1.2.1 and 1.4.3, with saves that carry over between them
- Game updates 1.1.0, 1.2.1 and 1.4.3; 1.2.1 and 1.4.3 saves carry over between them
- Takes NSP and XCI game files, and compressed NSZ and XCZ as they are: the setup unpacks them itself
- 60 fps by default; up to 144 fps or the monitor's refresh rate (experimental)
- Custom resolutions (1080p, 2k...), supersampling and AMD FSR 1 upscaling
- 16:10 (Steam Deck, handhelds), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
- NVIDIA DLSS / DLAA on RTX GPUs: the setup downloads `nvngx_dlss.dll` from NVIDIA's DLSS SDK repository
(github.com/NVIDIA/DLSS, NVIDIA RTX SDK license) when it finds an RTX GPU; untick it in the setup to skip it
- HDR10 output with peak, game and HUD brightness settings
- 16:10 (Steam Deck, handhelds), 3:2 (Surface, Framework), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
- Anisotropic texture filtering up to 16x, for sharp ground into the distance
- In-game settings menu (F1) and performance overlay (F10)
- Discord Rich Presence ("Playing TotK Recomp" with the play time), off by default, own application id supported
- Setup and in-game menu in English, Russian, Brazilian Portuguese and Simplified Chinese
- Keyboard, mouse and gamepad with remapping, rumble and gyro
- Xbox button prompts: a layout that keeps the Switch button positions and shows the Xbox labels
- Stereo or 5.1 surround sound output
- Xbox button prompts: LB, RB, LT, RT, Menu and View, and A, B, X and Y as labelled on the button to press, with either button layout
- Shaders prepared at install instead of during play
- Mods made for emulators: unpack a mod into the `mods` folder next to `totk.exe`; switch and order them in the menu. RomFS
mods, TKMM's merged output and ExeFS code patches (`.ips`, `.pchtxt`) work; the game rebuilds itself for code patches
([docs/mods.md](docs/mods.md))
- Atmosphère cheats: put a cheat file into a mod and switch its cheats in the menu ([docs/cheats.md](docs/cheats.md))
## Planned
- Linux and the Steam Deck
- Mods made for emulators: unpack a mod into a `mods` folder
- VRR
## Installing
Requirements: Windows 10 or later, an x86-64 CPU with AVX2, a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, about 36 GiB free during setup (20 GiB after).
### Windows
Requirements: Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA machines get a faster build), a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, about 36 GiB free during setup (20 GiB after).
1. Download `TotK-Setup-<version>-windows.zip` from [Releases](../../releases/latest) and unpack it.
2. Start `TotK-Setup.exe`. If SmartScreen warns about an unsigned app, choose *More info*, then *Run anyway*.
3. Pick your game files, the language and an install folder, and press *Start*. The setup builds the game once.
3. Pick your game files, the language and an install folder, and press *Start*. The setup builds the game once. With
an NVIDIA RTX GPU it also downloads NVIDIA DLSS, unless you untick *Download NVIDIA DLSS*; turn DLSS on in the
menu (F1).
4. Play `totk.exe` from the install folder.
To update, run a newer `TotK-Setup.exe` on the same folder; saves and settings are kept. Settings are in the in-game menu and in `totk.ini` ([totk.ini.example](totk.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md).
### Linux
Requirements: as on Windows, plus Linux 6.7 or later and glibc 2.34 or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3).
1. Download `TotK-Setup-x86_64.AppImage` from [Releases](../../releases/latest), or build it ([docs/installer.md](docs/installer.md), "Linux release").
2. Make it executable (`chmod +x TotK-Setup-x86_64.AppImage`) and start it. On a Steam Deck, do this in Desktop Mode.
3. Pick your game files, the language and an install folder, and press *Start*.
4. Play `totk` from the install folder.
To update, run a newer setup on the same folder; saves and settings are kept. Settings are in the in-game menu and in `totk.ini` ([totk.ini.example](totk.ini.example) lists every key). Controls: [docs/controls.md](docs/controls.md).
## Credits
+6 -1
View File
@@ -11,6 +11,8 @@ Code copied or adapted from another project is listed here with its source, the
| MacNativeGames TotK frame-rate patches: 60 fps present-interval controller, real-time frame step and its present sampling, hair and cloth clock, trampoline slow-motion check (instruction words and 1.2.1 sites), player position chain; quick-menu capture background RomFS overlay | https://github.com/ly05010419/MacNativeGames @ `ebe621de99bfc48f3c42f2d2aa73e04fbcc6eae2` (`games/totk/runtime/game_patches.c`, `game_version.h`, `world_survey.c`, `games/totk/scripts/make_romfs_patches.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/frame_rate.c`, `recomp/patches-v121.txt`, `tools/make_romfs_patches.py` |
| MacNativeGames TotK 1.4.3 fade fix: the four distance / LOD and pre-actor fade sites, their instruction words and the floor of the fade step at 1 | https://github.com/ly05010419/MacNativeGames @ `3e9e7f67fdfdd3404134dcfa0f24969a9425a33f` (`games/totk/runtime/game_patches_v143.c` patch_fade_increment), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set fade; the floor without flags, clz >> 5, as in pull request #78) |
| MacNativeGames TotK aspect ratio: the camera and viewport sites that hand the perspective projection its aspect ratio, and the UI layout edits for a canvas wider than 16:9 | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/runtime/aspect_patch.h`, `aspect_math.h`, `game_patches_v143.c`, `games/totk/scripts/ultrawide_layout.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v143.txt` (set aspect), `tools/ultrawide_layout.py`, `tools/make_romfs_patches.py` (--aspect) |
| MacNativeGames TotK button icons: the Xbox controller's shoulder buttons, triggers, Menu, View and d-pad drawn with letters from the game's own text fonts, the .bfotf key, the button guide's images and their block-linear layout, the resource size table raised for the archives that grew | https://github.com/ly05010419/MacNativeGames @ `933b6a0d1438de160208a0a13eb5e8d7a2cb8094` (`games/totk/scripts/make_button_icons.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/button_icons.py`, `tools/make_romfs_patches.py` (--prompts) |
| RenoDX TotK mod (game mod by souperman9, tonemapper by Musa, RenoDX framework by ShortFuse, RenoVK fork by Ritsu): the approach of the HDR tonemap (the game's 1 - e^-x curve kept below a pivot and continued along its tangent above it, the saturation grade applied to the colour scaled into 0..1) and the observation that the passes after the tonemap keep values above 1. No code copied; the shader is our own | https://github.com/clshortfuse/renodx @ `b6f87272ad1bfdc0e6444910a93606815944a085` (`src/games/totk/tonemap.slang`, `addon.cpp`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag`, `runtime/vk_hdr.c` |
| TOTK-Ultrawide, the community mod for emulators: the list of UI panes the ultrawide layout edits and the distances they move (data only, no code copied; the edit code is our own, and MacNativeGames' `ultrawide_layout.py` in the row above already carries the same values). The mod's repository URL and licence version are not recorded in this repository | TOTK-Ultrawide (community mod; URL not recorded here) | GPL (version not recorded here) | `tools/ultrawide_layout.py` (`KEEP_ROOT`, `edits`, `anim_edits`) |
| MacNativeGames TotK virtual-core preemption: asynchronous preemption of an owner in top-level game code, priority inheritance through guest mutexes, the SDK's preemption-disable protocol (TLR+0x100/+0x102, SVC 0x36), the core-wait diagnostics | https://github.com/ly05010419/MacNativeGames @ `33e8fa83052ea1ae09a3583137008e9797e12950` (`games/totk/runtime/threads.c`, commits 8bf0893 and 19730cd), Copyright (c) 2026 MacNativeGames contributors | MIT | `runtime/kernel.c` (SuspendThread and a register-saving stub replace SIGUSR2), `runtime/hostfn.c`, `runtime/svc.c` (host depth) |
| MacNativeGames RomFS index builder (`TOTKRFS1` layout) | https://github.com/ly05010419/MacNativeGames @ `b75be8cfa704df3701e86130c8ced268e79d6b11` (`games/totk/scripts/prepare_romfs_backend.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `tools/romfs_index.py` |
@@ -37,6 +39,7 @@ Code copied or adapted from another project is listed here with its source, the
| AMD FidelityFX Super Resolution 1.0 (`ffx_a.h`, `ffx_fsr1.h`, unmodified; licence text in `runtime/shaders/ffx/LICENSE.txt`) | https://github.com/GPUOpen-Effects/FidelityFX-FSR @ `a21ffb8f6c13233ba336352bdff293894c706575`, Copyright (c) 2021 Advanced Micro Devices, Inc. | MIT | `runtime/shaders/ffx/`, `runtime/shaders/fsr_easu.comp`, `runtime/shaders/fsr_rcas.comp` (compiled into `runtime/fsr_easu_spv.h`, `runtime/fsr_rcas_spv.h`) |
| AMD Display Library (ADL) declarations: `AdapterInfo`, `ADLSingleSensorData`, `ADLPMLogDataOutput`, the `ADL_PMLOG_*` sensor numbers and the `ADL2_*` function signatures | https://github.com/GPUOpen-LibrariesAndSDKs/display-library @ `84c881cb2136f04287eb3af12de00a9e2867aa9e` (`include/adl_structures.h`, `include/adl_defines.h`, `include/adl_sdk.h`), Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc. | MIT | `runtime/perfmon.c` |
| Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd`), static link | https://github.com/facebook/zstd, Copyright (c) Meta Platforms, Inc. and affiliates | BSD-3-Clause | `runtime/resolution.c` (the HeapDef table), `runtime/ov_zstd.c` (the game's zstd frames decoded on the host) |
| musl `fma.c`, `fmaf.c` and `scalbn.c`, adapted into one file for the x86-64-v2 build | https://github.com/kraj/musl @ `5a56ad13e1e0790c06122978aad2a14d6b9efc1a` (mirror of https://git.musl-libc.org/cgit/musl), Copyright (c) 2005-2020 Rich Felker and contributors | MIT | `runtime/fma.c` (the correctly rounded fma/fmaf the FP lowering needs on CPUs without FMA3) |
## Compiler runtime in a source build
@@ -64,8 +67,10 @@ 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/` |
| numpy 2.5.4 (with its bundled OpenBLAS), lz4 4.4.5, cryptography 50.0.2 (with its bundled OpenSSL), fontTools 4.68.0, 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; fontTools MIT; 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/` |
| SDL 3.4.18 upstream Windows release (`SDL3.dll`) and mingw development package (headers, `libSDL3.dll.a`) | https://github.com/libsdl-org/SDL/releases/tag/release-3.4.18 | zlib | `bin/SDL3.dll`, `runtime/deps/` |
| libopus 1.6.1 (MSYS2 `mingw-w64-ucrt-x86_64-opus` 1.6.1-2; `libopus-0.dll` imports only KERNEL32 and the UCRT api-ms-win-crt DLLs), headers and import library | https://repo.msys2.org/mingw/ucrt64/ | BSD-3-Clause | `bin/libopus-0.dll`, `runtime/deps/` |
+1 -1
View File
@@ -1 +1 @@
0.2.1
0.3.7
+1
View File
@@ -10,6 +10,7 @@ updates = ['C:\Games\TotK\update-1.4.3.nsp']
keys = 'C:\Switch\prod.keys'
# Optional: a firmware NCA directory (an unpacked firmware, or an emulator's installed registered/ directory).
# tools/extract_fonts.py takes the system shared fonts from it; the game needs them only with Chinese or Korean text.
# The installer asks for the same folder (--firmware) when the language needs those fonts.
# firmware = 'C:\Switch\firmware'
[work]
+29 -6
View File
@@ -54,7 +54,9 @@ MacNativeGames cannot run on x86-64 as is: its CPU path executes ARM64 code nati
libzstd on the host (`ov_zstd.c`, `TOTK_ZSTD_NATIVE`); the lifted decoder took almost three times as long.
- Settings and menu: every key with its default is in one table (`cfg.c`), read from `totk.ini` and `TOTK_*`
variables and written back when the in-game menu changes it. The menu and the performance overlay are Dear ImGui,
drawn into the swapchain image (`runtime/ui/`); their text is in `ui_text.c` (English, Russian).
drawn into the swapchain image (`runtime/ui/`); their text is in `ui_text.c` (English, Russian, Brazilian Portuguese,
Simplified Chinese). The Chinese glyphs come from a system font (Microsoft YaHei, `plat_font_path`) merged into the
font atlas.
- OS calls the host makes go through `runtime/platform/` (`platform.h`, `win32.c`), so a Linux or macOS host adds one file instead of touching the rest. The split is not finished: `tools/check_platform.py` lists the Win32 uses left in the other files.
#### Threads and cores
@@ -74,15 +76,16 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- Spin waits. Loops that only another thread can end (857 in 1.4.3, 864 in 1.2.1, found by `tools/spin_waits.py`, the
nxnative search) send their back-edges through `rt_spin_gate`, which pauses the core once per pass and hands it to a
waiter after 50 us of polling (`TOTK_SPIN`, `TOTK_SPIN_HANDOFF_US`).
- `TOTK_FREE_CORES=1` turns the gate off, for experiments.
- `free_cores = 1` (`TOTK_FREE_CORES=1`) turns the gate off, for experiments only: the game crashes or hangs, usually
at start, because its job workers all report core 0 and share per-core memory (#212).
### 4. Graphics
- NVN front end from MacNativeGames (`nvn_api_*.c`, `nvn_obj.h`, texture layout and deswizzle). The game records command buffers through it; nothing reaches the GPU until the game submits.
- Vulkan 1.3 back end in place of MacNativeGames' Metal one. At `QueueSubmitCommands` the recorded NVN commands are replayed into Vulkan command buffers (`vk_exec*.c`).
- Memory pools. Apple's unified memory let MacNativeGames use the game's pools zero-copy. The back end imports the pools as host memory (`VK_EXT_external_memory_host`). On a discrete GPU, pools bound as buffers are mirrored in device-local memory and the pages the CPU wrote (write watch, above) are uploaded before a replay; idle pool regions are made read-only so the write-watch queries skip them (`vk_memory.c`). GPU-rendered textures are written back when the CPU reads them: on a pool invalidate, on a CPU copy, and for small targets once the CPU waits for a fence (`vk_texture.c`).
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is off by default because no timing has shown a gain yet.
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. `TOTK_VK_RECORDER=0` makes every call on the submitting thread.
- Replay thread. `TOTK_REPLAY_ASYNC=1` moves the replay off the submitting thread onto a queue at most one frame behind the game; reads of GPU results wait for the newest writer instead of draining the queue (`vk_backend.c`). It is on by default: where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in the Depths, issue #88), elsewhere it is neutral; it adds up to a frame of latency, and `replay_async = 0` turns it off.
- Recorder thread. The Vulkan calls the replay makes into its command buffer are made by a thread of their own (`vk_rec.c`, after Savage Wind Recomp and LAS Recomp): each `vkCmd*` copies its arguments into a packet of a ring and the recorder makes the calls in order; calls into other command buffers are made at once; the recorder also ends the command buffer, and the submit waits until it has. In the town on an RX 9070 the replay takes 17% less time on the submitting thread. It is off by default on AMD GPUs, where the driver loses the device or hangs the GPU at startup with it (`TOTK_VK_RECORDER=1` forces it on, `=0` makes every call on the submitting thread).
- Uniform snapshot. The game rewrites per-object uniform blocks on other threads soon after it submits the frame
that reads them, while the replay still records it. At the start of a submission's replay every uniform range it
binds from CPU-written pools is copied, and the draws bind the copy (`vk_usnap.c`, after MacNativeGames'
@@ -96,12 +99,32 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
RCAS) on the present. An `output_res` that is not 16:9 (more than 1% off, `res_is_16x9`) also switches on the
`aspect` patch set, which hands the output's ratio to the camera, and `romfs.c` `overlay_init` loads the
`romfs-patches-<w>x<h>` overlay for it when it exists (`tools/make_romfs_patches.py --aspect`, laid out by
`tools/ultrawide_layout.py`; the installer builds ten sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160, 32:9
3840x1080, 5120x1440 and 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600). A wider layout moves the HUD to the
`tools/ultrawide_layout.py`; the installer builds fifteen sizes: 21:9 2560x1080, 3440x1440, 3840x1600, 5120x2160,
32:9 3840x1080, 5120x1440, 16:10 1280x800, 1680x1050, 1920x1200, 2560x1600 and 3:2 1920x1280, 2160x1440, 2256x1504,
2880x1920, 3000x2000). A wider layout moves the HUD to the
screen's corners, a narrower one to its top and bottom edges. Other sizes get the camera of their shape and the 16:9
HUD stretched over it. The `aspect` setting (`fit`, the default; `stretch`) decides what the present does
when the window's shape differs from the output's: `fit` letterboxes, where a 16:9 game in a wider window used to be
stretched.
- HDR (`hdr = 1`, `vk_hdr.c`). The game tonemaps its scene into an r11g11b10f target, upscales it and draws the HUD
into another float target, and a last draw copies that into the 8-bit sRGB window texture it presents. With HDR on,
the tonemap's fragment shader (recognised by its constants and resource names, not a hash) runs as
`shaders/hdr_tonemap.frag`, which keeps highlights up to the display's peak; the present reads the float target the
window texture was copied from (any other last draw into it, such as a menu or a movie, leaves the window texture,
shown at the HUD's white); a compute pass turns the picture into PQ code values in Rec. 2020 for an HDR10
swapchain, which FSR 1 or the blit then scale; the in-game menu is drawn into an RGBA8 image and blended on at the
HUD's white. Without an HDR10 surface format (Windows with "Use HDR" off, a compositor without colour management)
the swapchain stays SDR. `TOTK_SHOTS` reads the window texture, so its pictures clip highlights in HDR.
- DLAA (`vk_dlaa.c`, `dlaa`, off by default). NVIDIA DLSS at the scene's own size, through the driver's NGX core loaded
at run time (no SDK code compiled in). It runs on the R11G11B10F light target right before the bloom chain's first
pass, so post effects and the HUD read the anti-aliased picture. The scene's draws get a sub-pixel Halton viewport
jitter; camera motion vectors are reprojected from the scene depth through this and last frame's view-projection of
the scene camera block (constant bank c4), in double precision on the CPU. Per-object motion (`dlaa_objmv`,
`vk_mvprev.c`, the per-object section of `vk_exec_draw.c`, translator patch 0015): solid scene draws whose position
reads an object's own uniform blocks (bones c5, place c7, c6) are drawn a second time with a vertex shader variant
that computes last frame's position from last frame's blocks (the game writes them into two copies that take turns;
the uniform snapshot keeps both) and writes the difference into the motion image. `TOTK_MV_CHECK` measures the
vectors against the picture, `TOTK_DLAA_TIMES` times the passes on the GPU. `fxaa` switches the game's own FXAA pass.
- Occlusion queries never block the CPU; a result that is not ready counts as visible.
- ASTC textures are uploaded as they are where the device reports `textureCompressionASTC_LDR`, and decoded by a compute shader everywhere else (`vk_astc.c`, `TOTK_ASTC`). The CPU decoder (`tex_astc*.c`) is the fallback and the test oracle.
- Draw state. A draw whose program, bound state and textures repeat reuses the previous draw's shaders and skips rebuilding its descriptor sets; descriptor sets are written through update templates (`vk_exec_draw.c`, `vk_pipeline.c`).
+33 -8
View File
@@ -1,7 +1,9 @@
# Building from source
Windows 10 or later, an x86-64 CPU with AVX2, and a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`. Allow about
60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB afterwards.
Windows 10 or later, an x86-64 CPU with SSE4.2, and a GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`. The
default build targets x86-64-v3 (AVX2/FMA); a CPU without them builds with `--cpu v2` instead and runs slower (see
"Build"). Allow about 60 GB for the work folder. Extraction wants about 55 GB free while it runs and keeps about 35 GB
afterwards.
Players who only want to play can use the [installer](installer.md) instead: it builds the game on their machine
without this toolchain.
@@ -52,7 +54,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 `v110` for 1.1.0. 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:
@@ -68,13 +71,15 @@ From the repository root, in PowerShell. Every step that depends on the game ver
keys file. `config.local.toml` is ignored by git. The tools read the keys file themselves.
3. Extract and verify. This writes `build\extracted\v0` (1.0.0) and `build\extracted\<ver>` for each update in
`updates`, identified by its hash; a hash mismatch stops the run:
`updates`, identified by its hash; a hash mismatch stops the run. With another `[work] dir` in `config.local.toml`,
`extracted` goes there instead, and `build_game.py` reads the ExeFS and RomFS from there:
```powershell
.venv-py314\Scripts\python tools\extract.py
```
4. Shader translator (once; no game files). This publishes `build\xlate\totk_xlate.exe`:
4. Shader translator (no game files; again after a change to its sources, and `tools\make_release.py` stops on an older
one). This publishes `build\xlate\totk_xlate.exe`:
```powershell
.venv\Scripts\python tools\build_xlate.py
@@ -89,6 +94,14 @@ From the repository root, in PowerShell. Every step that depends on the game ver
.venv\Scripts\python tools\build_game.py --version <ver>
```
`--cpu v2` targets x86-64-v2 (SSE4.2, no AVX2/FMA) for older CPUs; the default `v3` needs AVX2. The v2 build links
`runtime/fma.c`, a correctly rounded software fma the FP lowering needs, and runs slower. Inline assembly,
target attributes and intrinsics are not bound by `-march`, so a v2 build runs `tools/check_isa.py` over the runtime
objects before it links: any AVX, BMI or other instruction above x86-64-v2 outside a function listed there as
dispatched by a runtime CPU check stops the build.
`gen.py` sizes its emit pool from the CPU count and the free memory; `--jobs N` sets it, and so does the environment variable `TOTK_LIFT_JOBS=N`, which the installer's lift step honours too (it has no `--jobs`). When a worker runs out of memory, the pool restarts with half the workers (`emit: out of memory with N workers, retrying with M`), down to one; only an out-of-memory error with one worker fails the step.
Lifting takes about 15 s, a clean compile about 200 s on 24 threads; later builds recompile only what changed.
A compiler cache in front of clang shortens it further: with `SCCACHE` set to an sccache executable, `build_game.py`
writes it into the ninja file ahead of `cc`, and every lift rewrites the generated chunks with the same contents, so
@@ -100,9 +113,9 @@ From the repository root, in PowerShell. Every step that depends on the game ver
```
`--patches` builds in the frame-rate, resolution and other patch sets, switchable at run time. The link copies
`SDL3.dll`, `libiconv-2.dll` and `libopus-0.dll` next to the exe.
For Xbox button prompts, run `make_romfs_patches.py` once more with `--xbox-prompts`: it writes
`build\<ver>\romfs-patches-xbox` ([controls.md](controls.md), "Xbox button prompts"); `build_game.py` copies it next
to `totk.exe` like `romfs-patches`.
For Xbox button prompts, run `make_romfs_patches.py` twice more, with `--prompts xbox` and `--prompts xbox-label`:
they write `build\<ver>\romfs-patches-xbox` and `romfs-patches-xbox-label` ([controls.md](controls.md), "Xbox
button prompts"), which `build_game.py` copies next to `totk.exe` like `romfs-patches`.
6. Prepare the shaders (once per build folder and translator). It needs `refs\MacNativeGames` (`tools\fetch_refs.sh`)
for the shared shader list:
@@ -114,6 +127,11 @@ From the repository root, in PowerShell. Every step that depends on the game ver
Without this step the first launch compiles shaders and pipelines during play and stutters until the caches fill.
The AMD driver keeps its pipeline cache per exe path and holds a limited amount: preparing another build folder can
evict this one's. `totk.exe --warm-shaders` rebuilds it from the shader cache.
When the driver's shader compiler crashes on a variant (NVIDIA 615.78.08 on an RTX 5080 does, issue #30),
`prepare_shaders.py` runs the driver step again with `--warm-shaders`, up to three times, without the variants the
driver was building at the crash. `shader-prepare-skip.txt` next to the exe lists them with the GPU and driver
version; they build at their first draw instead, another driver version builds them again, and so does deleting the
file.
7. Run. The game keeps running when its window loses the focus (`assume_focus = 1` in `totk.ini`, the default; the
Game tab switches it). With 0 it pauses, as it does on the Switch when Home is pressed.
@@ -182,6 +200,10 @@ Before a commit:
`git config core.hooksPath .githooks` runs it before each local commit.
- `build\<ver>\totk.exe --selftest`: layout, dispatch table and host-function registry, without game files.
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
files); a change to the settings passes `totk.exe --probe-cfg`.
The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter
change passes these, all run with `.venv`:
@@ -195,6 +217,9 @@ change passes these, all run with `.venv`:
| `tests\excl_mt.py` | the exclusive monitor helpers under contention and ABA, on several threads | no |
| `tests\jumptables_check.py` | `recomp/jumptables.py` against a Unicorn oracle; must report 0 disagreements, on 1.4.3 too | yes |
A `--cpu v2` build passes the same differential tests with `--cflags=-march=x86-64-v2`; they compile `runtime/fma.c`
too, so they check the software fma the v2 build links rather than the C runtime's.
Runtime probes, each a mode of `totk.exe` (the list is at the top of `runtime/main.c`): `--probe-kernel`,
`--probe-vkexec`, `--probe-nvn-tex` and `--probe-shader` cover most runtime changes. `--probe-shader` and the draw
part of `--probe-vkexec` translate shaders of the census below: they look for `build/<ver>/shaders/jobs.json` of the
+74
View File
@@ -0,0 +1,74 @@
# Cheats
The port reads Atmosphère cheat files, the format Switch homebrew and the emulators use. It ships no cheat files: a cheat file goes into a mod in the `mods` folder next to `totk.exe`, like any other mod ([mods.md](mods.md)). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
## Adding a cheat file
1. Find the cheat file for your update. Cheat sites and collections name it after the first 8 bytes of the game's build id:
| Update | File |
|---|---|
| 1.2.1 | `9B4E43650501A4D4.txt` |
| 1.4.3 | `277178B7DBA1B6D4.txt` |
The Cheats tab names the file the installed game reads when no mod holds one, and `logs\totk.log` says `cheats: no enabled mod holds cheats/<id>.txt`.
2. Put it in the `cheats` folder of a mod, a folder of its own under `mods`:
```
mods\
My cheats\
cheats\
277178B7DBA1B6D4.txt
```
A download laid out for the Switch's SD card or an emulator works as it is: unpack it into a folder of its own under `mods`, and the port finds `atmosphere\contents\0100F2C0115B6000\cheats`, `contents\0100F2C0115B6000\cheats` or `0100F2C0115B6000\cheats` inside it. The file name's case does not matter.
3. Start the game. Cheats that write the game's memory work now.
4. Cheats that change the game's code need the game rebuilt with them. When the game starts and finds such cheats it did not build, it opens the setup's rebuild window, which builds them in (a few minutes; the shaders stay prepared) and starts the game again. Until then they are greyed out with "after the rebuild". When the rebuild fails or you cancel it, the game starts without them and does not try again for the same cheats; "Rebuild the game now" on the Cheats tab tries again. `mods_rebuild = off` under `[mods]` in `totk.ini` turns the automatic rebuild off; running `TotK-Setup.exe` on the same folder does the same job by hand. Saves and settings are kept.
5. Switch the cheats on the Cheats tab of the menu.
The Cheats tab lists the cheats grouped by mod, in the order of the Mods tab. A mod switched off on the Mods tab, or one the port cannot use, contributes no cheats; the change applies at the next start. The Mods tab says how many cheats each mod brought.
## The file format
Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat that is always on, and the cheat's opcodes follow as 8-digit hex words. A name with no opcodes is shown as a heading. The file's title line (the one with `TID=`) is not shown.
```
[The Legend of Zelda: Tears of the Kingdom v1.4.3 TID=0100F2C0115B6000 BID=277178B7DBA1B6D4]
[Player]
[A code cheat]
040A0000 XXXXXXXX YYYYYYYY
[A data cheat (Hold ZL+X)]
80000104
580F0000 XXXXXXXX
640F0000 00000000 YYYYYYYY
20000000
```
The words above are placeholders: `XXXXXXXX` an offset, `YYYYYYYY` a value.
Each cheat's switch is `cheat_<hash> = 1`, where the hash is FNV-1a 32 of `<mod folder name>/<cheat name>`. A name repeated in one file gets `#2`, `#3` and so on. The build names each code cheat the same way, so renaming a mod's folder resets its cheats' switches to off and makes the next start rebuild its code cheats. Rename the folder before you switch its cheats on.
## How cheats run
The game's code is compiled ahead of time, so a cheat cannot write new instructions into it while the game runs. The port sorts each cheat by what it writes (`tools/cheats.py` at setup, `runtime/cheats.c` in game):
| Kind | What the cheat does | How the port runs it |
|---|---|---|
| code | Opcode 0 writes into main's code | Built into the game at setup as a patch set (`recomp/patches.py`); switches at once. |
| key | Code writes inside a button condition (`8` mask, writes, `21` else, the original words, `20`) | A patch set that is on while the cheat is on and the buttons are held. |
| data | Writes data only: heap, main's data, pointer chains, button conditions | Runs in a cheat VM after dmnt's, opcodes 0 to A, every presented frame while on. |
| master | `{name}` with code writes | A patch set that is always on. |
| restore | Writes back only the game's original instructions | Not needed: switching a cheat off restores them. Greyed out. |
A cheat whose code writes call or jump into a code cave the cheat writes itself (into main's zero padding) gets the cave inlined at the call site. The setup builds the code cheats of every mod in the `mods` folder, switched on or off, so switching a mod on later needs only a restart. A code cheat added or edited after the last build waits for the rebuild; a data cheat runs at the next start.
Not supported, and shown greyed out with the reason:
- the 0xC and 0xF opcode extensions;
- opcode 5 with a register offset;
- code writes mixed with other opcodes, except the button pattern above;
- code writes that change the CPU flags differently from the game's instruction, or code caves with branches or pc-relative instructions.
`build-cache\cheats.status` next to `totk.exe`, written by the setup, records which cheats were built and how; the log line `cheats: ...` sums them up at start. When the code cheats cannot be built, the setup warns and installs the game without them.
+21 -9
View File
@@ -7,9 +7,8 @@ Input comes from the first SDL3 gamepad, the keyboard and the mouse, merged into
- `nintendo`: by position, as on a Switch (the table below).
- `xbox`: south A, east B, west X, north Y, as an Xbox pad labels them; on a PlayStation pad cross is A, circle B, square X, triangle Y.
- `auto` (default): `nintendo` for a pad whose east button SDL reports labelled A (Switch Pro Controller, Nintendo-layout pads), `xbox` for the others.
- `xbox_prompts`: the buttons as `nintendo`, and the game's prompts name the Xbox labels (see Xbox button prompts below).
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. With `nintendo`, `xbox` and `auto` the game's button prompts stay Switch letters; `xbox_prompts` changes them.
The log names the layout in effect (`input: gamepad layout ...`); the menu shows it under *Automatic*. Which controller's buttons the game's prompts show is `TOTK_PROMPTS` (see Xbox button prompts below). 0.1.9's `pad_layout = xbox_prompts` is read as `pad_layout = nintendo` with `prompts = xbox`, and the menu writes it that way.
| Switch | Gamepad (by position, Nintendo layout) | Keyboard and mouse | Action in the world |
|---|---|---|---|
@@ -33,16 +32,29 @@ Esc is unbound in the game; it opens the settings menu and releases the mouse cu
## Xbox button prompts
`pad_layout = xbox_prompts` (*Xbox prompts (A right)* on the Controls tab) is for an Xbox pad played by position. The buttons work as with `nintendo`: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel). The letters the game shows are swapped to match the labels, A with B and X with Y, so each prompt names the button to press. The other layouts leave the prompts alone: with `xbox` the Switch letters already match the labels.
`TOTK_PROMPTS` (`prompts`, the *Button prompts* row on the Controls tab, read at start) picks the controller whose buttons the game's prompts show: `nintendo` (default) keeps the game's own, `xbox` shows an Xbox controller's.
The swapped letters are part of the game's textures, fonts and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the letter the game draws for it, and the prompt then names the wrong button. Use `xbox_prompts` with the default bindings.
- L, R, ZL, ZR, Plus and Minus become LB, RB, LT, RT, Menu and View, and the d-pad prompts a cross with an arrow for each direction meant. The letters on the keys are cut out in the game's own text font.
- A, B, X and Y name the label of the button the pad layout presses, so each prompt names the button to press. `pad_layout = nintendo` plays an Xbox pad by position: the right button, labelled B, does Switch A (interact, confirm), and the bottom one, labelled A, does Switch B (run, cancel), so the prompts swap A with B and X with Y. `xbox` and `auto` put Switch A on the button labelled A, and the letters stay.
The buttons change at once, but the game reads its UI archive and fonts once at boot, so the prompts change at the next start (the menu says *prompts change after restart*). `xbox_prompts` loads `romfs-patches-xbox` next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`): the same 60 fps patches, with the swap made from the game's own files by `tools/make_romfs_patches.py --xbox-prompts`, which the setup runs next to the plain one:
| Switch | `nintendo` layout | `xbox` or `auto` layout |
|---|---|---|
| A, B, X, Y | B, A, Y, X | A, B, X, Y |
| L, R | LB, RB | LB, RB |
| ZL, ZR | LT, RT | LT, RT |
| Plus, Minus | Menu, View | Menu, View |
| D-pad | cross | cross |
- the action prompt (`DoCommand_00`) draws the letter images `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped, in a diamond that names the other face buttons. The diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it); both are swapped. The diamond's shape shows the button's position and stays.
- every other prompt is text in the button font (`scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`, glyphs U+E040 to U+E043); the A and B, X and Y glyphs are swapped.
The action prompt (`DoCommand_00`) shows its button in a diamond whose shape gives the face buttons' positions as on a Switch, with the other three letters around it. With `nintendo` its letters are swapped with the rest and every one names the button in that place. With `xbox` and `auto` the letters are right but the diamond keeps the Switch positions: A shows on its right, while the button labelled A is at the bottom. The sticks keep the Switch icons, and so do prompts drawn as fixed images (some tutorial animations).
Without the folder (an install made by an older setup, or one where the setup could not prepare it and showed a warning) the menu says the files are missing, the log has `RomFS overlay: pad_layout = xbox_prompts, but ...`, and the Switch prompts stay; running the setup again on the install folder writes it. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --xbox-prompts` writes `build\<ver>\romfs-patches-xbox`.
The prompts are part of the game's fonts, textures and messages, so they follow the layout and not the bindings. A button remapped with a `bind_` line or on the Controls tab keeps the prompt the game draws for it, which then names the wrong button. Use the Xbox prompts with the default bindings.
The game reads its UI archive and fonts once at boot, so the prompts change at the next start, also after `pad_layout` changes between `nintendo` and the others (the menu says *prompts change after restart*); the buttons change at once. Under that note the menu offers *Restart now*: after a confirmation it writes `totk.ini`, closes the game as *Quit game* does and starts the game again with the same command line, which waits until the old process is gone before it opens the log and the saves (`runtime/gfx_present.c`, `gfx_window_closed`; `plat_restart` and `plat_restart_wait` in `runtime/platform/win32.c` and `linux.c`). Progress since the last save is lost, as with *Quit game*; while the game writes a save, the confirmation waits. With `prompts = xbox` the game loads `romfs-patches-xbox` (`pad_layout = nintendo`) or `romfs-patches-xbox-label` (`xbox`, `auto`) next to `romfs-index.bin` in place of `romfs-patches` (`runtime/romfs.c`, `overlay_init`; `runtime/input_map.c`, `input_prompts_folder`): the same 60 fps patches, with the prompts drawn from the game's own files by `tools/make_romfs_patches.py --prompts xbox` or `--prompts xbox-label` (`tools/button_icons.py`, after MacNativeGames' `make_button_icons.py`), which the setup runs next to the plain one:
- every prompt but the button guide in play is text in the button font, `scft/External.bfotf` in each `Font/Font*.Nin_NX_NVN.bfarc.zs`: glyphs U+E040 to U+E043 (A B X Y), U+E044 to U+E047 (L R ZL ZR), U+E04B and U+E04C (Plus, Minus), U+E049 and U+E060 to U+E065 (the d-pad). fontTools writes the font again, so the archives grow and the folder also carries the resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`) with their entries raised by as much; without that the game stops at start.
- the button guide in play draws L and ZL from the images `Nt_KeyTexL_00` and `Nt_KeyTexZL_00` of the Common archive's `timg/__Combined.bntx`, painted with the same icons. The action prompt draws its button from `Nt_KeyTexA/B/X/Y_00`, whose image data is swapped for `nintendo`, and the diamond's letters are text the game takes from `LayoutMsg/DoCommand_00.msbt` of each language's `Mals` archive (and the layout's own text panes behind it), swapped the same way.
The Switch prompts stay, with a note under *Button prompts* and a `RomFS overlay: ...` line in the log, when the folder is missing (an install made by an older setup, or one where the setup could not prepare it and showed a warning; running the setup again on the install folder writes it), when it fails its check, with a game resolution of another shape than 16:9 (its own UI layout wins; 16:10 and 21:9), and when the `romfs_mods` folder replaces one of its files: `romfs-patches` then loads in its place, where two overlays replacing one file would stop the game. In a source build, `.venv-py314\Scripts\python tools\make_romfs_patches.py --version <ver> --prompts xbox` (and `--prompts xbox-label`) writes `build\<ver>\romfs-patches-xbox` (`-label`); it needs fontTools from `tools/requirements-py314.txt`. `--xbox-prompts` is the older name of `--prompts xbox`.
## Window keys
@@ -82,7 +94,7 @@ Mouse motion (raw input) becomes right-stick deflection. Each presented frame ta
`TOTK_MOUSE_SENS` (0 to 10, default 1; higher turns faster, 0 turns the mouse camera and the cursor capture off). While the window is the foreground window the cursor is hidden and confined to the client area. It is released on focus loss, minimise, while F11 changes the window, during a size or move loop and while a text entry is open. Esc releases it by hand: the cursor shows, the mouse camera and mouse buttons stop, and the window can be moved, minimised or closed. A mouse click inside the client area captures it again; that click does not reach the game. Activating the window by a click on its title bar or border does not capture, so it can be dragged or closed right away; Alt+Tab or a click in the client area captures as before. `TOTK_INPUT=none` disables the capture too.
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it.
`TOTK_STICK_DEADZONE` (0 to 50, default 8) is the gamepad stick dead zone in percent of full deflection, the same for both sticks. It is round: a stick inside the circle reads centred, and the rest of its travel is scaled to start at 0 at the edge, so full deflection still reaches full. Raise it for a stick that drifts. The game's own dead zone applies after it. `TOTK_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows under *Analog sticks* on the Controls tab) set one stick each and fall back to `TOTK_STICK_DEADZONE` while unset, so a worn stick can get a larger zone than the other. With a gamepad connected each row shows its stick's deflection now, before the dead zone (*stick now*): let go of the stick and set the row a few percent above the value it rests at.
The stick direction bits (`StickLLeft` to `StickRDown`) are derived from the sticks at 50% deflection. A gamepad plugged in later is picked up within one second; with none connected the log says `input: no gamepad`.
+96 -28
View File
@@ -9,20 +9,26 @@ holds only the game; the installer and its tools stay outside it.
1. Downloads `TotK-Setup-<version>-windows.zip` from GitHub Releases (none is published yet), unpacks it anywhere and
starts `TotK-Setup.exe`.
2. Picks:
- the base game (XCI or NSP) and the 1.2.1 or 1.4.3 update (NSP);
- the base game (XCI or NSP) and the 1.1.0, 1.2.1 or 1.4.3 update (NSP); an NSZ or XCZ (compressed by the nsz
tool) works as it is, the setup unpacks it itself;
- their `prod.keys`;
- optionally the firmware folder (`system/Contents/registered` of an unpacked firmware or an emulator's install),
needed for Chinese, Korean and Traditional Chinese text;
- the game language and the install folder.
3. Waits for the setup, one time. The window shows each step and its progress.
4. Plays `totk.exe` from the install folder. The installer and the source NSP or XCI can be deleted.
4. Plays `totk.exe` from the install folder. The installer and the source NSP or XCI (or NSZ, XCZ) can be deleted.
The install folder (`main.c` `portable_paths()` reads this layout; it stays portable):
```
<install folder>/
totk.exe, SDL3.dll, libopus-0.dll, totk_xlate.exe
nvngx_dlss.dll NVIDIA DLSS, downloaded by the setup's DLSS step when the player keeps it ticked
totk.ini language; the other keys as commented defaults (totk.ini.example)
exefs/, romfs/ extracted from the player's files
shared-fonts/ the Switch system fonts, with Chinese, Korean or Traditional Chinese text
profile/ saves
mods/ the player's mods, one folder per mod (created by the menu's "Open mods folder")
logs/ totk.log of the last run, totk.1.log to totk.4.log of the runs before it
shader and pipeline caches prepared by the setup; shader-cache/shader-cache.pack holds the translations
totk-install.json what built this folder: release version, input hashes, finished steps
@@ -31,43 +37,80 @@ The install folder (`main.c` `portable_paths()` reads this layout; it stays port
## Updating the port
The player runs a newer installer on the same folder. It reads `totk-install.json`, keeps `romfs/`, `exefs/`,
`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.
`shared-fonts/`, `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
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.
Given an update file other than the recorded one (1.1.0, then 1.2.1, then 1.4.3), the installer extracts and builds again from it
and keeps `profile/`. Saves carry over both ways between 1.2.1 and 1.4.3: 1.4.3 converts a 1.2.1 save on load, and 1.2.1
loads the saves 1.4.3 wrote.
## Setup steps
1. **Check** before writing anything: the files open with the keys, the update is 1.2.1 or 1.4.3 (by hash, as
1. **Check** before writing anything: an NSZ or XCZ input is unpacked first (below), the files open with the keys, the update is 1.1.0, 1.2.1 or 1.4.3 (by hash, as
`tools/extract.py`), the keys are new enough for the update, enough space is free on the target disk (about 36 GiB at the peak of a
first install, 20 GiB after it), the CPU has
AVX2 (`-march=x86-64-v3`), a Vulkan 1.3 GPU exists. Each failure is one clear message.
2. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
3. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
to the installer.
4. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
first install, 20 GiB after it), the CPU is at least x86-64-v2 (SSE4.2; with AVX2/FMA the faster `v3` build is used,
otherwise the `v2` one), a Vulkan 1.3 GPU exists. Each failure is one clear message.
2. **Fonts** with Chinese, Korean or Traditional Chinese text and a firmware folder: extract the Switch system fonts
from the firmware into `shared-fonts/` (MacNativeGames' `extract_shared_fonts.py`, which verifies every NCA layer
and BFTTF header). Without a firmware folder the setup warns and goes on; the game then lacks those glyphs.
3. **Extract** ExeFS and RomFS into the install folder and verify the hashes.
4. **Lift** the ExeFS to C (`recomp/gen.py`, `recomp/patches-v121.txt` or `patches-v143.txt`) into a temp folder next
to the installer. The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets first
(`tools/cheats.py`, `tools/exefs_patches.py`, status in `build-cache/cheats.status` and `exefs.status`); a file the
tools refuse costs those cheats or patches, not the install.
5. **Compile** the generated C, `modules.c`, `romfs.c`, `patches.c` and `ov_zstd.c` (they bake the per-install layout, RomFS
hashes and the game version's patch page) and link
them with the prebuilt runtime library into `totk.exe` (`build_game.py --runtime-lib`; about 3 min on 24 cores).
5. **Prepare** the RomFS patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
them with the prebuilt runtime library for the host's level (`libtotkrt.a` or `libtotkrt-v2.a`) into `totk.exe`
(`build_game.py --runtime-lib`; about 3 min on 24 cores).
6. **Prepare** (only when the runtime, the translator or the game version changed, not for a new lift) the RomFS
patches and the shaders (`tools/make_romfs_patches.py`, `tools/prepare_shaders.py`,
`totk.exe --prepare-shaders`) in the install folder, then fold the shader cache's loose files (four per shader, about
117,000) into `shader-cache/shader-cache.pack` (`tools/pack_shader_cache.py`). totk.exe reads the pack first and
writes the translations it makes in play as loose files; running the tool again with the game closed folds those in.
The patches are written twice: `romfs-patches` and `romfs-patches-xbox` (`--xbox-prompts`, the A/B and X/Y prompts
swapped for `pad_layout = xbox_prompts`).
6. **Clean up** the temp folder (generated C and objects, about 1.3 GB).
The patches are written three times: `romfs-patches`, and `romfs-patches-xbox` and `romfs-patches-xbox-label`
(`--prompts`: Xbox button prompts for `prompts = xbox`, the first for `pad_layout = nintendo`, the second for `xbox`
and `auto`). The two Xbox folders are optional: when one cannot be made, the setup warns and finishes.
When the graphics driver crashes while building the shaders, `prepare_shaders.py` runs that part again without the
variants it was building ([building.md](building.md), step 6), and the setup finishes with a warning that names how
many were left out.
7. **DLSS** (the window's tick box, on by default, or `--dlss`; Windows with an NVIDIA RTX GPU): download
`nvngx_dlss.dll` from NVIDIA's own DLSS SDK repository (github.com/NVIDIA/DLSS, a pinned tag, NVIDIA RTX SDK
license) into the install folder and check its size and SHA-256 (`steps.py`, `Dlss`). The release does not ship
the library; NGX loads it from the folder of totk.exe for `dlaa` and the DLSS modes. A DLSS library already in the
folder (a newer one the player put there) is kept. Without an RTX GPU the step only logs and does nothing; a failed
download warns, the game runs without DLSS, and the next run of the setup tries again.
8. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
`build-cache/rebuild.txt` in the install folder.
A cancelled or failed setup resumes from the last finished step (`totk-install.json`).
## Rebuilding for the mods
The generated C and the objects stay in the temp folder, so the next lift rewrites only the chunks whose C changed and
Ninja recompiles only those: a new or edited code cheat costs the lift (under a minute on 24 cores, a few minutes on 4)
and a few dozen chunks, not a full compile, and the shaders stay prepared.
`build-cache/rebuild.txt` holds the command that opens the setup window in its rebuild mode on the install folder
(`installer/gui.py --rebuild <install folder>`, started through `python -c` so the release folder is on `sys.path`
whatever the working folder), one argument per line. When an enabled mod has code cheats or ExeFS patch files the build
lacks, `totk.exe` runs it at start and exits (`runtime/cheats.c` `rebuild_if_needed`); the
window runs the setup on the folder at once and starts the game again when it succeeds. A failed or cancelled rebuild
offers to start the game with the code it has; `build-cache/rebuild.skip` then keeps the same files from starting it
again, until "Rebuild the game now" on the Cheats or Mods tab. `[mods] mods_rebuild = off` turns it off. The rebuild
needs the release folder the setup ran from: a moved or deleted release folder means running a setup again.
## Command line
`installer/` (stdlib only; `installer/layout.py` maps the release folder and, in a repository, the dev toolchain):
```
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR] [--verify]
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR] [--temp DIR] [--verify]
```
- On a folder with `totk-install.json` only `--dest` is needed; the game files only when the extraction must run again.
- `--firmware DIR` is the folder holding the firmware NCAs: an unpacked firmware, or the emulators' installed
`Contents/registered` (the parent `Contents` folder works too). It is read only with Chinese, Korean or Traditional
Chinese text, and only when `shared-fonts/` is not already in the install folder.
- stdout: one JSON event per line, types `start`, `plan`, `step` (started, done, skipped), `progress` (step, phase,
done/total/unit), `space` (bytes needed and free on the install disk and the temp disk), `warning`, `error`, `end` (`installer/events.py`). Messages are ids with parameters;
`installer/text/en.json` holds their English text. The human-readable log goes to `<temp>/logs/`.
@@ -79,9 +122,13 @@ python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR
the merged RomFS (15.9 GiB) before it deletes them. The check computes the exact figure.
- Window (`python -m installer.gui`, `installer/gui.py` draws, `installer/guilogic.py` decides): form, step list with
progress, space line, collapsible log, Cancel (Ctrl+Break to the CLI's process group, then it waits for the exit),
finish screen with Open folder. English or Russian, chosen from the Windows display language, switchable in the
window; every sentence is an id in `installer/text/<en|ru>.json`. A folder with `totk-install.json` needs only the
folder. `--selftest [--shots DIR]` builds every screen in both languages and closes itself.
finish screen with Open folder. English, Russian, Brazilian Portuguese or Simplified Chinese, chosen from the Windows
display language (zh-CN and zh-SG give Chinese; the Traditional Chinese regions give English), switchable in the
window; every sentence is an id in `installer/text/<en|ru|pt-BR|zh-Hans>.json`. A Chinese window still starts with
English as the game language, because Chinese game text needs the Switch shared fonts, which the setup extracts only
from a firmware folder. A folder with
`totk-install.json` needs only the folder. `--selftest [--shots DIR]` builds every screen in every language and
closes itself.
- `tests/installer_gui_test.py`: event model, form, catalogs, cancel, the self-closing window and one end-to-end run
of the CLI in `--verify` mode (needs `build/install-test` and `build/extracted/v121`; see its docstring).
- `tests/installer_test.py`: bookkeeping with fake steps. `tests/installer_e2e.py`: a dev-mode run with the files of
@@ -93,11 +140,12 @@ The unpacked release; `tools/make_release.py` assembles it and `--zip` packs it:
```
TotK-Setup.exe starts python/pythonw.exe -m installer.gui (installer/launcher/launcher.c, no console)
installer/ the installer: steps, CLI, window, en/ru text catalog
installer/ the installer: steps, CLI, window, en/ru/zh-Hans text catalog
python/ Python 3.14.9 embeddable + numpy, lz4, cryptography, cffi, pycparser (+ tkinter for the window)
recomp/, tools/ the lifter and the build, extraction and shader scripts the steps run (copied from the repo)
llvm/ llvm-mingw, x86_64-only subset (Apache-2.0 with LLVM exception; mingw-w64 parts permissive)
runtime/ libtotkrt.a and the headers the generated code and modules.c/romfs.c/patches.c include
runtime/ libtotkrt.a, libtotkrt-v2.a and the headers the generated code and modules.c/romfs.c/patches.c
include
bin/ totk_xlate.exe, hactool.exe, SDL3.dll, libopus-0.dll, ninja.exe
data/ shader_list.tsv (MacNativeGames, MIT)
THIRD_PARTY_NOTICES.md
@@ -109,11 +157,24 @@ totk.ini.example, VERSION the default ini; the release string (the repository's
- The release contains nothing game-derived; `make_release.py` runs `tools/check_content.py` over the folder before it
finishes.
## Switch system fonts
Simplified Chinese, Traditional Chinese and Korean text needs the Switch system fonts (`0-standard.bfttf` and the other
nn::pl shared fonts). The game does not ship them. Without them it exits before its first frame in those languages
(`rt_fatal` in `runtime/ov_system.c`). Supply them from your own console, in either way:
- Give the setup the firmware folder (`--firmware DIR`, or the window's firmware field); the Fonts step extracts them to
`shared-fonts/` in the install folder.
- Run `python tools/extract_fonts.py --firmware <dir>` with `.venv-py314` and the keys from `config.local.toml`, or
point `TOTK_SHARED_FONTS` at a folder that holds the extracted fonts.
## Linux release
`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. The command-line setup needs the desktop session's display (`DISPLAY` or `WAYLAND_DISPLAY`) because preparing the
shaders opens a window; the check step fails at once without it. 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:
```
@@ -124,8 +185,8 @@ python/ python-build-standalone 3.14 (with tkinter) + the installe
tools/ and recomp/ on sys.path
llvm/ LLVM 23 (clang, lld, llvm-ar) and llvm/sysroot: the files of the Steam Runtime 3 "sniper" SDK the
game's compile and link read (about 130 files)
runtime/ libtotkrt.a, headers, the install-side sources, totk.png; deps/ with the Vulkan headers, VMA and a
static zstd
runtime/ libtotkrt.a, libtotkrt-v2.a, headers, the install-side sources, totk.png; deps/ with the Vulkan
headers, VMA and a static zstd
bin/ totk_xlate, hactool, ninja, libSDL3.so.0
```
@@ -160,8 +221,15 @@ bin/ totk_xlate, hactool, ninja, libSDL3.so.0
the file and asks to copy it again (`extract.DamagedFile`). A failed merge checks the base Program NCA alone to tell
which file is damaged.
- Unsigned exe: SmartScreen warns on first start.
- NSZ and XCZ input (game files compressed by the nsz tool: NCAs stored as NCZ, their sections decrypted and
compressed with zstd): the check step tells them by their content, not their name (an NSP or XCI holding NCZ files),
so the file dialog has one filter for all four. It unpacks each into `<temp>/unpacked/<base|update>.nsp`
(`tools/ncz.py`), an NSP of the NSZ's files or of the XCZ's secure partition, and the extraction reads that.
Unpacking encrypts the sections again, so every NCA comes back byte for byte, and checks each against the hash its
name carries; a mismatch is `check.input_damaged`. It needs the unpacked size free in the temp folder (checked
first), runs at about 500 MB/s, and an interrupted setup reuses a finished NSP of the same file. The extract step
deletes the folder when it is done. An XCZ update is refused like an XCI one.
## Later
- Re-warm the driver cache on start after a move, GPU or driver change.
- NSZ and XCZ input.
+5
View File
@@ -68,6 +68,7 @@ Source shorthands:
| 2.9 | **LOD**: TSC min/max LOD clamp, LOD bias, `TWO_D_NO_MIPMAP` texture type, a min filter without mips stays on the base level; TIC carries its own max anisotropy and aniso quality fields. | `minLod`/`maxLod`, `mipLodBias` limited by `maxSamplerLodBias`. | yuzu: anisotropy heuristics for mods forcing trilinear (YZ-2306, PR 10744). Ours: non-mip filter → `maxLod = minLod + 0.25`, bias clamped (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.10 | **Texture gather rounding**: Maxwell sub-texel precision differs from AMD/Intel; edge texels picked by TLD4 differ by one. | Implementation-defined `subTexelPrecisionBits`. | yuzu adds a sub-pixel offset to gathers on non-NVIDIA hosts: fixed BotW shadow artifacts at object edges ("shader_recompiler: Add subpixel offset for correct rounding at ImageGather", <https://git.openpunk.com/OpenPunk/yuzu/src/commit/780240e6979b198e7bd10feaad5399b8b4b63762>, title only). Ours: the translator biases gather coordinates by the host's `subTexelPrecisionBits` on every non-NVIDIA GPU (`vk_caps.c` → job `gather_bias` → Ryujinx `TexturePass.InsertCoordGatherBias`); TotK on RADV drew dashed dark lines along wall and floor edges in the prologue caves without it. `TOTK_GATHER_BIAS=0` turns it off. | — |
| 2.11 | **Gather offsets**: `TLD4.PTP` takes four per-texel offsets, possibly from registers. | `ConstOffsets` must be constant. | Ours: `QueryHostSupportsNonConstantTextureOffset() => false` makes Ryujinx emulate register offsets (`tools/xlate/Program.cs`). | RYU-SH `Translation/Optimizations` (TexturePass) |
| 2.13 | **TXD gradient register order**: TotK's terrain shaders fill the gradient registers of a `TXD` per coordinate (du/dx, du/dy, dv/dx, dv/dy). | Vulkan's `Grad` takes dPdx (du/dx, dv/dx) and dPdy (du/dy, dv/dy). | Ryujinx's `EmitTxd` takes the first two registers as dPdx and the last two as dPdy, so the mip level came from the wrong derivatives and was too sharp on terrain faces turned across the screen: patches that followed the faces crawled when the camera moved (1.4.3, the Depths; found by the user from their F9 frame, a gradient sample's coordinate and gradients traced back to `DPdx` and `DPdy` of the interpolants). Ours: `gfx_shader.c` `patch_txd_transpose` swaps the two inner components of every two-component gradient pair in the loaded SPIR-V (the shader cache is untouched). `TOTK_TXD_TRANSPOSE=0` turns it off. Only 2D gradients are handled; 3D and cube ones are left as translated. | — |
| 2.12 | **Depth compare** (`TSC` depth compare + `TIC_4_DEPTH_TEXTURE`), min/max reduction filtering. | `compareEnable`; reduction mode needs `samplerFilterMinmax` and is not allowed with compare. | Ours: reduction dropped when compare is on (`vk_texture.c` `make_sampler`). | G80TEX |
| 2.13 | **ASTC / BC**: native ASTC; zero ASTC block = error colour on hosts. | AMD and NVIDIA desktop GPUs lack ASTC: decode to RGBA8. yuzu offered ASTC recompression to BC1/BC3 for low VRAM (YZ-2305, PR 10398). | Ours: native upload where the device samples ASTC, else a compute-shader decode (`vk_astc.c`, `TOTK_ASTC`), CPU decode (`tex_astc.c`) as the fallback. | `runtime/vk_astc.c` |
| 2.14 | **Linear CPU-written textures** (`FLAGS_LINEAR`, small ramps, glyph caches): CPU writes visible to the GPU directly. | Must re-upload or use linear images over host memory. | NXN Mario: sky black without its 64×1 RGBA16F ramps. | NXN |
@@ -85,6 +86,9 @@ Source shorthands:
| 3.7 | **Integer division** does not exist (compiled to `XMAD`/`IMAD` sequences); MUFU.RCP(0) = +Inf, LG2(0) = -Inf. | Same IEEE edge cases through `1/x`, `Log2`. | Low risk. | — |
| 3.8 | **FP64** in some TotK cutscene shaders. | No FP64 on some hosts. | yuzu FP64 → FP32 (YZ-2306, PR 10693); ours: `softfloat64` job flag (`tools/xlate/Program.cs` `QueryHostSupportsShaderFloat64`). | — |
| 3.9 | **Integer vertex inputs read as raw bits** (bone indices `R8UI`). | Float-declared inputs convert integers. | NXN Mario: stretched characters; re-translated with `uint` inputs. Ours: attribute types passed to `QueryGraphicsState` (`Program.cs`). | NXN |
| 3.10 | **BFE / BFI fields past bit 31** (as PTX `bfe` / `bfi`): a field that runs past bit 31 is cut there; one that starts past it extracts 0 (signed: bit 31 repeated) and inserts nothing. TotK 1.4.3's Depths terrain vertex shaders (42 variants) take the position from a register, `((2y + x) * 8) & 0xff` with 5-bit x and y, so up to 248. | `OpBitField*Extract` / `OpBitFieldInsert` undefined when offset or offset + count exceeds 32; AMD's bitfield instructions take offset and width modulo 32. | Ryujinx passes the fields through. UltraTears saw the Depths walls and ground missing on the Steam Deck and a Linux RX 570 (RADV). Ours: clamped where the field is not a constant inside the word (translator patch 0012). RX 9070 on Windows drew the Depths the same before and after. | PTX ISA `bfe`, `bfi` |
| 3.11 | **SHF by 0** gives the unshifted word (C for `SHF.L`, A for `SHF.R`). | Ryujinx's funnel shifts the other word by 32: undefined in SPIR-V; AMD shifts take the low 5 bits and give A \| C. | yuzu fixed Depths terrain gaps in its funnel shift (PR 10459). Ours: a select on a zero amount (translator patch 0013). In 1.4.3 every register SHF pairs its word with 0, so the result was right already. | — |
| 3.12 | **Registers read before any write** on a path (paths the structurizer adds included) have no defined value in the game's code; UltraTears reports 0 from the NVIDIA driver. | An `OpVariable` without an initializer loads an undefined value; Mesa folds undefined values freely. | Ours: every function-local variable starts at 0 (`OpConstantNull`, translator patch 0014). | — |
## 4. Memory model
@@ -142,6 +146,7 @@ Source shorthands:
| TotK | "detective mode" Link on AMD | AMD EDS3 colour-blend driver regression | YZ-2306 PR 10946 |
| BotW | shadow artifacts at object edges (AMD) | gather rounding | yuzu commit 780240e (2.10) |
| TotK | dashed dark lines along wall and floor edges, changing with the camera angle (AMD, RADV) | gather rounding | gather bias (2.10) |
| TotK | blocky foliage shadows that jump instead of sliding | not a GPU difference: the engine sizes its sun shadow map to the area it covers, down to about 400 near the ground, and each change of size moves the shadow texels | fixed size, `shadow_res` (the shadow set of `recomp/patches-v121.txt`; the community mods force 1024 the same way) |
| BotW | black textures on GCN | HTILE (depth compression) | `AMD_DEBUG=nohyperz` wiki (1.9) |
| BotW | 3D scene black | deferred texture handles | NXN |
| BotW | albedo scattered squares | duplicate colour attachments | NXN |
+107
View File
@@ -0,0 +1,107 @@
# Mods
TotK Recomp loads the mods players make for the Switch emulators (Ryujinx, yuzu and its forks): texture and model
packs, gameplay and text changes from GameBanana and similar sites, TKMM's merged output, code patches (`.ips`,
`.pchtxt`) and Atmosphère cheat files. No mod or cheat file ships with the port.
## Install a mod
1. Open the settings menu (F1, Esc, or Minus and Plus together on a controller), go to the Mods tab and choose
**Open mods folder**. That is the `mods` folder next to `totk.exe`; the menu creates it when it is missing.
2. Make one folder per mod in it and unpack the mod's archive into that folder. The mod's own folders (`romfs`,
`exefs`, `cheats`) must sit directly inside the mod's folder:
```
mods\
Better Grass\
romfs\
...
Fast Travel Anywhere\
exefs\
patch.pchtxt
My cheats\
cheats\
277178B7DBA1B6D4.txt
```
An archive laid out for a Switch SD card or an emulator works as it is. Inside the mod's folder the port also finds
`atmosphere\contents\0100F2C0115B6000\romfs`, `contents\0100F2C0115B6000\romfs` and `0100F2C0115B6000\romfs`, the
same places for `exefs` and `cheats`, and `atmosphere\exefs_patches\<name>\` for code patches.
3. Start the game. A mod that patches the game's code makes the game rebuild itself first (see Code patches below).
4. Open the Mods tab to check the mod. Each row says how many game files the mod replaces and adds, how many cheats it
brings, and why a part of it is not used.
## Order and switches
The Mods tab lists every folder in `mods`. Select a mod and press A or Enter to switch it on or off. The top face button
or Ctrl+Up moves it up, the left face button or Ctrl+Down moves it down. When two mods change the same file, the
higher one wins; the lower mod's row says how many of its files the higher one covers. Changes apply after a restart.
The menu saves the order and the switches in `totk.ini`, under `[mods]`, by folder name: `mods_order` and
`mods_disabled`. A folder the list does not name yet, such as a new or renamed one, goes below the named ones.
### Mods that change the same files
Two mods that edit the same game file do not combine: the higher one wins the whole file. Merge such mods with
[TKMM](https://github.com/TKMM-Team/Tkmm) (TotK Mod Manager) and set its export location to an empty folder inside
`mods`, for example `mods\TKMM`. The exported folder then counts as one mod here. Mods packaged for TKMM only
(`.tkcl` files) go the same way.
The port merges the resource size table on its own: at every start with mods on, it combines the tables the mods ship
and raises the entries for files a mod made larger or added. A separate RESTBL mod is not needed.
## Code patches (ExeFS)
A mod with `.ips` or `.pchtxt` files patches the game's code. A patch is for one game update: an `.ips` file is named
after the build id of the update's code, a `.pchtxt` file names it on its `@nsobid` line. The game's code is compiled
ahead of time, so these patches are built into `totk.exe`:
1. The first start after such a mod is added closes the game and opens the setup's rebuild window. The rebuild takes
a few minutes (the shaders stay prepared), then the game starts again. Saves and settings are kept.
2. Until the rebuild succeeds, the whole mod stays off, its other files included.
3. After the rebuild the mod switches on and off from the Mods tab like any other, without another rebuild.
The rebuild needs the setup folder the game was installed from. When that folder was moved or deleted, run
`TotK-Setup.exe` again on the same install folder: it does the same job. `mods_rebuild = off` under `[mods]` in
`totk.ini` turns the automatic rebuild off.
A code patch over one of the port's own patches (the 60 FPS patch, the resolution patch and the other entries under
**Built into the port**) turns that built-in patch off while the mod is on. The mod's row names it.
## Cheats
A cheat file is part of a mod: `mods\<mod>\cheats\<build id>.txt`, switched cheat by cheat on the Cheats tab. Cheats
that change the game's code need the same rebuild as code patches. [cheats.md](cheats.md) explains the file names for
each game update, the rebuild and which cheats cannot run.
## Renaming a mod folder
The port keys a mod by its folder name. A renamed folder counts as a new mod:
- it moves below the mods the list names, and comes back switched on;
- its cheats come back switched off;
- its code patches and code cheats are built again, so the next start runs the rebuild.
Rename mod folders before you set them up in the menu, not after.
## What does not work
| On the Mods tab | Why | What to do |
|---|---|---|
| This folder has no romfs, exefs or cheats files. | The archive was unpacked one level too deep (`mods\Mod\Mod\romfs`), or it holds no game files. | Move the folders up so `romfs` sits directly in the mod's folder. |
| ... files are made for another game version and are left out. | The mod's files carry another update's version in their names (`.Product.<number>.`). | Download the mod's release for your game update. |
| Its code patches (exefs) are made for another game version. | The patch files name another update's build id. | Same: get the release for your update. |
| This mod brings its own game code (a subsdk plugin or a replaced main). | Mods such as UltraCam ship compiled Switch code. The port cannot load it. | No workaround; the whole mod stays off. |
| This mod patches the game code (exefs) in a way the port cannot apply. | The patch writes the game's data, not its code, or changes code in a form the port cannot build. | No workaround; the reason follows on the row. |
| The folder name contains \|. | The mod list uses `\|` between names. | Rename the folder. |
A mod never stops the game from starting. Whatever the port cannot use is left out and shown on the Mods tab.
## When something looks wrong
1. Switch the newest mod off on the Mods tab and restart. If the problem goes away, check that the mod is made for your
game update and does not fight another mod over the same files.
2. `logs\totk.log` next to `totk.exe` names every mod at the start (lines starting with `mods:`), the cheats
(`cheats:`) and the resource size table (`RomFS: resource size table:`). Open log folder on the Advanced tab of
the menu shows the logs.
3. When reporting a problem, attach `totk.log` and list your mods with the links you downloaded them from.
+14 -2
View File
@@ -6,9 +6,10 @@ from it may be shared.
## What you need
- Windows 10 or later, an x86-64 CPU with AVX2, about 60 GB free.
- Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA recommended: they build the default, faster version), about
60 GB free.
- A GPU with Vulkan 1.3 and `VK_EXT_external_memory_host`, with a current driver.
- *Tears of the Kingdom* base game (1.0.0) and the 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
- *Tears of the Kingdom* base game (1.0.0) and the 1.1.0, 1.2.1 or 1.4.3 update, dumped from your own Switch as NSP or XCI.
- `prod.keys` dumped from your own Switch. Never send it to anyone, and never paste it into a log or an issue.
## Build
@@ -38,6 +39,10 @@ build\<ver>\totk.exe
`TOTK_VK_VALIDATION=1` turns on the Vulkan validation layer (it comes with the Vulkan SDK). It makes the game slower;
do one run with it and one without (`set TOTK_VK_VALIDATION=`).
If the game stops with a lost device (the log says `device lost` or `VK_ERROR_DEVICE_LOST`), the lines starting with
`vk: pass marks` name the render pass or dispatch the GPU was running. AMD GPUs write them by default. On an NVIDIA GPU,
`set TOTK_PASS_CRUMBS=1` (or GPU pass marks: On in the Advanced tab) before the run that reproduces the crash.
The game keeps running behind other windows (`assume_focus = 0` in `totk.ini` pauses it instead). Shaders the preparation did not cover compile on first use, so
the first minutes of play can stutter.
Play into the open field (load a save, walk around for a minute, fight something if you can), then close the window.
@@ -51,6 +56,13 @@ A laptop with an iGPU and a discrete GPU, or two cards, lists every GPU at the t
The line `vk: using gpu N: <name>` says which one ran.
## Mods
Mods and cheats made for the emulators go into `mods/` next to `totk.exe`, one folder per mod. [mods.md](mods.md) covers
installing them, their order, code patches, what does not work and what to check when something looks wrong;
[cheats.md](cheats.md) covers cheat files. The log names every mod at the start (`mods: <name>: ...`); when you
report a problem with mods, list them with their download links.
## What to send back
1. `logs\totk.log` from each run. It contains no keys and no game files. The lines that matter most start with
+3 -2
View File
@@ -1,9 +1,10 @@
"""The TotK installer: builds a playable folder from the player's own game files (docs/installer.md).
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR] [--temp DIR]
python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR] [--root DIR]
[--temp DIR]
Modules: layout (where the bundled tools are, release or repository), events (JSON progress on stdout and the log
file), state (totk-install.json), runner (which steps run: resume and update), steps (the six steps), ini (totk.ini),
file), state (totk-install.json), runner (which steps run: resume and update), steps (the eight steps), ini (totk.ini),
cli (the command line). text/en.json holds the English text of every message id the events carry.
Standard library only, apart from what the extraction tools import (cryptography). Runs on Python 3.14.
+19 -9
View File
@@ -1,10 +1,15 @@
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--root DIR]
[--temp DIR] [--verify]
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
[--root DIR] [--temp DIR] [--verify] [--dlss]
A first install needs every option. On a folder with totk-install.json (resume after a cancel or failure, or update
with a newer release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed
only when the extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed,
2 bad command line, 3 cancelled (Ctrl+C or Ctrl+Break).
A first install needs every option. --firmware names the Switch firmware folder (an unpacked firmware or an emulator's
Contents/registered); the setup extracts the system shared fonts from it when the language is Chinese, Korean or
Traditional Chinese. On a folder with totk-install.json (resume after a cancel or failure, or update with a newer
release) only --dest is needed: --lang defaults to the recorded language, and the game files are needed only when the
extraction has to run again. Prints JSON events on stdout (events.py). Exit code: 0 done, 1 failed, 2 bad command
line, 3 cancelled (Ctrl+C or Ctrl+Break).
--dlss downloads NVIDIA's DLSS library (nvngx_dlss.dll) from NVIDIA's DLSS SDK repository into the install folder on
Windows with an NVIDIA RTX GPU (steps.py, Dlss).
"""
import argparse
import hashlib
@@ -17,7 +22,7 @@ from . import layout as layouts
from . import state as statefile
from .events import Reporter
from .runner import Context, run_steps
from .steps import all_steps
from .steps import all_steps, migrate_state
EXIT = {'ok': 0, 'failed': 1, 'cancelled': 3}
@@ -45,15 +50,19 @@ 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('--base', type=Path, help='base game, XCI or NSP (or NSZ, XCZ: unpacked first)')
ap.add_argument('--update', type=Path, help='the update NSP or NSZ: 1.1.0, 1.2.1 or 1.4.3')
ap.add_argument('--keys', type=Path, help='prod.keys')
ap.add_argument('--firmware', type=Path, help='Switch firmware folder (Contents/registered) for the shared fonts '
'of Chinese, Korean and Traditional Chinese text')
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')
ap.add_argument('--root', type=Path, help='release folder or repository (default: the folder above installer/)')
ap.add_argument('--temp', type=Path, help='temp folder (default: temp/ of the release on Windows, .setup-temp in '
'the install folder on Linux, build/install-temp in a repository)')
ap.add_argument('--verify', action='store_true', help='hash every kept file, also those this release made')
ap.add_argument('--dlss', action='store_true', help="download NVIDIA's nvngx_dlss.dll (NVIDIA RTX SDK license) next "
'to the game, for dlaa and the DLSS modes')
args = ap.parse_args(argv)
# The window cancels with Ctrl+Break to the installer's process group (CREATE_NEW_PROCESS_GROUP), which also reaches
@@ -75,6 +84,7 @@ def main(argv=None):
rep.emit('end', result='failed', seconds=0)
return 1
ctx = Context(layout, dest, temp, rep, args, state=state, verify=args.verify)
migrate_state(ctx)
steps = all_steps()
rep.emit('start', release=layout.release, mode=ctx.mode, dev=layout.dev, dest=str(dest), log=str(log),
steps=[s.id for s in steps])
+83 -29
View File
@@ -1,11 +1,14 @@
"""The installer window: python -m installer.gui
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the game language and the
install folder, runs `python -m installer ...` as a subprocess and shows its JSON events. Decisions live in guilogic.py;
this file only draws. The window text is in installer/text/<en|ru>.json under "ui.*" and "launcher.*" ids.
A front end over the CLI (docs/installer.md): it collects the game files, the keys file path, the optional firmware
folder, the game language and the install folder, runs `python -m installer ...` as a subprocess and shows its JSON
events. Decisions live in guilogic.py; this file only draws. The window text is in
installer/text/<en|ru|pt-BR|zh-Hans>.json under "ui.*" and "launcher.*" ids.
--selftest [--shots DIR] build every screen in both languages and close by itself (PNG per screen with --shots)
--lang en|ru window language (default: the Windows display language)
--selftest [--shots DIR] build every screen in every language and close by itself (PNG per screen with --shots)
--lang en|ru|pt-BR|zh-Hans window language (default: the Windows display language)
--rebuild DIR rebuild the install folder DIR for its mods (totk.exe starts this, build-cache/rebuild.txt):
the run starts at once with that folder, and the game starts again when it succeeds
"""
import argparse
import json
@@ -23,8 +26,8 @@ except ImportError:
from . import ini
from . import layout as layouts
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGES, Catalog, Form, Model, Process,
amount, default_game_language, describe_event, parse_line, windows_ui_language)
from .guilogic import (DONE, FAILED, PENDING, RUNNING, SKIPPED, UI_LANGUAGE_LABELS, UI_LANGUAGES, Catalog, Form, Model,
Process, amount, default_game_language, describe_event, parse_line, windows_ui_language)
POLL_MS = 100
GLYPH = {PENDING: '\u25cb', RUNNING: '\u25b6', DONE: '\u2714', SKIPPED: '\u2013', FAILED: '\u2716'}
@@ -42,8 +45,9 @@ def fix_tcl_paths():
class App:
def __init__(self, root, ui_lang, layout, run_args=None):
def __init__(self, root, ui_lang, layout, run_args=None, rebuild=None):
self.root, self.layout = root, layout
self.rebuild = rebuild # the install folder of --rebuild, or None
self.cat = Catalog(ui_lang)
self.model = Model()
self.process = None
@@ -51,7 +55,7 @@ class App:
self.run_args = run_args or {} # extra CLI options for tests: temp, verify
self.log_entries = [] # events and raw lines of the run, rendered by render_log
self.bound = [] # (widget, option, id, params) retranslated on a language switch
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'dest')}
self.vars = {n: tk.StringVar() for n in ('base', 'update', 'keys', 'firmware', 'dest')}
self.vars['lang'] = tk.StringVar()
self.lang_code = default_game_language(ui_lang)
self.screen = None
@@ -66,8 +70,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')
@@ -81,16 +87,22 @@ class App:
version.pack(side='left', padx=(10, 0), pady=(6, 0))
sw = ttk.Frame(top)
sw.pack(side='right')
self.ui_var = tk.StringVar(value=self.cat.lang)
for code in UI_LANGUAGES:
ttk.Radiobutton(sw, text=code.upper(), value=code, variable=self.ui_var, style='Toolbutton',
command=self.switch_language).pack(side='left', padx=1)
self.ui_var = tk.StringVar(value=self.cat.lang) # the window language code; the box shows its name
labels = [UI_LANGUAGE_LABELS[c] for c in UI_LANGUAGES]
self.ui_box = ttk.Combobox(sw, values=labels, state='readonly', width=max(map(len, labels)) + 1)
self.ui_box.set(UI_LANGUAGE_LABELS[self.cat.lang])
self.ui_box.bind('<<ComboboxSelected>>', self.pick_language)
self.ui_box.pack(side='left')
self.body = ttk.Frame(root, padding=PAD)
self.body.pack(fill='both', expand=True)
self.frames = {'form': self.build_form(), 'run': self.build_run(), 'finish': self.build_finish()}
self.vars['lang'].set(self.lang_label(self.lang_code))
self.vars['dest'].trace_add('write', lambda *_: self.refresh_form())
self.show('form')
if rebuild:
self.bind_text(self.title, 'text', 'ui.rebuild_title')
self.vars['dest'].set(str(rebuild))
root.after(POLL_MS, self.start)
# ---- text -----------------------------------------------------------------------------------------------------
@@ -118,8 +130,13 @@ class App:
self.refresh_finish()
self.render_log()
def pick_language(self, _event=None):
self.ui_var.set(UI_LANGUAGES[self.ui_box.current()])
self.switch_language()
def switch_language(self):
self.cat.set_language(self.ui_var.get())
self.ui_box.set(UI_LANGUAGE_LABELS[self.ui_var.get()])
code = self.selected_lang()
self.retext()
self.vars['lang'].set(self.lang_label(code))
@@ -195,6 +212,10 @@ class App:
b.grid(row=i, column=2, padx=(8, 0))
self.form_rows[name] = lab
i = len(rows)
self.label(grid, 'ui.firmware').grid(row=i, column=0, sticky='w', pady=4)
ttk.Entry(grid, textvariable=self.vars['firmware']).grid(row=i, column=1, sticky='ew', pady=4)
self.button(grid, 'ui.browse', self.pick_firmware).grid(row=i, column=2, padx=(8, 0))
i += 1
self.label(grid, 'ui.game_language').grid(row=i, column=0, sticky='w', pady=4)
self.lang_box = ttk.Combobox(grid, textvariable=self.vars['lang'], state='readonly', width=30)
self.lang_box.grid(row=i, column=1, sticky='w', pady=4)
@@ -204,6 +225,14 @@ class App:
self.label(grid, 'ui.dest').grid(row=i + 1, column=0, sticky='w', pady=4)
ttk.Entry(grid, textvariable=self.vars['dest']).grid(row=i + 1, column=1, sticky='ew', pady=4)
self.button(grid, 'ui.browse', self.pick_dest).grid(row=i + 1, column=2, padx=(8, 0))
self.label(f, 'ui.compressed_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
self.label(f, 'ui.firmware_help', wraplength=620, justify='left').pack(anchor='w', pady=(8, 0))
# On by default and named with its source and licence: the player sees what is downloaded and can untick it
self.dlss_var = tk.BooleanVar(value=True)
dlss = ttk.Checkbutton(f, variable=self.dlss_var)
self.bind_text(dlss, 'text', 'ui.dlss')
dlss.pack(anchor='w', pady=(8, 0))
self.label(f, 'ui.dlss_help', wraplength=620, justify='left').pack(anchor='w', padx=(22, 0))
self.update_hint = ttk.Label(f, wraplength=620, justify='left')
self.update_hint.pack(anchor='w', pady=(8, 0))
self.form_error = ttk.Label(f, style='Err.TLabel', wraplength=620, justify='left')
@@ -219,7 +248,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 +262,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')
@@ -250,6 +279,8 @@ class App:
self.open_button.pack(side='right', padx=(0, 8))
self.back_button = self.button(bar, 'ui.back', lambda: self.show('form'))
self.back_button.pack(side='left')
if self.rebuild: # a failed or cancelled rebuild: the game starts with the code it has
self.button(bar, 'ui.start_game', self.start_game).pack(side='right', padx=(0, 8))
self.finish_title = ttk.Label(f, style='Title.TLabel', wraplength=620, justify='left')
self.finish_title.pack(anchor='w')
self.finish_text = ttk.Label(f, wraplength=620, justify='left')
@@ -264,7 +295,8 @@ class App:
def form(self):
return Form(base=self.vars['base'].get(), update=self.vars['update'].get(), keys=self.vars['keys'].get(),
lang=self.selected_lang(), dest=self.vars['dest'].get())
firmware=self.vars['firmware'].get(), lang=self.selected_lang(), dest=self.vars['dest'].get(),
dlss=self.dlss_var.get())
def refresh_form(self):
form = self.form()
@@ -279,15 +311,19 @@ class App:
self.fit()
def pick_file(self, name, pick_id):
types = [(self.cat('ui.type_game'), '*.nsp *.xci'), (self.cat('ui.type_all'), '*.*')]
if name == 'update':
types = [(self.cat('ui.type_nsp'), '*.nsp'), (self.cat('ui.type_all'), '*.*')]
# One filter for every game file: the setup tells an NSZ or XCZ from a plain file by its content (Check.unpack)
types = [(self.cat('ui.type_game'), '*.nsp *.xci *.nsz *.xcz'), (self.cat('ui.type_all'), '*.*')]
if name == 'keys':
types = [(self.cat('ui.type_keys'), '*.keys'), (self.cat('ui.type_all'), '*.*')]
path = filedialog.askopenfilename(title=self.cat(pick_id), filetypes=types)
if path:
self.vars[name].set(os.path.normpath(path))
def pick_firmware(self):
path = filedialog.askdirectory(title=self.cat('ui.pick_firmware'), mustexist=True)
if path:
self.vars['firmware'].set(os.path.normpath(path))
def pick_dest(self):
path = filedialog.askdirectory(title=self.cat('ui.pick_dest'), mustexist=False)
if path:
@@ -303,8 +339,11 @@ class App:
self.fit()
return
self.form_error.configure(text='')
command = form.command(self.layout.python, root=self.layout.root if self.layout.dev else None,
temp=self.run_args.get('temp'), verify=self.run_args.get('verify', False))
# in a repository the Python running this window: the tools' embeddable Python there has no ._pth entry for
# the repository, so `-m installer` would not find the package
python = Path(sys.executable) if self.layout.dev else self.layout.python
command = form.command(python, root=self.layout.root if self.layout.dev else None,
temp=self.run_args.get('temp'), verify_hashes=self.run_args.get('verify', False))
self.begin(command, self.layout.root)
def begin(self, command, cwd):
@@ -350,6 +389,9 @@ class App:
return
self.model.finish(code)
self.process = None
if self.rebuild and self.model.result == 'ok' and not self.closing:
self.start_game()
return
self.refresh_run()
self.refresh_finish()
self.show('finish')
@@ -378,7 +420,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'))
@@ -480,6 +522,15 @@ class App:
else:
self.open_button.state(['disabled' if not Path(m.dest or '.').is_dir() else '!disabled'])
def start_game(self):
"""Starts the game of the install folder and closes the window."""
dest = Path(self.vars['dest'].get().strip())
exe = dest / ('totk.exe' if os.name == 'nt' else 'totk')
flags = subprocess.DETACHED_PROCESS if os.name == 'nt' else 0
if exe.is_file():
subprocess.Popen([str(exe)], cwd=str(dest), creationflags=flags, close_fds=True)
self.root.destroy()
def open_folder(self):
dest = self.model.dest
if dest and Path(dest).is_dir():
@@ -493,10 +544,10 @@ class App:
# ---- self test ---------------------------------------------------------------------------------------------------
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/TotK', 'log': 'D:\\log.txt',
'steps': ['check', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup']}
SAMPLE_RUN = [
SAMPLE_START,
{'type': 'plan', 'run': ['check', 'extract'], 'skip': {}},
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
{'type': 'step', 'step': 'check', 'state': 'started'},
{'type': 'progress', 'step': 'check', 'phase': 'inputs'},
{'type': 'space', 'dest': 'Z:/selftest/TotK', 'need': 36_400_000_000, 'free': 210_000_000_000, 'temp': 'C:\\t',
@@ -509,12 +560,14 @@ SAMPLE_RUN = [
]
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'lift': 'current', 'compile': 'current',
'prepare': 'current', 'cleanup': 'not_needed'}},
SAMPLE_OK = [SAMPLE_START, {'type': 'plan', 'run': ['check', 'extract'], 'skip': {'fonts': 'current', 'lift': 'current',
'compile': 'current', 'prepare': 'current',
'cleanup': 'not_needed'}},
{'type': 'step', 'step': 'check', 'state': 'started'},
{'type': 'step', 'step': 'check', 'state': 'done', 'seconds': 1.0},
{'type': 'step', 'step': 'extract', 'state': 'started'},
{'type': 'step', 'step': 'extract', 'state': 'done', 'seconds': 1.0},
{'type': 'step', 'step': 'fonts', 'state': 'skipped', 'reason': 'current'},
{'type': 'step', 'step': 'lift', 'state': 'skipped', 'reason': 'current'},
{'type': 'step', 'step': 'compile', 'state': 'skipped', 'reason': 'current'},
{'type': 'step', 'step': 'prepare', 'state': 'skipped', 'reason': 'current'},
@@ -532,7 +585,7 @@ def grab(root, path):
def selftest(layout, shots=None):
"""Every screen in both languages; the window closes itself. Returns 0 when nothing raised."""
"""Every screen in every language; the window closes itself. Returns 0 when nothing raised."""
fix_tcl_paths()
root = tk.Tk()
root.geometry('+60+40')
@@ -610,13 +663,14 @@ def main(argv=None):
ap.add_argument('--shots', type=Path)
ap.add_argument('--lang', choices=UI_LANGUAGES)
ap.add_argument('--root', type=Path)
ap.add_argument('--rebuild', type=Path)
args = ap.parse_args(argv)
layout = layouts.find(args.root)
if args.selftest:
return selftest(layout, args.shots)
fix_tcl_paths()
root = tk.Tk()
App(root, args.lang or windows_ui_language(), layout)
App(root, args.lang or windows_ui_language(), layout, rebuild=args.rebuild)
root.mainloop()
return 0
+38 -9
View File
@@ -24,11 +24,17 @@ from . import state as statefile
from .events import render
TEXT = Path(__file__).resolve().parent / 'text'
UI_LANGUAGES = ('en', 'ru')
STEP_ORDER = ('check', 'extract', 'lift', 'compile', 'prepare', 'cleanup')
UI_LANGUAGES = ('en', 'ru', 'pt-BR', 'zh-Hans')
# what the language switch in the window's corner shows for each
UI_LANGUAGE_LABELS = {'en': 'English', 'ru': 'Русский', 'pt-BR': 'Português (Brasil)', 'zh-Hans': '简体中文'}
STEP_ORDER = ('check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup')
# message parameter -> catalog id prefix of the names that replace its raw values
PARAM_NAMES = {'lang': 'lang.', 'known': 'lang.', 'step': 'step.', 'input': 'param.input.'}
LANG_RU = 0x19 # primary language id of Russian in a Windows LANGID
LANG_PT_BR = 0x0416 # Portuguese (Brazil), the whole LANGID: Portugal's Portuguese stays English
# Windows LANGIDs that write Simplified Chinese: zh-CN, zh-SG. zh-TW, zh-HK and zh-MO write Traditional Chinese, which
# the window does not have.
LANGIDS_ZH_HANS = (0x0804, 0x1004)
def load_texts(lang):
@@ -76,18 +82,35 @@ def flat(value):
def windows_ui_language():
"""'ru' when the display language (Windows; elsewhere LC_ALL, LC_MESSAGES or LANG) is Russian, else 'en'."""
"""The window language for the display language: the Windows one (ui_language_for_langid); elsewhere LC_ALL,
LC_MESSAGES or LANG ('ru', 'pt_BR', 'zh_CN' or 'zh_SG', else English)."""
if os.name != 'nt':
loc = os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or ''
return 'ru' if loc.lower().startswith('ru') else 'en'
loc = (os.environ.get('LC_ALL') or os.environ.get('LC_MESSAGES') or os.environ.get('LANG') or '').lower()
return ('ru' if loc.startswith('ru') else 'pt-BR' if loc.startswith(('pt_br', 'pt-br'))
else 'zh-Hans' if loc.startswith(('zh_cn', 'zh-cn', 'zh_sg', 'zh-sg', 'zh_hans', 'zh-hans')) else 'en')
try:
langid = ctypes.windll.kernel32.GetUserDefaultUILanguage()
except (AttributeError, OSError):
return 'en'
return 'ru' if langid & 0x3ff == LANG_RU else 'en'
return ui_language_for_langid(langid)
def ui_language_for_langid(langid):
"""'ru' for a Russian Windows LANGID, 'pt-BR' for Brazilian Portuguese, 'zh-Hans' for a Simplified Chinese one,
else 'en'."""
if langid & 0x3ff == LANG_RU:
return 'ru'
if langid == LANG_PT_BR:
return 'pt-BR'
if langid in LANGIDS_ZH_HANS:
return 'zh-Hans'
return 'en'
def default_game_language(ui):
"""The game language the form starts with. A Chinese window still starts with English: Chinese game text needs
the Switch shared fonts (ini.NEED_SHARED_FONTS), which the setup extracts only from a firmware folder, and the game
stops when they are missing; the player can still pick Chinese."""
return 'ru' if ui == 'ru' else 'en-US'
@@ -111,8 +134,10 @@ class Form:
base: str = ''
update: str = ''
keys: str = ''
firmware: str = ''
lang: str = ''
dest: str = ''
dlss: bool = False
def installed_state(self):
"""The recorded state of the install folder (totk-install.json), or None. Never raises."""
@@ -147,20 +172,24 @@ class Form:
value = getattr(self, name).strip()
if value and not Path(value).is_file():
found.append(f'ui.missing_{name}')
if self.firmware.strip() and not Path(self.firmware.strip()).is_dir():
found.append('ui.missing_firmware')
return found
def command(self, python, root=None, temp=None, verify=False):
def command(self, python, root=None, temp=None, verify_hashes=False):
cmd = [str(python), '-m', 'installer']
for flag, value in (('--base', self.base), ('--update', self.update), ('--keys', self.keys),
('--lang', self.lang), ('--dest', self.dest)):
('--firmware', self.firmware), ('--lang', self.lang), ('--dest', self.dest)):
if value.strip():
cmd += [flag, value.strip()]
if root:
cmd += ['--root', str(root)]
if temp:
cmd += ['--temp', str(temp)]
if verify:
if verify_hashes:
cmd.append('--verify')
if self.dlss:
cmd.append('--dlss')
return cmd
+17 -2
View File
@@ -1,5 +1,5 @@
// Host check of the installer's first step, compiled with the bundled clang at install time: does the CPU run
// -march=x86-64-v3 code, and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
// -march=x86-64-v3 code (or only x86-64-v2), and does a Vulkan 1.3 device exist. Prints one JSON object on stdout.
// The Vulkan loader is opened at run time, so a machine without one gets "loader": false instead of a failed start.
#include <cpuid.h>
#include <stdio.h>
@@ -25,10 +25,24 @@ static void json_string(const char *s) {
putchar('"');
}
// The x86-64-v2 level from CPUID: SSE3, SSSE3, SSE4.1, SSE4.2, POPCNT, CMPXCHG16B and LAHF/SAHF. Every CPU with
// SSE4.2 has the others, but the level is what the v2 build is compiled for, so check them all.
static int x86_64_v2(void) {
unsigned a, b, c, d;
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
const unsigned sse3 = 1u << 0, ssse3 = 1u << 9, cx16 = 1u << 13, sse41 = 1u << 19, sse42 = 1u << 20,
popcnt = 1u << 23;
if ((c & (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt)) != (sse3 | ssse3 | cx16 | sse41 | sse42 | popcnt))
return 0;
if (!__get_cpuid(0x80000001, &a, &b, &c, &d)) return 0;
return (c & (1u << 0)) != 0; // LAHF/SAHF
}
// The x86-64-v3 level from CPUID: AVX, AVX2, BMI1, BMI2, F16C, FMA, LZCNT and MOVBE, with the OS saving the YMM state.
// Not __builtin_cpu_supports("x86-64-v3"): clang reads that from the C runtime's CPU model, and libgcc before GCC 12
// (the Linux release's sysroot has GCC 10) has no level bits, so it answers false on every CPU.
static int x86_64_v3(void) {
if (!x86_64_v2()) return 0;
unsigned a, b, c, d;
if (!__get_cpuid(1, &a, &b, &c, &d)) return 0;
const unsigned fma = 1u << 12, movbe = 1u << 22, osxsave = 1u << 27, avx = 1u << 28, f16c = 1u << 29;
@@ -44,7 +58,8 @@ static int x86_64_v3(void) {
}
int main(void) {
printf("{\"x86_64_v3\": %s, ", x86_64_v3() ? "true" : "false");
printf("{\"x86_64_v2\": %s, \"x86_64_v3\": %s, ", x86_64_v2() ? "true" : "false",
x86_64_v3() ? "true" : "false");
void *lib = open_lib();
PFN_vkGetInstanceProcAddr gipa = lib ? (PFN_vkGetInstanceProcAddr)lib_sym(lib, "vkGetInstanceProcAddr") : NULL;
if (!gipa) {
+2 -1
View File
@@ -14,7 +14,8 @@ LANGUAGES = ('ja', 'en-US', 'en-GB', 'fr', 'fr-CA', 'de', 'it', 'es', 'es-419',
'zh-Hans', 'zh-Hant')
# Older installer builds wrote these two codes; they are read as the canonical ones and rewritten on the next install.
ALIASES = {'zh-CN': 'zh-Hans', 'zh-TW': 'zh-Hant'}
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the installer does not extract.
# Their text falls back to the Switch shared fonts (runtime/ov_system.c), which the fonts step extracts from the
# firmware folder when the player gives one.
NEED_SHARED_FONTS = ('ko', 'zh-Hans', 'zh-Hant')
ACTIVE = re.compile(r'^(\s*)(?:totk_)?lang(\s*=\s*)(.*?)(\s*)$', re.IGNORECASE)
+16 -4
View File
@@ -9,14 +9,26 @@
#include "launcher_text.h"
#define LANGID_RUSSIAN 0x19
#define LANGID_RUSSIAN 0x19 /* primary language id */
#define LANGID_PT_BR 0x0416 /* the whole LANGID: Portugal's Portuguese stays English */
#define PATH_CHARS (MAX_PATH * 2)
/* The language of the window (installer/guilogic.py ui_language_for_langid): Russian, Simplified Chinese (zh-CN and
zh-SG; the Traditional Chinese regions get English, as the window has no Traditional Chinese), else English. */
static void fail(void)
{
int russian = (GetUserDefaultUILanguage() & 0x3ff) == LANGID_RUSSIAN;
MessageBoxW(NULL, russian ? LAUNCHER_PYTHON_MISSING_RU : LAUNCHER_PYTHON_MISSING_EN,
russian ? LAUNCHER_TITLE_RU : LAUNCHER_TITLE_EN, MB_OK | MB_ICONERROR);
LANGID ui = GetUserDefaultUILanguage();
int russian = (ui & 0x3ff) == LANGID_RUSSIAN, brazilian = ui == LANGID_PT_BR;
int chinese = ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED) ||
ui == MAKELANGID(LANG_CHINESE, SUBLANG_CHINESE_SINGAPORE);
MessageBoxW(NULL,
russian ? LAUNCHER_PYTHON_MISSING_RU
: brazilian ? LAUNCHER_PYTHON_MISSING_PT_BR
: chinese ? LAUNCHER_PYTHON_MISSING_ZH_HANS
: LAUNCHER_PYTHON_MISSING_EN,
russian ? LAUNCHER_TITLE_RU : brazilian ? LAUNCHER_TITLE_PT_BR : chinese ? LAUNCHER_TITLE_ZH_HANS
: LAUNCHER_TITLE_EN,
MB_OK | MB_ICONERROR);
}
int WINAPI WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command_line, int show)
+26 -8
View File
@@ -7,7 +7,8 @@ Release (docs/installer.md "Release layout"), root = the unpacked folder:
recomp/, tools/ the lifter and the scripts; recomp/patches-<v>.txt, tools/data/expected-v<version>.json for
each supported update (SUPPORTED)
llvm/ llvm-mingw (bin/clang.exe)
runtime/ libtotkrt.a, the headers, install-side sources (romfs.c, patches.c, ov_zstd.c) and totk.ico; runtime/deps/ holds
runtime/ libtotkrt.a and libtotkrt-v2.a (x86-64-v3 / v2), the headers, install-side sources (romfs.c,
patches.c, ov_zstd.c) and totk.ico; runtime/deps/ holds
include/ + lib/ of Vulkan, SDL3, Opus, libvpx and VMA (build_game.py --deps)
bin/ totk_xlate.exe (+ totk_xlate.id), hactool.exe, ninja.exe, SDL3.dll, libopus-0.dll
data/shader_list.tsv the MacNativeGames shader list
@@ -21,8 +22,8 @@ against), runtime/deps/ with include/ and the static Opus, libvpx and zstd, bin/
Dev mode, root = a repository checkout (no python/python.exe): build/py314-embed/py/python.exe (the embeddable
Python the release ships, with the same packages), else this Python; toolchain/llvm-mingw-*/ and toolchain/deps-mingw;
runtime/ as the sources with build/install-rtlib/libtotkrt.a (tools/build_game.py --make-runtime-lib --out
build/install-rtlib); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
runtime/ as the sources with build/install-rtlib/libtotkrt.a and libtotkrt-v2.a (tools/build_game.py --make-runtime-lib
--out build/install-rtlib, once per --cpu); MSYS2 UCRT64 ninja.exe; toolchain/hactool; build/xlate/totk_xlate.exe; the shader list from
refs/MacNativeGames; build/install-temp. The version is VERSION with -dev and the commit: 0.1.0-dev+973c626. On Linux,
dev mode takes clang, lld and ninja from PATH and the system's libraries (no deps folder, no SDL3 copy).
"""
@@ -36,7 +37,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.0') # updates the installer builds; the check identifies the player's by hash
def version_tag(version):
@@ -64,11 +65,20 @@ class Layout:
ini_example: Path | None = None
temp: Path | None = None
sysroot: Path | None = None # Linux release: what the game links against, instead of the host's
runtime_lib_v2: Path | None = None # libtotkrt-v2.a: the runtime for CPUs without AVX2 (build_game.py --cpu v2)
@property
def clang(self):
return self.llvm / 'bin' / ('clang' + EXE)
def runtime_lib_for(self, cpu):
"""The prebuilt runtime library for cpu (v2 or v3). None in dev mode when it was not built: the runtime is
then compiled from the sources in runtime/ instead."""
lib = self.runtime_lib_v2 if cpu == 'v2' else self.runtime_lib
if lib and lib.is_file():
return lib
return None if self.dev else lib
def patches(self, version):
"""The code patch table for recomp/gen.py."""
return self.recomp / f'patches-{version_tag(version)}.txt'
@@ -77,13 +87,18 @@ class Layout:
"""The expected hashes of the update."""
return self.tools / 'data' / f'expected-v{version}.json'
def missing(self):
"""Paths the steps need that do not exist."""
need = [self.python, self.tools / 'extract.py', self.tools / 'build_game.py', self.tools / 'prepare_shaders.py',
def missing(self, cpu='v3'):
"""Paths the steps need that do not exist. cpu picks the runtime library the host will use."""
lib = self.runtime_lib_for(cpu)
need = [self.python, self.tools / 'extract.py', self.tools / 'ncz.py', self.tools / 'build_game.py',
self.tools / 'prepare_shaders.py',
self.tools / 'pack_shader_cache.py', self.tools / 'make_romfs_patches.py', self.tools / 'ultrawide_layout.py',
self.tools / 'button_icons.py',
self.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py',
self.recomp / 'gen.py',
self.clang,
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, self.runtime_lib, self.ninja, self.hactool,
*((self.deps / 'include', self.deps / 'lib') if self.deps else ()), self.runtime, *([lib] if lib else ()),
self.ninja, self.hactool,
self.xlate, *self.dlls, *(f(v) for v in SUPPORTED for f in (self.patches, self.expected))]
if self.shader_list:
need.append(self.shader_list)
@@ -109,6 +124,7 @@ def release_layout(root):
tools=root / 'tools', recomp=root / 'recomp',
llvm=root / 'llvm', deps=root / 'runtime' / 'deps', runtime=root / 'runtime',
runtime_lib=root / 'runtime' / 'libtotkrt.a',
runtime_lib_v2=root / 'runtime' / 'libtotkrt-v2.a',
ninja=root / 'bin' / ('ninja' + EXE), hactool=root / 'bin' / ('hactool' + EXE),
xlate=root / 'bin' / ('totk_xlate' + EXE),
dlls=(root / 'bin' / 'SDL3.dll', root / 'bin' / 'libopus-0.dll') if WIN else (root / 'bin' / 'libSDL3.so.0',),
@@ -134,6 +150,7 @@ def dev_layout(root):
tools=root / 'tools', recomp=root / 'recomp',
llvm=trees[-1] if trees else root / 'toolchain' / 'llvm-mingw-missing', deps=deps,
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
ninja=UCRT / ('ninja' + EXE), hactool=root / 'toolchain' / 'hactool' / ('hactool' + EXE),
xlate=root / 'build' / 'xlate' / ('totk_xlate' + EXE),
dlls=tuple(sorted((deps / 'bin').glob('*.dll'))),
@@ -153,6 +170,7 @@ def dev_layout_posix(root):
python=Path(sys.executable), tools=root / 'tools', recomp=root / 'recomp',
llvm=Path(clang).parent.parent if clang else root / 'toolchain' / 'clang-missing', deps=None,
runtime=root / 'runtime', runtime_lib=root / 'build' / 'install-rtlib' / 'libtotkrt.a',
runtime_lib_v2=root / 'build' / 'install-rtlib' / 'libtotkrt-v2.a',
ninja=Path(ninja) if ninja else root / 'toolchain' / 'ninja-missing',
hactool=root / 'toolchain' / 'hactool' / 'hactool', xlate=root / 'build' / 'xlate' / 'totk_xlate',
shader_list=None, ini_example=root / 'totk.ini.example', temp=root / 'build' / 'install-temp')
+561 -60
View File
@@ -1,17 +1,26 @@
"""The six setup steps of docs/installer.md: check, extract, lift, compile, prepare, cleanup.
"""The eight setup steps of docs/installer.md: check, fonts, extract, lift, compile, prepare, dlss, cleanup.
Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); lift,
compile and prepare run the bundled Python on recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and
tools/prepare_shaders.py, whose output goes to <temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call
here: the host check is a small C program built with the bundled clang (hostcheck.c).
Extraction runs in this process through tools/extract.py (the package's sibling tools/, which ships with it); fonts
runs the vendored extract_shared_fonts.py on the firmware folder; lift, compile and prepare run the bundled Python on
recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepare_shaders.py, whose output goes to
<temp>/logs/<step>-<tool>.log and is read for progress. No Win32 call here: the host check is a small C program built
with the bundled clang (hostcheck.c).
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, totk.exe and its DLLs, romfs-index.bin,
romfs-patches/, romfs-patches-xbox/, totk_xlate.exe, the shader state of totk.exe --prepare-shaders, totk.ini,
totk-install.json.
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), gen/ (C), build/ (objects),
logs/ (kept by the cleanup).
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
and its DLLs, nvngx_dlss.dll (with --dlss), romfs-index.bin, romfs-patches/, romfs-patches-xbox/, romfs-patches-xbox-label/, totk_xlate.exe, the
shader state of totk.exe --prepare-shaders, totk.ini, totk-install.json.
Temp folder (<temp>/<dest name>-<hash>): check/ (ExeFS of both inputs, the host check), unpacked/ (the NSP of an NSZ
or XCZ input until the extraction is done), gen/ (C), build/ (objects), logs/. The cleanup keeps gen/, build/ and
logs/: the next run lifts into the same gen/, where recomp/gen.py rewrites only the chunks whose C changed, and Ninja
in build/ recompiles only those. A change of the mods' code cheats then costs the lift (about a minute) and the chunks
it touches, not a full compile.
Install folder build-cache/: cheats.status (tools/cheats.py), exefs.status (tools/exefs_patches.py) and rebuild.txt, the command that starts this setup in
its rebuild mode on the install folder (installer/gui.py --rebuild), one argument per line; totk.exe runs it at start
when an enabled mod has code cheats the build lacks (runtime/cheats.c).
"""
import hashlib
import http.client
import json
import os
import re
@@ -21,6 +30,7 @@ import subprocess
import sys
import threading
import time
import urllib.request
from pathlib import Path
from . import ini
@@ -29,6 +39,7 @@ from .runner import InstallError, Step
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / 'tools'))
import extract # noqa: E402
import ncz # noqa: E402
import shader_list # noqa: E402
from extract_exefs import (common_tickets, container_entries, load_keys, nca_ctr_key, nca_inventory, # noqa: E402
program_nca)
@@ -167,16 +178,41 @@ def raise_if_damaged(error, name, path):
raise InstallError('check.input_damaged', input=name, path=str(path), detail=str(error)) from None
def game_file(ctx, name):
"""The file the extraction reads for the base or the update: the NSP that Check.unpack made from an NSZ or XCZ,
else the chosen file itself."""
return getattr(ctx, 'unpacked', {}).get(name) or Path(getattr(ctx.options, name))
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):
return extract.load_expected()[game_version(ctx)]
# Releases before 0.3.1 recorded the 0x20000 update as 1.1.1 (pull request #220); it is 1.1.0 (#371), with the same
# files, hashes and patch sites under new names.
RENAMED_VERSIONS = {'1.1.1': '1.1.0'}
def migrate_state(ctx):
"""Read a recorded update by its current name. The extraction it recorded stays: its record gets the key of the
renamed expected file, and Extract.current and the full check of output another release kept
(runner.Context.verify_kept) still test the files against the expected hashes."""
old = ctx.state.get('version')
if old not in RENAMED_VERSIONS:
return
ctx.state['version'] = RENAMED_VERSIONS[old]
record = ctx.state['steps'].get('extract')
if record:
record['key'] = Extract().key(ctx)
ctx.reporter.log(f'state: the update recorded as {old} is {ctx.state["version"]}')
def shader_list_text(layout):
if layout.shader_list:
return layout.shader_list.read_text(encoding='utf-8')
@@ -217,15 +253,100 @@ def size_of(paths):
return sum(p.stat().st_size for p in paths if p.is_file())
# The Switch system shared fonts (runtime/ov_system.c): TotK falls back to them for Chinese, Korean and Traditional
# Chinese text. tools/vendor/macnativegames/extract_shared_fonts.py writes these names.
SHARED_FONTS = 'shared-fonts'
SHARED_FONT_FILES = ('0-standard.bfttf', '1-chinese-simplified.bfttf', '2-chinese-simplified-extension.bfttf',
'3-chinese-traditional.bfttf', '4-korean.bfttf', '5-nintendo-extension.bfttf')
FONTS_NEED = 32 << 20 # the six BFTTF files and their manifest
def requested_lang(ctx):
"""The language this run uses: --lang, else the one recorded for the install (Check canonicalizes it)."""
return ini.canonical_language(getattr(ctx.options, 'lang', None) or ctx.state.get('lang'))
def firmware_dir(value):
"""The firmware NCA folder behind a --firmware path: the folder itself, or its registered/ child (the emulators'
layout, Contents/registered)."""
if not value:
return None
path = Path(value)
return path / 'registered' if (path / 'registered').is_dir() else path
def firmware_has_ncas(folder):
"""Whether folder holds firmware NCAs: .nca files, or the hashed directories with a 00 file (registered/)."""
try:
entries = list(Path(folder).iterdir())
except OSError:
return False
return any((e.is_file() and e.suffix.lower() == '.nca') or (e.is_dir() and (e / '00').is_file()) for e in entries)
def firmware_stamp(folder):
"""A cheap identity of the firmware NCAs: names, sizes and mtimes, not their contents (the folder is ~326 MB)."""
try:
entries = sorted(Path(folder).iterdir())
except OSError:
return 'missing'
rows = []
for entry in entries:
if entry.is_dir():
file = entry / '00'
if not file.is_file():
continue
elif not entry.is_file() or entry.suffix.lower() != '.nca':
continue
else:
file = entry
st = file.stat()
rows.append((entry.name, st.st_size, st.st_mtime_ns))
return rows
def shared_fonts_ready(dest):
"""Whether dest/shared-fonts holds a complete set that the game can load."""
folder = Path(dest) / SHARED_FONTS
return (folder / 'manifest.json').is_file() and all((folder / name).is_file() for name in SHARED_FONT_FILES)
# ---- 1. check
UPDATE_TITLE_ID = '0100F2C0115B6800'
def base_misuse(records):
"""Name why a file chosen as the base game holds an update or a combined package, or return None.
The Meta NCA (content type 1) carries the package's own title id: the update's id (...800) for an update NSP, and
several Meta NCAs mean base, update and DLC repacked into one file.
"""
metadata = [r['title_id'] for r, _ in records if r['content_type'] == 1]
if len(metadata) > 1:
return 'check.base_combined'
if UPDATE_TITLE_ID in metadata:
return 'check.base_is_update'
return None
class Check(Step):
id = 'check'
always = True
def key(self, ctx):
# The compile step fingerprints the CPU level, and every key is computed before a step runs: probe the host
# here, once, so both steps see the same answer (the check step itself is always run). A failed probe is
# reported by run(), where the error names this step.
try:
self.host(ctx)
except InstallError:
pass
return ''
def run(self, ctx):
layout, rep, dest = ctx.layout, ctx.reporter, ctx.dest
missing = layout.missing()
missing = layout.missing(getattr(ctx, 'cpu', 'v3'))
if missing:
raise InstallError('check.release_incomplete', paths=missing[:5])
if dest.exists() and not dest.is_dir():
@@ -233,13 +354,35 @@ class Check(Step):
own = (ctx.temp, *ctx.temp.parents) # a temp folder inside the install folder (installer/cli.py) is ours
if ctx.mode == 'install' and dest.is_dir() and any(p not in own for p in dest.iterdir()):
raise InstallError('check.dest_not_empty', path=str(dest))
lang = ini.canonical_language(ctx.options.lang or ctx.state.get('lang'))
lang = requested_lang(ctx)
if not lang:
raise InstallError('check.lang_missing')
if lang not in ini.LANGUAGES:
raise InstallError('check.lang_unknown', lang=lang, known=', '.join(ini.LANGUAGES))
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
if firmware is not None:
if not firmware.is_dir():
raise InstallError('check.firmware_missing', path=str(firmware))
if not firmware_has_ncas(firmware):
raise InstallError('check.firmware_empty', path=str(firmware))
if lang in ini.NEED_SHARED_FONTS:
rep.warning('warn.lang_needs_fonts', lang=lang)
if firmware is None:
rep.warning('warn.lang_needs_fonts', lang=lang)
elif not shared_fonts_ready(dest):
keys = getattr(ctx.options, 'keys', None)
if not keys or not Path(keys).is_file():
raise InstallError('check.fonts_keys_needed')
try:
key_values = load_keys(Path(keys))
except (ValueError, OSError, UnicodeDecodeError):
raise InstallError('check.keys_unreadable') from None
if 'header_key' not in key_values:
raise InstallError('check.keys_no_header_key')
if (sys.platform == 'linux' and ctx.will_run('prepare')
and not (os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'))):
# totk --prepare-shaders opens a window and needs a present-capable queue family; without a display it
# fails after lift and compile (~15 min), so stop here
raise InstallError('check.no_display')
ctx.lang = lang
ctx.temp.mkdir(parents=True, exist_ok=True)
@@ -258,6 +401,8 @@ class Check(Step):
return {}
def host(self, ctx):
if getattr(ctx, 'host', None) is not None:
return
layout, work = ctx.layout, ctx.temp / 'check'
glibc = linux_glibc()
if glibc and glibc < GLIBC_NEEDED and not layout.dev:
@@ -282,8 +427,10 @@ class Check(Step):
except ValueError:
raise InstallError('check.host_probe_failed', code=r.returncode) from None
ctx.reporter.log(f'host: {r.stdout.strip()}')
if not host['x86_64_v3']:
raise InstallError('check.cpu_no_avx2')
if not host['x86_64_v2']:
raise InstallError('check.cpu_too_old')
# v3 is the faster build; v2 runs on CPUs without AVX2/FMA (runtime/fma.c supplies the fused multiply-add).
ctx.cpu = 'v3' if host['x86_64_v3'] else 'v2'
if not host['loader']:
raise InstallError('check.vulkan_missing')
devices = host['devices']
@@ -314,14 +461,73 @@ class Check(Step):
raise InstallError('check.keys_unreadable') from None
if 'header_key' not in keys:
raise InstallError('check.keys_no_header_key')
self.unpack(ctx)
total = 0
for name, is_update in (('base', False), ('update', True)):
path = Path(getattr(o, name))
path = game_file(ctx, name)
with path.open('rb') as f:
total += self.open_input(f, path, keys, name, is_update)
del keys
return total
def unpack(self, ctx):
"""Unpack an NSZ or XCZ (a game file compressed by the nsz tool) into <temp>/unpacked/<base|update>.nsp with
tools/ncz.py, which checks every NCA against its hash; the extraction then reads that NSP. An earlier run's NSP
of the same file is reused. The extract step removes the folder once it is done."""
o, rep, folder = ctx.options, ctx.reporter, ctx.temp / 'unpacked'
ctx.unpacked = {}
jobs = []
for name in ('base', 'update'):
path = Path(getattr(o, name))
try:
found = ncz.compressed(path)
if found is None:
continue
if name == 'update' and found[0] != 'nsz':
raise InstallError('check.update_not_nsp')
size = ncz.unpacked_size(path)
except ValueError as e:
raise_if_damaged(e, name, path)
raise InstallError('check.not_game_file', input=name) from None
st = path.stat()
jobs.append((name, path, size, f'{path.resolve()}\n{st.st_size}\n{st.st_mtime_ns}\n{size}\n'))
if not jobs:
remove(folder)
return
done = []
for name, path, size, tag in jobs:
target, stamp = folder / f'{name}.nsp', folder / f'{name}.source'
try:
if target.stat().st_size == size and stamp.read_text(encoding='utf-8') == tag:
done.append(name)
ctx.unpacked[name] = target
except OSError:
pass
need = sum(size for name, _, size, _ in jobs if name not in done)
free, _ = free_bytes(folder)
rep.log(f'unpack: {", ".join(name for name, *_ in jobs)}: {need / GiB:.1f} GiB to write, '
f'{free / GiB:.1f} free')
if need > free:
raise InstallError('check.disk_space', path=str(folder), need_gb=round(need / 1e9, 1),
free_gb=round(free / 1e9, 1))
for name, path, size, tag in jobs:
if name in done:
continue
target, stamp = folder / f'{name}.nsp', folder / f'{name}.source'
remove(stamp)
try:
folder.mkdir(parents=True, exist_ok=True)
ncz.unpack(path, target, lambda d, t, n=name: rep.progress('check', 'unpack_' + n, d, t, 'bytes'))
stamp.write_text(tag, encoding='utf-8')
except ncz.DamagedFile as e:
raise InstallError('check.input_damaged', input=name, path=str(e.path), detail=e.detail) from None
except ValueError as e:
raise_if_damaged(e, name, path)
raise InstallError('check.unpack_failed', input=name, detail=str(e)) from None
except OSError as e:
raise InstallError('check.unpack_failed', input=name, detail=str(e)) from None
ctx.unpacked[name] = target
@staticmethod
def open_input(f, path, keys, name, is_update):
try:
@@ -350,6 +556,10 @@ class Check(Step):
except ValueError as e:
if is_update and 'BKTR' in str(e):
raise InstallError('check.update_is_base') from None
if not is_update:
reason = base_misuse(records)
if reason:
raise InstallError(reason) from None
raise InstallError('check.update_not_totk' if is_update else 'check.base_not_totk', detail=str(e)) from None
try:
nca_ctr_key(header, keys, tickets)
@@ -368,6 +578,8 @@ class Check(Step):
romfs = ctx.dest / 'romfs' / 'romfs.bin'
if romfs.is_file(): # a resumed extraction replaces it
dest_need -= romfs.stat().st_size
if ctx.will_run('fonts'):
dest_need += FONTS_NEED
if ctx.will_run('prepare') or ctx.will_run('extract'):
dest_need += DEST_EXTRA
if ctx.will_run('lift') or ctx.will_run('compile'):
@@ -396,9 +608,10 @@ class Check(Step):
out = work / name
remove(out)
try:
extract.extract_exefs(Path(getattr(o, name)), Path(o.keys), out, logs, update=is_update)
extract.extract_exefs(game_file(ctx, name), Path(o.keys), out, logs, update=is_update)
except extract.DamagedFile as e:
raise InstallError('check.input_damaged', input=name, path=str(e.path), detail=e.detail) from None
raise InstallError('check.input_damaged', input=name, path=str(getattr(o, name)),
detail=e.detail) from None
except ValueError:
raise InstallError('check.exefs_failed', input=name, log=str(logs / f'extract_exefs-{name}.log')) \
from None
@@ -419,7 +632,72 @@ class Check(Step):
ctx.state['version'] = found
# ---- 2. extract
# ---- 2. fonts
class Fonts(Step):
"""The Switch system fonts for Chinese, Korean and Traditional Chinese text (tools/extract_fonts.py, which runs the
vendored extract_shared_fonts.py). The firmware folder is optional: without it the setup warns and the game runs
with those glyphs missing. The extraction verifies the firmware NCAs and writes the six BFTTF files the console
puts in shared memory."""
id = 'fonts'
def key(self, ctx):
lang = requested_lang(ctx)
if lang not in ini.NEED_SHARED_FONTS:
return fingerprint('fonts-1', 'not-needed')
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
if firmware is None:
return fingerprint('fonts-1', 'no-firmware')
return fingerprint('fonts-1', lang, str(firmware), firmware_stamp(firmware))
def current(self, ctx, record):
if requested_lang(ctx) not in ini.NEED_SHARED_FONTS or not getattr(ctx.options, 'firmware', None):
return True
return shared_fonts_ready(ctx.dest)
def verify(self, ctx, record):
# An update run may omit --firmware; the installed set still has to be there for these languages.
if requested_lang(ctx) in ini.NEED_SHARED_FONTS:
return shared_fonts_ready(ctx.dest)
return True
def run(self, ctx):
lang = getattr(ctx, 'lang', None) or requested_lang(ctx)
if lang not in ini.NEED_SHARED_FONTS:
return {}
firmware = firmware_dir(getattr(ctx.options, 'firmware', None))
if firmware is None:
return {} # Check warned: the game runs, some text may not show
keys = getattr(ctx.options, 'keys', None)
if not keys:
raise InstallError('check.fonts_keys_needed')
out = ctx.temp / 'shared-fonts'
remove(out)
ctx.reporter.progress('fonts', 'extract', force=True)
tool = ctx.layout.tools / 'vendor' / 'macnativegames' / 'extract_shared_fonts.py'
code, log = run_tool(ctx, 'fonts', tool, ['--keys', keys, '--firmware', firmware, '--output', out])
if code:
detail = ''
try:
detail = log.read_text(encoding='utf-8', errors='replace').strip().splitlines()[-1][:300]
except (OSError, IndexError):
pass
raise InstallError('fonts.failed', code=code, detail=detail, log=str(log))
missing = [name for name in SHARED_FONT_FILES if not (out / name).is_file()]
if missing or not (out / 'manifest.json').is_file():
raise InstallError('fonts.incomplete', files=', '.join(missing) or 'manifest.json')
ctx.reporter.progress('fonts', 'copy', force=True)
try:
ctx.dest.mkdir(parents=True, exist_ok=True)
remove(ctx.dest / SHARED_FONTS)
shutil.move(str(out), str(ctx.dest / SHARED_FONTS))
except OSError as e:
raise InstallError('fonts.copy_failed', path=str(ctx.dest / SHARED_FONTS), detail=str(e)) from None
return {'files': len(SHARED_FONT_FILES)}
# ---- 3. extract
class Extract(Step):
id = 'extract'
@@ -452,9 +730,9 @@ class Extract(Step):
def run(self, ctx):
o, dest, rep = ctx.options, ctx.dest, ctx.reporter
expected = expected_update(ctx)
base, update, keys = Path(o.base), Path(o.update), Path(o.keys)
keys = Path(o.keys)
inputs = {}
for name, path in (('base', base), ('update', update)):
for name, path in (('base', Path(o.base)), ('update', Path(o.update))):
known = ctx.state['inputs'].get(name, {})
st = path.stat()
if known.get('size') == st.st_size and known.get('mtime_ns') == st.st_mtime_ns and known.get('sha256'):
@@ -464,6 +742,7 @@ class Extract(Step):
inputs[name] = {'name': path.name, 'size': st.st_size, 'mtime_ns': st.st_mtime_ns, 'sha256': digest}
ctx.state['inputs'] = inputs
ctx.save()
base, update = game_file(ctx, 'base'), game_file(ctx, 'update')
dest.mkdir(parents=True, exist_ok=True)
for stale in dest.glob('update-merge-*'): # a killed merge leaves its NCA copies (and key copies) behind
@@ -475,7 +754,7 @@ class Extract(Step):
try:
extract.extract_exefs(update, keys, ctx.temp / 'check' / 'update', ctx.temp / 'logs', update=True)
except extract.DamagedFile as e:
raise InstallError('check.input_damaged', input='update', path=str(e.path), detail=e.detail) from None
raise InstallError('check.input_damaged', input='update', path=str(o.update), detail=e.detail) from None
except ValueError:
raise InstallError('check.exefs_failed', input='update',
log=str(ctx.temp / 'logs' / 'extract_exefs-update.log')) from None
@@ -497,6 +776,7 @@ class Extract(Step):
raise InstallError('extract.romfs_mismatch', version=game_version(ctx), size=report['romfs_size'],
sha256=report['romfs_sha256'][:16])
(dest / 'profile').mkdir(exist_ok=True)
remove(ctx.temp / 'unpacked') # the NSPs unpacked from an NSZ or XCZ (Check.unpack)
st = (romfs / 'romfs.bin').stat()
return {'romfs_size': st.st_size, 'romfs_mtime_ns': st.st_mtime_ns}
@@ -518,7 +798,8 @@ class Extract(Step):
try:
extract.merge_update_romfs(base, update, keys, ctx.layout.hactool, romfs, logs, phase=phase)
except extract.DamagedFile as e:
raise InstallError('check.input_damaged', input='base' if e.path == base else 'update', path=str(e.path),
name = 'base' if e.path == base else 'update'
raise InstallError('check.input_damaged', input=name, path=str(getattr(ctx.options, name)),
detail=e.detail) from None
except ValueError as e:
raise InstallError('extract.romfs_merge_failed', detail=str(e), log=str(logs / 'merge_romfs-'
@@ -527,18 +808,69 @@ class Extract(Step):
watch.stop()
# ---- 3. lift
# ---- 4. lift
EMITTED = re.compile(r'^emitted (\d+)/(\d+) chunks')
DISCOVERED = re.compile(r'^(\w+): \d+ functions \(.*discovery')
CHEAT_PLACES = ('cheats', 'atmosphere/contents/0100F2C0115B6000/cheats', 'contents/0100F2C0115B6000/cheats',
'0100F2C0115B6000/cheats')
def cheat_files(ctx):
"""The Atmosphere cheat files of the mods folder (<dest>/mods/<mod>/cheats/*.txt and the Atmosphere places, as
tools/cheats.py finds them), for the lift key. All of them: the ones the build reads go by the build id of the
installed main, and the keys are computed before Extract runs (runner plan), when a fresh install has no main and an
update may still have another version's."""
mods = ctx.dest / 'mods'
if not mods.is_dir():
return []
return sorted(f for mod in mods.iterdir() if mod.is_dir() for where in CHEAT_PLACES
for f in ((mod / where).glob('*.txt') if (mod / where).is_dir() else ()))
EXEFS_PLACES = ('exefs', 'atmosphere/contents/0100F2C0115B6000/exefs', 'contents/0100F2C0115B6000/exefs',
'0100F2C0115B6000/exefs')
def exefs_files(ctx):
"""The ExeFS patch files of the mods folder (.ips and .pchtxt in each mod's exefs places and its
atmosphere/exefs_patches/<name>/, as tools/exefs_patches.py finds them), for the lift key."""
mods = ctx.dest / 'mods'
if not mods.is_dir():
return []
out = []
for mod in (m for m in mods.iterdir() if m.is_dir()):
folders = [mod / w for w in EXEFS_PLACES if (mod / w).is_dir()]
patches = mod / 'atmosphere' / 'exefs_patches'
folders += [d for d in patches.iterdir() if d.is_dir()] if patches.is_dir() else []
out += [f for d in folders for f in d.iterdir() if f.is_file() and f.suffix.lower() in ('.ips', '.pchtxt')]
return sorted(out)
def exefs_status(ctx):
"""The build's ExeFS patch status file (tools/exefs_patches.py --status), which runtime/mods.c reads."""
return ctx.dest / 'build-cache' / 'exefs.status'
def cheats_status(ctx):
"""The build's cheat status file (tools/cheats.py --status), which runtime/cheats.c reads: in build-cache, not in a
mod's own cheats folder, which the player (or TKMM's merge) may empty."""
return ctx.dest / 'build-cache' / 'cheats.status'
class Lift(Step):
id = 'lift'
transient = True
def key(self, ctx):
return fingerprint('lift-1', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)))
# fingerprint takes a file's name and content only; the paths under mods/ go in too, because tools/cheats.py
# and tools/exefs_patches.py name each patch set after its mod folder, so a renamed folder needs a lift
files = cheat_files(ctx) + exefs_files(ctx)
return fingerprint('lift-4', ctx.keys['extract'], ctx.layout.recomp, ctx.layout.patches(game_version(ctx)),
ctx.layout.tools / 'cheats.py', ctx.layout.tools / 'exefs_patches.py', *files,
*(f.relative_to(ctx.dest / 'mods').as_posix() for f in files))
def current(self, ctx, record):
gen = ctx.temp / 'gen'
@@ -546,8 +878,10 @@ class Lift(Step):
return (gen / 'layout.json').is_file() and stamp.is_file() and stamp.read_text().strip() == ctx.keys['lift']
def run(self, ctx):
# gen/ of the last run stays: gen.py rewrites only the chunks whose C changed (and removes the ones a smaller
# partition no longer has), so Ninja recompiles only those
layout, rep, gen = ctx.layout, ctx.reporter, ctx.temp / 'gen'
remove(gen)
(gen / STAMP).unlink(missing_ok=True)
rep.progress('lift', 'discover', force=True)
def parse(line):
@@ -555,28 +889,62 @@ class Lift(Step):
if m:
rep.progress('lift', 'emit', int(m.group(1)), int(m.group(2)), 'chunks')
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py',
['--exefs', ctx.dest / 'exefs', '--out', gen, '--patches', layout.patches(game_version(ctx))], parse)
# The code cheats of the mods' cheat files and the mods' ExeFS patch files become patch sets (tools/cheats.py,
# tools/exefs_patches.py); their status files tell the runtime what the build has. Both are the player's
# downloads, so files the tools refuse cost those cheats or patches, not the install: the game is built
# without them and the setup warns.
plain = layout.patches(game_version(ctx))
stages = [('cheats.py', 'patches-cheats.txt', cheats_status(ctx), cheat_files(ctx), 'warn.cheats_skipped'),
('exefs_patches.py', 'patches-exefs.txt', exefs_status(ctx), exefs_files(ctx), 'warn.exefs_skipped')]
patches, skipped = plain, {}
for tool, out, status, inputs, warn in stages:
status.unlink(missing_ok=True) # no status: the runtime treats every such file as not built
if not inputs:
continue
code, log = run_tool(ctx, 'lift', layout.tools / tool,
['--exefs', ctx.dest / 'exefs', '--mods', ctx.dest / 'mods', '--patches', patches,
'--out', ctx.temp / out, '--status', status])
if code:
status.unlink(missing_ok=True)
skipped[warn] = log
else:
patches = ctx.temp / out
args = ['--exefs', ctx.dest / 'exefs', '--out', gen]
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', patches], parse)
if code and patches != plain:
for tool, out, status, inputs, warn in stages:
if inputs and warn not in skipped:
status.unlink(missing_ok=True)
skipped[warn] = log
code, log = run_tool(ctx, 'lift', layout.recomp / 'gen.py', args + ['--patches', plain], parse)
if code:
raise InstallError('lift.failed', code=code, log=str(log))
for warn, log in skipped.items():
rep.warning(warn, log=str(log))
(gen / STAMP).write_text(ctx.keys['lift'] + '\n')
return {}
# ---- 4. compile
# ---- 5. compile
NINJA_LINE = re.compile(r'^\[(\d+)/(\d+)\] ')
def runtime_key(ctx):
"""Fingerprint of what totk.exe is built from besides the lifted code: the runtime, the toolchain and the build
tools. Shader preparation depends on these, not on the code cheats a lift adds."""
layout, cpu = ctx.layout, getattr(ctx, 'cpu', 'v3')
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py', 'check_isa.py')]
return fingerprint('runtime-1', cpu, layout.runtime_lib_for(cpu), layout.runtime, layout.deps or 'system',
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
class Compile(Step):
id = 'compile'
def key(self, ctx):
layout = ctx.layout
clang = subprocess.run([str(layout.clang), '--version'], capture_output=True, text=True).stdout
tools = [layout.tools / n for n in ('build_game.py', 'romfs_index.py', 'check_content.py')]
return fingerprint('compile-1', ctx.keys['lift'], layout.runtime, layout.runtime_lib, layout.deps or 'system',
layout.sysroot or 'host', *tools, clang, *[d.name for d in layout.dlls])
return fingerprint('compile-3', ctx.keys['lift'], runtime_key(ctx))
def current(self, ctx, record):
exe = ctx.dest / ('totk' + EXE)
@@ -600,10 +968,12 @@ class Compile(Step):
rep.progress('compile', 'link', force=True)
rep.progress('compile', 'compile', force=True)
cpu = getattr(ctx, 'cpu', 'v3')
lib = layout.runtime_lib_for(cpu)
code, log = run_tool(ctx, 'compile', layout.tools / 'build_game.py', [
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm,
'--gen', ctx.temp / 'gen', '--out', build, '--toolchain', layout.llvm, '--cpu', cpu,
*(['--deps', layout.deps] if layout.deps else []), *(['--sysroot', layout.sysroot] if layout.sysroot else []),
'--runtime', layout.runtime, '--runtime-lib', layout.runtime_lib, '--ninja', layout.ninja,
'--runtime', layout.runtime, *(['--runtime-lib', lib] if lib else ()), '--ninja', layout.ninja,
'--version', version_tag(game_version(ctx)), '--expected', layout.expected(game_version(ctx)),
'--exefs', dest / 'exefs', '--romfs', dest / 'romfs',
'--profile', dest / 'profile'], parse)
@@ -617,18 +987,23 @@ class Compile(Step):
replace_file(build / 'romfs-index.bin', dest / 'romfs-index.bin')
for dll in layout.dlls:
replace_file(dll, dest / dll.name)
return {'exe_sha256': file_sha256(dest / exe.name)}
return {'exe_sha256': file_sha256(dest / exe.name), 'cpu': cpu}
# ---- 5. prepare
# ---- 6. prepare
TRANSLATED = re.compile(r'gfx_shader: prepare: (\d+) of (\d+) pairs')
# runtime/vk_pipeline.c lb_warm_prepared: variants the driver crashed on in an earlier run (tools/prepare_shaders.py
# runs the driver step again after such a crash)
SKIPPED = re.compile(r'prepare-shaders: (\d+) variants skipped: the graphics driver crashed')
# The game resolutions of another shape than 16:9 that runtime/ui/menu.cpp row_output_res lists: 21:9 and 32:9 monitors,
# and 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops)
# 16:10 screens (the Steam Deck's 1280x800, handhelds and laptops) and 3:2 screens (Surface and Framework laptops and
# tablets)
ASPECT_SIZES = ('2560x1080', '3440x1440', '3840x1600', '5120x2160', '3840x1080', '5120x1440',
'1280x800', '1680x1050', '1920x1200', '2560x1600')
'1280x800', '1680x1050', '1920x1200', '2560x1600',
'1920x1280', '2160x1440', '2256x1504', '2880x1920', '3000x2000')
class Prepare(Step):
@@ -638,8 +1013,10 @@ class Prepare(Step):
layout = ctx.layout
xlate_id = layout.xlate.with_suffix('.id')
tools = [layout.tools / n for n in ('prepare_shaders.py', 'shader_census.py', 'make_romfs_patches.py',
'pack_shader_cache.py', 'ultrawide_layout.py')]
return fingerprint('prepare-1', ctx.keys['compile'], layout.xlate, xlate_id, shader_list_text(layout), *tools)
'pack_shader_cache.py', 'ultrawide_layout.py', 'button_icons.py')]
# not the compile key: a rebuild for the mods' code cheats keeps the prepared shaders
return fingerprint('prepare-2', ctx.keys['extract'], runtime_key(ctx), layout.xlate, xlate_id,
shader_list_text(layout), *tools)
def current(self, ctx, record):
dest = ctx.dest
@@ -654,10 +1031,18 @@ class Prepare(Step):
# Another translator: its cache keys differ, so the old translations would only take space.
for name in ('shader-cache', 'shader-prepared.bin'):
remove(dest / name)
replace_file(layout.xlate, xlate)
# totk.exe starts totk_xlate.exe as a child while it prepares shaders; a preparation that was stopped from
# outside leaves that child holding this file, and Windows then refuses to replace it (issue #174).
try:
replace_file(layout.xlate, xlate)
except PermissionError:
raise InstallError('prepare.xlate_in_use', path=str(xlate)) from None
xlate_id = layout.xlate.with_suffix('.id')
if xlate_id.is_file():
replace_file(xlate_id, xlate.with_suffix('.id'))
try:
replace_file(xlate_id, xlate.with_suffix('.id'))
except PermissionError:
raise InstallError('prepare.xlate_in_use', path=str(xlate.with_suffix('.id'))) from None
else:
remove(xlate.with_suffix('.id'))
@@ -678,26 +1063,37 @@ class Prepare(Step):
dest / f'romfs-patches-{size}', '--aspect', size])
if code:
raise InstallError('prepare.romfs_patches_failed', code=code, log=str(log))
# romfs-patches-xbox: the same with the A/B and X/Y prompts swapped, for pad_layout = xbox_prompts (written on
# every install, so the menu choice works without another run of the setup). Optional: when it fails, the game
# keeps the Switch prompts and the menu says the files are missing, so the setup warns and goes on.
remove(dest / 'romfs-patches-xbox')
code, log = run_tool(ctx, 'prepare-xbox', layout.tools / 'make_romfs_patches.py',
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out',
dest / 'romfs-patches-xbox', '--xbox-prompts'])
if code:
remove(dest / 'romfs-patches-xbox')
rep.warning('warn.xbox_prompts_failed', code=code, log=str(log))
# The same with Xbox button icons in the prompts, for prompts = xbox (make_romfs_patches.py --prompts):
# romfs-patches-xbox for the Nintendo pad layout, romfs-patches-xbox-label for xbox and auto (runtime/input_map.c
# input_prompts_folder). Both are written on every install, so the menu choices work without another run of
# the setup. Optional: when one fails, the game keeps the Switch prompts with that layout and the menu says the
# files are missing, so the setup warns once and goes on.
failed = None
for style in ('xbox', 'xbox-label'):
out = dest / f'romfs-patches-{style}'
remove(out)
code, log = run_tool(ctx, f'prepare-{style}', layout.tools / 'make_romfs_patches.py',
['--version', version_tag(game_version(ctx)), '--romfs', dest / 'romfs', '--out', out,
'--prompts', style])
if code:
remove(out)
failed = failed or (code, log)
if failed:
rep.warning('warn.xbox_prompts_failed', code=failed[0], log=str(failed[1]))
shader_list = layout.shader_list
if not shader_list:
shader_list = ctx.temp / 'shader_list.tsv'
shader_list.write_text(shader_list_text(layout), encoding='utf-8', newline='\n')
skipped = [0]
def parse(line):
m = TRANSLATED.search(line)
if m:
rep.progress('prepare', 'translate', int(m.group(1)), int(m.group(2)), 'shaders')
elif SKIPPED.search(line):
skipped[0] = int(SKIPPED.search(line).group(1))
elif 'listed variants' in line:
rep.progress('prepare', 'translate', force=True)
elif 'gfx_shader: prepare:' in line and 'translated' in line and 'programs' in line:
@@ -709,6 +1105,8 @@ class Prepare(Step):
dest / 'exefs', '--list', shader_list], parse)
if code or not (dest / 'shader-prepared.bin').is_file():
raise InstallError('prepare.shaders_failed', code=code, log=str(log))
if skipped[0]:
rep.warning('warn.shaders_skipped', count=skipped[0], log=str(log))
# One shader-cache.pack instead of four files per shader (~117,000): totk.exe reads the pack first.
rep.progress('prepare', 'pack', force=True)
@@ -718,7 +1116,82 @@ class Prepare(Step):
return {'xlate_sha256': file_sha256(xlate)}
# ---- 6. clean up
# ---- 7. NVIDIA DLSS
# NVIDIA's DLSS library, which NGX loads from the folder of totk.exe (runtime/vk_dlaa.c). The setup does not ship it:
# with --dlss it downloads NVIDIA's own copy from the DLSS SDK repository (NVIDIA RTX SDK license) at a pinned tag and
# checks its hash. The game runs without it; dlaa and the DLSS modes then do nothing.
DLSS_DLL = 'nvngx_dlss.dll'
DLSS_VERSION = '310.9.1'
DLSS_URL = f'https://raw.githubusercontent.com/NVIDIA/DLSS/v{DLSS_VERSION}/lib/Windows_x86_64/rel/{DLSS_DLL}'
DLSS_SIZE = 58956912
DLSS_SHA256 = '3975567b8943c53acce397f2b72380092f84f162d00b0d2c7d08a1025c563983'
DLSS_LICENSE = 'https://github.com/NVIDIA/DLSS/blob/main/LICENSE.txt'
def nvidia_rtx(host):
"""An NVIDIA RTX GPU among the host check's Vulkan devices: the GPUs DLSS runs on."""
return any('NVIDIA' in d['name'].upper() and 'RTX' in d['name'].upper() for d in (host or {}).get('devices', []))
class Dlss(Step):
"""With --dlss on Windows and an NVIDIA RTX GPU: nvngx_dlss.dll next to totk.exe. A DLSS library the player put
there (another version) is kept. A failed download warns and is tried again on the next run; the game runs
without it."""
id = 'dlss'
def key(self, ctx):
if not getattr(ctx.options, 'dlss', False):
return fingerprint('dlss-1', 'off')
return fingerprint('dlss-1', DLSS_VERSION, DLSS_SHA256)
def current(self, ctx, record):
if record.get('failed'):
return False
return not record.get('installed') or (ctx.dest / DLSS_DLL).is_file()
def run(self, ctx):
if not getattr(ctx.options, 'dlss', False):
return {}
if os.name != 'nt':
return {'skipped': 'not_windows'}
if not nvidia_rtx(getattr(ctx, 'host', None)):
# The box is ticked by default, so most players without an RTX GPU never chose it: a log line, no warning
ctx.reporter.log('dlss: no NVIDIA RTX GPU among the Vulkan devices, nothing downloaded')
return {'skipped': 'no_rtx'}
rep, target = ctx.reporter, ctx.dest / DLSS_DLL
if target.is_file():
if file_sha256(target) == DLSS_SHA256:
return {'installed': DLSS_VERSION}
rep.warning('warn.dlss_kept', path=str(target))
return {'kept': True}
part = ctx.temp / (DLSS_DLL + '.part')
rep.progress('dlss', 'download', 0, DLSS_SIZE, 'bytes', force=True)
rep.log(f'dlss: {DLSS_URL} (license {DLSS_LICENSE})')
try:
part.parent.mkdir(parents=True, exist_ok=True)
h, done = hashlib.sha256(), 0
with urllib.request.urlopen(DLSS_URL, timeout=60) as r, open(part, 'wb') as f:
while block := r.read(1 << 20):
f.write(block)
h.update(block)
done += len(block)
rep.progress('dlss', 'download', min(done, DLSS_SIZE), DLSS_SIZE, 'bytes')
if done != DLSS_SIZE or h.hexdigest() != DLSS_SHA256:
raise OSError(f'{done} bytes, SHA-256 {h.hexdigest()[:16]}, not the expected file')
replace_file(part, target)
remove(part)
# urllib's errors, timeouts and a bad file; a connection cut mid-file raises http.client.IncompleteRead,
# which is not an OSError
except (OSError, http.client.HTTPException) as e:
remove(part)
rep.warning('warn.dlss_download_failed', url=DLSS_URL, detail=str(e)[:300])
return {'failed': True}
return {'installed': DLSS_VERSION}
# ---- 8. clean up
class Cleanup(Step):
id = 'cleanup'
@@ -736,12 +1209,40 @@ class Cleanup(Step):
rep.progress('cleanup', 'temp', force=True)
for stale in dest.glob('update-merge-*'):
remove(stale)
for name in ('gen', 'build', 'check', 'shader_list.tsv'):
for name in ('check', 'unpacked', 'shader_list.tsv', 'shared-fonts'): # gen/ and build/ stay for the next rebuild
remove(ctx.temp / name)
romfs_problem(dest / 'romfs', expected_update(ctx))
write_rebuild_command(ctx)
return {}
# Puts the release folder (the argument after -c) on sys.path and runs installer.gui with the rest. `-m installer.gui`
# finds the package only through the release Python's ._pth: the Linux release's Python and the dev-mode embeddable one
# lack that entry, and totk.exe starts the command in the install folder.
REBUILD_BOOT = ('import runpy, sys; sys.path.insert(0, sys.argv.pop(1)); '
"runpy.run_module('installer.gui', run_name='__main__', alter_sys=True)")
def rebuild_command(ctx):
"""The command that starts the setup window in its rebuild mode on this install folder (installer/gui.py
--rebuild): pythonw beside the setup's Python on Windows, so no console opens. In a repository that is the Python
running this setup: the tools' embeddable Python there has no tkinter."""
layout = ctx.layout
python = Path(sys.executable) if layout.dev else layout.python
if os.name == 'nt' and python.with_name('pythonw.exe').is_file():
python = python.with_name('pythonw.exe')
return [str(python), '-c', REBUILD_BOOT, str(layout.root), '--root', str(layout.root), '--rebuild', str(ctx.dest)]
def write_rebuild_command(ctx):
"""build-cache/rebuild.txt in the install folder: rebuild_command, one argument per line, for totk.exe."""
cache = ctx.dest / 'build-cache'
cache.mkdir(exist_ok=True)
tmp = cache / 'rebuild.txt.tmp'
tmp.write_text('\n'.join(rebuild_command(ctx)) + '\n', encoding='utf-8', newline='\n')
os.replace(tmp, cache / 'rebuild.txt')
def romfs_problem(romfs, expected):
"""Raise cleanup.romfs_unreadable unless romfs/ still holds the verified image. The game opens romfs.bin at start;
an install whose romfs/ went empty and unreadable after the extraction verified it (a folder Windows was still
@@ -758,4 +1259,4 @@ def romfs_problem(romfs, expected):
def all_steps():
return [Check(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Dlss(), Cleanup()]
+43 -12
View File
@@ -1,16 +1,22 @@
{
"step.check": "Checking the computer and the game files",
"step.fonts": "Extracting the system fonts",
"step.extract": "Extracting the game",
"step.lift": "Translating the game code to C",
"step.compile": "Compiling {game_exe}",
"step.prepare": "Preparing the shaders",
"step.dlss": "Downloading NVIDIA DLSS",
"step.cleanup": "Cleaning up",
"phase.check.host": "Checking the processor and the graphics card",
"phase.check.verify": "Checking the files already installed",
"phase.check.verify_romfs": "Checking the installed game data",
"phase.check.inputs": "Opening the game files with the keys",
"phase.check.unpack_base": "Unpacking the compressed base game",
"phase.check.unpack_update": "Unpacking the compressed update",
"phase.check.versions": "Checking the game versions",
"phase.fonts.extract": "Extracting the shared fonts from the firmware",
"phase.fonts.copy": "Copying the shared fonts into the install folder",
"phase.extract.hash_base": "Computing the checksum of the base game",
"phase.extract.hash_update": "Computing the checksum of the update",
"phase.extract.exefs": "Extracting the game code",
@@ -27,6 +33,7 @@
"phase.prepare.translate": "Translating the shaders",
"phase.prepare.libraries": "Building the shaders for the graphics driver",
"phase.prepare.pack": "Packing the shader cache into one file",
"phase.dlss.download": "Downloading nvngx_dlss.dll from NVIDIA's DLSS SDK",
"phase.cleanup.ini": "Writing totk.ini",
"phase.cleanup.temp": "Removing temporary files",
@@ -44,36 +51,47 @@
"check.toolchain_broken": "The bundled compiler does not work: {detail}",
"check.glibc_old": "This system's C library is glibc {version}; the setup's compiler needs {need} or later (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 and newer have it). Updating the system fixes this.",
"check.host_probe_failed": "The computer check did not run (exit code {code}).",
"check.cpu_no_avx2": "This processor lacks AVX2 (x86-64-v3), which the game needs. Intel Haswell (2013) or AMD Excavator / Zen and newer have it.",
"check.cpu_too_old": "This processor lacks x86-64-v2 (SSE4.2, POPCNT and CMPXCHG16B), the oldest level the game is built for. Intel Nehalem (2008) or AMD Bulldozer (2011) and newer have it.",
"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.no_display": "Preparing the shaders needs a display. Run the setup from the desktop session, or export DISPLAY=:0 (on a Steam Deck in Game Mode also XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "The game files are needed: choose the base game, the 1.1.0 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.",
"check.keys_wrong": "The keys cannot open the {input} file. Use the prod.keys of your console.",
"check.keys_too_old": "The keys file is too old for the {input} file (needs key generation {generation}). Dump prod.keys from a console with newer firmware.",
"check.not_game_file": "The {input} file is not an XCI or NSP game file.",
"check.not_game_file": "The {input} file is not an XCI, NSP, XCZ or NSZ game file.",
"check.file_unreadable": "The {input} file cannot be read: {detail}",
"check.unpack_failed": "The compressed {input} file could not be unpacked: {detail}",
"check.ticket_personalized": "The {input} file carries a personalized ticket; a common ticket is needed.",
"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.base_is_update": "The base game file is an update. Choose the base game NSP or XCI (version 1.0.0) here and this file as the update.",
"check.base_combined": "The base game file holds several packages (base game, update or DLC in one file). Choose a base game dump that contains only the game.",
"check.update_not_nsp": "The update must be an NSP or NSZ file.",
"check.update_is_base": "The update file holds a base game. Choose the 1.1.0 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.",
"check.base_wrong_version": "The base game is not version 1.0.0 ({modules} differ).",
"check.update_wrong_version": "The update is version {found}; this release takes {wanted}.",
"check.disk_space": "Not enough free space for {path}: {need_gb} GB needed, {free_gb} GB free.",
"check.firmware_missing": "The firmware folder {path} does not exist. Choose the firmware's Contents/registered folder.",
"check.firmware_empty": "The firmware folder {path} holds no NCA files. Choose the firmware's Contents/registered folder.",
"check.fonts_keys_needed": "The shared fonts for this language are not installed yet; choose prod.keys so the setup can extract them from the firmware.",
"extract.exefs_mismatch": "The extracted game code does not match version {version} ({modules}).",
"extract.romfs_merge_failed": "Applying the update to the game data failed: {detail}. Details: {log}",
"extract.romfs_mismatch": "The extracted game data does not match version {version} (size {size}, checksum {sha256}...).",
"fonts.failed": "Extracting the shared fonts failed (exit code {code}): {detail}. Details: {log}",
"fonts.incomplete": "The shared font extraction did not write every file ({files}).",
"fonts.copy_failed": "The shared fonts cannot be copied into {path}: {detail}",
"lift.failed": "Translating the game code failed (exit code {code}). Details: {log}",
"compile.failed": "Compiling {game_exe} failed (exit code {code}). Details: {log}",
"compile.exe_in_use": "{path} is in use. Close the game and run the installer again.",
"prepare.xlate_in_use": "{path} is in use. Close the game, end any totk_xlate process left from an earlier run, then run the installer again.",
"prepare.shader_list_missing": "The shader list is missing.",
"prepare.romfs_patches_failed": "Writing the game data patches failed (exit code {code}). Details: {log}",
"prepare.shaders_failed": "Preparing the shaders failed (exit code {code}). Details: {log}",
@@ -81,28 +99,40 @@
"prepare.pack_failed": "Packing the shader cache failed (exit code {code}). Details: {log}",
"cleanup.ini_failed": "totk.ini cannot be written: {detail}",
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, which the installer does not extract; some text may not show.",
"warn.lang_needs_fonts": "Text in {lang} needs the Switch system fonts, and none were given. The game will not start in this language until they are added. Choose the firmware folder (Contents/registered), or see \"Switch system fonts\" in docs/installer.md.",
"warn.npdm_differs": "main.npdm differs from the reference {version} dump; only the code modules are used.",
"warn.cheats_skipped": "The cheats of the mods folder that change the game's code could not be built; the game installs without them. Details: {log}",
"warn.exefs_skipped": "The mods' ExeFS patches (.ips, .pchtxt) could not be built; the game installs without them. Details: {log}",
"warn.xbox_prompts_failed": "The Xbox button prompts could not be prepared (exit code {code}); the game keeps the Switch prompts. Details: {log}",
"warn.shaders_skipped": "The graphics driver crashed while building some shaders, so {count} were left out of the preparation. The game builds them when it first needs them, and the driver may crash there too; a driver update may fix it. Details: {log}",
"warn.kernel_old": "Linux {version} lacks the write tracking the game relies on (6.7 or later); the game may run very slowly. Updating the system fixes this.",
"warn.dlss_kept": "A DLSS library is already in the install folder ({path}); the setup keeps it.",
"warn.dlss_download_failed": "NVIDIA DLSS could not be downloaded from {url}: {detail}. The game runs without it; the next setup run tries again.",
"ui.title": "TotK Setup",
"ui.rebuild_title": "Rebuilding the game for your mods",
"ui.start_game": "Start the game",
"ui.version": "version {version}",
"ui.intro": "Builds a playable Tears of the Kingdom folder from your own game files. This takes a while and runs once.",
"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.base": "Base game (NSP, XCI, NSZ or XCZ)",
"ui.update": "Update 1.1.0 or 1.2.1 or 1.4.3 (NSP or NSZ)",
"ui.keys": "prod.keys",
"ui.firmware": "Firmware folder (optional)",
"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.0 or 1.2.1 or 1.4.3 update file",
"ui.pick_keys": "Choose prod.keys",
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
"ui.pick_dest": "Choose the install folder",
"ui.type_game": "Game files (NSP, XCI)",
"ui.type_nsp": "NSP files",
"ui.firmware_help": "For Chinese, Korean and Traditional Chinese text: the firmware's Contents/registered folder, for example from Ryujinx or Yuzu. The setup copies the Switch system fonts from it into the install folder.",
"ui.dlss": "Download NVIDIA DLSS (nvngx_dlss.dll) for NVIDIA RTX graphics cards",
"ui.dlss_help": "From NVIDIA's DLSS SDK on GitHub, under NVIDIA's RTX SDK license (github.com/NVIDIA/DLSS). It makes the dlaa and DLSS settings work; without it they do nothing.",
"ui.compressed_help": "Compressed NSZ and XCZ files work as they are: the setup unpacks them by itself (until the game data is extracted, that needs as much free space as the unpacked files take).",
"ui.type_game": "Game files (NSP, XCI, NSZ, XCZ)",
"ui.type_keys": "Key files (*.keys)",
"ui.type_all": "All files",
"ui.start": "Start",
@@ -116,12 +146,13 @@
"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.0 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.",
"ui.missing_update": "The update file does not exist.",
"ui.missing_keys": "The keys file does not exist.",
"ui.missing_firmware": "The firmware folder does not exist.",
"ui.running_install": "Installing",
"ui.running_resume": "Continuing the earlier setup",
"ui.running_update": "Updating the installation",
+208
View File
@@ -0,0 +1,208 @@
{
"step.check": "Verificando o computador e os arquivos do jogo",
"step.fonts": "Extraindo as fontes do sistema",
"step.extract": "Extraindo o jogo",
"step.lift": "Traduzindo o código do jogo para C",
"step.compile": "Compilando o {game_exe}",
"step.prepare": "Preparando os shaders",
"step.dlss": "Baixando o NVIDIA DLSS",
"step.cleanup": "Finalizando",
"phase.check.host": "Verificando o processador e a placa de vídeo",
"phase.check.verify": "Verificando os arquivos já instalados",
"phase.check.verify_romfs": "Verificando os dados do jogo instalados",
"phase.check.inputs": "Abrindo os arquivos do jogo com as chaves",
"phase.check.unpack_base": "Descompactando o jogo base compactado",
"phase.check.unpack_update": "Descompactando a atualização compactada",
"phase.check.versions": "Verificando as versões do jogo",
"phase.fonts.extract": "Extraindo as fontes compartilhadas do firmware",
"phase.fonts.copy": "Copiando as fontes compartilhadas para a pasta de instalação",
"phase.extract.hash_base": "Calculando o checksum do jogo base",
"phase.extract.hash_update": "Calculando o checksum da atualização",
"phase.extract.exefs": "Extraindo o código do jogo",
"phase.extract.copy": "Copiando os dados do jogo",
"phase.extract.merge": "Aplicando a atualização aos dados do jogo",
"phase.extract.diagnose": "Procurando o arquivo do jogo danificado",
"phase.extract.index": "Verificando os dados do jogo",
"phase.lift.discover": "Encontrando as funções",
"phase.lift.emit": "Gerando o código C",
"phase.compile.compile": "Compilando",
"phase.compile.link": "Ligando o {game_exe}",
"phase.prepare.romfs_patches": "Gravando os patches de 60 fps nos dados do jogo",
"phase.prepare.seed": "Lendo os shaders dos dados do jogo",
"phase.prepare.translate": "Traduzindo os shaders",
"phase.prepare.libraries": "Montando os shaders para o driver de vídeo",
"phase.prepare.pack": "Juntando o cache de shaders num único arquivo",
"phase.dlss.download": "Baixando o nvngx_dlss.dll do DLSS SDK da NVIDIA",
"phase.cleanup.ini": "Gravando o totk.ini",
"phase.cleanup.temp": "Removendo os arquivos temporários",
"cli.usage": "Linha de comando inválida: {detail}",
"internal": "Erro inesperado ({exception}): {detail}",
"state.unreadable": "Não foi possível ler {path}. Escolha outra pasta ou apague esse arquivo.",
"state.newer_format": "{path} foi gravado por um instalador mais novo. Use esse instalador ou outra pasta.",
"state.output_changed": "Os arquivos instalados da etapa '{step}' mudaram desde que foram criados; essa etapa será executada de novo.",
"check.release_incomplete": "A pasta do instalador está incompleta; faltam: {paths}. Extraia o pacote de novo.",
"check.dest_not_folder": "O local de instalação {path} é um arquivo, não uma pasta.",
"check.dest_not_empty": "A pasta de instalação {path} não está vazia e não tem uma instalação anterior. Escolha uma pasta vazia.",
"check.lang_missing": "Escolha o idioma do jogo.",
"check.lang_unknown": "Idioma desconhecido '{lang}'. Conhecidos: {known}.",
"check.toolchain_broken": "O compilador incluído não funciona: {detail}",
"check.glibc_old": "A biblioteca C deste sistema é a glibc {version}; o compilador da instalação precisa da {need} ou mais nova (Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 e mais novos têm). Atualizar o sistema resolve.",
"check.host_probe_failed": "A verificação do computador não rodou (código de saída {code}).",
"check.cpu_too_old": "Este processador não tem x86-64-v2 (SSE4.2, POPCNT e CMPXCHG16B), o nível mais antigo para o qual o jogo é compilado. Intel Nehalem (2008) ou AMD Bulldozer (2011) e mais novos têm.",
"check.vulkan_missing": "Nenhum driver Vulkan está instalado. Instale o driver mais recente da sua placa de vídeo.",
"check.vulkan_no_device": "O Vulkan não encontra nenhuma placa de vídeo. Instale o driver mais recente da sua placa de vídeo.",
"check.vulkan_too_old": "O jogo precisa de uma placa de vídeo com Vulkan 1.3; encontrado: {devices}. Atualize o driver de vídeo.",
"check.no_display": "Preparar os shaders exige uma tela. Execute a instalação na sessão da área de trabalho ou use export DISPLAY=:0 (no Steam Deck em Game Mode, também XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "Os arquivos do jogo são necessários: escolha o jogo base, a atualização 1.1.0, 1.2.1 ou 1.4.3 e o prod.keys.",
"check.input_missing": "Arquivo não encontrado ({input}): {path}",
"check.keys_unreadable": "Não foi possível ler o arquivo de chaves. Escolha um arquivo prod.keys.",
"check.keys_no_header_key": "O arquivo de chaves não tem header_key. Extraia o prod.keys do seu console de novo.",
"check.keys_wrong": "As chaves não abrem o arquivo de {input}. Use o prod.keys do seu console.",
"check.keys_too_old": "O arquivo de chaves é antigo demais para o arquivo de {input} (precisa da geração de chaves {generation}). Extraia o prod.keys de um console com firmware mais novo.",
"check.not_game_file": "O arquivo de {input} não é um arquivo de jogo XCI, NSP, XCZ ou NSZ.",
"check.file_unreadable": "Não foi possível ler o arquivo de {input}: {detail}",
"check.unpack_failed": "Não foi possível descompactar o arquivo compactado de {input}: {detail}",
"check.ticket_personalized": "O arquivo de {input} tem um ticket personalizado; é preciso um ticket comum.",
"check.ticket_missing": "O arquivo de {input} não tem o ticket de que o conteúdo precisa.",
"check.base_not_totk": "O arquivo do jogo base não é The Legend of Zelda: Tears of the Kingdom ({detail}).",
"check.base_is_update": "O arquivo do jogo base é uma atualização. Escolha aqui o NSP ou XCI do jogo base (versão 1.0.0) e este arquivo como a atualização.",
"check.base_combined": "O arquivo do jogo base contém vários pacotes (jogo base, atualização ou DLC em um só arquivo). Escolha um dump do jogo base que contenha só o jogo.",
"check.update_not_nsp": "A atualização precisa ser um arquivo NSP ou NSZ.",
"check.update_is_base": "O arquivo de atualização contém um jogo base. Escolha o NSP da atualização 1.1.0, 1.2.1 ou 1.4.3.",
"check.update_not_totk": "O arquivo de atualização não é uma atualização de Tears of the Kingdom ({detail}).",
"check.exefs_failed": "O código do jogo no arquivo de {input} falhou nas verificações; veja {log}.",
"check.input_damaged": "O arquivo de {input} está danificado ou incompleto: {path} ({detail}). Copie esse arquivo de novo da origem, ou extraia-o de novo, e então execute a instalação outra vez.",
"check.base_wrong_version": "O jogo base não é a versão 1.0.0 ({modules} diferem).",
"check.update_wrong_version": "A atualização é a versão {found}; esta versão do instalador aceita {wanted}.",
"check.disk_space": "Espaço livre insuficiente em {path}: são necessários {need_gb} GB, há {free_gb} GB livres.",
"check.firmware_missing": "A pasta de firmware {path} não existe. Escolha a pasta Contents/registered do firmware.",
"check.firmware_empty": "A pasta de firmware {path} não contém arquivos NCA. Escolha a pasta Contents/registered do firmware.",
"check.fonts_keys_needed": "As fontes compartilhadas deste idioma ainda não estão instaladas; escolha o prod.keys para que a instalação possa extraí-las do firmware.",
"extract.exefs_mismatch": "O código do jogo extraído não corresponde à versão {version} ({modules}).",
"extract.romfs_merge_failed": "Falha ao aplicar a atualização aos dados do jogo: {detail}. Detalhes: {log}",
"extract.romfs_mismatch": "Os dados do jogo extraídos não correspondem à versão {version} (tamanho {size}, checksum {sha256}...).",
"fonts.failed": "A extração das fontes compartilhadas falhou (código de saída {code}): {detail}. Detalhes: {log}",
"fonts.incomplete": "A extração das fontes compartilhadas não gravou todos os arquivos ({files}).",
"fonts.copy_failed": "Não foi possível copiar as fontes compartilhadas para {path}: {detail}",
"lift.failed": "Falha ao traduzir o código do jogo (código de saída {code}). Detalhes: {log}",
"compile.failed": "Falha ao compilar o {game_exe} (código de saída {code}). Detalhes: {log}",
"compile.exe_in_use": "{path} está em uso. Feche o jogo e execute o instalador de novo.",
"prepare.xlate_in_use": "{path} está em uso. Feche o jogo, encerre qualquer processo totk_xlate que tenha sobrado de uma execução anterior e execute o instalador de novo.",
"prepare.shader_list_missing": "A lista de shaders está faltando.",
"prepare.romfs_patches_failed": "Falha ao gravar os patches nos dados do jogo (código de saída {code}). Detalhes: {log}",
"prepare.shaders_failed": "Falha ao preparar os shaders (código de saída {code}). Detalhes: {log}",
"cleanup.romfs_unreadable": "Os dados do jogo em {path} estão faltando ou não podem ser lidos depois da instalação. Reinicie o Windows e execute o instalador de novo na mesma pasta: ele extrai os dados do jogo outra vez e mantém o resto.",
"prepare.pack_failed": "Falha ao juntar o cache de shaders (código de saída {code}). Detalhes: {log}",
"cleanup.ini_failed": "Não foi possível gravar o totk.ini: {detail}",
"warn.lang_needs_fonts": "Textos em {lang} precisam das fontes de sistema do Switch, e nenhuma foi informada. O jogo não inicia neste idioma até que sejam adicionadas. Escolha a pasta do firmware (Contents/registered) ou veja \"Switch system fonts\" em docs/installer.md.",
"warn.cheats_skipped": "Não foi possível compilar os cheats da pasta de mods que alteram o código do jogo; o jogo é instalado sem eles. Detalhes: {log}",
"warn.exefs_skipped": "Não foi possível compilar os patches ExeFS dos mods (.ips, .pchtxt); o jogo é instalado sem eles. Detalhes: {log}",
"warn.npdm_differs": "O main.npdm difere do dump de referência da versão {version}; só os módulos de código são usados.",
"warn.xbox_prompts_failed": "Não foi possível preparar os ícones de botão do Xbox (código de saída {code}); o jogo mantém os ícones do Switch. Detalhes: {log}",
"warn.shaders_skipped": "O driver gráfico travou ao compilar alguns shaders, então {count} ficaram fora da preparação. O jogo os compila quando precisar deles pela primeira vez, e o driver pode travar ali também; uma atualização do driver pode resolver. Detalhes: {log}",
"warn.kernel_old": "O Linux {version} não tem o rastreamento de escrita de que o jogo depende (6.7 ou mais novo); o jogo pode rodar muito devagar. Atualizar o sistema resolve.",
"warn.dlss_kept": "Já existe uma biblioteca DLSS na pasta de instalação ({path}); a instalação a mantém.",
"warn.dlss_download_failed": "Não foi possível baixar o NVIDIA DLSS de {url}: {detail}. O jogo funciona sem ele; a próxima execução da instalação tenta de novo.",
"ui.title": "Instalação do TotK",
"ui.rebuild_title": "Recompilando o jogo para os seus mods",
"ui.start_game": "Iniciar o jogo",
"ui.version": "versão {version}",
"ui.intro": "Monta uma pasta jogável de Tears of the Kingdom a partir dos seus próprios arquivos do jogo. Isso demora um pouco e só precisa ser feito uma vez.",
"ui.notice": "A pasta que esta instalação monta contém o jogo. Ela é só para seu uso pessoal; não compartilhe nem envie para a internet.",
"ui.keys_private": "O arquivo de chaves é lido no lugar. O conteúdo dele nunca é mostrado, copiado nem guardado.",
"ui.base": "Jogo base (NSP, XCI, NSZ ou XCZ)",
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP ou NSZ)",
"ui.keys": "prod.keys",
"ui.firmware": "Pasta de firmware (opcional)",
"ui.game_language": "Idioma do jogo",
"ui.dest": "Pasta de instalação",
"ui.browse": "Procurar...",
"ui.pick_game": "Escolha o arquivo do jogo base",
"ui.pick_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3",
"ui.pick_keys": "Escolha o prod.keys",
"ui.pick_firmware": "Escolha a pasta de firmware do Switch (Contents/registered)",
"ui.pick_dest": "Escolha a pasta de instalação",
"ui.firmware_help": "Para textos em chinês, coreano e chinês tradicional: a pasta Contents/registered do firmware, por exemplo do Ryujinx ou do Yuzu. A instalação copia dela as fontes do sistema do Switch para a pasta de instalação.",
"ui.dlss": "Baixar o NVIDIA DLSS (nvngx_dlss.dll) para placas de vídeo NVIDIA RTX",
"ui.dlss_help": "Do DLSS SDK da NVIDIA no GitHub, sob a licença NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Sem ele as opções dlaa e DLSS não fazem nada.",
"ui.compressed_help": "Arquivos compactados NSZ e XCZ funcionam do jeito que estão: a instalação os descompacta sozinha (para isso, até os dados do jogo serem extraídos, ela precisa de tanto espaço livre quanto ocupam os arquivos descompactados).",
"ui.type_game": "Arquivos de jogo (NSP, XCI, NSZ, XCZ)",
"ui.type_keys": "Arquivos de chaves (*.keys)",
"ui.type_all": "Todos os arquivos",
"ui.start": "Iniciar",
"ui.cancel": "Cancelar",
"ui.cancelling": "Cancelando...",
"ui.show_log": "Mostrar log",
"ui.hide_log": "Ocultar log",
"ui.close": "Fechar",
"ui.back": "Voltar",
"ui.open_folder": "Abrir pasta",
"ui.update_mode": "Esta pasta tem uma instalação anterior. A instalação vai continuar ou atualizá-la; os arquivos do jogo só são pedidos se forem necessários de novo.",
"ui.need_dest": "Escolha a pasta de instalação.",
"ui.need_base": "Escolha o arquivo do jogo base.",
"ui.need_update": "Escolha o arquivo da atualização 1.1.0, 1.2.1 ou 1.4.3.",
"ui.need_keys": "Escolha o arquivo prod.keys.",
"ui.need_lang": "Escolha o idioma do jogo.",
"ui.missing_base": "O arquivo do jogo base não existe.",
"ui.missing_update": "O arquivo de atualização não existe.",
"ui.missing_keys": "O arquivo de chaves não existe.",
"ui.missing_firmware": "A pasta de firmware não existe.",
"ui.running_install": "Instalando",
"ui.running_resume": "Continuando a instalação anterior",
"ui.running_update": "Atualizando a instalação",
"ui.state_started": "iniciada",
"ui.state_done": "concluída",
"ui.state_skipped": "pulada",
"ui.state_pending": "aguardando",
"ui.state_running": "em andamento",
"ui.state_failed": "falhou",
"ui.log_warning": "Aviso",
"ui.log_error": "Erro",
"ui.log_space": "Espaço necessário: {need} GB, livre: {free} GB",
"ui.log_end": "Concluído",
"ui.result_ok": "O jogo está pronto",
"ui.result_failed": "A instalação falhou",
"ui.result_cancelled": "A instalação foi cancelada",
"ui.done_text": "Abra o {game_exe} em {dest}. O programa de instalação e os arquivos do jogo que você escolheu não são mais necessários.",
"ui.cancelled_text": "As etapas concluídas são mantidas. Inicie a instalação de novo com a mesma pasta para continuar.",
"ui.log_file": "Arquivo de log: {path}",
"ui.process_died": "A instalação parou de forma inesperada (código de saída {code}).",
"ui.confirm_cancel": "A instalação está em andamento. Cancelar e fechar a janela?",
"ui.space_one": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB).",
"ui.space_two": "Espaço livre necessário em {path}: {need} GB (livre: {free} GB); em {temp}: {temp_need} GB (livre: {temp_free} GB).",
"ui.amount_bytes": "{done} / {total} GB",
"ui.amount_chunks": "{done} / {total} partes",
"ui.amount_objects": "{done} / {total} arquivos",
"ui.amount_shaders": "{done} / {total} shaders",
"ui.amount_other": "{done} / {total} {unit}",
"ui.seconds": "{n} s",
"param.input.base": "jogo base",
"param.input.update": "atualização",
"param.input.keys": "chaves",
"launcher.title": "Instalação do TotK",
"launcher.python_missing": "Os arquivos de instalação estão incompletos: a pasta python ao lado do TotK-Setup.exe não existe. Extraia o arquivo compactado inteiro de novo e abra o TotK-Setup.exe a partir da pasta extraída.",
"lang.ja": "Japonês",
"lang.en-US": "Inglês (EUA)",
"lang.en-GB": "Inglês (Reino Unido)",
"lang.fr": "Francês",
"lang.fr-CA": "Francês (Canadá)",
"lang.de": "Alemão",
"lang.it": "Italiano",
"lang.es": "Espanhol",
"lang.es-419": "Espanhol (América Latina)",
"lang.nl": "Holandês",
"lang.pt": "Português",
"lang.pt-BR": "Português (Brasil)",
"lang.ru": "Russo",
"lang.ko": "Coreano",
"lang.zh-Hans": "Chinês (Simplificado)",
"lang.zh-Hant": "Chinês (Tradicional)"
}
+43 -12
View File
@@ -1,16 +1,22 @@
{
"step.check": "Проверка компьютера и игровых файлов",
"step.fonts": "Извлечение системных шрифтов",
"step.extract": "Распаковка игры",
"step.lift": "Перевод кода игры в C",
"step.compile": "Сборка {game_exe}",
"step.prepare": "Подготовка шейдеров",
"step.dlss": "Загрузка NVIDIA DLSS",
"step.cleanup": "Уборка",
"phase.check.host": "Проверка процессора и видеокарты",
"phase.check.verify": "Проверка уже установленных файлов",
"phase.check.verify_romfs": "Проверка установленных данных игры",
"phase.check.inputs": "Открытие игровых файлов с помощью ключей",
"phase.check.unpack_base": "Распаковка сжатой базовой игры",
"phase.check.unpack_update": "Распаковка сжатого обновления",
"phase.check.versions": "Проверка версий игры",
"phase.fonts.extract": "Извлечение общих шрифтов из прошивки",
"phase.fonts.copy": "Копирование общих шрифтов в папку установки",
"phase.extract.hash_base": "Подсчёт контрольной суммы базовой игры",
"phase.extract.hash_update": "Подсчёт контрольной суммы обновления",
"phase.extract.exefs": "Распаковка кода игры",
@@ -27,6 +33,7 @@
"phase.prepare.translate": "Трансляция шейдеров",
"phase.prepare.libraries": "Сборка шейдеров для драйвера видеокарты",
"phase.prepare.pack": "Упаковка кэша шейдеров в один файл",
"phase.dlss.download": "Загрузка nvngx_dlss.dll из DLSS SDK от NVIDIA",
"phase.cleanup.ini": "Запись totk.ini",
"phase.cleanup.temp": "Удаление временных файлов",
@@ -44,36 +51,47 @@
"check.toolchain_broken": "Встроенный компилятор не работает: {detail}",
"check.glibc_old": "Библиотека C этой системы — glibc {version}; компилятору установщика нужна {need} или новее (она есть в Ubuntu 22.04, Debian 12, Fedora 35, SteamOS 3 и новее). Обновление системы это исправит.",
"check.host_probe_failed": "Проверка компьютера не выполнилась (код выхода {code}).",
"check.cpu_no_avx2": "Этому процессору не хватает AVX2 (x86-64-v3), который нужен игре. AVX2 есть у Intel Haswell (2013) и новее, у AMD Excavator и Zen и новее.",
"check.cpu_too_old": "Этому процессору не хватает x86-64-v2 (SSE4.2, POPCNT и CMPXCHG16B) — минимального уровня, под который собрана игра. Он есть у Intel Nehalem (2008) и AMD Bulldozer (2011) и новее.",
"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.no_display": "Для подготовки шейдеров нужен дисплей. Запустите установку из сеанса рабочего стола или выполните export DISPLAY=:0 (на Steam Deck в игровом режиме ещё XDG_RUNTIME_DIR=/run/user/1000).",
"check.inputs_needed": "Нужны игровые файлы: выберите базовую игру, обновление 1.1.0 или 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 со своей консоли.",
"check.keys_wrong": "Эти ключи не открывают файл «{input}». Используйте prod.keys со своей консоли.",
"check.keys_too_old": "Файл ключей слишком старый для файла «{input}» (нужно поколение ключей {generation}). Получите prod.keys с консоли с более новой прошивкой.",
"check.not_game_file": "Файл «{input}» не является игровым файлом XCI или NSP.",
"check.not_game_file": "Файл «{input}» не является игровым файлом XCI, NSP, XCZ или NSZ.",
"check.file_unreadable": "Не удаётся прочитать файл «{input}»: {detail}",
"check.unpack_failed": "Не удалось распаковать сжатый файл «{input}»: {detail}",
"check.ticket_personalized": "В файле «{input}» личный тикет; нужен общий тикет.",
"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.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
"check.update_not_nsp": "Обновление должно быть файлом NSP или NSZ.",
"check.update_is_base": "Файл обновления содержит базовую игру. Выберите NSP с обновлением 1.1.0 или 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}). Скопируйте этот файл заново из источника или сделайте дамп ещё раз, затем снова запустите установку.",
"check.base_wrong_version": "Базовая игра не версии 1.0.0 (отличаются модули: {modules}).",
"check.update_wrong_version": "Версия обновления {found}; этот выпуск принимает {wanted}.",
"check.disk_space": "Не хватает свободного места для {path}: нужно {need_gb} ГБ, свободно {free_gb} ГБ.",
"check.firmware_missing": "Папки прошивки {path} нет. Выберите папку Contents/registered прошивки.",
"check.firmware_empty": "В папке прошивки {path} нет файлов NCA. Выберите папку Contents/registered прошивки.",
"check.fonts_keys_needed": "Общие шрифты для этого языка ещё не установлены; выберите prod.keys, чтобы установка извлекла их из прошивки.",
"extract.exefs_mismatch": "Распакованный код игры не соответствует версии {version} ({modules}).",
"extract.romfs_merge_failed": "Не удалось применить обновление к данным игры: {detail}. Подробности: {log}",
"extract.romfs_mismatch": "Распакованные данные игры не соответствуют версии {version} (размер {size}, контрольная сумма {sha256}...).",
"fonts.failed": "Не удалось извлечь общие шрифты (код выхода {code}): {detail}. Подробности: {log}",
"fonts.incomplete": "При извлечении общих шрифтов записаны не все файлы ({files}).",
"fonts.copy_failed": "Не удаётся скопировать общие шрифты в {path}: {detail}",
"lift.failed": "Не удалось перевести код игры (код выхода {code}). Подробности: {log}",
"compile.failed": "Не удалось собрать {game_exe} (код выхода {code}). Подробности: {log}",
"compile.exe_in_use": "Файл {path} занят. Закройте игру и запустите установку снова.",
"prepare.xlate_in_use": "Файл {path} занят. Закройте игру, завершите процесс totk_xlate, оставшийся от прошлого запуска, и запустите установку снова.",
"prepare.shader_list_missing": "Нет списка шейдеров.",
"prepare.romfs_patches_failed": "Не удалось записать патчи данных игры (код выхода {code}). Подробности: {log}",
"prepare.shaders_failed": "Не удалось подготовить шейдеры (код выхода {code}). Подробности: {log}",
@@ -81,28 +99,40 @@
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, которые установка не извлекает; часть текста может не отображаться.",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch, а они не указаны. Игра не запустится на этом языке, пока их не добавят. Выберите папку прошивки (Contents/registered) или см. раздел «Switch system fonts» в docs/installer.md.",
"warn.npdm_differs": "main.npdm отличается от эталонного дампа {version}; используются только модули с кодом.",
"warn.cheats_skipped": "Не удалось собрать читы из папки модов, меняющие код игры; игра установится без них. Подробности: {log}",
"warn.exefs_skipped": "Не удалось собрать ExeFS-патчи модов (.ips, .pchtxt); игра установится без них. Подробности: {log}",
"warn.xbox_prompts_failed": "Не удалось подготовить подсказки кнопок Xbox (код выхода {code}); игра оставит подсказки Switch. Подробности: {log}",
"warn.shaders_skipped": "Графический драйвер аварийно завершился при сборке части шейдеров, поэтому {count} из них пропущены при подготовке. Игра соберёт их, когда они впервые понадобятся, и драйвер может упасть и там; обновление драйвера может это исправить. Подробности: {log}",
"warn.kernel_old": "В Linux {version} нет отслеживания записи, на которое опирается игра (нужна версия 6.7 или новее); игра может работать очень медленно. Обновление системы это исправит.",
"warn.dlss_kept": "В папке установки уже есть библиотека DLSS ({path}); установка её оставляет.",
"warn.dlss_download_failed": "Не удалось загрузить NVIDIA DLSS с {url}: {detail}. Игра работает и без него; при следующем запуске установка попробует снова.",
"ui.title": "Установка TotK",
"ui.rebuild_title": "Пересборка игры под ваши моды",
"ui.start_game": "Запустить игру",
"ui.version": "версия {version}",
"ui.intro": "Собирает готовую к запуску папку с Tears of the Kingdom из ваших игровых файлов. Занимает много времени и выполняется один раз.",
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
"ui.base": "Базовая игра (NSP или XCI)",
"ui.update": "Обновление 1.2.1 или 1.4.3 (NSP)",
"ui.base": "Базовая игра (NSP, XCI, NSZ или XCZ)",
"ui.update": "Обновление 1.1.0 или 1.2.1 или 1.4.3 (NSP или NSZ)",
"ui.keys": "prod.keys",
"ui.firmware": "Папка прошивки (необязательно)",
"ui.game_language": "Язык игры",
"ui.dest": "Папка установки",
"ui.browse": "Обзор...",
"ui.pick_game": "Выберите файл базовой игры",
"ui.pick_update": "Выберите файл обновления 1.2.1 или 1.4.3",
"ui.pick_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3",
"ui.pick_keys": "Выберите prod.keys",
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
"ui.pick_dest": "Выберите папку установки",
"ui.type_game": "Файлы игры (NSP, XCI)",
"ui.type_nsp": "Файлы NSP",
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
"ui.dlss": "Загрузить NVIDIA DLSS (nvngx_dlss.dll) для видеокарт NVIDIA RTX",
"ui.dlss_help": "Из DLSS SDK от NVIDIA на GitHub, по лицензии NVIDIA RTX SDK (github.com/NVIDIA/DLSS). Без него настройки dlaa и DLSS ничего не делают.",
"ui.compressed_help": "Сжатые файлы NSZ и XCZ подходят как есть: установка сама их распакует (для этого, пока извлекаются данные игры, нужно столько свободного места, сколько занимают распакованные файлы).",
"ui.type_game": "Файлы игры (NSP, XCI, NSZ, XCZ)",
"ui.type_keys": "Файлы ключей (*.keys)",
"ui.type_all": "Все файлы",
"ui.start": "Начать",
@@ -116,12 +146,13 @@
"ui.update_mode": "В этой папке уже есть установка. Программа продолжит или обновит её; файлы игры понадобятся, только если их снова придётся читать.",
"ui.need_dest": "Выберите папку установки.",
"ui.need_base": "Выберите файл базовой игры.",
"ui.need_update": "Выберите файл обновления 1.2.1 или 1.4.3.",
"ui.need_update": "Выберите файл обновления 1.1.0 или 1.2.1 или 1.4.3.",
"ui.need_keys": "Выберите файл prod.keys.",
"ui.need_lang": "Выберите язык игры.",
"ui.missing_base": "Файл базовой игры не найден.",
"ui.missing_update": "Файл обновления не найден.",
"ui.missing_keys": "Файл ключей не найден.",
"ui.missing_firmware": "Папки прошивки нет.",
"ui.running_install": "Установка",
"ui.running_resume": "Продолжение прерванной установки",
"ui.running_update": "Обновление установки",
+208
View File
@@ -0,0 +1,208 @@
{
"step.check": "检查电脑和游戏文件",
"step.fonts": "正在提取系统字体",
"step.extract": "解包游戏",
"step.lift": "将游戏代码转换为 C 代码",
"step.compile": "编译 {game_exe}",
"step.prepare": "准备着色器",
"step.dlss": "下载 NVIDIA DLSS",
"step.cleanup": "清理",
"phase.check.host": "检查处理器和显卡",
"phase.check.verify": "检查已安装的文件",
"phase.check.verify_romfs": "检查已安装的游戏数据",
"phase.check.inputs": "用密钥打开游戏文件",
"phase.check.unpack_base": "正在解压压缩的游戏本体",
"phase.check.unpack_update": "正在解压压缩的更新",
"phase.check.versions": "检查游戏版本",
"phase.fonts.extract": "正在从固件中提取共享字体",
"phase.fonts.copy": "正在将共享字体复制到安装文件夹",
"phase.extract.hash_base": "计算游戏本体的校验和",
"phase.extract.hash_update": "计算更新的校验和",
"phase.extract.exefs": "解包游戏代码",
"phase.extract.copy": "复制游戏数据",
"phase.extract.merge": "将更新应用到游戏数据",
"phase.extract.diagnose": "查找损坏的游戏文件",
"phase.extract.index": "校验游戏数据",
"phase.lift.discover": "查找函数",
"phase.lift.emit": "生成 C 代码",
"phase.compile.compile": "编译",
"phase.compile.link": "链接 {game_exe}",
"phase.prepare.romfs_patches": "写入 60 帧游戏数据补丁",
"phase.prepare.seed": "从游戏数据中读取着色器",
"phase.prepare.translate": "转译着色器",
"phase.prepare.libraries": "为显卡驱动构建着色器",
"phase.prepare.pack": "将着色器缓存打包成一个文件",
"phase.dlss.download": "从 NVIDIA 的 DLSS SDK 下载 nvngx_dlss.dll",
"phase.cleanup.ini": "写入 totk.ini",
"phase.cleanup.temp": "删除临时文件",
"cli.usage": "命令行无效:{detail}",
"internal": "意外错误({exception}):{detail}",
"state.unreadable": "无法读取 {path}。请选择其他文件夹,或删除该文件。",
"state.newer_format": "{path} 是由更新版本的安装程序写入的。请使用那个安装程序,或选择其他文件夹。",
"state.output_changed": "步骤“{step}”生成的已安装文件在生成后被改动过;该步骤将重新运行。",
"check.release_incomplete": "安装程序文件夹不完整,缺少:{paths}。请重新解压发布包。",
"check.dest_not_folder": "安装位置 {path} 是一个文件,而不是文件夹。",
"check.dest_not_empty": "安装文件夹 {path} 不是空的,其中也没有之前的安装。请选择一个空文件夹。",
"check.lang_missing": "请选择游戏语言。",
"check.lang_unknown": "未知语言“{lang}”。可用语言:{known}。",
"check.toolchain_broken": "自带的编译器无法工作:{detail}",
"check.glibc_old": "此系统的 C 库为 glibc {version};安装程序的编译器需要 {need} 或更新版本(Ubuntu 22.04、Debian 12、Fedora 35、SteamOS 3 及更新版本都满足)。更新系统即可解决。",
"check.host_probe_failed": "电脑检查未能运行(退出代码 {code})。",
"check.cpu_too_old": "此处理器不支持 x86-64-v2(SSE4.2、POPCNT 和 CMPXCHG16B),这是游戏所需的最低指令集。Intel Nehalem(2008 年)或 AMD Bulldozer(2011 年)及更新的处理器都支持。",
"check.vulkan_missing": "未安装 Vulkan 驱动。请为你的显卡安装最新的驱动程序。",
"check.vulkan_no_device": "Vulkan 找不到显卡。请为你的显卡安装最新的驱动程序。",
"check.vulkan_too_old": "游戏需要支持 Vulkan 1.3 的显卡;检测到:{devices}。请更新显卡驱动程序。",
"check.no_display": "准备着色器需要显示器。请在桌面会话中运行安装程序,或执行 export DISPLAY=:0(Steam Deck 游戏模式下还需 XDG_RUNTIME_DIR=/run/user/1000)。",
"check.inputs_needed": "需要游戏文件:请选择游戏本体、1.1.0、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。",
"check.keys_wrong": "这些密钥无法打开{input}文件。请使用你自己主机的 prod.keys。",
"check.keys_too_old": "密钥文件对{input}文件来说太旧(需要密钥代数 {generation})。请从固件更新的主机导出 prod.keys。",
"check.not_game_file": "{input}文件不是 XCI、NSP、XCZ 或 NSZ 游戏文件。",
"check.file_unreadable": "无法读取{input}文件:{detail}",
"check.unpack_failed": "无法解压压缩的{input}文件:{detail}",
"check.ticket_personalized": "{input}文件带有个人化票据(personalized ticket);需要通用票据(common ticket)。",
"check.ticket_missing": "{input}文件缺少其内容所需的票据(ticket)。",
"check.base_not_totk": "游戏本体文件不是《塞尔达传说 王国之泪》({detail})。",
"check.base_is_update": "基础游戏文件是一个更新。请在这里选择基础游戏的 NSP 或 XCI(版本 1.0.0),并把此文件选为更新。",
"check.base_combined": "基础游戏文件包含多个内容包(基础游戏、更新或 DLC 合在一个文件中)。请选择只包含游戏本体的基础游戏转储。",
"check.update_not_nsp": "更新必须是 NSP 或 NSZ 文件。",
"check.update_is_base": "更新文件里是游戏本体。请选择 1.1.0、1.2.1 或 1.4.3 的更新 NSP。",
"check.update_not_totk": "更新文件不是《王国之泪》的更新({detail})。",
"check.exefs_failed": "{input}文件的游戏代码未通过检查;详见 {log}。",
"check.input_damaged": "{input}文件已损坏或不完整:{path}({detail})。请从来源重新复制该文件或重新导出,然后再次运行安装程序。",
"check.base_wrong_version": "游戏本体不是 1.0.0 版本({modules} 不一致)。",
"check.update_wrong_version": "更新的版本是 {found};此发布版需要 {wanted}。",
"check.disk_space": "{path} 的可用空间不足:需要 {need_gb} GB,可用 {free_gb} GB。",
"check.firmware_missing": "固件文件夹 {path} 不存在。请选择固件的 Contents/registered 文件夹。",
"check.firmware_empty": "固件文件夹 {path} 中没有 NCA 文件。请选择固件的 Contents/registered 文件夹。",
"check.fonts_keys_needed": "此语言的共享字体尚未安装;请选择 prod.keys,以便安装程序从固件中提取它们。",
"extract.exefs_mismatch": "解包出的游戏代码与 {version} 版本不符({modules})。",
"extract.romfs_merge_failed": "将更新应用到游戏数据失败:{detail}。详情:{log}",
"extract.romfs_mismatch": "解包出的游戏数据与 {version} 版本不符(大小 {size},校验和 {sha256}...)。",
"fonts.failed": "提取共享字体失败(退出代码 {code}):{detail}。详情:{log}",
"fonts.incomplete": "共享字体提取没有写出全部文件({files})。",
"fonts.copy_failed": "无法将共享字体复制到 {path}:{detail}",
"lift.failed": "游戏代码转换失败(退出代码 {code})。详情:{log}",
"compile.failed": "编译 {game_exe} 失败(退出代码 {code})。详情:{log}",
"compile.exe_in_use": "{path} 正在使用中。请关闭游戏后重新运行安装程序。",
"prepare.xlate_in_use": "{path} 正在被占用。请关闭游戏,结束上次残留的 totk_xlate 进程,再重新运行安装程序。",
"prepare.shader_list_missing": "缺少着色器列表。",
"prepare.romfs_patches_failed": "写入游戏数据补丁失败(退出代码 {code})。详情:{log}",
"prepare.shaders_failed": "准备着色器失败(退出代码 {code})。详情:{log}",
"cleanup.romfs_unreadable": "安装完成后,{path} 中的游戏数据缺失或无法读取。请重启 Windows,然后对同一文件夹再次运行安装程序:它会重新提取游戏数据,其余内容保留。",
"prepare.pack_failed": "打包着色器缓存失败(退出代码 {code})。详情:{log}",
"cleanup.ini_failed": "无法写入 totk.ini:{detail}",
"warn.lang_needs_fonts": "{lang}文本需要 Switch 系统字体,但未提供。在添加这些字体之前,游戏无法以该语言启动。请选择固件文件夹(Contents/registered),或参阅 docs/installer.md 中的“Switch system fonts”一节。",
"warn.cheats_skipped": "无法构建 mods 文件夹中修改游戏代码的金手指;游戏将不含它们进行安装。详情:{log}",
"warn.exefs_skipped": "无法构建模组的 ExeFS 补丁(.ips、.pchtxt);游戏将不含它们进行安装。详情:{log}",
"warn.npdm_differs": "main.npdm 与参考的 {version} 导出不同;只使用其中的代码模块。",
"warn.xbox_prompts_failed": "无法准备 Xbox 按键提示(退出代码 {code});游戏将继续使用 Switch 按键提示。详情:{log}",
"warn.shaders_skipped": "显卡驱动在编译部分着色器时崩溃,因此有 {count} 个着色器未被预先准备。游戏会在首次需要时编译它们,驱动在那时也可能崩溃;更新驱动可能会解决。详情:{log}",
"warn.kernel_old": "Linux {version} 缺少游戏依赖的写入跟踪功能(需要 6.7 或更新版本);游戏可能运行得很慢。更新系统即可解决。",
"warn.dlss_kept": "安装文件夹中已有 DLSS 库({path});安装程序将保留它。",
"warn.dlss_download_failed": "无法从 {url} 下载 NVIDIA DLSS:{detail}。游戏无需它也能运行;下次运行安装程序时会重试。",
"ui.title": "TotK 安装程序",
"ui.rebuild_title": "正在为你的模组重新构建游戏",
"ui.start_game": "启动游戏",
"ui.version": "版本 {version}",
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
"ui.base": "游戏本体(NSP、XCI、NSZ 或 XCZ)",
"ui.update": "1.1.0、1.2.1 或 1.4.3 更新(NSP 或 NSZ)",
"ui.keys": "prod.keys",
"ui.firmware": "固件文件夹(可选)",
"ui.game_language": "游戏语言",
"ui.dest": "安装文件夹",
"ui.browse": "浏览...",
"ui.pick_game": "选择游戏本体文件",
"ui.pick_update": "选择 1.1.0、1.2.1 或 1.4.3 更新文件",
"ui.pick_keys": "选择 prod.keys",
"ui.pick_firmware": "选择 Switch 固件文件夹(Contents/registered)",
"ui.pick_dest": "选择安装文件夹",
"ui.firmware_help": "用于中文、韩文和繁体中文文本:固件的 Contents/registered 文件夹,例如来自 Ryujinx 或 Yuzu。安装程序会从中把 Switch 系统字体复制到安装文件夹。",
"ui.dlss": "为 NVIDIA RTX 显卡下载 NVIDIA DLSS(nvngx_dlss.dll)",
"ui.dlss_help": "来自 GitHub 上 NVIDIA 的 DLSS SDK,遵循 NVIDIA RTX SDK 许可(github.com/NVIDIA/DLSS)。没有它,dlaa 和 DLSS 设置不起作用。",
"ui.compressed_help": "压缩的 NSZ 和 XCZ 文件可以直接使用:安装程序会自行解压(在游戏数据提取完成之前,需要与解压后文件大小相同的可用空间)。",
"ui.type_game": "游戏文件(NSP、XCI、NSZ、XCZ)",
"ui.type_keys": "密钥文件(*.keys)",
"ui.type_all": "所有文件",
"ui.start": "开始",
"ui.cancel": "取消",
"ui.cancelling": "正在取消...",
"ui.show_log": "显示日志",
"ui.hide_log": "隐藏日志",
"ui.close": "关闭",
"ui.back": "返回",
"ui.open_folder": "打开文件夹",
"ui.update_mode": "此文件夹中已有之前的安装。安装程序会继续或更新它;只有在需要时才会再次要求提供游戏文件。",
"ui.need_dest": "请选择安装文件夹。",
"ui.need_base": "请选择游戏本体文件。",
"ui.need_update": "请选择 1.1.0、1.2.1 或 1.4.3 更新文件。",
"ui.need_keys": "请选择 prod.keys 文件。",
"ui.need_lang": "请选择游戏语言。",
"ui.missing_base": "游戏本体文件不存在。",
"ui.missing_update": "更新文件不存在。",
"ui.missing_keys": "密钥文件不存在。",
"ui.missing_firmware": "固件文件夹不存在。",
"ui.running_install": "正在安装",
"ui.running_resume": "正在继续之前的安装",
"ui.running_update": "正在更新安装",
"ui.state_started": "已开始",
"ui.state_done": "完成",
"ui.state_skipped": "已跳过",
"ui.state_pending": "等待中",
"ui.state_running": "进行中",
"ui.state_failed": "失败",
"ui.log_warning": "警告",
"ui.log_error": "错误",
"ui.log_space": "所需空间:{need} GB,可用:{free} GB",
"ui.log_end": "已结束",
"ui.result_ok": "游戏已准备就绪",
"ui.result_failed": "安装失败",
"ui.result_cancelled": "安装已取消",
"ui.done_text": "从 {dest} 启动 {game_exe}。安装程序和你选择的游戏文件都不再需要了。",
"ui.cancelled_text": "已完成的步骤会保留。用同一个文件夹再次运行安装程序即可继续。",
"ui.log_file": "日志文件:{path}",
"ui.process_died": "安装程序意外停止(退出代码 {code})。",
"ui.confirm_cancel": "安装正在进行。要取消安装并关闭窗口吗?",
"ui.space_one": "{path} 需要的可用空间:{need} GB(可用:{free} GB)。",
"ui.space_two": "{path} 需要的可用空间:{need} GB(可用:{free} GB);{temp} 需要:{temp_need} GB(可用:{temp_free} GB)。",
"ui.amount_bytes": "{done} / {total} GB",
"ui.amount_chunks": "{done} / {total} 部分",
"ui.amount_objects": "{done} / {total} 个文件",
"ui.amount_shaders": "{done} / {total} 个着色器",
"ui.amount_other": "{done} / {total} {unit}",
"ui.seconds": "{n} 秒",
"param.input.base": "游戏本体",
"param.input.update": "更新",
"param.input.keys": "密钥",
"launcher.title": "TotK 安装程序",
"launcher.python_missing": "安装文件不完整:TotK-Setup.exe 旁边缺少 python 文件夹。请重新解压整个压缩包,然后从解压出的文件夹中启动 TotK-Setup.exe。",
"lang.ja": "日语",
"lang.en-US": "英语(美国)",
"lang.en-GB": "英语(英国)",
"lang.fr": "法语",
"lang.fr-CA": "法语(加拿大)",
"lang.de": "德语",
"lang.it": "意大利语",
"lang.es": "西班牙语",
"lang.es-419": "西班牙语(拉丁美洲)",
"lang.nl": "荷兰语",
"lang.pt": "葡萄牙语",
"lang.pt-BR": "葡萄牙语(巴西)",
"lang.ru": "俄语",
"lang.ko": "韩语",
"lang.zh-Hans": "中文(简体)",
"lang.zh-Hant": "中文(繁体)"
}
+114 -51
View File
@@ -211,7 +211,7 @@ class Emitter:
def __init__(self, mod, starts, patched=None, reg_locals=regs.LOCALS, callee_saved=True, spins=None):
self.mod = mod
self.starts = starts
self.patched = patched or {} # {address: (set index, [replacement words])} (recomp/patches.py)
self.patched = patched or {} # {address: [(set index, [replacement words])]} (recomp/patches.py)
self.spins = spins or {} # {spin wait back-edge: its head} (recomp/patches.py `spin`)
self.reg_locals = reg_locals # recomp/regs.py: registers per function in C locals
self.kept = regs.CALLEE_SAVED if callee_saved else frozenset() # not reloaded after a call
@@ -250,33 +250,54 @@ class Emitter:
self.unsupported[re.sub(r'\b[0-9a-f]{8}\b', '', msg).strip()] += 1
elif k == 'asm':
raise ValueError(f'{entry + 4 * i:#x}: asm instruction from the portable lifter')
for a in self.patched: # a replacement's branch into the function needs its label
if entry <= a < end:
for words in self.replacements(a):
r = words[-1]
if r.kind in ('b', 'bcond') and entry <= r.target < end:
labels.add(r.target)
return labels, tables
def replacements(self, a):
"""Lifted replacement words of the patched site a."""
return [self.mod.lifter.lift(w, a) for w in self.patched[a][1]]
"""The lifted words of each alternative of the patched site a, in set order. Only the last word of an
alternative may branch (bl, b, b.cond / cbz / tbz, ret)."""
out = []
for _, words in self.patched[a]:
lifted = [self.mod.lifter.lift(w, a) for w in words]
for j, r in enumerate(lifted):
last = j == len(lifted) - 1
if r.kind != 'op' and not (last and r.kind in ('bl', 'b', 'bcond', 'ret')):
raise patches.PatchError(f'{a:#x}: replacement word {words[j]:08x} is a {r.kind}')
out.append(lifted)
return out
@staticmethod
def flow(insns, entry, end, tables):
def flow(self, insns, entry, end, tables):
"""Successors of each instruction inside the function [entry, end). Index len(insns) stands for falling
through into the next function and has none."""
through into the next function and has none. A patched site has the successors of all its alternatives."""
def idx(t):
return [(t - entry) // 4] if entry <= t < end else []
succ = []
for i, ins in enumerate(insns):
def of(ins, i):
k = ins.kind
if k in ('op', 'bl', 'blr'):
succ.append([i + 1])
elif k == 'b':
succ.append(idx(ins.target))
elif k == 'bcond':
succ.append(idx(ins.target) + [i + 1])
elif k == 'br':
return [i + 1]
if k == 'b':
return idx(ins.target)
if k == 'bcond':
return idx(ins.target) + [i + 1]
if k == 'br':
jt = tables.get(entry + 4 * i)
succ.append([t for x in jt for t in idx(x)] if jt and jt != 'all' else [])
else: # ret, trap
succ.append([])
return [t for x in jt for t in idx(x)] if jt and jt != 'all' else []
return [] # ret, trap
succ = []
for i, ins in enumerate(insns):
s = of(ins, i)
a = entry + 4 * i
if a in self.patched:
for words in self.replacements(a):
s += [t for t in of(words[-1], i) if t not in s]
succ.append(s)
succ.append([])
return succ
@@ -294,27 +315,34 @@ class Emitter:
exits.add(i) # through rt_dispatch, also a table's default
if a in self.patched:
effects.append(self.patched_effect(ins, a))
if any(r.kind == 'bl' for r in self.replacements(a)):
calls.add(i)
for words in self.replacements(a):
r = words[-1]
if r.kind == 'bl':
calls.add(i)
elif r.kind in ('b', 'bcond') and not entry <= r.target < end:
exits.add(i)
else:
effects.append(flags.effect(ins.cond if k == 'bcond' else ins.code or ''))
effects.append((False, None))
return flags.plan(effects, succ, calls, exits)
def patched_effect(self, ins, a):
"""Flag effect of a patched site: both alternatives must have the same one, in at most one word each."""
"""Flag effect of a patched site: every alternative (the original and each replacement) must write the same
flags, in at most one word each; the site reads flags when any alternative does (a b.cond reads them)."""
alts = []
for words in ([ins], self.replacements(a)):
fx = [flags.effect(w.code or '') for w in words if w.kind == 'op']
for words in [[ins]] + self.replacements(a):
fx = [flags.effect(w.code or '') if w.kind == 'op' else flags.effect(w.cond) for w in words
if w.kind in ('op', 'bcond')]
fx = [e for e in fx if e[0] or e[1]]
if len(fx) > 1:
raise patches.PatchError(f'{a:#x}: several flag-using words in one alternative')
alts.append(fx[0] if fx else (False, None))
if alts[0][1] != alts[1][1]:
raise patches.PatchError(f'{a:#x}: the alternatives write different flags ({alts[0][1]}, {alts[1][1]})')
if (alts[0][1] or alts[0][0] or alts[1][0]) and ins.kind == 'bl':
if any(e[1] != alts[0][1] for e in alts):
raise patches.PatchError(f'{a:#x}: the alternatives write different flags ({[e[1] for e in alts]})')
reads = any(e[0] for e in alts)
if (alts[0][1] or reads) and (ins.kind == 'bl' or any(w[-1].kind == 'bl' for w in self.replacements(a))):
raise patches.PatchError(f'{a:#x}: a patched call that uses flags')
return alts[0][0] or alts[1][0], alts[0][1]
return reads, alts[0][1]
def function(self, entry, end):
"""(lines of C, referenced function starts) for the guest function [entry, end)."""
@@ -385,23 +413,30 @@ class Emitter:
code = fx(i, code)
return f'{spill}{code}{reload}' if regs.ESCAPE.search(code) else code
def one(i, r, a):
"""C of one lifted word of a patched site."""
if r.kind == 'op':
return op(i, r.code)
if r.kind == 'bl':
return call(r.target, a)
if r.kind == 'b':
return jump(r.target, i)
if r.kind == 'bcond':
return f'if ({fx(i, r.cond)}) {{ {jump(r.target, i)} }}'
if r.kind == 'ret':
return f'{spill}return;'
raise patches.PatchError(f'{a:#x}: patched instruction is a {r.kind}')
def patch(i, ins, a):
"""if (switch) { replacement } else { original } for a patched site (recomp/patches.py)."""
k, words = self.patched[a]
if ins.kind not in ('op', 'bl'):
"""if (switch) { replacement } else if ... else { original } for a patched site (recomp/patches.py)."""
if ins.kind not in ('op', 'bl', 'b', 'bcond'):
raise patches.PatchError(f'{a:#x}: patched instruction is a {ins.kind}')
repl = []
for j, (w, r) in enumerate(zip(words, self.replacements(a))):
if r.kind == 'op':
repl.append(op(i, r.code))
elif r.kind == 'bl' and j == len(words) - 1:
repl.append(call(r.target, a))
else:
raise patches.PatchError(f'{a:#x}: replacement word {w:08x} is a {r.kind}')
orig = op(i, ins.code) if ins.kind == 'op' else call(ins.target, a)
line = ' '
for (k, _), words in zip(self.patched[a], self.replacements(a)):
line += f' if (rt_patch_on[{k}]) {{ {" ".join(one(i, r, a) for r in words)} }} else'
self.uses_patches = True
self.stats['patched_sites'] += 1
return f' if (rt_patch_on[{k}]) {{ {" ".join(repl)} }} else {{ {orig} }}'
return f'{line} {{ {one(i, ins, a)} }}'
body = [] # (instruction index or None for a label, line); a line belongs to the instruction it lifts
@@ -445,7 +480,7 @@ class Emitter:
else:
raise ValueError(f'{a:#x}: unknown instruction kind {k!r}')
n = len(insns)
if k not in TERMINATORS: # falls through into the next function
if k not in TERMINATORS or end - 4 in self.patched: # falls through into the next function
if end < mod.hi:
emit(n, ' ' + jump(end, n))
else:
@@ -523,7 +558,8 @@ def _retry(fn, *args):
_W = {} # worker state: 'mods' {name: (path, base)}, 'starts' {name: sorted array}, 'emitters' {name: Emitter}
def _worker_init(mods, starts, table, opts):
def _worker_init(mods, starts, table, opts, jobs):
_W['jobs'] = jobs
_W['mods'] = mods
_W['starts'] = starts
_W['patches'] = table
@@ -546,6 +582,10 @@ def _emit_chunk(task):
"""Worker: write one chunk. Returns its statistics."""
name, number, path, fns = task
t0 = time.perf_counter()
# Test hook: a pool wider than this raises the MemoryError a busy machine does, so the retry can be exercised.
limit = os.environ.get('TOTK_LIFT_TEST_OOM_ABOVE')
if limit and _W['jobs'] > int(limit) and number % 2 == 1:
raise MemoryError
e = _emitter(name)
e.stats.clear()
e.unsupported.clear()
@@ -597,6 +637,14 @@ def pick_jobs(requested):
"""Worker count and the reason for it: --jobs if given, else the CPU count capped by the free memory."""
if requested:
return requested, '--jobs'
env = os.environ.get('TOTK_LIFT_JOBS', '').strip()
if env:
try:
if int(env) >= 1:
return int(env), 'TOTK_LIFT_JOBS'
except ValueError:
pass
print(f'ignoring TOTK_LIFT_JOBS={env!r}: not a positive integer', file=sys.stderr, flush=True)
cpus = os.cpu_count() or 4
free = hostmem.available_ram()
if free is None:
@@ -670,16 +718,31 @@ def main():
t_emit = time.perf_counter()
ctx = mp.get_context('spawn')
jobs, why = pick_jobs(args.jobs)
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts)) as pool:
for i, r in enumerate(pool.imap_unordered(_emit_chunk, [t for _, t in tasks], chunksize=1)):
results[r['module']].append(r)
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
if (i + 1) % 200 == 0 or i + 1 == len(tasks):
print(f'emitted {i + 1}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s', file=sys.stderr,
flush=True)
pending = [t for _, t in tasks]
emitted = 0
while True:
print(f'emitting with {jobs} workers ({why})', file=sys.stderr, flush=True)
try:
# Chunk writes are atomic and idempotent, so a chunk lost with a failed pool is simply emitted again.
with ctx.Pool(jobs, initializer=_worker_init, initargs=(paths, starts_abs, table, opts, jobs)) as pool:
for r in pool.imap_unordered(_emit_chunk, pending, chunksize=1):
results[r['module']].append(r)
if r['error']:
errors.append(r['error'])
done_at[r['module']] = time.perf_counter() - t_emit
emitted += 1
if emitted % 200 == 0 or emitted == len(tasks):
print(f'emitted {emitted}/{len(tasks)} chunks, {time.perf_counter() - t_emit:.0f}s',
file=sys.stderr, flush=True)
break
except MemoryError:
if jobs == 1:
raise
smaller = jobs // 2
print(f'emit: out of memory with {jobs} workers, retrying with {smaller}', file=sys.stderr, flush=True)
jobs, why = smaller, 'after out of memory'
done = {(r['module'], r['number']) for rs in results.values() for r in rs}
pending = [t for t in pending if (t[0], t[1]) not in done]
for name, d in info.items():
rs = sorted(results[name], key=lambda r: r['number'])
+957
View File
@@ -0,0 +1,957 @@
# Code patches for TotK 1.1.0 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.0 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.0
# 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/v110/exefs --patches recomp/patches-v110.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
+27
View File
@@ -9,6 +9,7 @@ set cloth TOTK_CLOTH_REALTIME 1
set bounce TOTK_HIGHFPS_FIX 1
set output TOTK_PATCH_OUTPUT 0
set scene TOTK_PATCH_SCENE 0
set shadow TOTK_PATCH_SHADOW 0
# 60 fps: the adaptive present-interval controller (main+0x8f26c8, 1.0.0 0x8f6bec) gets the minimum interval 1.
site fps60 main 8f273c 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
@@ -32,6 +33,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)
@@ -75,6 +82,17 @@ site aspect main 12521cc bd414260 f81f0fea @page:x10 bd403940 f84107ea # ldr s0
site aspect main 19b36f0 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
site aspect main 1ecdf68 1e211800 f81f0fea @page:x10 bd403940 f84107ea # fdiv s0, s0, s1 (viewport)
# Texture pool offsets past 2 GiB (MacNativeGames ca311e2d). The texture loader keeps a texture's place in its pool as a
# 32-bit offset (main+0xad4294: sub x25, x9, x10, stored as the word w25) and widens it as a signed number for
# nvnTextureBuilderSetStorage's offset and for SetPackagedTextureData's source (the pool's CPU address + offset). The
# shipped heap table keeps every pool far below 2 GiB; a texture pack's heap table with TOTK_APP_POOL raised can make
# one larger (MacNativeGames saw a 3.58 GiB pool), and every texture past 2 GiB then got storage 4 GiB below its pool.
# The offset is widened unsigned instead, the same value below 2 GiB, right up to 4 GiB. 1.4.3 needs no patch: its
# loaders (main+0x583c34, main+0x27ed214) keep the offset in 64 bits.
set pooloff TOTK_POOL_OFFSET_PATCH 1
site pooloff main ad43f0 93407f22 2a1903e2 # sxtw x2, w25 -> mov w2, w25 (SetStorage offset)
site pooloff main ad440c 8b39c101 8b394101 # add x1, x8, w25, sxtw -> add x1, x8, w25, uxtw (packaged data source)
# Spin waits, from tools/spin_waits.py --exefs build/extracted/v121/exefs --patches recomp/patches-v121.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 main ac600 ac620 ac630 # 13 instructions; loads
@@ -941,3 +959,12 @@ 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
# Sun shadow map size (runtime/resolution.c shadow_init switches the set on). Each frame the engine sizes its shadow
# cascades (a D16 4-layer array) in main+0xcebdf4: a base size (960 in the field), interpolated down by how close the
# cover is, rounded up to 8 at main+0xcec22c and stored as the width (+0x3d8 of the cascade set) and, through the packed
# store at +0xe4, the height (+0x3f8). Near the ground it falls to about 400, which draws foliage shadows blocky, and
# every change of size moves the shadow texels, so leaf shadows jump instead of sliding. The page's fixed size (+92)
# goes in just before the rounding, as the community's shadow resolution mods do (1.1.0: mov w11, #1024 over the
# computation). Found by a write watch on the size field (the writer was this function, and no other).
site shadow main cec22c 11001d6b @page:x11 b9405d6b 11001d6b # add w11, w11, #7 -> adrp x11, page; ldr w11, [x11, #92]; add w11, w11, #7
+31
View File
@@ -10,6 +10,8 @@ set cloth TOTK_CLOTH_REALTIME 1
set bounce TOTK_HIGHFPS_FIX 1
set output TOTK_PATCH_OUTPUT 0
set scene TOTK_PATCH_SCENE 0
set shadow TOTK_PATCH_SHADOW 0
set cascade TOTK_PATCH_CASCADE 0
# 60 fps: the adaptive present-interval controller (main+0x2db174, 1.2.1 0x8f26c8) gets the minimum interval 1.
site fps60 main 2db204 6b08015f 7100055f # clamp: cmp w10, #1 (was cmp w10, w8 = minimum)
@@ -32,6 +34,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 +94,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
@@ -960,3 +978,16 @@ spin subsdk0 1c4e54 1c4e60 # 4 instructions; loads
spin sdk 2d3654 2d3668 # 6 instructions; loads
spin sdk 443864 443884 # 9 instructions; loads
spin sdk 541e54 541f98 # 32 instructions; loads
# Sun shadow map size (patches-v121.txt explains the set and the page). 1.4.3 interpolates the size between a minimum
# and the base and rounds it up to 8 at main+0x683604; the rounded value is the width (+0x3d8) and, through the packed
# store at +0xf4, the height (+0x3f8). The page's size goes in just before the rounding.
site shadow main 683604 11001d6b @page:x11 b9405d6b 11001d6b # add w11, w11, #7 -> adrp x11, page; ldr w11, [x11, #92]; add w11, w11, #7
# Sun shadow cascade distances (runtime/resolution.c res_shadow_range switches the set on). The depth shadow parameter
# object (main+0x6fa3c0) registers depth_shadow_near_0_1..3 = 20, 40, 80 m and depth_shadow_far_0 = 160 m as
# immediates, and the field's .bgsdw files do not override them; with the set on they come from page +64..+76.
site cascade main 6fabb8 52a83408 @page:x8 b9404108 # mov w8, #20.0 -> ldr w8, [page+64]
site cascade main 6fac40 52a84408 @page:x8 b9404508 # mov w8, #40.0 -> ldr w8, [page+68]
site cascade main 6facc8 52a85408 @page:x8 b9404908 # mov w8, #80.0 -> ldr w8, [page+72]
site cascade main 6fad4c 52a86408 @page:x8 b9404d08 # mov w8, #160.0 -> ldr w8, [page+76]
+22 -15
View File
@@ -13,14 +13,17 @@ 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
set takes words outside these.
(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), +92..95 the sun shadow map size
(runtime/resolution.c shadow_init, set shadow). 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
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Replacement
words must not branch except a final `bl` (lifted with the site's return address); the original must not be a branch
other than `bl`. So the function's control flow, labels and jump tables are the original's. The generator writes
patches.h (set names, switches, the page address) next to layout.h for the runtime.
`if (rt_patch_on[<set>]) { replacement } else { original }`: one byte load and a branch per executed site. Several
sets may patch one address (cheats that write different values there): `if (a) {..} else if (b) {..} else {..}`, the
first set in file order wins. The original may be any instruction but br, blr and ret; a replacement's last word may be
a bl (lifted with the site's return address), a b or b.cond / cbz / tbz (its target joins the function's labels when
inside it) or a ret; earlier words must not branch. Every alternative of a site writes the same flags. The generator
writes patches.h (set names, switches, the page address) next to layout.h for the runtime.
A `spin` line names a spin wait found by tools/spin_waits.py: a loop with head <head> that only another thread can end,
and the direct branches back to it (module offsets). Each back-edge (checked: a b, b.cond, cbz / cbnz or tbz / tbnz to
@@ -45,7 +48,8 @@ def adrp(pc, target, rd):
class Table:
"""sets: [(name, env, default)]; sites: {module: {offset: (set index, [word | ('page', reg)], original)}}; page."""
"""sets: [(name, env, default)]; sites: {module: {offset: [(set index, [word | ('page', reg)], original)]}} (the
alternatives of a site in file order); page."""
def __init__(self, sets=(), sites=None, page=0, spins=None):
self.sets = list(sets)
@@ -58,11 +62,12 @@ class Table:
return {base + b: base + h for b, h in self.spins.get(name, {}).items()}
def for_module(self, name, base):
"""{absolute address: (set index, [words])} with @page resolved."""
"""{absolute address: [(set index, [words])]} with @page resolved."""
out = {}
for off, (k, repl, _) in self.sites.get(name, {}).items():
for off, alts in self.sites.get(name, {}).items():
a = base + off
out[a] = (k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
out[a] = [(k, [adrp(a, self.page, w[1]) if isinstance(w, tuple) else w for w in repl])
for k, repl, _ in alts]
return out
@@ -92,10 +97,12 @@ def load(path, mods):
repl.append(('page', int(w[7:])))
else:
repl.append(int(w, 16))
mod = sites.setdefault(f[2], {})
if off in mod:
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice')
mod[off] = (index[f[1]], repl, orig)
alts = sites.setdefault(f[2], {}).setdefault(off, [])
if any(k == index[f[1]] for k, _, _ in alts):
raise PatchError(f'{where}: {f[2]}+{off:#x} patched twice by set {f[1]}')
if alts and alts[0][2] != orig:
raise PatchError(f'{where}: {f[2]}+{off:#x} expects {orig:08x}, an earlier line {alts[0][2]:08x}')
alts.append((index[f[1]], repl, orig))
elif f[0] == 'spin' and len(f) >= 4:
if f[1] not in bases:
raise PatchError(f'{where}: module {f[1]} not in the layout')
@@ -117,7 +124,7 @@ def load(path, mods):
def check(table, mod, name):
"""Stops on a site whose original word differs from the module's."""
for off, (_, _, want) in table.sites.get(name, {}).items():
for off, [(_, _, want), *_] in table.sites.get(name, {}).items():
a = mod.base + off
if not mod.in_text(a):
raise PatchError(f'{name}+{off:#x}: patch site outside .text')
+400
View File
@@ -0,0 +1,400 @@
// Amiibo dump loader and Horizon OS NFP state emulation.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "amiibo.h"
#include "host.h"
#include "platform/platform.h"
static PlatMutex g_amiibo_lock = PLAT_MUTEX_INIT;
// 540-byte raw NTAG215 binary dump
static uint8_t g_dump[AMIIBO_DUMP_SIZE];
static bool g_has_dump = false;
static AmiiboInfo g_info;
static bool g_device_present = true;
static bool g_detecting = false;
static bool g_tag_present = false;
static bool g_mounted = false;
static bool g_auto_remove = true;
static float g_auto_remove_delay_s = 2.5f;
static uint64_t g_tap_timestamp_ns = 0;
static bool g_timer_active = false;
static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
if (size != AMIIBO_DUMP_SIZE) {
return false;
}
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C). An amiibo's is F1 10 FF EE, not the E1 of a plain
// NDEF tag (3dbrew, "Amiibo"); the CC is not encrypted, so encrypted dumps carry it too.
static const uint8_t amiibo_cc[4] = {0xF1, 0x10, 0xFF, 0xEE};
if (memcmp(data + 0x0C, amiibo_cc, sizeof amiibo_cc)) {
return false;
}
// UID sanity: UID (bytes 0..2 and 4..7) cannot be all zeroes or all 0xFF.
if ((data[0] == 0 && data[1] == 0 && data[2] == 0 && data[4] == 0 &&
data[5] == 0 && data[6] == 0 && data[7] == 0) ||
(data[0] == 0xFF && data[1] == 0xFF && data[2] == 0xFF && data[4] == 0xFF &&
data[5] == 0xFF && data[6] == 0xFF && data[7] == 0xFF)) {
return false;
}
// ISO 14443-3 / NTAG215 Bit Check Character (BCC) verification.
// Page 0 byte 3 (BCC0) = 0x88 ^ UID[0] ^ UID[1] ^ UID[2]
// Page 2 byte 0 (BCC1) = UID[3] ^ UID[4] ^ UID[5] ^ UID[6]
uint8_t bcc0 = (uint8_t)(0x88 ^ data[0] ^ data[1] ^ data[2]);
uint8_t bcc1 = (uint8_t)(data[4] ^ data[5] ^ data[6] ^ data[7]);
if (data[3] != bcc0 || data[8] != bcc1) {
return false;
}
return true;
}
// The 7-byte UID of an NTAG215 dump: bytes 0..2, then 4..7. Byte 3 is the check byte BCC0.
static void copy_uid(uint8_t out[7], const uint8_t *dump) {
memcpy(out, dump, 3);
memcpy(out + 3, dump + 4, 4);
}
static void parse_dump_info(const char *path) {
memset(&g_info, 0, sizeof(g_info));
if (path) {
snprintf(g_info.path, sizeof(g_info.path), "%s", path);
const char *slash = strrchr(path, '\\');
const char *fslash = strrchr(path, '/');
const char *name = slash ? slash + 1 : (fslash ? fslash + 1 : path);
if (slash && fslash) {
name = (slash > fslash ? slash : fslash) + 1;
}
snprintf(g_info.filename, sizeof(g_info.filename), "%s", name);
}
copy_uid(g_info.uuid, g_dump);
g_info.uuid_len = 7;
// Amiibo ID at offset 0x54 (8 bytes, 3dbrew "Amiibo"): character ID and variant, figure type, model number
// (big-endian), series, format version 02
memcpy(g_info.character_id, g_dump + 0x54, 3);
g_info.nfp_type = g_dump[0x57];
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
g_info.series_id = g_dump[0x5A];
g_info.version = g_dump[0x5B];
}
void amiibo_init(void) {
plat_mutex_lock(&g_amiibo_lock);
g_device_present = true;
g_detecting = false;
g_tag_present = false;
g_mounted = false;
g_has_dump = false;
g_timer_active = false;
memset(g_dump, 0, sizeof(g_dump));
memset(&g_info, 0, sizeof(g_info));
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_set_device_present(bool present) {
plat_mutex_lock(&g_amiibo_lock);
g_device_present = present;
plat_mutex_unlock(&g_amiibo_lock);
}
bool amiibo_is_device_present(void) {
plat_mutex_lock(&g_amiibo_lock);
bool res = g_device_present;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
// While the game waits for a tag it stops and starts detection again several times a second (tens with the menu open),
// so the log names the first few and then a count every 200th start.
static uint64_t g_detect_starts;
static uint64_t g_detect_stop_ns; // the last stop: the menu holds "searching" across the game's restarts
void amiibo_start_detection(void) {
plat_mutex_lock(&g_amiibo_lock);
g_detecting = true;
uint64_t n = ++g_detect_starts;
if (n <= 3) rt_log("amiibo: start detection");
else if (n % 200 == 0) rt_log("amiibo: detection started %llu times so far", (unsigned long long)n);
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_stop_detection(void) {
plat_mutex_lock(&g_amiibo_lock);
if (g_detecting) g_detect_stop_ns = plat_now_ns();
g_detecting = false;
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
plat_mutex_unlock(&g_amiibo_lock);
}
bool amiibo_is_detecting(void) {
plat_mutex_lock(&g_amiibo_lock);
bool res = g_detecting;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
bool amiibo_mount(void) {
plat_mutex_lock(&g_amiibo_lock);
if (!g_device_present || !g_tag_present) {
plat_mutex_unlock(&g_amiibo_lock);
return false;
}
g_mounted = true;
rt_log("amiibo: mounted tag '%s'", g_info.filename[0] ? g_info.filename : "unknown");
plat_mutex_unlock(&g_amiibo_lock);
return true;
}
void amiibo_unmount(void) {
plat_mutex_lock(&g_amiibo_lock);
g_mounted = false;
rt_log("amiibo: unmounted tag");
plat_mutex_unlock(&g_amiibo_lock);
}
bool amiibo_is_mounted(void) {
plat_mutex_lock(&g_amiibo_lock);
bool res = g_mounted;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
AmiiboDeviceState amiibo_get_device_state(void) {
plat_mutex_lock(&g_amiibo_lock);
if (!g_device_present) {
plat_mutex_unlock(&g_amiibo_lock);
return AMIIBO_DEV_STATE_UNAVAILABLE;
}
if (g_mounted) {
plat_mutex_unlock(&g_amiibo_lock);
return AMIIBO_DEV_STATE_TAG_MOUNTED;
}
if (g_tag_present) {
plat_mutex_unlock(&g_amiibo_lock);
return AMIIBO_DEV_STATE_TAG_FOUND;
}
if (g_detecting) {
plat_mutex_unlock(&g_amiibo_lock);
return AMIIBO_DEV_STATE_SEARCHING;
}
plat_mutex_unlock(&g_amiibo_lock);
return AMIIBO_DEV_STATE_INITIALIZED;
}
AmiiboDeviceState amiibo_get_reader_state(void) {
AmiiboDeviceState state = amiibo_get_device_state();
if (state != AMIIBO_DEV_STATE_INITIALIZED) return state;
plat_mutex_lock(&g_amiibo_lock);
bool recent = g_detect_stop_ns && plat_now_ns() - g_detect_stop_ns < 1000000000ull;
plat_mutex_unlock(&g_amiibo_lock);
return recent ? AMIIBO_DEV_STATE_SEARCHING : state;
}
bool amiibo_load_file(const char *path) {
if (!path || !*path) return false;
uint64_t size = 0;
uint8_t *data = plat_file_read(path, &size);
if (!data) {
rt_log("amiibo: failed to read file: %s", path);
return false;
}
if (size != AMIIBO_DUMP_SIZE) {
rt_log("amiibo: invalid dump size (%llu bytes, expected exactly %d): %s",
(unsigned long long)size, AMIIBO_DUMP_SIZE, path);
free(data);
return false;
}
if (!amiibo_validate_dump(data, size)) {
rt_log("amiibo: dump failed NTAG215 structure sanity checks: %s", path);
free(data);
return false;
}
plat_mutex_lock(&g_amiibo_lock);
memcpy(g_dump, data, AMIIBO_DUMP_SIZE);
g_has_dump = true;
parse_dump_info(path);
plat_mutex_unlock(&g_amiibo_lock);
free(data);
rt_log("amiibo: loaded dump '%s' (char_id: %02x%02x%02x, series: %02x, num: %04x)",
g_info.filename, g_info.character_id[0], g_info.character_id[1], g_info.character_id[2],
g_info.series_id, g_info.numbering_id);
return true;
}
void amiibo_tap_tag(void) {
plat_mutex_lock(&g_amiibo_lock);
if (!g_has_dump) {
plat_mutex_unlock(&g_amiibo_lock);
return;
}
g_tag_present = true;
g_tap_timestamp_ns = plat_now_ns();
if (g_auto_remove) {
g_timer_active = true;
}
rt_log("amiibo: tag tapped on reader ('%s')", g_info.filename);
plat_mutex_unlock(&g_amiibo_lock);
}
bool amiibo_scan_and_tap(const char *path) {
if (!amiibo_load_file(path)) return false;
amiibo_tap_tag();
return true;
}
void amiibo_remove_tag(void) {
plat_mutex_lock(&g_amiibo_lock);
if (g_tag_present || g_mounted) {
g_tag_present = false;
g_mounted = false;
g_timer_active = false;
rt_log("amiibo: tag removed from reader");
}
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_tick(void) {
plat_mutex_lock(&g_amiibo_lock);
if (g_timer_active && g_tag_present) {
uint64_t now = plat_now_ns();
uint64_t diff = now > g_tap_timestamp_ns ? now - g_tap_timestamp_ns : 0;
uint64_t limit_ns = (uint64_t)(g_auto_remove_delay_s * 1000000000.0f);
if (diff >= limit_ns) {
g_tag_present = false;
g_mounted = false;
g_timer_active = false;
rt_log("amiibo: auto-removed tag after %.1fs timeout", (double)g_auto_remove_delay_s);
}
}
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_set_auto_remove(bool enable) {
plat_mutex_lock(&g_amiibo_lock);
g_auto_remove = enable;
if (!enable) g_timer_active = false;
plat_mutex_unlock(&g_amiibo_lock);
}
bool amiibo_get_auto_remove(void) {
plat_mutex_lock(&g_amiibo_lock);
bool res = g_auto_remove;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
void amiibo_set_auto_remove_delay_s(float seconds) {
plat_mutex_lock(&g_amiibo_lock);
if (seconds >= 0.5f && seconds <= 10.0f) g_auto_remove_delay_s = seconds;
plat_mutex_unlock(&g_amiibo_lock);
}
float amiibo_get_auto_remove_delay_s(void) {
plat_mutex_lock(&g_amiibo_lock);
float res = g_auto_remove_delay_s;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
bool amiibo_has_tag(void) {
plat_mutex_lock(&g_amiibo_lock);
bool res = g_has_dump && g_tag_present;
plat_mutex_unlock(&g_amiibo_lock);
return res;
}
const AmiiboInfo *amiibo_get_current_info(void) {
return &g_info;
}
const uint8_t *amiibo_get_raw_data(void) {
return g_dump;
}
void amiibo_get_tag_info(void *out_tag_info) {
plat_mutex_lock(&g_amiibo_lock);
uint8_t *out = (uint8_t *)out_tag_info;
memset(out, 0, 0x58);
// UUID: offset 0x00, 10 bytes max
copy_uid(out, g_dump);
out[0x0A] = 7; // UUID length
// Protocol at offset 0x20: TypeA = 1
*(uint32_t *)(out + 0x20) = 1;
// TagType at offset 0x24: Type2 / NTAG215 = 2
*(uint32_t *)(out + 0x24) = 2;
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_get_model_info(void *out_model_info) {
plat_mutex_lock(&g_amiibo_lock);
uint8_t *out = (uint8_t *)out_model_info;
memset(out, 0, 0x40);
// nn::nfp::ModelInfo from the amiibo ID at 0x54: character ID and variant in the tag's byte order, series, model
// number little-endian (big-endian on the tag), figure type. The game tells figures of one character apart by the
// model number.
memcpy(out + 0x00, g_dump + 0x54, 3); // character ID, variant
out[0x03] = g_dump[0x5A]; // series
out[0x04] = g_dump[0x59]; // model number
out[0x05] = g_dump[0x58];
out[0x06] = g_dump[0x57]; // figure type
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_get_common_info(void *out_common_info) {
plat_mutex_lock(&g_amiibo_lock);
uint8_t *out = (uint8_t *)out_common_info;
memset(out, 0, 0x40);
// Last write date: 0x00..0x03
out[0x00] = 0xE7; out[0x01] = 0x07; // 2023, u16 little-endian
out[0x02] = 0x05; out[0x03] = 0x0C; // May 12
// Write counter: 0x04..0x05
out[0x04] = 0x00; out[0x05] = 0x01;
// Version: 0x06
out[0x06] = 0x02;
// Application area size: 0x08..0x0B = 0xD8 (216 bytes)
*(uint32_t *)(out + 0x08) = 0xD8;
plat_mutex_unlock(&g_amiibo_lock);
}
void amiibo_get_register_info(void *out_register_info) {
plat_mutex_lock(&g_amiibo_lock);
uint8_t *out = (uint8_t *)out_register_info;
memset(out, 0, 0x100);
// Mii CharInfo at 0x00..0x57 (dummy valid mii)
out[0x00] = 0x03; // version 3
// First write date at 0x58..0x5B
out[0x58] = 0xE7; out[0x59] = 0x07; // 2023, u16 little-endian
out[0x5A] = 0x05; out[0x5B] = 0x0C;
// Amiibo Name at 0x5C: copy up to 40 bytes NUL-terminated
const char *name = g_info.filename[0] ? g_info.filename : "Amiibo";
snprintf((char *)(out + 0x5C), 0x29, "%s", name);
plat_mutex_unlock(&g_amiibo_lock);
}
int amiibo_get_application_area(void *out_buf, uint32_t max_size) {
plat_mutex_lock(&g_amiibo_lock);
if (!g_has_dump || !out_buf) {
plat_mutex_unlock(&g_amiibo_lock);
return 0;
}
uint32_t to_copy = max_size < 0xD8 ? max_size : 0xD8;
// Decrypted/payload application area starts at offset 0xDC (or 0x0A0..0x178 in raw dump)
if (0xDC + to_copy <= AMIIBO_DUMP_SIZE) {
memcpy(out_buf, g_dump + 0xDC, to_copy);
} else {
memset(out_buf, 0, to_copy);
}
plat_mutex_unlock(&g_amiibo_lock);
return (int)to_copy;
}
+87
View File
@@ -0,0 +1,87 @@
// Amiibo loader and state management subsystem (540-byte NTAG215 binary dumps).
#pragma once
#include <stdbool.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#define AMIIBO_DUMP_SIZE 540
// Device state corresponding to nn::nfp / nn::nfc DeviceState
typedef enum {
AMIIBO_DEV_STATE_INITIALIZED = 0,
AMIIBO_DEV_STATE_SEARCHING = 1,
AMIIBO_DEV_STATE_TAG_FOUND = 2,
AMIIBO_DEV_STATE_TAG_REMOVED = 3,
AMIIBO_DEV_STATE_TAG_MOUNTED = 4,
AMIIBO_DEV_STATE_UNAVAILABLE = 5
} AmiiboDeviceState;
// Amiibo identification info
typedef struct {
uint8_t uuid[10];
uint8_t uuid_len;
uint8_t character_id[3];
uint8_t series_id;
uint16_t numbering_id;
uint8_t nfp_type;
uint8_t version;
char path[512];
char filename[128];
} AmiiboInfo;
// Subsystem lifecycle
void amiibo_init(void);
// Hardware / device emulation status
void amiibo_set_device_present(bool present);
bool amiibo_is_device_present(void);
// Tag detection & lifecycle
void amiibo_start_detection(void);
void amiibo_stop_detection(void);
bool amiibo_is_detecting(void);
// Tag mounting
bool amiibo_mount(void);
void amiibo_unmount(void);
bool amiibo_is_mounted(void);
// State query
AmiiboDeviceState amiibo_get_device_state(void);
// The state the menu shows: searching also within a second after a stop, since the game restarts detection many times
// a second while it waits for a tag. The game reads amiibo_get_device_state.
AmiiboDeviceState amiibo_get_reader_state(void);
// File loading & triggering actions
bool amiibo_load_file(const char *path);
bool amiibo_scan_and_tap(const char *path);
void amiibo_tap_tag(void);
void amiibo_remove_tag(void);
// Timer tick called during menu / frame updates
void amiibo_tick(void);
// Options
void amiibo_set_auto_remove(bool enable);
bool amiibo_get_auto_remove(void);
void amiibo_set_auto_remove_delay_s(float seconds);
float amiibo_get_auto_remove_delay_s(void);
// Tag inspection
bool amiibo_has_tag(void);
const AmiiboInfo *amiibo_get_current_info(void);
const uint8_t *amiibo_get_raw_data(void);
// Horizon OS buffers extraction (called by NFP IPC/override services)
void amiibo_get_tag_info(void *out_tag_info);
void amiibo_get_model_info(void *out_model_info);
void amiibo_get_common_info(void *out_common_info);
void amiibo_get_register_info(void *out_register_info);
int amiibo_get_application_area(void *out_buf, uint32_t max_size);
#ifdef __cplusplus
}
#endif
+7 -4
View File
@@ -22,8 +22,11 @@ struct AudioEffectDsp {
};
static int channel_ok(unsigned n) { return n==1 || n==2 || n==4 || n==6; }
static unsigned sample_count(unsigned rate, float ms) { return (unsigned)nearbyintf(rate*ms); }
/* The reference DelayLine wraps at CurrentSampleCount-1, including when its
backing allocation is larger. Preserve this timing for sample comparisons. */
/* The ring has `count` slots. The reference DelayLine wraps its cursor at
CurrentSampleCount-1 while its taps index modulo CurrentSampleCount, so once
per turn every tap reads a slot that is never written (a one-sample dropout
per tap, audible as grain). The cursor and the taps share one ring length here;
each delay is one sample longer than the reference. */
static void line_set(DelayLine *d, unsigned n) { d->count=n<d->max?n:d->max; d->at=0; }
static void line_init(DelayLine *d, unsigned rate, float ms) {
d->max=sample_count(rate,ms); d->data=calloc((size_t)d->max+1,sizeof(float));
@@ -32,12 +35,12 @@ static void line_init(DelayLine *d, unsigned rate, float ms) {
static float line_read(const DelayLine *d) { return d->data[d->at]; }
static float line_update(DelayLine *d,float v) {
float old=line_read(d);d->data[d->at++]=v;
if(d->at >= (d->count?d->count-1:0))d->at=0;
if(d->at >= d->count)d->at=0;
return old;
}
static float line_tap(const DelayLine *d,unsigned sample,int offset) {
int at=(int)d->at-(int)sample-offset;
if(at<0)at+=(int)d->count;
if(d->count){at%=(int)d->count;if(at<0)at+=(int)d->count;}
if(at<0 || (unsigned)at>d->max)rt_fatal("audio effect delay tap bounds");
return d->data[at];
}
+144 -46
View File
@@ -5,10 +5,11 @@
//
// Per 5 ms frame the render thread (audren.c, renderer lock held) decodes every started voice (PCM8/16/24/32, float,
// DSP-ADPCM) along the sample path the voice clock consumes, resamples it linearly, runs the voice biquads, scales it
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's stereo mix into a
// single-producer single-consumer ring. The SDL audio thread drains that ring without taking any lock: an empty ring
// into its destination mix, folds the submixes, runs the mix effects and writes the device sink's mix (stereo, or 5.1
// with TOTK_AUDIO_CHANNELS, output_channels) into a single-producer single-consumer ring. The SDL audio thread drains
// that ring without taking any lock: an empty ring
// plays silence and never blocks the render thread or the guest. TOTK_AUDIO=none (or TOTK_AUDIO_OFF) mixes without a
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20) is the queued audio the output waits for after start or underrun.
// device; TOTK_AUDIO_PREFILL_MS (0..500, default 20, 60 on Linux) is the queued audio the output waits for after start or underrun.
// The device's clock paces the render thread, as the audio hardware paces the Switch's DSP (audio_mixer_pace_ns): a
// frame is rendered when the ring holds less than the output target, max(prefill, two device periods), so the queue and
// the delay from the game to the speakers stay there and the host and device clocks cannot drift apart. LASRecomp and
@@ -32,7 +33,8 @@
#include "ipc.h"
#define MAX_FRAME 1024
#define RING_FRAMES 16384 // stereo frames, about 340 ms at 48 kHz
#define RING_FRAMES 16384 // output frames, about 340 ms at 48 kHz
#define OUT_MAX 6 // output channels at most: 5.1
#define QUEUE_SLACK_MS 40 // queued audio above the target plus the device's request that ring_pull skips
#define STALL_MS 50 // the device took nothing this long (at least 4 periods): the timer paces
#define SPLITTER_UNUSED 0xffffffffu
@@ -51,7 +53,8 @@ typedef struct Mixer {
int *offset; // first mix buffer of each mix id
SplitInfo *split; int nsplit; SplitDest *dest; int ndest;
SDL_AudioStream *stream; int device_ok, rate;
float ring[RING_FRAMES * 2];
int channels; // output channels, 2 or 6 (output_channels)
float ring[RING_FRAMES * OUT_MAX];
_Atomic uint64_t wr, rd, underruns, overruns, missing_frames, callback_frames, callback_nonzero, skipped_frames;
_Atomic int playing; int ready; // ready: the callback's own state
int target, period; // output target and device period, frames (output_open)
@@ -61,7 +64,7 @@ typedef struct Mixer {
EffectState *effect; int *effect_order; unsigned effect_reported;
uint64_t *sink_address; uint32_t *sink_size, *sink_offset, *sink_old, *sink_written;
unsigned reported; // one-time reports
float voice_scratch[6][MAX_FRAME], filter_scratch[MAX_FRAME], output_scratch[MAX_FRAME * 2];
float voice_scratch[6][MAX_FRAME], filter_scratch[MAX_FRAME], output_scratch[MAX_FRAME * OUT_MAX];
double level_sum, level_peak; uint64_t level_count, level_frames; int first_sound_logged;
int prefill; // render blocks
} Mixer;
@@ -90,16 +93,47 @@ static void report_once(Mixer *m, unsigned bit, const char *msg, int value) {
// ---- output: SDL3 audio stream fed from the ring
// How much audio the output waits for before it starts, and again after an underrun: TOTK_AUDIO_PREFILL_MS rounded up
// to whole 5 ms render blocks. Returns -1 for a value outside 0..500 ms.
// The Linux default is higher: at 20 ms PipeWire's queue (max(prefill, two 1024-sample periods)) underran
// thousands of times within minutes in busy scenes, and 60 ms removed the underruns.
#ifdef __linux__
#define PREFILL_DEFAULT_MS 60
#else
#define PREFILL_DEFAULT_MS 20
#endif
static int prefill_blocks_for(const char *e) {
if (!e || !*e) return 4;
if (!e || !*e) return PREFILL_DEFAULT_MS / 5;
char *end;
double ms = strtod(e, &end);
if (end == e || *end || !(ms >= 0) || ms > 500) return -1;
return (int)ceil(ms / 5.0);
}
// Fills `frames` stereo frames from the ring (zeros for what is missing). Lock-free; runs on SDL's audio thread.
// TOTK_AUDIO_CHANNELS: the output channels of this run, decided once. stereo gives 2; 5.1 gives 6 (FL FR C LFE RL RR,
// the device sink's order and SDL's); auto gives 6 when the default playback device has at least six channels (a 5.1
// or 7.1 speaker setup in Windows; SDL places 5.1 on 7.1) and 2 otherwise. IAudioDevice reports the same count to the
// game (GetActiveChannelCount), which picks its sink layout from it. TOTK_AUDIO=none: 2, without touching SDL.
static int output_channels(void) {
static _Atomic int decided;
int n = atomic_load(&decided);
if (n) return n;
const char *want = cfg_value("TOTK_AUDIO_CHANNELS"), *mode = cfg_get("TOTK_AUDIO");
if ((mode && !strcmp(mode, "none")) || cfg_get("TOTK_AUDIO_OFF")) n = 2;
else if (!strcmp(want, "5.1")) n = 6;
else if (!strcmp(want, "stereo")) n = 2;
else {
SDL_AudioSpec dev = {0};
n = SDL_InitSubSystem(SDL_INIT_AUDIO) && SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &dev, NULL) &&
dev.channels >= 6 ? 6 : 2;
rt_log("audio: TOTK_AUDIO_CHANNELS=auto: the default device has %d channels, output %s", dev.channels,
n == 6 ? "5.1" : "stereo");
}
int expected = 0;
return atomic_compare_exchange_strong(&decided, &expected, n) ? n : expected;
}
// Fills `frames` output frames from the ring (zeros for what is missing). Lock-free; runs on SDL's audio thread.
static void ring_pull(Mixer *m, float *out, uint32_t frames) {
const int oc = m->channels;
atomic_fetch_add(&m->callback_frames, frames);
unsigned epoch = atomic_load_explicit(&m->stop_epoch, memory_order_acquire);
uint64_t rd = atomic_load(&m->rd), wr = atomic_load_explicit(&m->wr, memory_order_acquire), have = wr - rd;
@@ -114,7 +148,7 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
m->ready = 0;
}
if (!atomic_load_explicit(&m->playing, memory_order_acquire)) {
memset(out, 0, (size_t)frames * 2 * sizeof(float));
memset(out, 0, (size_t)frames * oc * sizeof(float));
m->ready = 0;
atomic_store_explicit(&m->rd, wr, memory_order_release);
return;
@@ -124,7 +158,7 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
if (prefill > RING_FRAMES) prefill = RING_FRAMES;
if (!m->ready) {
if (have < prefill) {
memset(out, 0, (size_t)frames * 2 * sizeof(float));
memset(out, 0, (size_t)frames * oc * sizeof(float));
return;
}
m->ready = 1;
@@ -140,13 +174,16 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
uint64_t nonzero = 0;
for (uint32_t i = 0; i < n; i++) {
uint64_t k = (rd + i) % RING_FRAMES;
out[2 * i] = m->ring[2 * k];
out[2 * i + 1] = m->ring[2 * k + 1];
if (out[2 * i] != 0 || out[2 * i + 1] != 0) nonzero++;
int sound = 0;
for (int c = 0; c < oc; c++) {
out[oc * i + c] = m->ring[oc * k + c];
sound |= out[oc * i + c] != 0;
}
nonzero += sound;
}
atomic_fetch_add(&m->callback_nonzero, nonzero);
if (n < frames) {
memset(out + 2 * n, 0, (size_t)(frames - n) * 2 * sizeof(float));
memset(out + (size_t)oc * n, 0, (size_t)(frames - n) * oc * sizeof(float));
atomic_fetch_add(&m->underruns, 1);
atomic_fetch_add(&m->missing_frames, frames - n);
m->ready = 0;
@@ -157,15 +194,16 @@ static void ring_pull(Mixer *m, float *out, uint32_t frames) {
static void SDLCALL sdl_pull(void *user, SDL_AudioStream *stream, int additional, int total) {
(void)total;
Mixer *m = user;
float tmp[2 * 1024];
float tmp[OUT_MAX * 1024];
const uint32_t frame_bytes = (uint32_t)m->channels * sizeof(float);
atomic_store_explicit(&m->pulled_ns, kern_now_ns(), memory_order_release);
while (additional > 0) {
uint32_t frames = (uint32_t)additional / (2 * sizeof(float));
uint32_t frames = (uint32_t)additional / frame_bytes;
if (!frames) break;
if (frames > 1024) frames = 1024;
ring_pull(m, tmp, frames);
if (!SDL_PutAudioStreamData(stream, tmp, (int)(frames * 2 * sizeof(float)))) break;
additional -= (int)(frames * 2 * sizeof(float));
if (!SDL_PutAudioStreamData(stream, tmp, (int)(frames * frame_bytes))) break;
additional -= (int)(frames * frame_bytes);
}
}
@@ -179,7 +217,7 @@ static void output_open(Mixer *m, int rate) {
rt_log("audio: SDL audio init failed (%s): mixing without sound", SDL_GetError());
return;
}
SDL_AudioSpec spec = {SDL_AUDIO_F32, 2, rate};
SDL_AudioSpec spec = {SDL_AUDIO_F32, m->channels, rate};
m->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, sdl_pull, m);
if (!m->stream) {
rt_log("audio: no output device (%s): mixing without sound", SDL_GetError());
@@ -192,7 +230,7 @@ static void output_open(Mixer *m, int rate) {
return;
}
m->device_ok = 1;
SDL_AudioSpec dev;
SDL_AudioSpec dev = {0};
int period = 0;
if (!SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(m->stream), &dev, &period) || period <= 0) period = m->frame;
m->period = period;
@@ -201,8 +239,9 @@ static void output_open(Mixer *m, int rate) {
uint64_t period_ns = (uint64_t)period * 1000000000ull / (uint64_t)rate;
m->stall_ns = 4 * period_ns > STALL_MS * 1000000ull ? 4 * period_ns : STALL_MS * 1000000ull;
const char *name = SDL_GetAudioDeviceName(SDL_GetAudioStreamDevice(m->stream));
rt_log("audio: SDL device '%s' opened, %d Hz stereo float, prefill %d ms, device period %d frames; the device paces "
"rendering at %.1f ms queued", name ? name : "?", rate, m->prefill * 5, period, m->target * 1000.0 / rate);
rt_log("audio: SDL device '%s' opened, %d Hz %s float (device %d channels), prefill %d ms, device period %d frames; "
"the device paces rendering at %.1f ms queued", name ? name : "?", rate, m->channels == 6 ? "5.1" : "stereo",
dev.channels, m->prefill * 5, period, m->target * 1000.0 / rate);
}
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
@@ -217,7 +256,8 @@ int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now) {
}
// Render thread only.
static void output_push(Mixer *m, const float *lr, int frames) {
static void output_push(Mixer *m, const float *out, int frames) {
const int oc = m->channels;
uint64_t wr = atomic_load(&m->wr), rd = atomic_load_explicit(&m->rd, memory_order_acquire);
if (m->device_ok && wr + (uint64_t)frames - rd > RING_FRAMES) { // device stalled: drop this frame
atomic_fetch_add(&m->overruns, 1);
@@ -225,24 +265,51 @@ static void output_push(Mixer *m, const float *lr, int frames) {
}
for (int i = 0; i < frames; i++) {
uint64_t k = (wr + (uint64_t)i) % RING_FRAMES;
m->ring[2 * k] = lr[2 * i];
m->ring[2 * k + 1] = lr[2 * i + 1];
memcpy(&m->ring[oc * k], &out[oc * i], (size_t)oc * sizeof(float));
}
atomic_store_explicit(&m->wr, wr + (uint64_t)frames, memory_order_release);
if (!m->device_ok) atomic_store(&m->rd, wr + (uint64_t)frames); // no device: the ring only keeps the latest frames
}
// TOTK_AUDIO_DUMP=<path>: the sink mix before the clamp as raw interleaved float32 stereo, and every 5 s one log line with
// how many frames were rendered less than 1 ms after the one before (a burst refill). File writes on the render thread:
// for diagnosis only.
static void sink_dump(const float *raw, int frames) {
static FILE *f;
static int opened;
static uint64_t last, window_start, total, bursts;
if (!opened) {
opened = 1;
const char *path = cfg_get("TOTK_AUDIO_DUMP");
if (path && *path) f = fopen(path, "wb");
}
if (!f) return;
fwrite(raw, sizeof(float), (size_t)frames * 2, f);
uint64_t now = kern_now_ns();
total++;
if (last && now - last < 1000000ull) bursts++;
last = now;
if (!window_start) window_start = now;
if (now - window_start >= 5000000000ull) {
rt_log("audio: dump: %llu frames in %.1f s, %llu rendered under 1 ms after the previous", (unsigned long long)total,
(now - window_start) / 1e9, (unsigned long long)bursts);
total = bursts = 0;
window_start = now;
fflush(f);
}
}
// Level meter (TOTK_TRACE=audio): one line per 5 s of output, and the first nonzero frame.
static void level_note(AudioRenderer *r, Mixer *m, const float *lr, int frames) {
static void level_note(AudioRenderer *r, Mixer *m, const float *out, int frames) {
if (!(rt_trace & RT_TRACE_AUDIO)) return;
int sound = 0;
for (int k = 0; k < 2 * frames; k++) {
double x = lr[k];
for (int k = 0; k < m->channels * frames; k++) {
double x = out[k];
m->level_sum += x * x;
if (fabs(x) > m->level_peak) m->level_peak = fabs(x);
if (x != 0) sound = 1;
}
m->level_count += 2 * (uint64_t)frames;
m->level_count += (uint64_t)m->channels * (uint64_t)frames;
m->level_frames += (uint64_t)frames;
if (sound && !m->first_sound_logged) {
m->first_sound_logged = 1;
@@ -290,9 +357,10 @@ void audio_mixer_open(AudioRenderer *r) {
const char *pe = cfg_get("TOTK_AUDIO_PREFILL_MS");
m->prefill = prefill_blocks_for(pe);
if (m->prefill < 0) {
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using 20", pe);
m->prefill = 4;
rt_log("audio: TOTK_AUDIO_PREFILL_MS=%s is not 0..500 ms: using %d", pe, PREFILL_DEFAULT_MS);
m->prefill = PREFILL_DEFAULT_MS / 5;
}
m->channels = output_channels();
r->mixer = m;
atomic_store(&m->playing, r->state == 0);
output_open(m, r->param.sample_rate);
@@ -505,8 +573,25 @@ static void walk_step(Walk *w) { /* the same transitions as advance_voice in a
walk_skip_empty(w);
}
/* Decodes `frames` output samples per channel into out[c][k]; the renderer advances the voice afterwards by the same amount. */
static double voice_step(const AudioRenderer *r, const ServerVoice *v) {
return v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
}
/* Source samples the voice consumes over `frames` output samples, counted with the exact accumulation voice_render steps by
(float rounding makes `fraction + seconds * rate * pitch` differ from it by one sample on some frames, a seam click);
leaves the fractional position in v->fraction. */
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames) {
const double step = voice_step(r, v);
double frac = v->fraction;
uint64_t consumed = 0;
for (int k = 0; k < frames; k++) {
frac += step;
while (frac >= 1.0) { frac -= 1.0; consumed++; }
}
v->fraction = frac;
return consumed;
}
static void voice_render(AudioRenderer *r, ServerVoice *v, float out[6][MAX_FRAME], int frames) {
const double step = v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0) / r->param.sample_rate;
const double step = voice_step(r, v);
Walk w = {v, v->index, v->offset, v->loops_done, {0}, 0, v->adpcm.history[0], v->adpcm.history[1], v->adpcm.buffer, v->adpcm.at};
memcpy(w.valid, v->valid, sizeof w.valid);
walk_skip_empty(&w);
@@ -545,6 +630,7 @@ static void mix_aux(AudioRenderer *r, int mix, const AudrenEffectIn *e) {
uint8_t *send=ipc_guest(p.send_info,128+(uint64_t)p.capacity*4), *ret=ipc_guest(p.return_info,128+(uint64_t)p.capacity*4);
uint32_t *sh=(uint32_t *)(send+64),*rh=(uint32_t *)(ret+64); int32_t *sd=(int32_t *)(send+128),*rd=(int32_t *)(ret+128);
uint32_t wr=sh[1],read=rh[0],total=p.channels*(uint32_t)m->frame;
if(e->enabled) report_once(m,6,"auxiliary effect in use, channels",(int)p.channels);
for(unsigned c=0;c<p.channels;c++) {
if(p.input[c]>=r->mixes[mix].buffer_count || p.output[c]>=r->mixes[mix].buffer_count)rt_fatal("auxiliary effect mix buffer");
float *input=mixbuf(m,m->offset[mix]+p.input[c]),*output=mixbuf(m,m->offset[mix]+p.output[c]);
@@ -793,30 +879,42 @@ void audio_mixer_render(AudioRenderer *r, double seconds) {
if (r->sinks[i].type == SINK_DEVICE && !sink) sink = &r->sinks[i];
else if (r->sinks[i].type == SINK_CIRCULAR){mix_circular_sink(r,i);circular=1;}
} else {m->sink_address[i]=0;m->sink_size[i]=m->sink_offset[i]=m->sink_old[i]=m->sink_written[i]=0;}
float *lr=m->output_scratch;
memset(lr, 0, sizeof(float) * 2 * (size_t)frames);
const int oc = m->channels;
float *out=m->output_scratch;
memset(out, 0, sizeof(float) * (size_t)oc * (size_t)frames);
if (sink && sink->input_count >= 1 && sink->input_count <= 6) {
if (!(m->reported & (1u << 6))) {
m->reported |= 1u << 6;
rt_log("audio: device sink has %u inputs, output %s", sink->input_count, oc == 6 ? "5.1" : "stereo");
}
const float gain = host_volume() * atomic_load(&device_volume) * (r->mixes[0].in_use ? r->mixes[0].volume : 1.0f);
const float *ch[6] = {0};
float raw[2 * MAX_FRAME];
for (uint32_t c = 0; c < sink->input_count; c++) if (sink->inputs[c] >= 0 && sink->inputs[c] < m->nbuf) ch[c] = mixbuf(m, sink->inputs[c]);
for (int k = 0; k < frames; k++) {
float l, rr;
if (sink->input_count >= 6) { /* FL FR C LFE RL RR -> stereo */
float s[OUT_MAX] = {0}; /* FL FR C LFE RL RR */
if (sink->input_count >= 6 && oc == 6) {
for (int c = 0; c < 6; c++) s[c] = ch[c] ? ch[c][k] : 0;
} else if (sink->input_count >= 6) { /* 5.1 -> stereo; the LFE is left out, as the Switch's downmix does */
float ff = sink->downmix_enabled ? sink->downmix[0] : 1.0f, cf = sink->downmix_enabled ? sink->downmix[1] : 0.7071f, sf = sink->downmix_enabled ? sink->downmix[2] : 0.7071f;
float C = ch[2] ? ch[2][k] : 0;
l = ff * (ch[0] ? ch[0][k] : 0) + cf * C + sf * (ch[4] ? ch[4][k] : 0);
rr = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
} else if (sink->input_count >= 2) { l = ch[0] ? ch[0][k] : 0; rr = ch[1] ? ch[1][k] : 0; }
else l = rr = ch[0] ? ch[0][k] : 0;
l *= gain; rr *= gain;
lr[2 * k] = l > 1 ? 1 : l < -1 ? -1 : l; lr[2 * k + 1] = rr > 1 ? 1 : rr < -1 ? -1 : rr;
s[0] = ff * (ch[0] ? ch[0][k] : 0) + cf * C + sf * (ch[4] ? ch[4][k] : 0);
s[1] = ff * (ch[1] ? ch[1][k] : 0) + cf * C + sf * (ch[5] ? ch[5][k] : 0);
} else if (sink->input_count >= 2) { s[0] = ch[0] ? ch[0][k] : 0; s[1] = ch[1] ? ch[1][k] : 0; }
else s[0] = s[1] = ch[0] ? ch[0][k] : 0;
raw[2 * k] = s[0] * gain; raw[2 * k + 1] = s[1] * gain; /* TOTK_AUDIO_DUMP: the front pair before clipping */
for (int c = 0; c < oc; c++) {
float x = s[c] * gain;
out[oc * k + c] = x > 1 ? 1 : x < -1 ? -1 : x;
}
}
sink_dump(raw, frames);
} else if (!sink && !circular) report_once(m, 5, "no device sink in use", r->param.sink_count);
level_note(r, m, lr, frames);
output_push(m, lr, frames);
level_note(r, m, out, frames);
output_push(m, out, frames);
}
// ---- IAudioDevice: one stereo output device
// ---- IAudioDevice: one output device, stereo or 5.1 (output_channels)
static const char device_name[] = "AudioTvOutput";
static void put_name(const IpcBuffer *b, IpcResponse *rs, int pointer) {
@@ -855,7 +953,7 @@ void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs)
put_name(&b, rs, pointer);
return;
}
case 5: { int32_t n = 2; IPC_OUT(rs, n); return; } // GetActiveChannelCount
case 5: { int32_t n = output_channels(); IPC_OUT(rs, n); return; } // GetActiveChannelCount
case 4: case 11: case 12: { // QueryAudioDeviceSystemEvent, ...InputEvent, ...OutputEvent
uint32_t ev = __atomic_load_n(&change_event, __ATOMIC_ACQUIRE);
if (!ev) {
+2 -2
View File
@@ -24,8 +24,8 @@ static void delay_set(Delay *d, unsigned n) { if (n > d->cap) rt_fatal("reverb d
static void delay_init(Delay *d, unsigned khz, float ms) { d->cap = samples(khz, ms); d->data = calloc(d->cap + 1, sizeof(float)); if (!d->data) rt_fatal("reverb delay allocation"); delay_set(d, d->cap); }
static float delay_update(Delay *d, float v) {
d->data[d->write++] = v; float out = d->data[d->read++];
if (d->read >= d->cap) d->read = 0;
if (d->write >= d->cap) d->write = 0;
if (d->read > d->cap) d->read = 0;
if (d->write > d->cap) d->write = 0;
return out;
}
static float delay_tap(const Delay *d, unsigned at) { int i = (int)d->write - (int)at - 1; if (i < 0) i += (int)d->cap + 1; return d->data[i]; }
+21 -6
View File
@@ -318,11 +318,9 @@ static void request_update(AudioRenderer *r, const uint8_t *input, uint64_t in_s
// ---- render tick: one frame each time the output device has taken the ring below its target (audio_mixer_pace_ns),
// or every sample_count / sample_rate seconds of the host clock without a device, while stopped or while it stalls
static void advance_voice(AudioRenderer *r, ServerVoice *v, double out_seconds) {
static void advance_voice(AudioRenderer *r, ServerVoice *v) {
if (!v->in.in_use || v->state != SV_STARTED || !v->in.sample_rate) return;
double want = v->fraction + out_seconds * v->in.sample_rate * (v->in.pitch > 0 ? v->in.pitch : 1.0);
uint64_t samples = (uint64_t)want;
v->fraction = want - (double)samples;
uint64_t samples = audio_voice_consumed(r, v, (int)r->param.sample_count);
while (samples && v->valid[v->index]) {
AudrenWaveBuffer *b = &v->buffers[v->index];
uint64_t length = (uint64_t)(b->end - b->start), left = length - v->offset;
@@ -354,7 +352,19 @@ static void render_thread(void *p) {
plat_thread_priority(PLAT_PRIO_HIGHEST);
const uint64_t frame_ns = (uint64_t)r->param.sample_count * 1000000000ull / (uint64_t)r->param.sample_rate;
const double frame = (double)r->param.sample_count / r->param.sample_rate;
uint64_t next = kern_now_ns();
// When the device pulls a period (1024 samples on PipeWire, 4-5 frames) the ring drops below its target
// and the frames would otherwise render back to back. On hardware the DSP renders one frame every 5 ms and
// the game's audio thread works in between; AUX effects need that time to fill the return ring, or mix_aux
// reads stale samples and the frame clicks at its seams. Device-paced frames stay at least 4/5 of a frame
// apart, which still refills a period well before the device drains it. Linux only: on Windows SDL pulls
// 480-sample periods, and a sink dump (TOTK_AUDIO_DUMP) of the bursts there showed no more seam spikes than
// chance, so a wait on this thread buys nothing.
#ifdef __linux__
const uint64_t gap_ns = frame_ns * 4 / 5;
#else
const uint64_t gap_ns = 0;
#endif
uint64_t next = kern_now_ns(), last = 0;
while (plat_cas32(&r->running, 0, 0)) {
uint64_t now = kern_now_ns();
int64_t pace = audio_mixer_pace_ns(r, now);
@@ -362,6 +372,10 @@ static void render_thread(void *p) {
plat_wait_ns((uint64_t)pace);
continue;
}
if (pace == 0 && last && now - last < gap_ns) {
plat_wait_ns(gap_ns - (now - last));
continue;
}
if (pace == 0) {
next = now + frame_ns; // where the timer goes on if the device stops pacing
} else {
@@ -369,11 +383,12 @@ static void render_thread(void *p) {
else if (now - next > 100000000ull) next = now; // fell far behind (debugger, sleep): resynchronise
next += frame_ns;
}
last = kern_now_ns();
plat_mutex_lock(&r->lock);
if (r->state == 0) {
voices_prepare(r);
audio_mixer_render(r, frame);
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i], frame);
for (int i = 0; i < r->param.voice_count; i++) advance_voice(r, &r->voices[i]);
r->elapsed_frames++;
uint64_t given = r->stop_released + r->empty_played + r->flushed;
if ((rt_trace & RT_TRACE_IPC) && given != r->logged_released && r->elapsed_frames % 1000 == 0) {
+1
View File
@@ -184,5 +184,6 @@ void audio_mixer_render(AudioRenderer *r, double seconds); // after audio_voic
// many ns (the ring is at its target), -1 leaves the pace to the render thread's timer (no device, renderer stopped,
// device stalled).
int64_t audio_mixer_pace_ns(AudioRenderer *r, uint64_t now);
uint64_t audio_voice_consumed(const AudioRenderer *r, ServerVoice *v, int frames); // source samples over one frame; advances v->fraction
int audio_buffer_empty(const AudrenWaveBuffer *b); // no samples to decode: played as soon as it is reached
void audio_device_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs); // IAudioDevice
+18 -12
View File
@@ -3,37 +3,41 @@
// 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.0, 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>
#include <string.h>
#include "cfg.h"
#include "cheats.h"
#include "fs.h"
#include "gfx.h"
#include "mods.h"
#include "nvn_api.h"
#include "platform/platform.h"
#include "vk.h"
#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.0 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_V110 ? 0xeb8c14ull : 0xe7a954ull)
#define NVN_RET (RT_V143 ? 0xbe0360ull : RT_V110 ? 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.0 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_110[] = {{"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_V110 ? inits_110 : inits_121)
static volatile int32_t inits_done, checkpoint_seen;
uint64_t rt_guest_start_ns;
@@ -340,8 +344,10 @@ void rt_boot(void) {
PlatThread *warm = plat_thread_new(prewarm_main, NULL, "prewarm");
ldr_load(rt_config()->exefs);
rt_patches_init();
mods_exefs_switch(); // after the patch sets' settings: an enabled mod's ExeFS patches on
frame_step_init();
res_init();
cheats_init(); // after the patch sets: a cheat's set follows its setting
cb_addr[0] = hostfn_register("<rtld loader_ready>", cb_loader_ready);
cb_addr[1] = hostfn_register("<rtld module_inits>", cb_module_inits);
cb_addr[2] = hostfn_register("<rtld module_finis>", cb_module_finis);
+159
View File
@@ -0,0 +1,159 @@
// Camera speed: TOTK_STICK_SENS (0.1 to 3, default 1, read at start) multiplies the game's own right-stick camera speeds.
// The game's Options menu offers five camera sensitivities, Very Low to Very High; their turn rates are the array
// StickSensitivity {0.8, 1.2, 1.6, 2.0, 2.4} in Component/PlayerCameraParam/PlayerCamera.game__component__
// PlayerCameraParam.bgyml inside Pack/Actor/PlayerCamera.pack.zs. romfs.c serves a copy of that pack with every entry
// multiplied (cam_pack_build), so the setting raises the camera's top speed as well as its speed at a partial tilt, at
// any of the five sensitivities. The mouse camera reaches the game as a right-stick deflection (input.c, mouse_camera),
// which divides it by the same factor (cam_speed_applied), so TOTK_MOUSE_SENS alone sets the mouse's speed.
//
// The pack is a SARC archive compressed with the dictionary pack.zsdic of Pack/ZsDic.pack.zs. A float in a BYML array
// is stored in the array itself, so the multiplied values take the bytes of the old ones and no offset moves.
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <zstd.h>
#include "cfg.h"
#include "host.h"
#define MEMBER "Component/PlayerCameraParam/PlayerCamera.game__component__PlayerCameraParam.bgyml"
#define MAX_RAW (64u << 20)
static float applied = 1.0f;
float cam_speed_wanted(void) {
double v = cfg_double("TOTK_STICK_SENS");
return (float)(v < 0.1 ? 0.1 : v > 3 ? 3 : v);
}
float cam_speed_applied(void) { return applied; }
static int u32_at(const uint8_t *p, size_t n, size_t off, uint32_t *v) {
if (off > n || n - off < 4) return 0;
memcpy(v, p + off, 4);
return 1;
}
// SARC member `name` of the archive p[0..n): its start and size; 0 when it is not there.
static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) {
uint32_t data;
uint16_t head, count, bom;
if (n < 0x14 || memcmp(p, "SARC", 4)) return 0;
memcpy(&head, p + 4, 2);
memcpy(&bom, p + 6, 2);
if (bom != 0xfeff || !u32_at(p, n, 12, &data) || head + 12u > n || memcmp(p + head, "SFAT", 4)) return 0;
memcpy(&count, p + head + 6, 2);
size_t names = head + 12 + (size_t)count * 16;
if (names + 8 > n || memcmp(p + names, "SFNT", 4)) return 0;
names += 8;
for (unsigned i = 0; i < count; i++) {
uint32_t attr, lo, hi;
size_t e = head + 12 + (size_t)i * 16;
if (!u32_at(p, n, e + 4, &attr) || !u32_at(p, n, e + 8, &lo) || !u32_at(p, n, e + 12, &hi)) return 0;
size_t s = names + (size_t)(attr & 0xffffff) * 4, len = strlen(name);
if (!(attr >> 24) || s + len + 1 > n || memcmp(p + s, name, len + 1)) continue;
if (lo > hi || data + (size_t)hi > n) return 0;
*at = data + lo;
*size = hi - lo;
return 1;
}
return 0;
}
// The StickSensitivity floats of the BYML p[0..n), multiplied by k; their count, 0 when the file has no such array.
static int scale_sensitivity(uint8_t *p, size_t n, float k, float was[8]) {
uint32_t keys, root, head;
if (n < 16 || memcmp(p, "YB", 2) || !u32_at(p, n, 4, &keys) || !u32_at(p, n, 12, &root) ||
!u32_at(p, n, root, &head) || (head & 0xff) != 0xc1 || !u32_at(p, n, keys, &head) || (head & 0xff) != 0xc2)
return 0;
uint32_t nkeys = head >> 8;
if (!u32_at(p, n, root, &head)) return 0;
for (uint32_t i = 0; i < head >> 8; i++) {
uint32_t e, at, arr, ah;
if (!u32_at(p, n, root + 4 + 8 * (size_t)i, &e) || (e & 0xffffff) >= nkeys ||
!u32_at(p, n, keys + 4 + 4 * (size_t)(e & 0xffffff), &at))
return 0;
const char *name = (const char *)p + keys + at;
if (keys + at + sizeof "StickSensitivity" > n || strcmp(name, "StickSensitivity")) continue;
if (e >> 24 != 0xc0 || !u32_at(p, n, root + 8 + 8 * (size_t)i, &arr) || !u32_at(p, n, arr, &ah) ||
(ah & 0xff) != 0xc0)
return 0;
uint32_t count = ah >> 8;
size_t vals = arr + 4 + ((count + 3) & ~3u);
if (!count || count > 8 || vals + 4 * (size_t)count > n) return 0;
for (uint32_t j = 0; j < count; j++)
if (p[arr + 4 + j] != 0xd2) return 0;
for (uint32_t j = 0; j < count; j++) {
float v;
memcpy(&v, p + vals + 4 * j, 4);
was[j] = v;
v *= k;
memcpy(p + vals + 4 * j, &v, 4);
}
return (int)count;
}
return 0;
}
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n) {
float k = cam_speed_wanted();
*out = NULL;
if (k == 1.0f) return 0;
int ok = 0;
uint8_t *dics = NULL, *raw = NULL;
ZSTD_DDict *dd = NULL;
ZSTD_CDict *cd = NULL;
void *z = NULL;
// The dictionaries: Pack/ZsDic.pack.zs is a SARC compressed without one.
unsigned long long dn = ZSTD_getFrameContentSize(zsdic, zsdic_n);
size_t dic_at, dic_n;
if (dn == ZSTD_CONTENTSIZE_UNKNOWN || dn == ZSTD_CONTENTSIZE_ERROR || dn > MAX_RAW || !(dics = malloc((size_t)dn)) ||
ZSTD_decompress(dics, (size_t)dn, zsdic, zsdic_n) != dn ||
!sarc_member(dics, (size_t)dn, "pack.zsdic", &dic_at, &dic_n)) {
rt_log("camera speed: no pack.zsdic in Pack/ZsDic.pack.zs; the camera keeps its own speed");
goto done;
}
unsigned long long rn = ZSTD_getFrameContentSize(in, n);
unsigned id = ZSTD_getDictID_fromFrame(in, n);
dd = ZSTD_createDDict(dics + dic_at, dic_n);
if (!dd || id != ZSTD_getDictID_fromDDict(dd) || rn == ZSTD_CONTENTSIZE_UNKNOWN ||
rn == ZSTD_CONTENTSIZE_ERROR || rn > MAX_RAW || !(raw = malloc((size_t)rn))) {
rt_log("camera speed: Pack/Actor/PlayerCamera.pack.zs is not a pack.zsdic frame with a size; the camera keeps its "
"own speed");
goto done;
}
ZSTD_DCtx *dctx = ZSTD_createDCtx();
size_t got = dctx ? ZSTD_decompress_usingDDict(dctx, raw, (size_t)rn, in, n, dd) : 0;
ZSTD_freeDCtx(dctx);
size_t at, size;
float was[8];
int count = !ZSTD_isError(got) && got == rn && sarc_member(raw, (size_t)rn, MEMBER, &at, &size)
? scale_sensitivity(raw + at, size, k, was) : 0;
if (!count) {
rt_log("camera speed: no StickSensitivity array in %s; the camera keeps its own speed", MEMBER);
goto done;
}
cd = ZSTD_createCDict(dics + dic_at, dic_n, 9);
size_t cap = ZSTD_compressBound((size_t)rn), zn = 0;
ZSTD_CCtx *cctx = cd ? ZSTD_createCCtx() : NULL;
if (cctx && (z = malloc(cap))) zn = ZSTD_compress_usingCDict(cctx, z, cap, raw, (size_t)rn, cd);
ZSTD_freeCCtx(cctx);
if (!z || !zn || ZSTD_isError(zn)) goto done;
*out = z;
*out_n = zn;
z = NULL;
ok = 1;
rt_log("camera speed: x%.2f, StickSensitivity %.2f %.2f %.2f %.2f %.2f -> %.2f %.2f %.2f %.2f %.2f", k, was[0],
count > 1 ? was[1] : 0, count > 2 ? was[2] : 0, count > 3 ? was[3] : 0, count > 4 ? was[4] : 0, was[0] * k,
count > 1 ? was[1] * k : 0, count > 2 ? was[2] * k : 0, count > 3 ? was[3] * k : 0,
count > 4 ? was[4] * k : 0);
done:
free(z);
free(raw);
free(dics);
ZSTD_freeDDict(dd);
ZSTD_freeCDict(cd);
return ok;
}
void cam_pack_served(void) { applied = cam_speed_wanted(); }
+95 -29
View File
@@ -30,27 +30,35 @@ static int save_ok = 1; // the last cfg_save_to wrote (o
// Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is
// logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line.
static const char *const known[] = {
"AB", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_OFF", "AUDIO_PREFILL_MS",
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS",
"BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT",
"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",
"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",
"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",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "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", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE",
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"PREWARM_XLATE", "PROBE_MOVIE", "PROBE_MOVIE_SHOT", "PROFILE", "PROFILE_CHAINS", "PROFILE_DIR", "PROFILE_FRAMES",
"PROFILE_MAIN", "PROFILE_RENDER", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"PROFILE_MAIN", "PROFILE_RENDER", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
"REPLAY_CHECK", "RESOLVE_CHECK", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"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",
"REPLAY_CHECK", "RESOLVE_CHECK", "RESTART_WAIT", "ROMFS", "ROMFS_INDEX", "ROMFS_MMAP", "ROMFS_MODS", "ROMFS_PATCHES",
"ROMFS_PROOF_DIR", "ROMFS_VERIFY", "RSTICK_DEADZONE", "RUMBLE", "RUMBLE_CURVE", "RUMBLE_STRENGTH", "RUMBLE_TRIGGERS", "SHADER_CACHE",
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "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",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "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,12 +67,23 @@ 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"}, {"ANISO", "display"},
{"COLOR_PRESET", "display"},
{"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"},
{"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"},
{"HDR_UI_NITS", "display"}, {"HDR_HUE", "display"}, {"SHADOW_RES", "display"},
{"DLAA", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"},
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
{"PCSS", "display"},
{"MODS_DIR", "mods"}, {"MODS_ORDER", "mods"}, {"MODS_DISABLED", "mods"}, {"MODS_REBUILD", "mods"},
{"MENU_LANG", "language"}, {"LANG", "language"}, {"OPERATION_MODE", "language"},
{"VOLUME", "audio"},
{"VOLUME", "audio"}, {"AUDIO_CHANNELS", "audio"},
{"MOTION", "input"}, {"RUMBLE_STRENGTH", "input"}, {"RUMBLE_CURVE", "input"}, {"RUMBLE_TRIGGERS", "input"},
{"PAD_LAYOUT", "input"}, {"STICK_DEADZONE", "input"}, {"MOUSE_SENS", "input"}, {"INPUT_RECORD", "input"},
{"PAD_LAYOUT", "input"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
{"STICK_SENS", "input"},
{"MOUSE_SENS", "input"}, {"WALK_KEY", "input"}, {"WALK_SCALE", "input"}, {"INPUT_RECORD", "input"},
{"BIND_A", "input"}, {"BIND_B", "input"}, {"BIND_X", "input"}, {"BIND_Y", "input"}, {"BIND_LCLICK", "input"},
{"BIND_RCLICK", "input"}, {"BIND_L", "input"}, {"BIND_R", "input"}, {"BIND_ZL", "input"}, {"BIND_ZR", "input"},
{"BIND_PLUS", "input"}, {"BIND_MINUS", "input"}, {"BIND_LEFT", "input"}, {"BIND_UP", "input"},
@@ -79,8 +98,9 @@ static const struct { const char *key, *section; } sections[] = {
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"DESC_TEMPLATE", "performance"},
{"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"},
{"TRACE", "diagnostics"}, {"FLIGHT", "diagnostics"}, {"PASS_CRUMBS", "diagnostics"}, {"FLIGHT_SPIKE_MS", "diagnostics"},
{"WB_FENCE_LIMIT", "diagnostics"}, {"WB_EXACT", "diagnostics"},
{"MV_SURVEY", "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,31 +115,56 @@ 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},
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE},
{"DLSS_PRESET", "default", "default|k|j", LIVE},
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
{"ASPECT", "fit", "fit|stretch", LIVE}, {"FOV", "100", "70..140", LIVE},
{"ANISO", "game", "game|2|4|8|16", RESTART}, // vk_texture.c sampler_features
// 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},
// HDR output (vk_hdr.c): on at start (the tonemap shader is swapped as shaders load); the nits apply at once
{"HDR", "0", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "80..500", LIVE},
{"HDR_UI_NITS", "203", "80..500", LIVE}, {"HDR_HUE", "80", "0..100", LIVE},
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
{"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART},
{"SHADOW_SCALE", "1", "1..4", RESTART},
{"SHADOW_RANGE", "100", "50..400", RESTART}, {"CONTACT_SHADOWS", "0", "0..100", RESTART},
{"PCSS", "0", "0..200", RESTART},
// Game
{"FPS_LIMIT", "60", "30|40|45|60|90|120|144|display", LIVE}, {"FPS60", "1", "0|1", LIVE},
{"REALTIME", "1", "0|1", RESTART}, {"CLOTH_REALTIME", "1", "0|1", RESTART},
{"MODS_REBUILD", "auto", "auto|off", RESTART},
{"HIGHFPS_FIX", "1", "0|1", RESTART},
{"LANG", "", "ja|en-US|fr|de|it|es|zh-CN|ko|nl|pt|ru|zh-TW|en-GB|fr-CA|es-419|zh-Hans|zh-Hant|pt-BR", RESTART},
{"OPERATION_MODE", "docked", "docked|handheld", RESTART},
{"ASSUME_FOCUS", "1", "0|1", LIVE}, // read on every focus change (ov_system.c)
// Sound, Controls, Menu
{"VOLUME", "100", "0..100", LIVE},
{"VOLUME", "100", "0..100", LIVE}, {"AUDIO_CHANNELS", "stereo", "auto|stereo|5.1", RESTART},
{"MOTION", "1", "0|1", LIVE}, {"RUMBLE_STRENGTH", "130", "0..300", LIVE}, {"RUMBLE_CURVE", "0.6", "0.2..1.5", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox|xbox_prompts", LIVE},
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"MOUSE_SENS", "1", "0..10", LIVE},
{"MENU_LANG", "", "en|ru", LIVE},
{"RUMBLE_TRIGGERS", "0", "0|1", LIVE}, {"PAD_LAYOUT", "auto", "auto|nintendo|xbox", LIVE},
{"PROMPTS", "nintendo", "nintendo|xbox", RESTART},
{"STICK_DEADZONE", "8", "0..50", LIVE}, {"STICK_SENS", "1", "0.1..3", RESTART},
{"LSTICK_DEADZONE", "", "0..50", LIVE}, {"RSTICK_DEADZONE", "", "0..50", LIVE},
{"MOUSE_SENS", "1", "0..10", LIVE},
{"WALK_KEY", "none", NULL, LIVE}, {"WALK_SCALE", "0.51", "0.2..0.9", LIVE},
{"MENU_LANG", "", "en|ru|pt-BR|zh-Hans", LIVE},
{"AMIIBO_DIR", "", NULL, LIVE},
// Discord Rich Presence (discord_rpc.c): off by default, the Menu tab of the settings menu switches it.
{"DISCORD_RPC", "0", "0|1", LIVE},
{"DISCORD_APP_ID", "1567232599846293504", NULL, LIVE},
{"DISCORD_IMAGE", "totk_512x512", NULL, LIVE},
// Advanced: scheduling
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "0", "0|1", RESTART},
{"ASYNC_PREEMPT", "1", "0|1|2", RESTART}, {"REPLAY_ASYNC", "1", "0|1", RESTART},
{"FREE_CORES", "0", "0|1", RESTART}, {"TIMER_1MS", "1", "0|1", RESTART},
// shaders and pipelines
{"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "fullscreen", "strict|any|fullscreen", RESTART},
{"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART},
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "", "0|1", RESTART},
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
// textures and memory
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "0", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
// render fixes
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
@@ -131,14 +176,15 @@ 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},
{"PASS_CRUMBS", "", "0|1", RESTART}, // vk_crumbs.c pass marks; unset: VK_AMD_buffer_marker only
{"INPUT_RECORD", "", NULL, RESTART},
};
static int is_known(const char *key) {
for (size_t i = 0; i < sizeof known / sizeof *known; i++)
if (!strcmp(known[i], key)) return 1;
return 0;
return !strncmp(key, "CHEAT_", 6); // cheats.c and the cheat patch sets (tools/cheats.py)
}
static void clear(void) {
@@ -176,6 +222,24 @@ static void add(char *key, char *value, int line) {
entries[n_entries++] = (CfgEntry){strdup(key), strdup(value), 0, 0};
}
// 0.1.9 had the Xbox prompts as a pad layout: pad_layout = xbox_prompts is pad_layout = nintendo with prompts = xbox.
// Read as those two, which the menu shows and the next cfg_save writes; a prompts line of the file's own wins. The
// value still works from the environment (input_pad_layout_pick, input_prompts_pick).
static void migrate(void) {
CfgEntry *layout = NULL, *prompts = NULL;
for (int i = 0; i < n_entries; i++) {
if (!strcmp(entries[i].key, "PAD_LAYOUT")) layout = &entries[i];
if (!strcmp(entries[i].key, "PROMPTS")) prompts = &entries[i];
}
if (!layout || strcasecmp(layout->value, "xbox_prompts")) return;
free(layout->value);
layout->value = strdup("nintendo");
layout->dirty = 1;
if (!prompts && n_entries < CFG_MAX_KEYS) entries[n_entries++] = (CfgEntry){strdup("PROMPTS"), strdup("xbox"), 1, 0};
rt_log("cfg: totk.ini pad_layout = xbox_prompts is read as pad_layout = nintendo and prompts = %s",
prompts ? prompts->value : "xbox");
}
int cfg_load(const char *path) {
clear();
uint64_t size = 0;
@@ -199,6 +263,7 @@ int cfg_load(const char *path) {
p = eol + 1;
}
free(data);
migrate();
return 1;
}
@@ -457,6 +522,7 @@ static void puts_(Out *o, const char *s) { put(o, s, strlen(s)); }
static const char *section_of(const char *key) {
for (size_t i = 0; i < sizeof sections / sizeof *sections; i++)
if (!strcmp(sections[i].key, key)) return sections[i].section;
if (!strncmp(key, "CHEAT_", 6)) return "cheats"; // cheats.c: one key per cheat of the cheat file
return NULL;
}
+67 -3
View File
@@ -1,4 +1,5 @@
// --probe-cfg: totk.ini parsing, the environment override and a missing file (cfg.c). Needs no game files.
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
@@ -272,6 +273,22 @@ int probe_cfg(void) {
"pad stick: a square gate's corner stays on the unit circle");
input_pad_stick(0, 32767 / 2 + 1, 0.2f, st);
CHECK(abs(st[1] - 32767 * 3 / 8) <= 3, "pad stick: half deflection past a 20% zone is (0.5 - 0.2) / 0.8 of full");
plat_setenv("TOTK_STICK_DEADZONE", "12");
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.12f) < 1e-6f,
"pad dead zone: both sticks follow stick_deadzone while lstick/rstick are unset");
plat_setenv("TOTK_RSTICK_DEADZONE", "20");
CHECK(fabsf(input_pad_deadzone(0) - 0.12f) < 1e-6f && fabsf(input_pad_deadzone(1) - 0.20f) < 1e-6f,
"pad dead zone: rstick_deadzone sets the right stick alone");
plat_setenv("TOTK_LSTICK_DEADZONE", "80");
CHECK(fabsf(input_pad_deadzone(0) - 0.5f) < 1e-6f, "pad dead zone: clamped to 50%");
plat_setenv("TOTK_STICK_DEADZONE", NULL);
plat_setenv("TOTK_LSTICK_DEADZONE", NULL);
plat_setenv("TOTK_RSTICK_DEADZONE", NULL);
CHECK(input_stick_percent(32767, 0) > 99.99 && input_stick_percent(32767, 0) < 100.01 &&
input_stick_percent(0, 0) == 0 && fabs(input_stick_percent(3000, -4000) - 5000 * 100.0 / 32767) < 1e-9,
"stick percent: an axis's end is 100%, the centre 0%, round in between");
// Gamepad layouts (input_map.c)
CHECK(input_pad_layout_pick("auto", 1) == PAD_LAYOUT_NINTENDO && input_pad_layout_pick(NULL, 0) == PAD_LAYOUT_XBOX,
"pad layout: automatic is Nintendo for an east button labelled A, Xbox otherwise");
@@ -279,7 +296,30 @@ int probe_cfg(void) {
"pad layout: a set layout wins over the labels");
CHECK(input_pad_layout_pick("xbox_prompts", 0) == PAD_LAYOUT_NINTENDO &&
input_pad_layout_pick("Xbox_Prompts", 1) == PAD_LAYOUT_NINTENDO,
"pad layout: Xbox prompts keep the Nintendo positions (only the prompts change, romfs.c overlay_init)");
"pad layout: 0.1.9's xbox_prompts keeps the Nintendo positions (only the prompts change, romfs.c overlay_init)");
// Button prompts (input_map.c)
CHECK(input_prompts_pick(NULL, "auto") == PROMPTS_NINTENDO && input_prompts_pick("", "xbox") == PROMPTS_NINTENDO &&
input_prompts_pick("Xbox", "nintendo") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox") == PROMPTS_NINTENDO,
"prompts: the game's own unless set to Xbox");
CHECK(input_prompts_pick(NULL, "xbox_prompts") == PROMPTS_XBOX && input_prompts_pick("nintendo", "xbox_prompts") == PROMPTS_NINTENDO,
"prompts: 0.1.9's pad_layout = xbox_prompts means Xbox while prompts is unset");
CHECK(!input_prompts_folder(PROMPTS_NINTENDO, "nintendo") && !input_prompts_folder(PROMPTS_NINTENDO, "xbox"),
"prompts: the game's own need no folder");
CHECK(EQ(input_prompts_folder(PROMPTS_XBOX, "nintendo"), "romfs-patches-xbox") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox_prompts"), "romfs-patches-xbox") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "xbox"), "romfs-patches-xbox-label") &&
EQ(input_prompts_folder(PROMPTS_XBOX, "auto"), "romfs-patches-xbox-label") &&
EQ(input_prompts_folder(PROMPTS_XBOX, NULL), "romfs-patches-xbox-label"),
"prompts: the face buttons by position for the Nintendo layout, by label for xbox and auto");
// romfs-patches-xbox shows A as B (tests/romfs_prompts_test.py checks the folders' contents). It is right exactly
// where the layout puts A on the east button, which an Xbox pad labels B.
input_map_reset();
static const char *const layouts[] = {"nintendo", "xbox", "auto", "xbox_prompts"};
for (int i = 0; i < 4; i++) {
int a_east = input_map_pad_buttons((uint8_t[PAD_COUNT]){[PAD_EAST] = 1}, input_pad_layout_pick(layouts[i], 0)) == 1ull << B_A;
CHECK(a_east == EQ(input_prompts_folder(PROMPTS_XBOX, layouts[i]), "romfs-patches-xbox"),
"prompts: the folder names A as B exactly when the layout puts A on the button labelled B");
}
uint8_t pd[PAD_COUNT] = {0};
pd[PAD_SOUTH] = pd[PAD_NORTH] = 1;
CHECK(input_map_pad_buttons(pd, PAD_LAYOUT_NINTENDO) == (1ull << B_B | 1ull << B_X) &&
@@ -370,6 +410,24 @@ int probe_cfg(void) {
plat_setenv("TOTK_FREE_CORES", "1");
CHECK(cfg_bool("TOTK_FREE_CORES") == 1, "free_cores 1: on");
plat_setenv("TOTK_FREE_CORES", NULL);
// 0.1.9's pad_layout = xbox_prompts (cfg.c migrate): read as the Nintendo layout with the Xbox prompts, saved so
plat_setenv("TOTK_PAD_LAYOUT", NULL);
plat_setenv("TOTK_PROMPTS", NULL);
static const char old_layout[] = "[input]\r\npad_layout = xbox_prompts\r\n";
CHECK(plat_file_write(path, old_layout, sizeof old_layout - 1) && cfg_load(path) == 1 &&
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "xbox"),
"0.1.9's pad_layout = xbox_prompts reads as nintendo with prompts = xbox");
CHECK(cfg_save_to(path) == 1, "the read pad layout and prompts save");
uint64_t saved_n = 0;
char *saved = (char *)plat_file_read(path, &saved_n);
CHECK(saved && strstr(saved, "pad_layout = nintendo") && strstr(saved, "prompts = xbox") && !strstr(saved, "xbox_prompts"),
"the next save writes the pad layout and the prompts");
free(saved);
static const char old_layout_own[] = "[input]\r\npad_layout = xbox_prompts\r\nprompts = nintendo\r\n";
CHECK(plat_file_write(path, old_layout_own, sizeof old_layout_own - 1) && cfg_load(path) == 1 &&
EQ(cfg_get("TOTK_PAD_LAYOUT"), "nintendo") && EQ(cfg_get("TOTK_PROMPTS"), "nintendo"),
"a prompts line of the file's own wins over 0.1.9's pad_layout = xbox_prompts");
plat_file_delete(path);
cfg_load("cfg-probe-missing.ini");
int listed = 0;
for (int i = 0; cfg_meta_at(i); i++) {
@@ -383,9 +441,10 @@ int probe_cfg(void) {
int ok = 0;
if (!m->values) ok = 1;
else if (strstr(m->values, "..")) {
long lo = strtol(m->values, NULL, 10), hi = strtol(strstr(m->values, "..") + 2, NULL, 10);
// strtod: ranges such as 0.2..0.9 have fractional ends
double lo = strtod(m->values, NULL), hi = strtod(strstr(m->values, "..") + 2, NULL);
double d = strtod(m->def, NULL);
ok = d >= (double)lo && d <= (double)hi;
ok = d >= lo && d <= hi;
} else {
size_t dl = strlen(m->def);
for (const char *v = m->values; *v;) {
@@ -397,6 +456,11 @@ int probe_cfg(void) {
if (!ok) fprintf(stderr, "FAIL cfg: default '%s' of %s is not one of its values '%s'\n", m->def, m->key, m->values), fails++;
}
CHECK(listed >= 30, "the defaults table was walked");
// cfg_meta answers with the first row of a key: a second row would be dead, and Reset would visit the key twice
for (int i = 0; cfg_meta_at(i); i++)
for (int j = i + 1; cfg_meta_at(j); j++)
if (!strcmp(cfg_meta_at(i)->key, cfg_meta_at(j)->key))
fprintf(stderr, "FAIL cfg: %s has two rows in the defaults table\n", cfg_meta_at(i)->key), fails++;
printf("probe-cfg: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}
+667
View File
@@ -0,0 +1,667 @@
// Cheats of the mods' Atmosphere cheat files (cheats.h). Each enabled mod the port can use (mods.h) may hold
// <build id>.txt in its cheats folder, the build id the first 8 bytes of main's NSO build id, as Atmosphere and the
// emulators name it; the file name's case does not matter. Each cheat's switch is the setting TOTK_CHEAT_<hash>
// (cheat_<hash> under [cheats] in totk.ini), the hash FNV-1a 32 of "<mod folder name>/<cheat name>", so two mods'
// cheats of one name keep their own switches.
//
// Code cheats (opcode 0 writes into main's code) cannot be applied here: the game's code is lifted C. tools/cheats.py
// turns the code cheats of every mod into patch sets cheat_<hash> when the game is built and writes
// build-cache\cheats.status next to totk.exe, one line per cheat: name hash, content hash, kind, buttons, note
// (tools/cheats.py has the format). A cheat whose status line is missing or has another content hash was added or
// edited after the build: a data cheat runs anyway, a code cheat waits for the setup to run again.
//
// Data cheats run in a VM after Atmosphere's dmnt cheat VM (opcodes 0-A; the 0xC.. and 0xF.. extensions are not
// supported), every presented frame while their switch is on, with registers cleared before each cheat. Memory is the
// guest's, mapped at its own addresses: main's image is written with its pages made writable for the write (read-only
// data included, as dmnt writes through the debugger), everything else after mem_query says the guest may read or write
// it. Buttons are input_poll's HidNpadButton bits, the same as the cheat format's key masks; they read 0 while the menu
// is open.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "cfg.h"
#include "cheats.h"
#include "host.h"
#include "input.h"
#include "mods.h"
#include "platform/platform.h"
#ifdef _WIN32
#define SEP "\\"
#else
#define SEP "/"
#endif
enum { STATUS_MAX = 160, OPS_PER_RUN = 1 << 20 };
typedef struct {
CheatInfo info;
uint32_t *words;
int n;
int dup; // 1 for the first cheat of its name in its file, 2 for the next ...
uint32_t content; // FNV-1a 32 of its words as tools/cheats.py writes them
int set; // patch set, -1 none
volatile int32_t on;
char note[STATUS_MAX];
} Cheat;
static Cheat *g_cheats;
static int g_n, g_cap, g_vm; // g_vm: some cheat needs cheats_frame
static char g_file[64]; // "<build id>.txt"
static char g_dir[PLAT_PATH_MAX]; // the folder of totk.exe, with a trailing separator
static int g_files; // mods whose file was read
static uint64_t g_main, g_main_size, g_code_end; // main's image; its code with the padding up to .rodata
static uint32_t fnv(const char *s, size_t n) {
uint32_t h = 0x811C9DC5u;
for (size_t i = 0; i < n; i++) h = (h ^ (uint8_t)s[i]) * 0x01000193u;
return h;
}
// The folder of the executable, with a trailing separator.
static int exe_dir(char *out, size_t n) {
if (!plat_exe_path(out, n)) return 0;
char *cut = plat_last_sep(out);
if (!cut) return 0;
cut[1] = 0;
return 1;
}
static int build_id(char out[17]) {
char path[PLAT_PATH_MAX];
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
PlatFile *f = plat_file_open(path);
if (!f) return 0;
uint8_t head[0x48];
int ok = plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
plat_file_close(f);
for (int i = 0; ok && i < 8; i++) snprintf(out + 2 * i, 3, "%02X", head[0x40 + i]);
return ok;
}
// ---- opcodes
// Words of the opcode at w (left words remain), 0 when it is cut short.
static int op_len(const uint32_t *w, int left) {
uint32_t f = w[0];
int t = f >> 28, width = (f >> 24) & 0xF, n;
switch (t) {
case 0: case 1: n = width == 8 ? 4 : 3; break;
case 2: case 8: n = 1; break;
case 3: n = width == 0 ? 2 : 1; break;
case 4: case 6: n = 3; break;
case 5: case 7: n = 2; break;
case 9: n = 1 + (((f >> 8) & 0xF) ? (width == 8 ? 2 : 1) : 0); break;
case 0xA: n = ((f >> 8) & 0xF) == 2 || ((f >> 8) & 0xF) == 4 || ((f >> 8) & 0xF) == 5 ? 2 : 1; break;
default: n = 1; break;
}
return n <= left ? n : 0;
}
// NULL when the VM runs the opcode, else why not (tools/cheats.py vm_ok).
static const char *vm_unsupported(const uint32_t *w, char *buf, int n) {
uint32_t f = w[0];
int t = f >> 28, width = (f >> 24) & 0xF;
if (t > 0xA || (t == 5 && ((f >> 12) & 0xF) > 1) || (t == 0xA && ((f >> 8) & 0xF) > 5)) {
snprintf(buf, n, "opcode %08X is not supported", f);
return buf;
}
if ((t <= 1 || t == 5 || t == 6 || t == 7 || t == 9 || t == 0xA) && width != 1 && width != 2 && width != 4 &&
width != 8) {
snprintf(buf, n, "opcode %08X has a bad width", f);
return buf;
}
return NULL;
}
// The kind of a cheat the build did not see: heading, data, rebuild (it writes main's code) or unsupported.
static CheatKind classify(Cheat *c) {
int ops = 0, ends = 0;
for (int i = 0; i < c->n;) {
int len = op_len(c->words + i, c->n - i);
if (!len) {
snprintf(c->note, sizeof c->note, "opcode %08X is cut short", c->words[i]);
return CHEAT_UNSUPPORTED;
}
uint32_t f = c->words[i];
ops++;
ends += f >> 28 == 2;
if (f >> 28 == 0 && ((f >> 20) & 0xF) == 0) {
uint64_t off = ((uint64_t)(f & 0xFF) << 32) | c->words[i + 1];
if (off < g_code_end - g_main) return CHEAT_REBUILD;
}
if (vm_unsupported(c->words + i, c->note, sizeof c->note)) return CHEAT_UNSUPPORTED;
i += len;
}
return ops == ends && ops <= 1 ? CHEAT_HEADING : CHEAT_DATA;
}
// ---- guest memory
static uint64_t region(int m) {
const RtGuestLayout *l = mem_layout();
switch (m) {
case 0: return g_main;
case 1: return l->heap.base;
case 2: return l->alias.base;
case 3: return l->aslr.base;
default: return 0;
}
}
static int in_main(uint64_t a, unsigned n) { return a >= g_main && a + n <= g_main + g_main_size; }
static int guest_ok(uint64_t a, unsigned n, uint32_t perm) {
if (in_main(a, n)) return 1;
RtMemoryInfo mi;
if (mem_query(a, &mi) || !mi.state || (mi.perm & perm) != perm) return 0;
return a + n <= mi.base + mi.size;
}
static int rd(uint64_t a, int width, uint64_t *v) {
if (!a || !guest_ok(a, (unsigned)width, 1)) return 0;
uint64_t x = 0;
memcpy(&x, (const void *)(uintptr_t)a, (size_t)width);
*v = x;
return 1;
}
// Writes n bytes into main's image a page at a time: a read-only page is writable for its part of the copy only.
static void wr_main(uint64_t a, const uint8_t *from, int n) {
while (n > 0) {
uint64_t page = a & ~0xFFFull, end;
int k = (int)(page + 0x1000 - a);
if (k > n) k = n;
int prot = plat_mem_state(page, page + 0x1000, &end);
if (prot != PLAT_PROT_READ && prot != PLAT_PROT_RW) return;
if (prot == PLAT_PROT_READ && !plat_mem_protect(page, 0x1000, PLAT_PROT_RW)) return;
memcpy((void *)(uintptr_t)a, from, (size_t)k);
if (prot == PLAT_PROT_READ) plat_mem_protect(page, 0x1000, PLAT_PROT_READ);
a += (uint64_t)k;
from += k;
n -= k;
}
}
static void wr(uint64_t a, int width, uint64_t v) {
uint64_t now;
if (!rd(a, width, &now) || !memcmp(&now, &v, (size_t)width)) return; // unchanged: no write, no protection change
void *p = (void *)(uintptr_t)a;
if (in_main(a, (unsigned)width)) {
wr_main(a, (const uint8_t *)&v, width);
} else if (guest_ok(a, (unsigned)width, 2)) {
memcpy(p, &v, (size_t)width); // a frozen region thaws on the write (memory.c)
}
}
static uint64_t masked(int width, uint64_t v) { return width >= 8 ? v : v & ((1ull << (8 * width)) - 1); }
static uint64_t arith(int op, uint64_t a, uint64_t b) {
switch (op) {
case 0: return a + b;
case 1: return a - b;
case 2: return a * b;
case 3: return b < 64 ? a << b : 0;
case 4: return b < 64 ? a >> b : 0;
case 5: return a & b;
case 6: return a | b;
case 7: return ~a;
case 8: return a ^ b;
default: return a; // 9: move
}
}
static int compare(int cond, uint64_t a, uint64_t b) {
switch (cond) {
case 1: return a > b;
case 2: return a >= b;
case 3: return a < b;
case 4: return a <= b;
case 5: return a == b;
case 6: return a != b;
default: return 0;
}
}
// From i (after a failed condition, or an else reached while running), the opcode to continue at: after the
// matching else (else_ok) or end.
static int skip(const uint32_t *w, int n, int i, int else_ok) {
int depth = 0;
while (i < n) {
int len = op_len(w + i, n - i);
if (!len) return n;
uint32_t f = w[i];
int t = f >> 28;
i += len;
if (t == 1 || t == 8) depth++;
else if (t == 2 && ((f >> 24) & 0xF) == 0 && depth-- == 0) return i;
else if (t == 2 && depth == 0 && else_ok) return i;
}
return n;
}
static uint64_t imm64(const uint32_t *w) { return (uint64_t)w[0] << 32 | w[1]; }
static void run(const Cheat *c, uint64_t keys) {
uint64_t r[16] = {0};
int loop[16] = {0};
const uint32_t *w = c->words;
int n = c->n, budget = OPS_PER_RUN;
for (int i = 0; i < n && budget-- > 0;) {
int len = op_len(w + i, n - i);
if (!len) return;
uint32_t f = w[i];
int t = f >> 28, width = (f >> 24) & 0xF, x = (f >> 20) & 0xF, y = (f >> 16) & 0xF, z = (f >> 12) & 0xF;
uint64_t rel = ((uint64_t)(f & 0xFF) << 32) | (len > 1 ? w[i + 1] : 0), v;
switch (t) {
case 0: // 0TMR00AA AAAAAAAA V..: store a value at region M + A + register R
wr(region(x) + rel + r[y], width, width == 8 ? imm64(w + i + 2) : w[i + 2]);
break;
case 1: // 1TMC00AA AAAAAAAA V..: if (region M + A) <cond C> V
if (!rd(region(x) + rel, width, &v) || !compare(y, v, width == 8 ? imm64(w + i + 2) : w[i + 2])) {
i = skip(w, n, i + len, 1);
continue;
}
break;
case 2: // 20000000 end, 21000000 else: an else reached while running skips to the end
if (width == 1) {
i = skip(w, n, i + len, 0);
continue;
}
break;
case 3: // 300R0000 VVVVVVVV loop start, 310R0000 loop end
if (width == 0) {
r[y] = w[i + 1];
loop[y] = i + len;
} else if (r[y] && --r[y]) {
i = loop[y];
continue;
}
break;
case 4: // 400R0000 V V: register R = V
r[y] = imm64(w + i + 1);
break;
case 5: // 5TMRI0AA AAAAAAAA: R = (I ? R : region M) + A, read
if (!rd(z ? r[y] + rel : region(x) + rel, width, &v)) return; // a broken pointer chain: nothing more
r[y] = v;
break;
case 6: { // 6T0RIor0 V V: store V at R (+ register r); I: R += width
uint64_t a = r[y] + (((f >> 8) & 0xF) ? r[(f >> 4) & 0xF] : 0);
wr(a, width, imm64(w + i + 1));
if (z) r[y] += (uint64_t)width;
break;
}
case 7: // 7T0RC000 VVVVVVVV: R = R <op C> V
r[y] = masked(width, arith(z, r[y], w[i + 1]));
break;
case 8: // 8kkkkkkk: if the buttons are held
if ((keys & (f & 0x0FFFFFFF)) != (f & 0x0FFFFFFF)) {
i = skip(w, n, i + len, 1);
continue;
}
break;
case 9: { // 9TCRS0s0 / 9TCRS100 V..: R = S <op C> (s or V)
uint64_t b = (f >> 8) & 0xF ? (width == 8 ? imm64(w + i + 1) : w[i + 1]) : r[(f >> 4) & 0xF];
r[y] = masked(width, arith(x, r[z], b));
break;
}
case 0xA: { // ATSRIOxa (aaaaaaaa): store register S at an address of type O
int o = (f >> 8) & 0xF, m = (f >> 4) & 0xF;
uint64_t imm = ((uint64_t)(f & 0xF) << 32) | (len > 1 ? w[i + 1] : 0), a;
switch (o) {
case 0: a = r[y]; break;
case 1: a = r[y] + r[m]; break;
case 2: a = r[y] + imm; break;
case 3: a = region(m) + r[y]; break;
case 4: a = region(m) + imm; break;
default: a = region(m) + r[y] + imm; break;
}
wr(a, width, r[x]);
if (z) r[y] += (uint64_t)width;
break;
}
default:
return;
}
i += len;
}
}
// ---- the file
static char *read_text(const char *path) {
uint64_t n = 0;
char *s = (char *)plat_file_read(path, &n);
if (s && n >= 3 && !memcmp(s, "\xEF\xBB\xBF", 3)) memmove(s, s + 3, (size_t)n - 2); // a UTF-8 BOM
return s;
}
static void add_word(Cheat *c, uint32_t v, int *cap) {
if (c->n == *cap) {
*cap = *cap ? *cap * 2 : 16;
c->words = (uint32_t *)realloc(c->words, sizeof *c->words * (size_t)*cap);
if (!c->words) rt_fatal("cheats: out of memory");
}
c->words[c->n++] = v;
}
// Appends the cheats of one mod's file; mod: the mod's folder name (kept).
static void parse(char *s, const char *mod) {
int first = g_n, wcap = 0;
for (char *line = s; line && *line;) {
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
if (end) *end = 0;
while (*line == ' ' || *line == '\t') line++;
size_t len = strlen(line);
while (len && (line[len - 1] == '\r' || line[len - 1] == ' ' || line[len - 1] == '\t')) line[--len] = 0;
if (len >= 2 && (line[0] == '[' || line[0] == '{') && (line[len - 1] == ']' || line[len - 1] == '}')) {
if (g_n == g_cap) {
g_cap = g_cap ? g_cap * 2 : 32;
g_cheats = (Cheat *)realloc(g_cheats, sizeof *g_cheats * (size_t)g_cap);
if (!g_cheats) rt_fatal("cheats: out of memory");
}
Cheat *c = &g_cheats[g_n++];
memset(c, 0, sizeof *c);
line[len - 1] = 0;
char *name = line + 1;
while (*name == ' ' || *name == '\t') name++;
size_t k = strlen(name);
while (k && (name[k - 1] == ' ' || name[k - 1] == '\t')) name[--k] = 0;
c->info.name = strdup(name);
c->info.mod = mod;
c->info.kind = line[0] == '{' ? CHEAT_MASTER : CHEAT_HEADING;
c->set = -1;
c->dup = 1; // a repeated name gets "#2", "#3" ... (tools/cheats.py parse)
for (int j = first; j < g_n - 1; j++) c->dup += !strcmp(g_cheats[j].info.name, c->info.name);
wcap = 0;
} else if (g_n > first) {
for (char *tok = strtok(line, " \t"); tok; tok = strtok(NULL, " \t")) {
char *e;
unsigned long v = strtoul(tok, &e, 16);
if (e - tok == 8 && !*e) add_word(&g_cheats[g_n - 1], (uint32_t)v, &wcap);
}
}
line = next;
}
}
// The file g_file in folder, its name in any case, into out; 0 when there is none.
typedef struct {
const char *dir;
char *out;
size_t cap;
int found;
} FindFile;
static int find_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
FindFile *f = (FindFile *)ctx;
if (kind == PLAT_FS_FILE && !strcasecmp(name, g_file) &&
snprintf(f->out, f->cap, "%s/%s", f->dir, name) < (int)f->cap)
f->found = 1;
return !f->found;
}
static int find_file(const char *folder, char *out, size_t cap) {
FindFile f = {folder, out, cap, 0};
plat_fs_list(folder, find_cb, &f);
return f.found;
}
// The status line of key, its fields split in place; 0 when none.
static int status_of(char *status, const char *key, char **fields) {
for (char *line = status; line && *line;) {
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
if (!strncmp(line, key, 8) && line[8] == '\t') {
static char copy[512];
size_t n = end ? (size_t)(end - line) : strlen(line);
if (n >= sizeof copy) n = sizeof copy - 1;
memcpy(copy, line, n);
copy[n] = 0;
if (n && copy[n - 1] == '\r') copy[n - 1] = 0;
int k = 0;
fields[k++] = copy;
for (char *p = copy; *p && k < 5; p++)
if (*p == '\t') *p = 0, fields[k++] = p + 1;
while (k < 5) fields[k++] = (char *)"";
return 1;
}
line = next;
}
return 0;
}
// The menu's font has no numero sign (U+2116), which cheat names often start with: they show "No" instead.
static void plain_name(char *s) {
for (char *p; (p = strstr(s, "\xE2\x84\x96")) != NULL;) {
p[0] = 'N', p[1] = 'o';
memmove(p + 2, p + 3, strlen(p + 3) + 1);
}
}
// ---- rebuild: code cheats and ExeFS patches the build lacks
// The setup writes build-cache\rebuild.txt (installer/steps.py write_rebuild_command), the command that starts its
// window in rebuild mode on this install folder, one argument per line. When an enabled mod has code cheats this build
// lacks (CHEAT_REBUILD) or ExeFS patch files it lacks (mods_exefs_pending), the game runs it and exits; the setup lifts
// and compiles them in and starts the game again.
// build-cache\rebuild.skip holds the key of the cheats a rebuild was started for: a rebuild that failed or was
// cancelled leaves them unbuilt, and the same set does not start it again. TOTK_MODS_REBUILD=off never starts it.
// Scripted runs (--seconds, --boot) never start it.
static void rebuild_if_needed(const char *dir) {
uint32_t key = 0x811C9DC5u;
int need = 0;
for (int i = 0; i < g_n; i++) {
const Cheat *c = &g_cheats[i];
if (c->info.kind != CHEAT_REBUILD) continue;
need++;
key = (key ^ fnv(c->info.key, strlen(c->info.key))) * 0x01000193u;
key = (key ^ c->content) * 0x01000193u;
}
int patches = mods_exefs_pending(&key);
if (!need && !patches) return;
char cmd_path[PLAT_PATH_MAX + 64], skip_path[PLAT_PATH_MAX + 64], skip[16] = "", want[16];
snprintf(cmd_path, sizeof cmd_path, "%sbuild-cache" SEP "rebuild.txt", dir);
snprintf(skip_path, sizeof skip_path, "%sbuild-cache" SEP "rebuild.skip", dir);
snprintf(want, sizeof want, "%08X", key);
char *old = read_text(skip_path);
if (old) snprintf(skip, sizeof skip, "%.8s", old);
free(old);
const char *why = !strcmp(cfg_value("TOTK_MODS_REBUILD"), "off") ? "mods_rebuild = off"
: rt_config()->seconds || rt_config()->boot ? "a scripted run"
: !strcmp(skip, want) ? "the last rebuild for them failed or was cancelled"
: NULL;
char *cmd = why ? NULL : read_text(cmd_path);
if (!why && !cmd) why = "no build-cache/rebuild.txt (a build without the setup)";
if (why) {
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; no rebuild: %s", need,
patches, why);
free(cmd);
return;
}
const char *argv[32];
int argc = 0;
for (char *line = cmd; line && *line && argc < 31;) {
char *end = strchr(line, '\n'), *next = end ? end + 1 : NULL;
if (end) *end = 0;
size_t n = strlen(line);
if (n && line[n - 1] == '\r') line[n - 1] = 0;
if (*line) argv[argc++] = line;
line = next;
}
argv[argc] = NULL;
if (argc < 2 || !plat_file_write(skip_path, want, strlen(want))) {
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; cannot start the "
"rebuild (%s)", need, patches, cmd_path);
free(cmd);
return;
}
rt_log("cheats: %d code cheats and %d ExeFS patch files are not built into the game; starting the rebuild: %s",
need, patches, argv[0]);
int ok = plat_spawn(argv, dir);
free(cmd);
if (!ok) {
rt_log("cheats: the rebuild did not start; the game starts with the code it has");
return;
}
prof_write(); // plat_exit runs no atexit handlers
plat_exit(0);
}
void cheats_init(void) {
const RtLoaded *m = ldr_count() ? ldr_module(0) : NULL;
char dir[PLAT_PATH_MAX], bid[17], path[2400]; // path: a mod's cheats folder (ModInfo.cheats) + the file
if (!m || !exe_dir(dir, sizeof dir) || !build_id(bid)) return;
snprintf(g_dir, sizeof g_dir, "%s", dir);
g_main = m->base;
g_main_size = m->size;
g_code_end = m->base + ((m->text_end + 0xFFF) & ~0xFFFull);
snprintf(g_file, sizeof g_file, "%s.txt", bid);
mods_scan();
for (int k = 0; k < mods_count(); k++) { // the Mods tab's order, top first
const ModInfo *mod = mods_at(k);
if (!mod->enabled || mod->problem != MOD_OK || !mod->cheats[0] || !find_file(mod->cheats, path, sizeof path))
continue;
char *text = read_text(path);
if (!text) {
rt_log("cheats: %s: cannot read %s", mod->name, path);
continue;
}
parse(text, strdup(mod->name));
free(text);
g_files++;
}
if (!g_files) {
rt_log("cheats: no enabled mod holds cheats/%s", g_file);
rebuild_if_needed(dir); // an enabled mod's ExeFS patches may still wait for it
return;
}
snprintf(path, sizeof path, "%sbuild-cache" SEP "cheats.status", dir);
char *status = read_text(path);
int counts[CHEAT_UNSUPPORTED + 1] = {0}, seen_dups = 0;
for (int i = 0; i < g_n; i++) {
Cheat *c = &g_cheats[i];
char named[900];
snprintf(named, sizeof named, c->dup > 1 ? "%s/%s#%d" : "%s/%s", c->info.mod, c->info.name, c->dup);
seen_dups += c->dup > 1;
char key[9], content[9];
snprintf(key, sizeof key, "%08X", fnv(named, strlen(named)));
size_t tn = (size_t)c->n * 9 + 1, tl = 0;
char *t = (char *)malloc(tn);
if (!t) rt_fatal("cheats: out of memory");
t[0] = 0;
for (int k = 0; k < c->n; k++) tl += (size_t)snprintf(t + tl, tn - tl, k ? " %08X" : "%08X", c->words[k]);
c->content = fnv(t, tl);
snprintf(content, sizeof content, "%08X", c->content);
free(t);
snprintf(c->info.key, sizeof c->info.key, "TOTK_CHEAT_%s", key);
int master = c->info.kind == CHEAT_MASTER;
char *f[5];
CheatKind kind;
if (status && status_of(status, key, f) && !strcmp(f[1], content)) {
const char *k = f[2];
kind = !strcmp(k, "code") ? CHEAT_CODE : !strcmp(k, "key") ? CHEAT_KEY : !strcmp(k, "master") ? CHEAT_MASTER
: !strcmp(k, "restore") ? CHEAT_RESTORE : !strcmp(k, "heading") ? CHEAT_HEADING
: !strcmp(k, "data") ? CHEAT_DATA : CHEAT_UNSUPPORTED;
c->info.buttons = (uint32_t)strtoul(f[3], NULL, 16);
snprintf(c->note, sizeof c->note, "%s", f[4]);
if (kind == CHEAT_CODE || kind == CHEAT_KEY || kind == CHEAT_MASTER) {
char set[32];
snprintf(set, sizeof set, "cheat_%08x", fnv(named, strlen(named)));
c->set = rt_patch_set(set);
if (c->set < 0 && kind != CHEAT_MASTER) kind = CHEAT_REBUILD;
}
} else {
kind = classify(c);
}
if (master && kind == CHEAT_DATA) kind = CHEAT_MASTER; // a data master code: the VM, always on
c->info.kind = kind;
c->info.note = c->note;
const char *v = cfg_get(c->info.key);
c->on = kind == CHEAT_MASTER || (v && *v && strcmp(v, "0") != 0);
if (kind == CHEAT_KEY && c->set >= 0) rt_patch_switch(c->set, 0); // on while the buttons are held
if (kind == CHEAT_KEY || kind == CHEAT_DATA || (kind == CHEAT_MASTER && c->set < 0)) g_vm = 1;
counts[kind]++;
}
free(status);
rebuild_if_needed(dir);
for (int i = 0; i < g_n; i++) plain_name((char *)g_cheats[i].info.name); // the hashes above read the file's bytes
int on = 0;
for (int i = 0; i < g_n; i++) on += g_cheats[i].on && g_cheats[i].info.kind != CHEAT_HEADING;
rt_log("cheats: %s in %d mods: %d cheats, %d on; %d code, %d while buttons are held, %d data, %d master, "
"%d restore, %d headings, %d wait for a rebuild, %d not supported%s",
g_file, g_files, g_n, on, counts[CHEAT_CODE], counts[CHEAT_KEY], counts[CHEAT_DATA], counts[CHEAT_MASTER],
counts[CHEAT_RESTORE], counts[CHEAT_HEADING], counts[CHEAT_REBUILD], counts[CHEAT_UNSUPPORTED],
seen_dups ? " (repeated names numbered)" : "");
}
int cheats_rebuild_pending(void) {
char path[PLAT_PATH_MAX + 64];
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.txt", g_dir);
uint32_t key = 0;
int need = mods_exefs_pending(&key);
for (int i = 0; i < g_n && !need; i++) need = g_cheats[i].info.kind == CHEAT_REBUILD;
return need && g_dir[0] && plat_file_exists(path);
}
void cheats_rebuild_allow(void) {
char path[PLAT_PATH_MAX + 64];
snprintf(path, sizeof path, "%sbuild-cache" SEP "rebuild.skip", g_dir);
plat_fs_unlink(path);
}
int cheats_count(void) { return g_n; }
const CheatInfo *cheats_info(int i) { return i >= 0 && i < g_n ? &g_cheats[i].info : NULL; }
int cheats_on(int i) { return i >= 0 && i < g_n && g_cheats[i].on; }
const char *cheats_file(void) { return g_file; }
void cheats_set(int i, int on) {
if (i < 0 || i >= g_n) return;
Cheat *c = &g_cheats[i];
if (c->info.kind != CHEAT_CODE && c->info.kind != CHEAT_KEY && c->info.kind != CHEAT_DATA) return;
plat_xchg32(&c->on, on != 0);
if (c->info.kind == CHEAT_CODE) rt_patch_switch(c->set, on); // CHEAT_KEY: cheats_frame follows the buttons
cfg_set(c->info.key, on ? "1" : "0");
rt_log("cheats: %s/%s %s", c->info.mod, c->info.name, on ? "on" : "off");
}
void cheats_frame(uint64_t frame) {
if (!g_vm || frame < 60) return; // the game polls its input (and starts SDL) before this
uint64_t keys = 0;
int32_t lx, ly, rx, ry;
input_poll(&keys, &lx, &ly, &rx, &ry); // 0 while the menu is open
for (int i = 0; i < g_n; i++) {
Cheat *c = &g_cheats[i];
switch (c->info.kind) {
case CHEAT_KEY:
if (c->set >= 0) {
int want = c->on && (keys & c->info.buttons) == c->info.buttons;
if (rt_patch_enabled(c->set) != want) rt_patch_switch(c->set, want);
}
break;
case CHEAT_DATA:
if (c->on) run(c, keys);
break;
case CHEAT_MASTER:
if (c->set < 0) run(c, keys);
break;
default:
break;
}
}
}
const char *cheats_buttons(uint32_t mask, char *buf, int n) {
static const char *const names[] = {"A", "B", "X", "Y", "Left stick press", "Right stick press", "L", "R", "ZL",
"ZR", "+", "-", "D-pad left", "D-pad up", "D-pad right", "D-pad down",
"Left stick left", "Left stick up", "Left stick right", "Left stick down",
"Right stick left", "Right stick up", "Right stick right", "Right stick down"};
int k = 0;
buf[0] = 0;
for (int b = 0; b < 24; b++)
if (mask & (1u << b)) k += snprintf(buf + k, (size_t)(n > k ? n - k : 0), "%s%s", k ? " + " : "", names[b]);
return buf;
}
+48
View File
@@ -0,0 +1,48 @@
// Cheats of the mods' Atmosphere cheat files (cheats.c): mods\<mod>\cheats\<build id>.txt of each enabled mod,
// switched from the menu's Cheats tab. A cheat that writes the game's instructions is a code patch set that tools/cheats.py built into the game
// (switched with rt_patch_switch); one that writes data runs in the cheat VM every presented frame.
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef enum {
CHEAT_HEADING, // no opcodes: a title or a group label
CHEAT_CODE, // a patch set
CHEAT_KEY, // a patch set on while the buttons are held
CHEAT_DATA, // the cheat VM
CHEAT_MASTER, // {master}: always on
CHEAT_RESTORE, // writes the original instructions back: nothing to do
CHEAT_REBUILD, // writes instructions, but the game was built without this version of it
CHEAT_UNSUPPORTED, // note says why
} CheatKind;
typedef struct {
const char *name; // UTF-8, as in the file
const char *mod; // the mod's folder name; cheats come grouped by mod, in the Mods tab's order
const char *note; // why it is unsupported ("" otherwise)
CheatKind kind;
uint32_t buttons; // CHEAT_KEY: HidNpadButton mask
char key[24]; // its setting, TOTK_CHEAT_<hash of "<mod>/<name>">
} CheatInfo;
void cheats_init(void); // after rt_patches_init: reads the mods' files and their build status
int cheats_count(void);
// 1 when a cheat of an enabled mod waits for a rebuild (CHEAT_REBUILD) and the setup left its rebuild command.
int cheats_rebuild_pending(void);
// The next start rebuilds again for cheats whose last rebuild failed or was cancelled (the menu restarts after it).
void cheats_rebuild_allow(void);
const CheatInfo *cheats_info(int i);
int cheats_on(int i); // the player's switch (1 for a master cheat)
void cheats_set(int i, int on); // switches at once and sets the cheat's setting (the menu saves totk.ini)
const char *cheats_file(void); // "<build id>.txt", the name the files have for this game version
// Every presented frame (the present path): the data cheats' VM and the button state of CHEAT_KEY cheats.
void cheats_frame(uint64_t frame);
// The button names of a mask ("ZL + Right stick left"), into buf.
const char *cheats_buttons(uint32_t mask, char *buf, int n);
#ifdef __cplusplus
}
#endif
+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,
};
+156
View File
@@ -0,0 +1,156 @@
// Generated by tools/build_shaders.py from runtime/shaders/contact_shadow.comp; do not edit.
// source sha256 b452a1920e59c4d83b2c994b0c6573cc1e479f7032a1310a7432d11c5c2e9908
#pragma once
#include <stdint.h>
static const uint32_t contact_shadow_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000001b1, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000069,
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00030047, 0x00000027,
0x00000002, 0x00050048, 0x00000027, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000027,
0x00000001, 0x00000023, 0x00000010, 0x00050048, 0x00000027, 0x00000002, 0x00000023, 0x00000020,
0x00050048, 0x00000027, 0x00000003, 0x00000023, 0x00000030, 0x00040047, 0x00000069, 0x0000000b,
0x0000001c, 0x00040047, 0x00000088, 0x00000021, 0x00000000, 0x00040047, 0x00000088, 0x00000022,
0x00000000, 0x00040047, 0x0000013f, 0x00000021, 0x00000001, 0x00040047, 0x0000013f, 0x00000022,
0x00000000, 0x00040047, 0x0000015c, 0x0000000b, 0x00000019, 0x00020013, 0x00000002, 0x00030021,
0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006,
0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x0004002b, 0x00000006, 0x0000001b,
0x40000000, 0x0004002b, 0x00000006, 0x0000001d, 0x3f800000, 0x00040015, 0x00000020, 0x00000020,
0x00000000, 0x0004002b, 0x00000020, 0x00000021, 0x00000000, 0x00040017, 0x00000026, 0x00000006,
0x00000004, 0x0006001e, 0x00000027, 0x00000026, 0x00000026, 0x00000026, 0x00000026, 0x00040020,
0x00000028, 0x00000009, 0x00000027, 0x0004003b, 0x00000028, 0x00000029, 0x00000009, 0x00040015,
0x0000002a, 0x00000020, 0x00000001, 0x0004002b, 0x0000002a, 0x0000002b, 0x00000000, 0x00040020,
0x0000002c, 0x00000009, 0x00000006, 0x0004002b, 0x00000020, 0x00000030, 0x00000001, 0x0004002b,
0x00000020, 0x0000003f, 0x00000002, 0x0004002b, 0x00000006, 0x00000043, 0x3f000000, 0x0004002b,
0x00000006, 0x00000059, 0x4253ee82, 0x0004002b, 0x00000006, 0x0000005b, 0x3d897143, 0x0004002b,
0x00000006, 0x0000005c, 0x3bbf4590, 0x0005002c, 0x00000007, 0x0000005d, 0x0000005b, 0x0000005c,
0x00040017, 0x00000064, 0x0000002a, 0x00000002, 0x00040017, 0x00000067, 0x00000020, 0x00000003,
0x00040020, 0x00000068, 0x00000001, 0x00000067, 0x0004003b, 0x00000068, 0x00000069, 0x00000001,
0x00040017, 0x0000006a, 0x00000020, 0x00000002, 0x00020014, 0x0000006e, 0x0004002b, 0x0000002a,
0x00000072, 0x00000002, 0x00090019, 0x00000085, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000086, 0x00000085, 0x00040020, 0x00000087,
0x00000000, 0x00000086, 0x0004003b, 0x00000087, 0x00000088, 0x00000000, 0x0004002b, 0x00000006,
0x0000008f, 0x00000000, 0x0004002b, 0x00000006, 0x00000092, 0x3f7fbe77, 0x0004002b, 0x00000006,
0x0000009e, 0x42a00000, 0x00040020, 0x000000a8, 0x00000009, 0x00000026, 0x0004002b, 0x0000002a,
0x000000b4, 0x00000001, 0x0004002b, 0x00000020, 0x000000b5, 0x00000003, 0x0004002b, 0x00000006,
0x000000bb, 0x3ecccccd, 0x0004002b, 0x00000006, 0x000000bc, 0x40400000, 0x0004002b, 0x00000006,
0x000000c0, 0x3e19999a, 0x0004002b, 0x00000006, 0x000000c2, 0x3c23d70a, 0x0004002b, 0x00000006,
0x000000cb, 0x3e800000, 0x0004002b, 0x0000002a, 0x000000da, 0x00000010, 0x0004002b, 0x00000006,
0x000000e6, 0xbd4ccccd, 0x0005002c, 0x00000007, 0x000000f0, 0x0000008f, 0x0000008f, 0x00040017,
0x000000f1, 0x0000006e, 0x00000002, 0x0005002c, 0x00000007, 0x000000f8, 0x0000001d, 0x0000001d,
0x0004002b, 0x00000006, 0x00000114, 0x3ca3d70a, 0x0004002b, 0x00000006, 0x00000116, 0x3b03126f,
0x0004002b, 0x00000006, 0x0000012b, 0x42200000, 0x0004002b, 0x0000002a, 0x00000137, 0x00000003,
0x00090019, 0x0000013d, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
0x00000008, 0x00040020, 0x0000013e, 0x00000000, 0x0000013d, 0x0004003b, 0x0000013e, 0x0000013f,
0x00000000, 0x0004002b, 0x00000020, 0x0000015b, 0x00000008, 0x0006002c, 0x00000067, 0x0000015c,
0x0000015b, 0x0000015b, 0x00000030, 0x0005002c, 0x00000007, 0x000001ad, 0x00000043, 0x00000043,
0x0004002b, 0x00000006, 0x000001af, 0x3dcccccd, 0x0004002b, 0x00000006, 0x000001b0, 0x3d800000,
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
0x0000015d, 0x00000000, 0x000300fb, 0x00000021, 0x0000015e, 0x000200f8, 0x0000015e, 0x0004003d,
0x00000067, 0x0000006b, 0x00000069, 0x0007004f, 0x0000006a, 0x0000006c, 0x0000006b, 0x0000006b,
0x00000000, 0x00000001, 0x0004007c, 0x00000064, 0x0000006d, 0x0000006c, 0x00050051, 0x0000002a,
0x00000071, 0x0000006d, 0x00000000, 0x00060041, 0x0000002c, 0x00000073, 0x00000029, 0x00000072,
0x00000021, 0x0004003d, 0x00000006, 0x00000074, 0x00000073, 0x0004006e, 0x0000002a, 0x00000075,
0x00000074, 0x000500af, 0x0000006e, 0x00000076, 0x00000071, 0x00000075, 0x000400a8, 0x0000006e,
0x00000077, 0x00000076, 0x000300f7, 0x00000079, 0x00000000, 0x000400fa, 0x00000077, 0x00000078,
0x00000079, 0x000200f8, 0x00000078, 0x00050051, 0x0000002a, 0x0000007b, 0x0000006d, 0x00000001,
0x00060041, 0x0000002c, 0x0000007c, 0x00000029, 0x00000072, 0x00000030, 0x0004003d, 0x00000006,
0x0000007d, 0x0000007c, 0x0004006e, 0x0000002a, 0x0000007e, 0x0000007d, 0x000500af, 0x0000006e,
0x0000007f, 0x0000007b, 0x0000007e, 0x000200f9, 0x00000079, 0x000200f8, 0x00000079, 0x000700f5,
0x0000006e, 0x00000080, 0x00000076, 0x0000015e, 0x0000007f, 0x00000078, 0x000300f7, 0x00000082,
0x00000000, 0x000400fa, 0x00000080, 0x00000081, 0x00000082, 0x000200f8, 0x00000081, 0x000200f9,
0x0000015d, 0x000200f8, 0x00000082, 0x0004003d, 0x00000086, 0x00000089, 0x00000088, 0x00040064,
0x00000085, 0x0000008b, 0x00000089, 0x0007005f, 0x00000026, 0x0000008c, 0x0000008b, 0x0000006d,
0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x0000008d, 0x0000008c, 0x00000000, 0x000500bc,
0x0000006e, 0x00000090, 0x0000008d, 0x0000008f, 0x000500be, 0x0000006e, 0x00000093, 0x0000008d,
0x00000092, 0x000500a6, 0x0000006e, 0x00000094, 0x00000090, 0x00000093, 0x000300f7, 0x00000096,
0x00000000, 0x000400fa, 0x00000094, 0x00000095, 0x00000096, 0x000200f8, 0x00000095, 0x000200f9,
0x0000015d, 0x000200f8, 0x00000096, 0x00060041, 0x0000002c, 0x0000009a, 0x00000029, 0x0000002b,
0x0000003f, 0x0004003d, 0x00000006, 0x0000009b, 0x0000009a, 0x00050085, 0x00000006, 0x0000009c,
0x0000008d, 0x0000009b, 0x000500ba, 0x0000006e, 0x0000009f, 0x0000009c, 0x0000009e, 0x000300f7,
0x000000a1, 0x00000000, 0x000400fa, 0x0000009f, 0x000000a0, 0x000000a1, 0x000200f8, 0x000000a0,
0x000200f9, 0x0000015d, 0x000200f8, 0x000000a1, 0x0004006f, 0x00000007, 0x000000a5, 0x0000006d,
0x00050081, 0x00000007, 0x000000a7, 0x000000a5, 0x000001ad, 0x00050041, 0x000000a8, 0x000000a9,
0x00000029, 0x00000072, 0x0004003d, 0x00000026, 0x000000aa, 0x000000a9, 0x0007004f, 0x00000007,
0x000000ab, 0x000000aa, 0x000000aa, 0x00000002, 0x00000003, 0x00050085, 0x00000007, 0x000000ac,
0x000000a7, 0x000000ab, 0x0005008e, 0x00000007, 0x00000167, 0x000000ac, 0x0000001b, 0x00050083,
0x00000007, 0x00000169, 0x00000167, 0x000000f8, 0x00050051, 0x00000006, 0x0000016b, 0x00000169,
0x00000000, 0x00050085, 0x00000006, 0x0000016d, 0x0000016b, 0x0000009c, 0x00060041, 0x0000002c,
0x0000016e, 0x00000029, 0x0000002b, 0x00000021, 0x0004003d, 0x00000006, 0x0000016f, 0x0000016e,
0x00050088, 0x00000006, 0x00000170, 0x0000016d, 0x0000016f, 0x00050051, 0x00000006, 0x00000172,
0x00000169, 0x00000001, 0x0004007f, 0x00000006, 0x00000173, 0x00000172, 0x00050085, 0x00000006,
0x00000175, 0x00000173, 0x0000009c, 0x00060041, 0x0000002c, 0x00000176, 0x00000029, 0x0000002b,
0x00000030, 0x0004003d, 0x00000006, 0x00000177, 0x00000176, 0x00050088, 0x00000006, 0x00000178,
0x00000175, 0x00000177, 0x0004007f, 0x00000006, 0x0000017a, 0x0000009c, 0x00060050, 0x0000000a,
0x0000017b, 0x00000170, 0x00000178, 0x0000017a, 0x00060041, 0x0000002c, 0x000000b6, 0x00000029,
0x000000b4, 0x000000b5, 0x0004003d, 0x00000006, 0x000000b7, 0x000000b6, 0x00050085, 0x00000006,
0x000000ba, 0x0000009c, 0x000001af, 0x0008000c, 0x00000006, 0x000000bd, 0x00000001, 0x0000002b,
0x000000ba, 0x000000bb, 0x000000bc, 0x00050085, 0x00000006, 0x000000be, 0x000000b7, 0x000000bd,
0x00050085, 0x00000006, 0x000000c3, 0x0000009c, 0x000000c2, 0x00050081, 0x00000006, 0x000000c4,
0x000000c0, 0x000000c3, 0x00050085, 0x00000006, 0x000000c8, 0x000000be, 0x000001b0, 0x00050094,
0x00000006, 0x0000017f, 0x000000a5, 0x0000005d, 0x0006000c, 0x00000006, 0x00000180, 0x00000001,
0x0000000a, 0x0000017f, 0x00050085, 0x00000006, 0x00000181, 0x00000059, 0x00000180, 0x0006000c,
0x00000006, 0x00000182, 0x00000001, 0x0000000a, 0x00000181, 0x00050081, 0x00000006, 0x000000d0,
0x000000cb, 0x00000182, 0x00050085, 0x00000006, 0x000000d1, 0x000000c8, 0x000000d0, 0x000200f9,
0x000000d4, 0x000200f8, 0x000000d4, 0x000700f5, 0x00000006, 0x000001a7, 0x000000d1, 0x000000a1,
0x00000129, 0x000000d7, 0x000700f5, 0x0000002a, 0x000001a6, 0x0000002b, 0x000000a1, 0x00000126,
0x000000d7, 0x000500b1, 0x0000006e, 0x000000db, 0x000001a6, 0x000000da, 0x000400f6, 0x000000d6,
0x000000d7, 0x00000000, 0x000400fa, 0x000000db, 0x000000d5, 0x000000d6, 0x000200f8, 0x000000d5,
0x00050041, 0x000000a8, 0x000000de, 0x00000029, 0x000000b4, 0x0004003d, 0x00000026, 0x000000df,
0x000000de, 0x0008004f, 0x0000000a, 0x000000e0, 0x000000df, 0x000000df, 0x00000000, 0x00000001,
0x00000002, 0x0005008e, 0x0000000a, 0x000000e2, 0x000000e0, 0x000001a7, 0x00050081, 0x0000000a,
0x000000e3, 0x0000017b, 0x000000e2, 0x00050051, 0x00000006, 0x000000e5, 0x000000e3, 0x00000002,
0x000500be, 0x0000006e, 0x000000e7, 0x000000e5, 0x000000e6, 0x000300f7, 0x000000e9, 0x00000000,
0x000400fa, 0x000000e7, 0x000000e8, 0x000000e9, 0x000200f8, 0x000000e8, 0x000200f9, 0x000000d6,
0x000200f8, 0x000000e9, 0x0004007f, 0x00000006, 0x00000188, 0x000000e5, 0x00050051, 0x00000006,
0x0000018a, 0x000000e3, 0x00000000, 0x00050085, 0x00000006, 0x0000018b, 0x00000043, 0x0000018a,
0x00050085, 0x00000006, 0x0000018e, 0x0000018b, 0x0000016f, 0x00050088, 0x00000006, 0x00000190,
0x0000018e, 0x00000188, 0x00050081, 0x00000006, 0x00000191, 0x00000043, 0x00000190, 0x00050051,
0x00000006, 0x00000193, 0x000000e3, 0x00000001, 0x00050085, 0x00000006, 0x00000194, 0x00000043,
0x00000193, 0x00050085, 0x00000006, 0x00000197, 0x00000194, 0x00000177, 0x00050088, 0x00000006,
0x00000199, 0x00000197, 0x00000188, 0x00050083, 0x00000006, 0x0000019a, 0x00000043, 0x00000199,
0x00050050, 0x00000007, 0x0000019b, 0x00000191, 0x0000019a, 0x000500b8, 0x000000f1, 0x000000f2,
0x0000019b, 0x000000f0, 0x0004009a, 0x0000006e, 0x000000f3, 0x000000f2, 0x000400a8, 0x0000006e,
0x000000f4, 0x000000f3, 0x000300f7, 0x000000f6, 0x00000000, 0x000400fa, 0x000000f4, 0x000000f5,
0x000000f6, 0x000200f8, 0x000000f5, 0x000500ba, 0x000000f1, 0x000000f9, 0x0000019b, 0x000000f8,
0x0004009a, 0x0000006e, 0x000000fa, 0x000000f9, 0x000200f9, 0x000000f6, 0x000200f8, 0x000000f6,
0x000700f5, 0x0000006e, 0x000000fb, 0x000000f3, 0x000000e9, 0x000000fa, 0x000000f5, 0x000300f7,
0x000000fd, 0x00000000, 0x000400fa, 0x000000fb, 0x000000fc, 0x000000fd, 0x000200f8, 0x000000fc,
0x000200f9, 0x000000d6, 0x000200f8, 0x000000fd, 0x0004003d, 0x00000086, 0x00000100, 0x00000088,
0x0007004f, 0x00000007, 0x00000104, 0x000000aa, 0x000000aa, 0x00000000, 0x00000001, 0x00050085,
0x00000007, 0x00000105, 0x0000019b, 0x00000104, 0x0004006e, 0x00000064, 0x00000106, 0x00000105,
0x00040064, 0x00000085, 0x00000107, 0x00000100, 0x0007005f, 0x00000026, 0x00000108, 0x00000107,
0x00000106, 0x00000002, 0x0000002b, 0x00050051, 0x00000006, 0x00000109, 0x00000108, 0x00000000,
0x00050085, 0x00000006, 0x0000010c, 0x00000109, 0x0000009b, 0x00050083, 0x00000006, 0x00000112,
0x00000188, 0x0000010c, 0x00050085, 0x00000006, 0x00000117, 0x0000009c, 0x00000116, 0x00050081,
0x00000006, 0x00000118, 0x00000114, 0x00000117, 0x000500ba, 0x0000006e, 0x00000119, 0x00000112,
0x00000118, 0x000500b8, 0x0000006e, 0x0000011c, 0x00000112, 0x000000c4, 0x000500a7, 0x0000006e,
0x0000011d, 0x00000119, 0x0000011c, 0x000300f7, 0x0000011f, 0x00000000, 0x000400fa, 0x0000011d,
0x0000011e, 0x0000011f, 0x000200f8, 0x0000011e, 0x0004006f, 0x00000006, 0x00000121, 0x000001a6,
0x00050085, 0x00000006, 0x00000122, 0x00000121, 0x000001b0, 0x00050083, 0x00000006, 0x00000123,
0x0000001d, 0x00000122, 0x000200f9, 0x000000d6, 0x000200f8, 0x0000011f, 0x000200f9, 0x000000d7,
0x000200f8, 0x000000d7, 0x00050080, 0x0000002a, 0x00000126, 0x000001a6, 0x000000b4, 0x00050081,
0x00000006, 0x00000129, 0x000001a7, 0x000000c8, 0x000200f9, 0x000000d4, 0x000200f8, 0x000000d6,
0x000b00f5, 0x00000006, 0x000001aa, 0x0000008f, 0x000000d4, 0x0000008f, 0x000000e8, 0x0000008f,
0x000000fc, 0x00000123, 0x0000011e, 0x0008000c, 0x00000006, 0x0000012d, 0x00000001, 0x00000031,
0x0000012b, 0x0000009e, 0x0000009c, 0x00050083, 0x00000006, 0x0000012e, 0x0000001d, 0x0000012d,
0x00060041, 0x0000002c, 0x00000130, 0x00000029, 0x0000002b, 0x000000b5, 0x0004003d, 0x00000006,
0x00000131, 0x00000130, 0x00050085, 0x00000006, 0x00000133, 0x00000131, 0x000001aa, 0x00050085,
0x00000006, 0x00000135, 0x00000133, 0x0000012e, 0x00050083, 0x00000006, 0x00000136, 0x0000001d,
0x00000135, 0x00060041, 0x0000002c, 0x00000138, 0x00000029, 0x00000137, 0x00000021, 0x0004003d,
0x00000006, 0x00000139, 0x00000138, 0x000500ba, 0x0000006e, 0x0000013a, 0x00000139, 0x00000043,
0x000300f7, 0x0000013c, 0x00000000, 0x000400fa, 0x0000013a, 0x0000013b, 0x0000013c, 0x000200f8,
0x0000013b, 0x0004003d, 0x0000013d, 0x00000140, 0x0000013f, 0x00070050, 0x00000026, 0x00000143,
0x00000136, 0x00000136, 0x00000136, 0x0000001d, 0x00040063, 0x00000140, 0x0000006d, 0x00000143,
0x000200f9, 0x0000015d, 0x000200f8, 0x0000013c, 0x000500be, 0x0000006e, 0x00000146, 0x00000136,
0x00000092, 0x000300f7, 0x00000148, 0x00000000, 0x000400fa, 0x00000146, 0x00000147, 0x00000148,
0x000200f8, 0x00000147, 0x000200f9, 0x0000015d, 0x000200f8, 0x00000148, 0x0004003d, 0x0000013d,
0x0000014c, 0x0000013f, 0x00050062, 0x00000026, 0x0000014e, 0x0000014c, 0x0000006d, 0x0004003d,
0x0000013d, 0x0000014f, 0x0000013f, 0x0008004f, 0x0000000a, 0x00000152, 0x0000014e, 0x0000014e,
0x00000000, 0x00000001, 0x00000002, 0x0005008e, 0x0000000a, 0x00000154, 0x00000152, 0x00000136,
0x00050051, 0x00000006, 0x00000156, 0x0000014e, 0x00000003, 0x00050051, 0x00000006, 0x00000157,
0x00000154, 0x00000000, 0x00050051, 0x00000006, 0x00000158, 0x00000154, 0x00000001, 0x00050051,
0x00000006, 0x00000159, 0x00000154, 0x00000002, 0x00070050, 0x00000026, 0x0000015a, 0x00000157,
0x00000158, 0x00000159, 0x00000156, 0x00040063, 0x0000014f, 0x0000006d, 0x0000015a, 0x000200f9,
0x0000015d, 0x000200f8, 0x0000015d, 0x000100fd, 0x00010038,
};
+1 -1
View File
@@ -109,7 +109,7 @@ EXCL(16, uint16_t)
EXCL(32, uint32_t)
EXCL(64, uint64_t)
// Exclusive pair of two X registers: one 16-byte-aligned 128-bit access (cmpxchg16b; x86-64-v3 has CX16).
// Exclusive pair of two X registers: one 16-byte-aligned 128-bit access (cmpxchg16b; x86-64-v2 has CX16).
// A pair of two W registers is a 64-bit exclusive and uses ldx64/stx64.
static inline __uint128_t ldxp128(Cpu *c, uint64_t a) {
__uint128_t v = __atomic_load_n((__uint128_t *)(uintptr_t)a, __ATOMIC_ACQUIRE);
+259
View File
@@ -0,0 +1,259 @@
// Discord Rich Presence: "Playing TotK Recomp" with the elapsed play time in the player's Discord status, through
// the local IPC of a running Discord client (platform.h's plat_ipc_*). Nothing leaves the machine but the activity
// itself, and nothing is sent while TOTK_DISCORD_RPC is off (the default: the status is information about the
// player's session). TOTK_DISCORD_APP_ID names the application whose name and artwork Discord shows — the default
// is the project's own application, registered as "TotK Recomp"; a player who wants their own name or
// art registers an application at discord.com/developers and puts its id in totk.ini — and TOTK_DISCORD_IMAGE the
// key of its uploaded art asset.
//
// One host thread owns the connection and never holds a virtual core (kernel.c: host work runs without one). It
// re-reads the settings when cfg_version changes (a menu change reaches it within half a second), connects to the
// first of Discord's discord-ipc-0..9 pipes that answers, and writes the activity again every 60 s: that write is
// the keep-alive, and one that fails closes the connection and tries again 15 s later. Discord clears the status by
// itself when the process ends and the pipe closes, so there is no shutdown path. Every answer frame is read and
// discarded: unread, they would fill the pipe's buffer and block the client's writes.
//
// The protocol is the documented IPC one (discord-rpc's src/rpc_connection.h): a frame is <opcode u32 little
// endian><length u32><length bytes of JSON>, with Handshake 0, Frame 1, Close 2, Ping 3 and Pong 4. The handshake
// (opcode 0) carries {"v":1,"client_id":...} and is answered by a Ready frame (opcode 1); every command,
// SET_ACTIVITY here, is a frame (opcode 1) answered by one, and a Ping the client sends is echoed as a Pong. A
// client that speaks another numbering (an early arRPC did) answers the handshake with a Close. --probe-discord
// runs the same exchange once, without the game and without the setting being on.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
enum { OP_HANDSHAKE = 0, OP_FRAME = 1, OP_CLOSE = 2, OP_PING = 3, OP_PONG = 4 };
#define APP_ID_CAP 24 // a Discord snowflake: at most 20 digits
#define IMAGE_KEY_CAP 64
#define ANSWER_CAP 8192 // the Ready frame carries the client's config; a few KiB
// A settings value that fits the JSON below without escaping: the app id is digits, an asset key letters, digits,
// '_' and '-' (what Discord's developer portal accepts for an asset key).
static int valid_app_id(const char *s) {
size_t n = strlen(s);
if (!n || n > 20) return 0;
for (const char *p = s; *p; p++)
if (*p < '0' || *p > '9') return 0;
return 1;
}
static int valid_image_key(const char *s) {
size_t n = strlen(s);
if (!n || n >= IMAGE_KEY_CAP) return 0;
for (const char *p = s; *p; p++)
if ((*p < 'a' || *p > 'z') && (*p < 'A' || *p > 'Z') && (*p < '0' || *p > '9') && *p != '_' && *p != '-')
return 0;
return 1;
}
// The setting's value, or its default when the value does not fit the protocol (logged: the player sees why their
// own app does not show). Called on the presence thread and by the probe, after cfg_version changed or once.
static void read_setting(const char *name, char *out, size_t cap, int (*valid)(const char *)) {
const char *v = cfg_value(name);
if (valid(v)) {
snprintf(out, cap, "%s", v);
return;
}
snprintf(out, cap, "%s", cfg_meta(name)->def);
rt_log("discord: %s does not fit what the Rich Presence protocol takes; using %s", name + strlen("TOTK_"), out);
}
static void le32(uint8_t *at, uint32_t v) {
at[0] = (uint8_t)v, at[1] = (uint8_t)(v >> 8), at[2] = (uint8_t)(v >> 16), at[3] = (uint8_t)(v >> 24);
}
static int write_frame(PlatIpc *p, uint32_t op, const char *json) {
uint8_t head[8];
le32(head, op);
le32(head + 4, (uint32_t)strlen(json));
return plat_ipc_write(p, head, sizeof head) && plat_ipc_write(p, json, strlen(json));
}
// One frame off the wire, its 8-byte header in as many reads as the peer made, then its payload; a payload longer
// than cap-1 fills buf with its first cap-1 bytes and the rest is still consumed. *op gets the opcode; buf is
// NUL-terminated. 1 on a whole frame, 0 when the deadline passed, -1 when the connection broke.
static int read_one_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint64_t deadline) {
uint8_t head[8];
size_t got = 0;
while (got < sizeof head) {
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
int n = plat_ipc_read(p, head + got, sizeof head - got, left);
if (n < 0) return -1;
if (!n) {
if (plat_now_ms() >= deadline) return 0;
continue;
}
got += (size_t)n;
}
*op = head[0] | head[1] << 8 | head[2] << 16 | (uint32_t)head[3] << 24;
uint32_t len = head[4] | head[5] << 8 | head[6] << 16 | (uint32_t)head[7] << 24;
size_t taken = 0;
while (taken < len) {
size_t want = len - taken > cap - 1 ? cap - 1 : len - taken;
uint32_t left = deadline > plat_now_ms() ? (uint32_t)(deadline - plat_now_ms()) : 0;
int n = plat_ipc_read(p, buf, want, left);
if (n < 0) return -1;
if (!n) {
if (plat_now_ms() >= deadline) return 0;
continue;
}
taken += (size_t)n;
}
buf[taken < cap - 1 ? taken : cap - 1] = 0;
return 1;
}
// The next frame that is not a Ping: a Ping the client sent to see we are alive is echoed as a Pong with its own
// payload, a Close (the client told us it is done) counts as a broken connection so the caller connects again
// later. Same return values as read_one_frame; the deadline covers the whole exchange.
static int read_frame(PlatIpc *p, uint32_t *op, char *buf, size_t cap, uint32_t ms) {
uint64_t deadline = plat_now_ms() + ms;
for (;;) {
int r = read_one_frame(p, op, buf, cap, deadline);
if (r != 1) return r;
if (*op == OP_PING) {
uint8_t head[8];
size_t n = strlen(buf);
le32(head, OP_PONG);
le32(head + 4, (uint32_t)n);
if (!plat_ipc_write(p, head, sizeof head) || !plat_ipc_write(p, buf, n)) return -1;
continue;
}
if (*op == OP_CLOSE) return -1;
return 1;
}
}
// The first of Discord's pipes that completes a handshake. NULL when no client is running or none answered in time;
// the caller tries again later.
static PlatIpc *connect_client(const char *app_id) {
for (int i = 0; i < 10; i++) {
char name[32], json[160];
snprintf(name, sizeof name, "discord-ipc-%d", i);
PlatIpc *p = plat_ipc_connect(name);
if (!p) continue;
snprintf(json, sizeof json, "{\"v\":1,\"client_id\":\"%s\"}", app_id);
if (!write_frame(p, OP_HANDSHAKE, json)) {
plat_ipc_close(p);
continue;
}
uint32_t op = 0;
char answer[ANSWER_CAP];
if (read_frame(p, &op, answer, sizeof answer, 2000) == 1 && op == OP_FRAME) return p; // the Ready event
plat_ipc_close(p); // this pipe answered but not with a Ready frame in 2 s: the next may belong to a client
}
return NULL;
}
static uint32_t next_nonce(void) {
static volatile int32_t n;
return (uint32_t)plat_inc32(&n);
}
// The activity as one SET_ACTIVITY frame: the game update and the port version as the details line ("Game 1.4.3 ·
// Recomp 0.3.7": the port version without its +commit suffix, the art's hover text keeps the full one; the dot goes
// as a JSON escape), the process's start as the timestamp Discord counts the elapsed time from, and the application's
// art asset. 1 when the client answered (a slow answer that ran out of the 2 s read is fine: the next drain takes
// it), 0 when the connection broke.
static int send_activity(PlatIpc *p, const char *image) {
char json[512];
int v = rt_game_version;
uint64_t start = (plat_unix_time_ns() - plat_process_uptime_ns()) / 1000000000ull;
int port_len = (int)strcspn(rt_port_version, "+");
snprintf(json, sizeof json,
"{\"cmd\":\"SET_ACTIVITY\",\"nonce\":\"%u\",\"args\":{\"pid\":%lu,\"activity\":{\"details\":\"Game "
"%d.%d.%d \\u00b7 Recomp %.*s\",\"timestamps\":{\"start\":%llu},\"assets\":{\"large_image\":\"%s\","
"\"large_text\":\"TotK Recomp %s\"}}}}",
next_nonce(), (unsigned long)plat_process_id(), v / 100, v / 10 % 10, v % 10, port_len, rt_port_version,
(unsigned long long)start, image, rt_port_version);
if (!write_frame(p, OP_FRAME, json)) return 0;
uint32_t op = 0;
char answer[ANSWER_CAP];
return read_frame(p, &op, answer, sizeof answer, 2000) >= 0;
}
// ---- the presence thread
static void discord_main(void *arg) {
(void)arg;
unsigned seen = 0;
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
PlatIpc *ipc = NULL;
uint64_t retry_at = 0, send_at = 0;
int said_no_client = 0;
for (;;) {
unsigned v = cfg_version();
if (v != seen) { // the settings changed (the menu writes them): pick the new ones up at once
seen = v;
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
if (ipc) plat_ipc_close(ipc), ipc = NULL; // the new app id needs a new handshake; the image is resent
said_no_client = 0;
}
if (!cfg_bool("TOTK_DISCORD_RPC")) {
if (ipc) plat_ipc_close(ipc), ipc = NULL;
plat_sleep_ms(500);
continue;
}
if (!ipc && plat_now_ms() >= retry_at) {
ipc = connect_client(app_id);
if (!ipc) {
retry_at = plat_now_ms() + 15000;
if (!said_no_client)
rt_log("discord: no client is running; trying again every 15 s while the setting is on");
said_no_client = 1;
} else {
rt_log("discord: connected (app %s)", app_id);
send_at = 0;
said_no_client = 0;
}
}
if (ipc && plat_now_ms() >= send_at) {
if (send_activity(ipc, image)) {
send_at = plat_now_ms() + 60000;
} else {
rt_log("discord: the connection broke; reconnecting");
plat_ipc_close(ipc);
ipc = NULL;
retry_at = plat_now_ms() + 15000;
}
}
plat_sleep_ms(500);
}
}
void discord_init(void) {
if (!plat_thread_spawn(discord_main, NULL, "discord", 0)) rt_log("discord: cannot start the presence thread");
}
// ---- --probe-discord: the exchange above without the game and the setting; needs no game files
int probe_discord(void) {
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL discord: %s\n", what); } } while (0)
char app_id[APP_ID_CAP], image[IMAGE_KEY_CAP];
read_setting("TOTK_DISCORD_APP_ID", app_id, sizeof app_id, valid_app_id);
read_setting("TOTK_DISCORD_IMAGE", image, sizeof image, valid_image_key);
PlatIpc *p = connect_client(app_id);
if (!p) {
fprintf(stderr, "discord: probe skipped: no client on discord-ipc-0..9\n");
return 0;
}
fprintf(stderr, "discord: connected (app %s)\n", app_id);
CHECK(send_activity(p, image), "SET_ACTIVITY written and answered");
fprintf(stderr, "discord: activity set; the status shows it for ten seconds, then the connection closes\n");
plat_sleep_ms(10000);
plat_ipc_close(p);
fprintf(stderr, "discord: probe %s\n", fails ? "FAILED" : "passed");
return fails != 0;
#undef CHECK
}
+1
View File
@@ -52,6 +52,7 @@ void rt_fatal(const char *fmt, ...) {
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_log("fatal: memory commit: this process %.0f MB, the system %.0f of %.0f MB", proc, used, limit);
kern_free_cores_note();
fflush(stdout);
fflush(stderr);
_Exit(70);
+66
View File
@@ -0,0 +1,66 @@
// Generated by tools/make_dlaa_mask.py: DLSS's reactive mask pass (vk_dlaa.c mask_pass). Do not edit.
#pragma once
#include <stdint.h>
static const uint32_t dlaa_mask_spv[] = {
0x07230203, 0x00010000, 0x00000000, 0x00000055, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000002, 0x6e69616d, 0x00000000, 0x0000001b,
0x00060010, 0x00000002, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000001b,
0x0000000b, 0x0000001c, 0x00040047, 0x00000018, 0x00000022, 0x00000000, 0x00040047, 0x00000018,
0x00000021, 0x00000000, 0x00040047, 0x00000019, 0x00000022, 0x00000000, 0x00040047, 0x00000019,
0x00000021, 0x00000001, 0x00040047, 0x0000001a, 0x00000022, 0x00000000, 0x00040047, 0x0000001a,
0x00000021, 0x00000002, 0x00030047, 0x0000001a, 0x00000019, 0x00030047, 0x00000015, 0x00000002,
0x00050048, 0x00000015, 0x00000000, 0x00000023, 0x00000000, 0x00020013, 0x00000003, 0x00030021,
0x00000004, 0x00000003, 0x00020014, 0x00000005, 0x00030016, 0x00000006, 0x00000020, 0x00040015,
0x00000007, 0x00000020, 0x00000001, 0x00040015, 0x00000008, 0x00000020, 0x00000000, 0x00040017,
0x00000009, 0x00000006, 0x00000002, 0x00040017, 0x0000000a, 0x00000006, 0x00000003, 0x00040017,
0x0000000b, 0x00000006, 0x00000004, 0x00040017, 0x0000000c, 0x00000007, 0x00000002, 0x00040017,
0x0000000d, 0x00000008, 0x00000002, 0x00040017, 0x0000000e, 0x00000008, 0x00000003, 0x00090019,
0x0000000f, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000,
0x0003001b, 0x00000010, 0x0000000f, 0x00090019, 0x00000011, 0x00000006, 0x00000001, 0x00000000,
0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000012, 0x00000000, 0x00000010,
0x00040020, 0x00000013, 0x00000000, 0x00000011, 0x00040020, 0x00000014, 0x00000001, 0x0000000e,
0x0003001e, 0x00000015, 0x0000000b, 0x00040020, 0x00000016, 0x00000009, 0x00000015, 0x00040020,
0x00000017, 0x00000009, 0x0000000b, 0x0004002b, 0x00000007, 0x0000001d, 0x00000000, 0x0004002b,
0x00000006, 0x0000001e, 0x00000000, 0x0004002b, 0x00000006, 0x0000001f, 0x3f800000, 0x0004002b,
0x00000006, 0x00000020, 0x38d1b717, 0x0004003b, 0x00000012, 0x00000018, 0x00000000, 0x0004003b,
0x00000012, 0x00000019, 0x00000000, 0x0004003b, 0x00000013, 0x0000001a, 0x00000000, 0x0004003b,
0x00000014, 0x0000001b, 0x00000001, 0x0004003b, 0x00000016, 0x0000001c, 0x00000009, 0x00050036,
0x00000003, 0x00000002, 0x00000000, 0x00000004, 0x000200f8, 0x00000021, 0x00050041, 0x00000017,
0x00000024, 0x0000001c, 0x0000001d, 0x0004003d, 0x0000000b, 0x00000025, 0x00000024, 0x0004003d,
0x0000000e, 0x00000026, 0x0000001b, 0x0007004f, 0x0000000d, 0x00000027, 0x00000026, 0x00000026,
0x00000000, 0x00000001, 0x00050051, 0x00000008, 0x00000028, 0x00000026, 0x00000000, 0x00040070,
0x00000006, 0x00000029, 0x00000028, 0x00050051, 0x00000008, 0x0000002a, 0x00000026, 0x00000001,
0x00040070, 0x00000006, 0x0000002b, 0x0000002a, 0x00050051, 0x00000006, 0x0000002c, 0x00000025,
0x00000000, 0x00050051, 0x00000006, 0x0000002d, 0x00000025, 0x00000001, 0x000500b8, 0x00000005,
0x0000002e, 0x00000029, 0x0000002c, 0x000500b8, 0x00000005, 0x0000002f, 0x0000002b, 0x0000002d,
0x000500a7, 0x00000005, 0x00000030, 0x0000002e, 0x0000002f, 0x000300f7, 0x00000023, 0x00000000,
0x000400fa, 0x00000030, 0x00000022, 0x00000023, 0x000200f8, 0x00000022, 0x0004007c, 0x0000000c,
0x00000031, 0x00000027, 0x0004003d, 0x00000010, 0x00000032, 0x00000018, 0x00040064, 0x0000000f,
0x00000033, 0x00000032, 0x0007005f, 0x0000000b, 0x00000034, 0x00000033, 0x00000031, 0x00000002,
0x0000001d, 0x0008004f, 0x0000000a, 0x00000035, 0x00000034, 0x00000034, 0x00000000, 0x00000001,
0x00000002, 0x0004003d, 0x00000010, 0x00000036, 0x00000019, 0x00040064, 0x0000000f, 0x00000037,
0x00000036, 0x0007005f, 0x0000000b, 0x00000038, 0x00000037, 0x00000031, 0x00000002, 0x0000001d,
0x0008004f, 0x0000000a, 0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x00000002,
0x00050083, 0x0000000a, 0x0000003a, 0x00000039, 0x00000035, 0x0006000c, 0x0000000a, 0x0000003b,
0x00000001, 0x00000004, 0x0000003a, 0x00050051, 0x00000006, 0x0000003c, 0x0000003b, 0x00000000,
0x00050051, 0x00000006, 0x0000003d, 0x0000003b, 0x00000001, 0x00050051, 0x00000006, 0x0000003e,
0x0000003b, 0x00000002, 0x0007000c, 0x00000006, 0x0000003f, 0x00000001, 0x00000028, 0x0000003c,
0x0000003d, 0x0007000c, 0x00000006, 0x00000040, 0x00000001, 0x00000028, 0x0000003f, 0x0000003e,
0x00050051, 0x00000006, 0x00000041, 0x00000039, 0x00000000, 0x00050051, 0x00000006, 0x00000042,
0x00000039, 0x00000001, 0x00050051, 0x00000006, 0x00000043, 0x00000039, 0x00000002, 0x0007000c,
0x00000006, 0x00000044, 0x00000001, 0x00000028, 0x00000041, 0x00000042, 0x0007000c, 0x00000006,
0x00000045, 0x00000001, 0x00000028, 0x00000044, 0x00000043, 0x00050051, 0x00000006, 0x00000046,
0x00000035, 0x00000000, 0x00050051, 0x00000006, 0x00000047, 0x00000035, 0x00000001, 0x00050051,
0x00000006, 0x00000048, 0x00000035, 0x00000002, 0x0007000c, 0x00000006, 0x00000049, 0x00000001,
0x00000028, 0x00000046, 0x00000047, 0x0007000c, 0x00000006, 0x0000004a, 0x00000001, 0x00000028,
0x00000049, 0x00000048, 0x0007000c, 0x00000006, 0x0000004b, 0x00000001, 0x00000028, 0x00000045,
0x0000004a, 0x0007000c, 0x00000006, 0x0000004c, 0x00000001, 0x00000028, 0x0000004b, 0x00000020,
0x00050088, 0x00000006, 0x0000004d, 0x00000040, 0x0000004c, 0x00050051, 0x00000006, 0x0000004e,
0x00000025, 0x00000002, 0x00050051, 0x00000006, 0x0000004f, 0x00000025, 0x00000003, 0x00050083,
0x00000006, 0x00000050, 0x0000004d, 0x0000004e, 0x00050085, 0x00000006, 0x00000051, 0x00000050,
0x0000004f, 0x0008000c, 0x00000006, 0x00000052, 0x00000001, 0x0000002b, 0x00000051, 0x0000001e,
0x0000001f, 0x00070050, 0x0000000b, 0x00000053, 0x00000052, 0x00000052, 0x00000052, 0x00000052,
0x0004003d, 0x00000011, 0x00000054, 0x0000001a, 0x00040063, 0x00000054, 0x00000031, 0x00000053,
0x000200f9, 0x00000023, 0x000200f8, 0x00000023, 0x000100fd, 0x00010038
};
+162
View File
@@ -0,0 +1,162 @@
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
// source sha256 e96a6d2d0cec26c47a76280363077c5982919bedc984f20ae9f1d185dc6ae674
#pragma once
#include <stdint.h>
static const uint32_t dlaa_mv_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x0000019a, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
0x00000000, 0x00000001, 0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
0x00060010, 0x00000004, 0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x00000036,
0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002, 0x00050048, 0x00000041, 0x00000000,
0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001, 0x00000023, 0x00000008, 0x00050048,
0x00000041, 0x00000002, 0x00000023, 0x00000010, 0x00050048, 0x00000041, 0x00000003, 0x00000023,
0x00000014, 0x00040047, 0x0000005d, 0x00000021, 0x00000000, 0x00040047, 0x0000005d, 0x00000022,
0x00000000, 0x00040047, 0x00000086, 0x00000006, 0x00000010, 0x00030047, 0x00000087, 0x00000002,
0x00050048, 0x00000087, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000089, 0x00000021,
0x00000002, 0x00040047, 0x00000089, 0x00000022, 0x00000000, 0x00040047, 0x0000009d, 0x00000006,
0x00000010, 0x00030047, 0x0000009e, 0x00000002, 0x00050048, 0x0000009e, 0x00000000, 0x00000023,
0x00000000, 0x00040047, 0x000000a0, 0x00000021, 0x00000003, 0x00040047, 0x000000a0, 0x00000022,
0x00000000, 0x00030047, 0x000000e7, 0x00000019, 0x00040047, 0x000000e7, 0x00000021, 0x00000001,
0x00040047, 0x000000e7, 0x00000022, 0x00000000, 0x00040047, 0x000000f6, 0x00000021, 0x00000004,
0x00040047, 0x000000f6, 0x00000022, 0x00000000, 0x00030047, 0x00000101, 0x00000019, 0x00040047,
0x00000101, 0x00000021, 0x00000007, 0x00040047, 0x00000101, 0x00000022, 0x00000000, 0x00030047,
0x0000012a, 0x00000018, 0x00040047, 0x0000012a, 0x00000021, 0x00000006, 0x00040047, 0x0000012a,
0x00000022, 0x00000000, 0x00030047, 0x0000013b, 0x00000019, 0x00040047, 0x0000013b, 0x00000021,
0x00000005, 0x00040047, 0x0000013b, 0x00000022, 0x00000000, 0x00040047, 0x00000146, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006,
0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000004, 0x00040018, 0x00000009, 0x00000007,
0x00000004, 0x0004002b, 0x00000006, 0x00000015, 0x3f800000, 0x0004002b, 0x00000006, 0x00000016,
0x00000000, 0x00040015, 0x0000002f, 0x00000020, 0x00000001, 0x00040017, 0x00000030, 0x0000002f,
0x00000002, 0x00040015, 0x00000033, 0x00000020, 0x00000000, 0x00040017, 0x00000034, 0x00000033,
0x00000003, 0x00040020, 0x00000035, 0x00000001, 0x00000034, 0x0004003b, 0x00000035, 0x00000036,
0x00000001, 0x00040017, 0x00000037, 0x00000033, 0x00000002, 0x00020014, 0x0000003b, 0x0004002b,
0x00000033, 0x0000003c, 0x00000000, 0x00040017, 0x00000040, 0x00000006, 0x00000002, 0x0006001e,
0x00000041, 0x00000040, 0x00000040, 0x00000006, 0x00000006, 0x00040020, 0x00000042, 0x00000009,
0x00000041, 0x0004003b, 0x00000042, 0x00000043, 0x00000009, 0x0004002b, 0x0000002f, 0x00000044,
0x00000000, 0x00040020, 0x00000045, 0x00000009, 0x00000006, 0x0004002b, 0x00000033, 0x0000004d,
0x00000001, 0x00090019, 0x0000005a, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000,
0x00000001, 0x00000000, 0x0003001b, 0x0000005b, 0x0000005a, 0x00040020, 0x0000005c, 0x00000000,
0x0000005b, 0x0004003b, 0x0000005c, 0x0000005d, 0x00000000, 0x0004002b, 0x00000006, 0x00000067,
0x3f000000, 0x0004002b, 0x0000002f, 0x0000006a, 0x00000001, 0x00040020, 0x0000006b, 0x00000009,
0x00000040, 0x0004002b, 0x00000006, 0x00000075, 0x40000000, 0x0004002b, 0x00000033, 0x00000085,
0x00000010, 0x0004001c, 0x00000086, 0x00000007, 0x00000085, 0x0003001e, 0x00000087, 0x00000086,
0x00040020, 0x00000088, 0x00000002, 0x00000087, 0x0004003b, 0x00000088, 0x00000089, 0x00000002,
0x0004002b, 0x0000002f, 0x0000008a, 0x00000003, 0x0004002b, 0x0000002f, 0x0000008b, 0x00000004,
0x0004002b, 0x0000002f, 0x0000008c, 0x00000005, 0x0004002b, 0x0000002f, 0x0000008d, 0x00000006,
0x00040020, 0x0000008f, 0x00000002, 0x00000007, 0x0004001c, 0x0000009d, 0x00000007, 0x00000085,
0x0003001e, 0x0000009e, 0x0000009d, 0x00040020, 0x0000009f, 0x00000002, 0x0000009e, 0x0004003b,
0x0000009f, 0x000000a0, 0x00000002, 0x0005002c, 0x00000040, 0x000000b4, 0x00000016, 0x00000016,
0x0004002b, 0x00000006, 0x000000b9, 0x2b8cbccc, 0x0004002b, 0x00000006, 0x000000c7, 0x358637bd,
0x00090019, 0x000000e5, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002,
0x00000007, 0x00040020, 0x000000e6, 0x00000000, 0x000000e5, 0x0004003b, 0x000000e6, 0x000000e7,
0x00000000, 0x0004002b, 0x0000002f, 0x000000ee, 0x00000002, 0x0004003b, 0x0000005c, 0x000000f6,
0x00000000, 0x0004002b, 0x00000006, 0x000000fc, 0x3e808081, 0x00090019, 0x000000ff, 0x00000006,
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000002, 0x0000000f, 0x00040020, 0x00000100,
0x00000000, 0x000000ff, 0x0004003b, 0x00000100, 0x00000101, 0x00000000, 0x0004002b, 0x0000002f,
0x00000119, 0xffffffff, 0x0004003b, 0x00000100, 0x0000012a, 0x00000000, 0x0005002c, 0x00000030,
0x00000131, 0x00000044, 0x00000044, 0x0004003b, 0x00000100, 0x0000013b, 0x00000000, 0x0004002b,
0x00000033, 0x00000145, 0x00000008, 0x0006002c, 0x00000034, 0x00000146, 0x00000145, 0x00000145,
0x0000004d, 0x0005002c, 0x00000040, 0x00000197, 0x00000067, 0x00000067, 0x0005002c, 0x00000030,
0x00000199, 0x0000006a, 0x0000006a, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x000300f7, 0x00000147, 0x00000000, 0x000300fb, 0x0000003c, 0x00000148,
0x000200f8, 0x00000148, 0x0004003d, 0x00000034, 0x00000038, 0x00000036, 0x0007004f, 0x00000037,
0x00000039, 0x00000038, 0x00000038, 0x00000000, 0x00000001, 0x0004007c, 0x00000030, 0x0000003a,
0x00000039, 0x00050051, 0x0000002f, 0x0000003f, 0x0000003a, 0x00000000, 0x00060041, 0x00000045,
0x00000046, 0x00000043, 0x00000044, 0x0000003c, 0x0004003d, 0x00000006, 0x00000047, 0x00000046,
0x0004006e, 0x0000002f, 0x00000048, 0x00000047, 0x000500af, 0x0000003b, 0x00000049, 0x0000003f,
0x00000048, 0x000400a8, 0x0000003b, 0x0000004a, 0x00000049, 0x000300f7, 0x0000004c, 0x00000000,
0x000400fa, 0x0000004a, 0x0000004b, 0x0000004c, 0x000200f8, 0x0000004b, 0x00050051, 0x0000002f,
0x0000004f, 0x0000003a, 0x00000001, 0x00060041, 0x00000045, 0x00000050, 0x00000043, 0x00000044,
0x0000004d, 0x0004003d, 0x00000006, 0x00000051, 0x00000050, 0x0004006e, 0x0000002f, 0x00000052,
0x00000051, 0x000500af, 0x0000003b, 0x00000053, 0x0000004f, 0x00000052, 0x000200f9, 0x0000004c,
0x000200f8, 0x0000004c, 0x000700f5, 0x0000003b, 0x00000054, 0x00000049, 0x00000148, 0x00000053,
0x0000004b, 0x000300f7, 0x00000056, 0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056,
0x000200f8, 0x00000055, 0x000200f9, 0x00000147, 0x000200f8, 0x00000056, 0x0004003d, 0x0000005b,
0x0000005e, 0x0000005d, 0x00040064, 0x0000005a, 0x00000060, 0x0000005e, 0x0007005f, 0x00000007,
0x00000061, 0x00000060, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006, 0x00000062,
0x00000061, 0x00000000, 0x0004006f, 0x00000040, 0x00000066, 0x0000003a, 0x00050081, 0x00000040,
0x00000069, 0x00000066, 0x00000197, 0x00050041, 0x0000006b, 0x0000006c, 0x00000043, 0x0000006a,
0x0004003d, 0x00000040, 0x0000006d, 0x0000006c, 0x00050083, 0x00000040, 0x0000006e, 0x00000069,
0x0000006d, 0x00050051, 0x00000006, 0x00000071, 0x0000006e, 0x00000000, 0x00050088, 0x00000006,
0x00000074, 0x00000071, 0x00000047, 0x00050085, 0x00000006, 0x00000076, 0x00000074, 0x00000075,
0x00050083, 0x00000006, 0x00000077, 0x00000076, 0x00000015, 0x00050051, 0x00000006, 0x00000079,
0x0000006e, 0x00000001, 0x00060041, 0x00000045, 0x0000007a, 0x00000043, 0x00000044, 0x0000004d,
0x0004003d, 0x00000006, 0x0000007b, 0x0000007a, 0x00050088, 0x00000006, 0x0000007c, 0x00000079,
0x0000007b, 0x00050085, 0x00000006, 0x0000007d, 0x0000007c, 0x00000075, 0x00050083, 0x00000006,
0x0000007e, 0x00000015, 0x0000007d, 0x00050085, 0x00000006, 0x00000080, 0x00000062, 0x00000075,
0x00050083, 0x00000006, 0x00000081, 0x00000080, 0x00000015, 0x00070050, 0x00000007, 0x00000082,
0x00000077, 0x0000007e, 0x00000081, 0x00000015, 0x00060041, 0x0000008f, 0x00000090, 0x00000089,
0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x00000091, 0x00000090, 0x00060041, 0x0000008f,
0x00000093, 0x00000089, 0x00000044, 0x0000008b, 0x0004003d, 0x00000007, 0x00000094, 0x00000093,
0x00060041, 0x0000008f, 0x00000096, 0x00000089, 0x00000044, 0x0000008c, 0x0004003d, 0x00000007,
0x00000097, 0x00000096, 0x00060041, 0x0000008f, 0x00000099, 0x00000089, 0x00000044, 0x0000008d,
0x0004003d, 0x00000007, 0x0000009a, 0x00000099, 0x00070050, 0x00000009, 0x00000167, 0x00000091,
0x00000094, 0x00000097, 0x0000009a, 0x00040054, 0x00000009, 0x00000168, 0x00000167, 0x00060041,
0x0000008f, 0x000000a2, 0x000000a0, 0x00000044, 0x0000008a, 0x0004003d, 0x00000007, 0x000000a3,
0x000000a2, 0x00060041, 0x0000008f, 0x000000a5, 0x000000a0, 0x00000044, 0x0000008b, 0x0004003d,
0x00000007, 0x000000a6, 0x000000a5, 0x00060041, 0x0000008f, 0x000000a8, 0x000000a0, 0x00000044,
0x0000008c, 0x0004003d, 0x00000007, 0x000000a9, 0x000000a8, 0x00060041, 0x0000008f, 0x000000ab,
0x000000a0, 0x00000044, 0x0000008d, 0x0004003d, 0x00000007, 0x000000ac, 0x000000ab, 0x00070050,
0x00000009, 0x00000183, 0x000000a3, 0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009,
0x00000184, 0x00000183, 0x0006000c, 0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000168,
0x00050091, 0x00000007, 0x000000b2, 0x000000b0, 0x00000082, 0x00050051, 0x00000006, 0x000000b7,
0x000000b2, 0x00000003, 0x0006000c, 0x00000006, 0x000000b8, 0x00000001, 0x00000004, 0x000000b7,
0x000500ba, 0x0000003b, 0x000000ba, 0x000000b8, 0x000000b9, 0x000300f7, 0x000000bc, 0x00000000,
0x000400fa, 0x000000ba, 0x000000bb, 0x000000bc, 0x000200f8, 0x000000bb, 0x00070050, 0x00000007,
0x000000c2, 0x000000b7, 0x000000b7, 0x000000b7, 0x000000b7, 0x00050088, 0x00000007, 0x000000c3,
0x000000b2, 0x000000c2, 0x00050091, 0x00000007, 0x000000c4, 0x00000184, 0x000000c3, 0x00050051,
0x00000006, 0x000000c6, 0x000000c4, 0x00000003, 0x000500ba, 0x0000003b, 0x000000c8, 0x000000c6,
0x000000c7, 0x000300f7, 0x000000ca, 0x00000000, 0x000400fa, 0x000000c8, 0x000000c9, 0x000000ca,
0x000200f8, 0x000000c9, 0x0007004f, 0x00000040, 0x000000cd, 0x000000c4, 0x000000c4, 0x00000000,
0x00000001, 0x00050050, 0x00000040, 0x000000d0, 0x000000c6, 0x000000c6, 0x00050088, 0x00000040,
0x000000d1, 0x000000cd, 0x000000d0, 0x00050051, 0x00000006, 0x000000d4, 0x000000d1, 0x00000000,
0x00050085, 0x00000006, 0x000000d5, 0x000000d4, 0x00000067, 0x00050081, 0x00000006, 0x000000d6,
0x000000d5, 0x00000067, 0x00050085, 0x00000006, 0x000000d9, 0x000000d6, 0x00000047, 0x00050051,
0x00000006, 0x000000db, 0x000000d1, 0x00000001, 0x00050085, 0x00000006, 0x000000dc, 0x000000db,
0x00000067, 0x00050083, 0x00000006, 0x000000dd, 0x00000067, 0x000000dc, 0x00050085, 0x00000006,
0x000000e0, 0x000000dd, 0x0000007b, 0x00050050, 0x00000040, 0x000000e1, 0x000000d9, 0x000000e0,
0x00050083, 0x00000040, 0x000000e4, 0x000000e1, 0x0000006e, 0x000200f9, 0x000000ca, 0x000200f8,
0x000000ca, 0x000700f5, 0x00000040, 0x00000190, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9,
0x000200f9, 0x000000bc, 0x000200f8, 0x000000bc, 0x000700f5, 0x00000040, 0x0000018f, 0x000000b4,
0x00000056, 0x00000190, 0x000000ca, 0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051,
0x00000006, 0x000000eb, 0x0000018f, 0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000018f,
0x00000001, 0x00070050, 0x00000007, 0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016,
0x00040063, 0x000000e8, 0x0000003a, 0x000000ed, 0x00050041, 0x00000045, 0x000000ef, 0x00000043,
0x000000ee, 0x0004003d, 0x00000006, 0x000000f0, 0x000000ef, 0x000500bc, 0x0000003b, 0x000000f1,
0x000000f0, 0x00000016, 0x000300f7, 0x000000f3, 0x00000000, 0x000400fa, 0x000000f1, 0x000000f2,
0x000000f3, 0x000200f8, 0x000000f2, 0x000200f9, 0x00000147, 0x000200f8, 0x000000f3, 0x0004003d,
0x0000005b, 0x000000f7, 0x000000f6, 0x00040064, 0x0000005a, 0x000000f9, 0x000000f7, 0x0007005f,
0x00000007, 0x000000fa, 0x000000f9, 0x0000003a, 0x00000002, 0x00000044, 0x00050051, 0x00000006,
0x000000fb, 0x000000fa, 0x00000001, 0x000500ba, 0x0000003b, 0x000000fd, 0x000000fb, 0x000000fc,
0x000600a9, 0x00000006, 0x000000fe, 0x000000fd, 0x00000015, 0x00000016, 0x0004003d, 0x000000ff,
0x00000102, 0x00000101, 0x00070050, 0x00000007, 0x00000105, 0x000000fe, 0x000000fe, 0x000000fe,
0x000000fe, 0x00040063, 0x00000102, 0x0000003a, 0x00000105, 0x00050041, 0x00000045, 0x00000107,
0x00000043, 0x0000008a, 0x0004003d, 0x00000006, 0x00000108, 0x00000107, 0x000500ba, 0x0000003b,
0x00000109, 0x00000108, 0x00000067, 0x000300f7, 0x0000010b, 0x00000000, 0x000400fa, 0x00000109,
0x0000010a, 0x0000010b, 0x000200f8, 0x0000010a, 0x00050081, 0x00000040, 0x0000010f, 0x0000006e,
0x0000018f, 0x0006000c, 0x00000040, 0x00000110, 0x00000001, 0x00000008, 0x0000010f, 0x0004006e,
0x00000030, 0x00000111, 0x00000110, 0x00050041, 0x0000006b, 0x00000113, 0x00000043, 0x00000044,
0x0004003d, 0x00000040, 0x00000114, 0x00000113, 0x0004006e, 0x00000030, 0x00000115, 0x00000114,
0x00050082, 0x00000030, 0x00000117, 0x00000115, 0x00000199, 0x000200f9, 0x0000011a, 0x000200f8,
0x0000011a, 0x000700f5, 0x00000006, 0x00000193, 0x00000016, 0x0000010a, 0x00000196, 0x0000011d,
0x000700f5, 0x0000002f, 0x00000191, 0x00000119, 0x0000010a, 0x0000013a, 0x0000011d, 0x000500b3,
0x0000003b, 0x00000120, 0x00000191, 0x0000006a, 0x000400f6, 0x0000011c, 0x0000011d, 0x00000000,
0x000400fa, 0x00000120, 0x0000011b, 0x0000011c, 0x000200f8, 0x0000011b, 0x000200f9, 0x00000122,
0x000200f8, 0x00000122, 0x000700f5, 0x00000006, 0x00000196, 0x00000193, 0x0000011b, 0x00000136,
0x00000123, 0x000700f5, 0x0000002f, 0x00000194, 0x00000119, 0x0000011b, 0x00000138, 0x00000123,
0x000500b3, 0x0000003b, 0x00000128, 0x00000194, 0x0000006a, 0x000400f6, 0x00000124, 0x00000123,
0x00000000, 0x000400fa, 0x00000128, 0x00000123, 0x00000124, 0x000200f8, 0x00000123, 0x0004003d,
0x000000ff, 0x0000012b, 0x0000012a, 0x00050050, 0x00000030, 0x0000012f, 0x00000194, 0x00000191,
0x00050080, 0x00000030, 0x00000130, 0x00000111, 0x0000012f, 0x0008000c, 0x00000030, 0x00000133,
0x00000001, 0x0000002d, 0x00000130, 0x00000131, 0x00000117, 0x00050062, 0x00000007, 0x00000134,
0x0000012b, 0x00000133, 0x00050051, 0x00000006, 0x00000135, 0x00000134, 0x00000000, 0x0007000c,
0x00000006, 0x00000136, 0x00000001, 0x00000028, 0x00000196, 0x00000135, 0x00050080, 0x0000002f,
0x00000138, 0x00000194, 0x0000006a, 0x000200f9, 0x00000122, 0x000200f8, 0x00000124, 0x000200f9,
0x0000011d, 0x000200f8, 0x0000011d, 0x00050080, 0x0000002f, 0x0000013a, 0x00000191, 0x0000006a,
0x000200f9, 0x0000011a, 0x000200f8, 0x0000011c, 0x000200f9, 0x0000010b, 0x000200f8, 0x0000010b,
0x000700f5, 0x00000006, 0x00000192, 0x00000016, 0x000000f3, 0x00000193, 0x0000011c, 0x0004003d,
0x000000ff, 0x0000013c, 0x0000013b, 0x0007000c, 0x00000006, 0x00000140, 0x00000001, 0x00000028,
0x000000fe, 0x00000192, 0x00050085, 0x00000006, 0x00000143, 0x00000140, 0x000000f0, 0x00070050,
0x00000007, 0x00000144, 0x00000143, 0x00000143, 0x00000143, 0x00000143, 0x00040063, 0x0000013c,
0x0000003a, 0x00000144, 0x000200f9, 0x00000147, 0x000200f8, 0x00000147, 0x000100fd, 0x00010038,
};
+246
View File
@@ -0,0 +1,246 @@
// Correctly rounded fma/fmaf for x86-64 builds without FMA3: the -march=x86-64-v2 build (docs/building.md).
//
// runtime/simd.h lowers AArch64 FMADD/FMSUB and the RECPS/RSQRTS helpers to __builtin_elementwise_fma, which clang
// turns into a call to fma/fmaf when the target has no FMA instruction. The C runtime is not a substitute: on CPUs
// without FMA3 the UCRT's software fallback is documented as rounding incorrectly by up to half an ulp, while the FP
// lowering needs the single rounding the ARM instruction gives. This file supplies it.
//
// Adapted from musl src/math/fma.c and src/math/fmaf.c (MIT, Copyright (c) 2005-2020 Rich Felker and contributors).
// The result is rounded in the MXCSR rounding mode the caller set (simd.h simd_rm_enter), and MXCSR FTZ and DAZ are
// honoured as the hardware FMA would, so runtime/simd.h's fast paths keep their assumptions: denormal inputs read as
// zero and denormal results as zero under FPCR.FZ, without a trip through the slow path.
//
// The definitions exist only where the compiler would emit the calls. A -march=x86-64-v3 build defines __FMA__ and
// emits vfmadd instead, so this file contributes nothing there.
#include <stdint.h>
#if defined(__x86_64__) && !defined(__FMA__)
#include <float.h>
#include <immintrin.h>
#define MXCSR_DAZ 0x0040u
#define MXCSR_FTZ 0x8000u
static double fma_flush_d(double x) {
union { double f; uint64_t i; } u = {x};
if (!(u.i & 0x7ff0000000000000ull) && (u.i & 0x000fffffffffffffull)) u.i &= 0x8000000000000000ull;
return u.f;
}
static float fma_flush_f(float x) {
union { float f; uint32_t i; } u = {x};
if (!(u.i & 0x7f800000u) && (u.i & 0x007fffffu)) u.i &= 0x80000000u;
return u.f;
}
// ---- float: the exact product fits in a double, so one extra term and a round-to-odd step decide the float
// rounding. Halfway and tiny results are the only ones where rounding the double first can change the float result.
float fmaf(float x, float y, float z) {
unsigned csr = _mm_getcsr();
if (csr & MXCSR_DAZ) x = fma_flush_f(x), y = fma_flush_f(y), z = fma_flush_f(z);
double xy = (double)x * y; // exact
union { double r; uint64_t i; } u = {xy + z};
int e = u.i >> 52 & 0x7ff;
int halfway = (u.i & 0x1fffffff) == 0x10000000;
int tiny = e <= 0x3ff - 126 && e >= 0x3ff - 149;
if (halfway || tiny) {
if (e != 0x7ff) {
// r+t is the exact sum in nearest rounding; otherwise t keeps its sign and whether it is zero, which
// is all the round-to-odd step needs. An odd sticky bit makes the final conversion round once, in
// whichever mode MXCSR asks for.
int s = u.i >> 63;
double t = s == (xy < z) ? xy - u.r + z : z - u.r + xy;
if (t) {
u.i -= s ^ (t < 0);
u.i |= 1;
}
}
}
float r = (float)u.r;
if (csr & MXCSR_FTZ) r = fma_flush_f(r);
return r;
}
// ---- double: the significands are multiplied and aligned as integers, then converted back with one rounding.
#define ASUINT64(x) ((union { double f; uint64_t i; }){x}).i
#define ZEROINFNAN (0x7ff - 0x3ff - 52 - 1)
struct fma_num { uint64_t m; int e; int sign; };
static struct fma_num fma_normalize(double x) {
uint64_t ix = ASUINT64(x);
int e = ix >> 52;
int sign = e & 0x800;
e &= 0x7ff;
if (!e) {
ix = ASUINT64(x * 0x1p63);
e = ix >> 52 & 0x7ff;
e = e ? e - 63 : 0x800;
}
ix &= (1ull << 52) - 1;
ix |= 1ull << 52;
ix <<= 1;
e -= 0x3ff + 52 + 1;
return (struct fma_num){ix, e, sign};
}
static void fma_mul(uint64_t *hi, uint64_t *lo, uint64_t x, uint64_t y) {
uint64_t xlo = (uint32_t)x, xhi = x >> 32;
uint64_t ylo = (uint32_t)y, yhi = y >> 32;
uint64_t t1 = xlo * ylo;
uint64_t t2 = xlo * yhi + xhi * ylo;
uint64_t t3 = xhi * yhi;
*lo = t1 + (t2 << 32);
*hi = t3 + (t2 >> 32) + (t1 > *lo);
}
// scalbn from musl src/math/scalbn.c (MIT): the fma's final scaling, kept here so the result does not depend on the
// C runtime's version. Its multiplications honour MXCSR like any other host FP operation.
static double fma_scalbn(double x, int n) {
union { double f; uint64_t i; } u;
double y = x;
if (n > 1023) {
y *= 0x1p1023;
n -= 1023;
if (n > 1023) {
y *= 0x1p1023;
n -= 1023;
if (n > 1023) n = 1023;
}
} else if (n < -1022) {
// Make sure the final n < -53, so the subnormal range rounds only once.
y *= 0x1p-1022 * 0x1p53;
n += 1022 - 53;
if (n < -1022) {
y *= 0x1p-1022 * 0x1p53;
n += 1022 - 53;
if (n < -1022) n = -1022;
}
}
u.i = (uint64_t)(0x3ff + n) << 52;
return y * u.f;
}
double fma(double x, double y, double z) {
#pragma STDC FENV_ACCESS ON
unsigned csr = _mm_getcsr();
if (csr & MXCSR_DAZ) x = fma_flush_d(x), y = fma_flush_d(y), z = fma_flush_d(z);
struct fma_num nx = fma_normalize(x), ny = fma_normalize(y), nz = fma_normalize(z);
if (nx.e >= ZEROINFNAN || ny.e >= ZEROINFNAN) return x * y + z;
if (nz.e >= ZEROINFNAN) {
if (nz.e > ZEROINFNAN) return x * y; // z == 0
return z;
}
uint64_t rhi, rlo, zhi, zlo;
fma_mul(&rhi, &rlo, nx.m, ny.m);
// Align the exponents: shift z left by kz and r right by kr with kz + kr == d.
int e = nx.e + ny.e;
int d = nz.e - e;
if (d > 0) {
if (d < 64) {
zlo = nz.m << d;
zhi = nz.m >> (64 - d);
} else {
zlo = 0;
zhi = nz.m;
e = nz.e - 64;
d -= 64;
if (d == 0) {
} else if (d < 64) {
rlo = rhi << (64 - d) | rlo >> d | !!(rlo << (64 - d));
rhi = rhi >> d;
} else {
rlo = 1;
rhi = 0;
}
}
} else {
zhi = 0;
d = -d;
if (d == 0) {
zlo = nz.m;
} else if (d < 64) {
zlo = nz.m >> d | !!(nz.m << (64 - d));
} else {
zlo = 1;
}
}
int sign = nx.sign ^ ny.sign;
int samesign = !(sign ^ nz.sign);
int nonzero = 1;
if (samesign) {
rlo += zlo;
rhi += zhi + (rlo < zlo);
} else {
uint64_t t = rlo;
rlo -= zlo;
rhi = rhi - zhi - (t < rlo);
if (rhi >> 63) {
rlo = -rlo;
rhi = -rhi - !!rlo;
sign = !sign;
}
nonzero = !!rhi;
}
// Keep the top 63 bits in rhi; the last bit is sticky.
if (nonzero) {
e += 64;
d = __builtin_clzll(rhi) - 1;
rhi = rhi << d | rlo >> (64 - d) | !!(rlo << d);
} else if (rlo) {
d = __builtin_clzll(rlo) - 1;
if (d < 0)
rhi = rlo >> 1 | (rlo & 1);
else
rhi = rlo << d;
} else {
return x * y + z; // exact +-0
}
e -= d;
int64_t i = rhi;
if (sign) i = -i;
double r = i; // |r| is in [0x1p62, 0x1p63]
if (e < -1022 - 62) {
// The result is subnormal before rounding.
if (e == -1022 - 63) {
double c = 0x1p63;
if (sign) c = -c;
if (r == c) {
// The smallest normal after rounding: whether it underflows depends on the architecture. A double
// to float conversion imitates the before-rounding rule the guest has.
float fltmin = 0x0.ffffff8p-63 * FLT_MIN * r;
r = DBL_MIN / FLT_MIN * fltmin;
return (csr & MXCSR_FTZ) ? fma_flush_d(r) : r;
}
// One bit is lost when scaled: add another top bit so the conversion rounds only once if inexact.
if (rhi << 53) {
i = rhi >> 1 | (rhi & 1) | 1ull << 62;
if (sign) i = -i;
r = i;
r = 2 * r - c;
// Raise underflow portably, so the compiler cannot optimize the expression away.
double tiny = DBL_MIN / FLT_MIN * r;
r += (double)(tiny * tiny) * (r - r);
}
} else {
// Only round once when scaled.
d = 10;
i = (rhi >> d | !!(rhi << (64 - d))) << d;
if (sign) i = -i;
r = i;
}
}
r = fma_scalbn(r, e);
if (csr & MXCSR_FTZ) r = fma_flush_d(r);
return r;
}
#endif
+18
View File
@@ -0,0 +1,18 @@
// The host's own folder dialog, for menu pages that point at a folder (the Amiibo tab): folder_pick_win.cpp with Win32,
// folder_pick_sdl.c with SDL. The menu has no text entry, so a folder is chosen here or by walking the page's list.
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
// done gets the chosen folder (UTF-8) or NULL when the player cancelled or the host has no dialog.
typedef void (*FolderPickDone)(const char *path, void *ctx);
// Opens the dialog at start (may be relative or missing) and returns at once; done runs later on another thread, once.
// The caller keeps one dialog open at a time.
void folder_pick(const char *start, FolderPickDone done, void *ctx);
#ifdef __cplusplus
}
#endif
+42
View File
@@ -0,0 +1,42 @@
// The SDL front end of folder_pick.h: SDL_ShowOpenFolderDialog (the desktop portal, else zenity), started on the window
// thread, which runs SDL's event loop that delivers the portal's answer.
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <SDL3/SDL.h>
#include "folder_pick.h"
#include "host.h"
#include "input_sdl.h"
typedef struct {
char start[PATH_MAX]; // absolute, or empty: the dialog's own default
FolderPickDone done;
void *ctx;
} PickReq;
static void SDLCALL on_pick(void *userdata, const char *const *list, int filter) {
(void)filter;
PickReq *r = userdata;
if (!list) rt_log("folder_pick: the folder dialog failed (%s)", SDL_GetError());
r->done(list && list[0] ? list[0] : NULL, r->ctx);
free(r);
}
static void show(void *arg) {
PickReq *r = arg;
SDL_ShowOpenFolderDialog(on_pick, r, gfx_window_sdl(), r->start[0] ? r->start : NULL, false);
}
void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = calloc(1, sizeof *r);
if (!r) {
done(NULL, ctx);
return;
}
r->done = done;
r->ctx = ctx;
if (start && *start && !realpath(start, r->start)) r->start[0] = 0;
gfx_window_sdl_run(show, r);
}
+84
View File
@@ -0,0 +1,84 @@
// The Win32 front end of folder_pick.h: the shell's folder dialog (IFileOpenDialog with FOS_PICKFOLDERS), owned by the
// game window, on a thread of its own so the present thread that draws the menu keeps running.
#include <windows.h>
#include <shobjidl.h>
#include <stdlib.h>
#include <string>
extern "C" {
#include "folder_pick.h"
#include "host.h"
#include "input_win.h"
#include "platform/platform.h"
}
struct PickReq {
std::string start;
FolderPickDone done;
void *ctx;
};
static std::wstring wide(const std::string &utf8) {
int n = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, NULL, 0);
std::wstring w(n > 0 ? n : 1, L'\0');
if (n > 0) MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &w[0], n);
w.resize(wcslen(w.c_str()));
return w;
}
static std::string utf8(const wchar_t *w) {
int n = WideCharToMultiByte(CP_UTF8, 0, w, -1, NULL, 0, NULL, NULL);
std::string s(n > 0 ? n : 1, '\0');
if (n > 0) WideCharToMultiByte(CP_UTF8, 0, w, -1, &s[0], n, NULL, NULL);
s.resize(strlen(s.c_str()));
return s;
}
// The dialog wants an absolute folder that exists; anything else leaves it at the shell's last folder.
static void set_start(IFileOpenDialog *d, const std::string &start) {
wchar_t full[MAX_PATH * 2];
if (start.empty() || !GetFullPathNameW(wide(start).c_str(), MAX_PATH * 2, full, NULL)) return;
IShellItem *item = NULL;
if (SUCCEEDED(SHCreateItemFromParsingName(full, NULL, IID_PPV_ARGS(&item)))) {
d->SetFolder(item);
item->Release();
}
}
static void pick_main(void *arg) {
PickReq *r = (PickReq *)arg;
std::string chosen;
HRESULT co = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);
IFileOpenDialog *d = NULL;
HRESULT hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&d));
if (SUCCEEDED(hr)) {
FILEOPENDIALOGOPTIONS opt = 0;
d->GetOptions(&opt);
d->SetOptions(opt | FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM | FOS_PATHMUSTEXIST);
set_start(d, r->start);
hr = d->Show(gfx_window_handle());
IShellItem *item = NULL;
if (SUCCEEDED(hr) && SUCCEEDED(hr = d->GetResult(&item))) {
PWSTR path = NULL;
if (SUCCEEDED(hr = item->GetDisplayName(SIGDN_FILESYSPATH, &path))) {
chosen = utf8(path);
CoTaskMemFree(path);
}
item->Release();
}
d->Release();
}
if (FAILED(hr) && hr != HRESULT_FROM_WIN32(ERROR_CANCELLED)) rt_log("folder_pick: the folder dialog failed (0x%08lx)", (unsigned long)hr);
r->done(chosen.empty() ? NULL : chosen.c_str(), r->ctx);
if (SUCCEEDED(co)) CoUninitialize();
delete r;
}
extern "C" void folder_pick(const char *start, FolderPickDone done, void *ctx) {
PickReq *r = new PickReq{start ? start : "", done, ctx};
if (!plat_thread_spawn(pick_main, r, "folder dialog", 0)) {
rt_log("folder_pick: cannot start the dialog thread");
done(NULL, ctx);
delete r;
}
}
+35 -11
View File
@@ -11,8 +11,20 @@
// game speed is right only when frames take exactly their interval. With the set realtime both read the patch page
// instead: +0 ratio = seconds * 30, +4 seconds (1.4.3: +8 interval = seconds * 60, the producer's nominal interval
// before its half-step coefficient), the moving average of the last 5 display intervals, at most 1/15 s
// (slower frames slow the game down, as on the console), no lower bound, so a higher frame-rate cap only changes the
// pacer (frame_step_cap_ns, TOTK_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
// (slower frames slow the game down, as on the console) and at least 1/240 s, so a higher frame-rate cap only changes
// the pacer (frame_step_cap_ns, TOTK_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
//
// The floor: with a shorter step (ratio below 0.125) 1.4.3 aborts within seconds of some save loads (issue #241, a
// 280 Hz monitor): Havok's hkDefaultMemoryImpl reports "Out of memory" for the buffer the physics pre-solve (profiler
// scope "TtPreSolve") resizes in main+0x1261630. Forced steps on the Depths save: 1/240 s plays, 1/244 s and shorter
// abort; the field, sky island and fight saves play at 1/280 s, the town save aborts after about 800 frames. The
// same short steps also crash a physics worker reading that buffer's contacts: an access violation in main+0x1292560
// or its caller main+0x1286870 (narrowphase contact setup, reached from the grid task main+0x12f1ea0), reading a
// Havok body's shape at body+0x78 as null or garbage (issues #29 and #92, 240 Hz monitors with fps_limit = display).
// Steps drawn from 1/252..1/230 s crash the Depths load there in seconds; 1/240..1/230 s plays. The frame limit
// stops at 240 too (LIMIT_MAX_HZ), so the floor only catches the average dipping under 1/240 s at that limit: a
// present late by less than a period keeps the pacer's phase (nvn_api_obj.c, pace), so the next display interval is
// shorter than the period. No limit makes the game run slower than the clock.
//
// The sample is the display time the pacer gives each present (nvnQueuePresentTexture), so the step follows the
// frames as shown, pacing waits included. With the step on, the pacer ignores the game's interval and holds a frame
@@ -47,6 +59,7 @@
#define STEP_MAX_S (1.0 / 15.0) // longest step; a longer frame slows the game down
#define STEP_RESET_S 0.25 // a gap this long (loading, a debugger) restarts the average
#define STEP_FIRST_S (1.0 / 60.0)
#define STEP_MIN_S (1.0 / 240.0) // shortest step; a shorter one makes the game's physics run out of memory
static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), seconds (high float)
static double step_now = STEP_FIRST_S; // the step the patch page holds: what the game steps each frame
@@ -90,14 +103,15 @@ void frame_step_init(void) {
int frame_step_realtime(void) { return step_word != NULL; }
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 360, or display (the
// refresh rate of the window's monitor). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
// The frame limit (menu, TOTK_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the
// refresh rate of the window's monitor, at most 240). A higher number gives 240, the rate of the shortest step the
// game survives (STEP_MIN_S). Unset, the older TOTK_FPS60=0 still means 30. A limit of 30 switches the set
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
// only the pacer changes, as the step has no lower bound. Without it the game steps whole 60 Hz vsyncs and the pacer
// only the pacer changes, as the step's floor is the limit's ceiling. Without it the game steps whole 60 Hz vsyncs and the pacer
// follows the game's interval, so the limit stops at 60.
#define LIMIT_MIN_HZ 30
#define LIMIT_MAX_HZ 360
#define LIMIT_MAX_HZ 240 // 1 / STEP_MIN_S
static int fps60_set = -2;
static unsigned limit_seen;
static int limit_read;
@@ -109,8 +123,13 @@ static int limit_parse(void) {
if (!strcmp(e, "display")) return 0;
char *end;
long n = strtol(e, &end, 10);
if (!*end && n >= LIMIT_MIN_HZ && n <= LIMIT_MAX_HZ) return (int)n;
rt_log("frame rate: TOTK_FPS_LIMIT=%s is not 30..360 or display; 60", e);
if (!*end && n > LIMIT_MAX_HZ) {
rt_log("frame rate: TOTK_FPS_LIMIT=%s is above %d, where the game's physics runs out of memory; %d", e,
LIMIT_MAX_HZ, LIMIT_MAX_HZ);
return LIMIT_MAX_HZ;
}
if (!*end && n >= LIMIT_MIN_HZ) return (int)n;
rt_log("frame rate: TOTK_FPS_LIMIT=%s is not %d..%d or display; 60", e, LIMIT_MIN_HZ, LIMIT_MAX_HZ);
return 60;
}
@@ -161,10 +180,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.0: [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.0 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_V110 ? 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;
@@ -254,6 +277,7 @@ void frame_step_present(uint64_t display_ns, int interval) {
double sum = 0;
for (unsigned i = 0; i < sample_count; i++) sum += samples[i];
step = sum / sample_count;
if (step < STEP_MIN_S) step = STEP_MIN_S;
step_store((float)step);
}
}
+9 -2
View File
@@ -38,8 +38,15 @@ uint64_t romfs_file_count(void); // files in the index
const char *romfs_entry(uint64_t i, uint64_t *offset, uint64_t *size); // path, image offset and size of index row i
const char *romfs_image_path(void); // the romfs.bin path fs_init opened
size_t romfs_overlay_count(void);
int romfs_xbox_prompts_ready(void); // romfs-patches-xbox was there at start
int romfs_xbox_prompts_active(void); // and was loaded (pad_layout = xbox_prompts at start)
int romfs_patches_loaded(void); // a romfs-patches manifest gave at least one file
// The button prompts (input_prompts_folder): the folder loaded at start, NULL for the game's own prompts; whether a
// folder had a manifest at start; and why the folder the settings asked for at start was not loaded (ROMFS_PROMPTS_OK
// for any other folder: it was not asked for).
enum { ROMFS_PROMPTS_OK, ROMFS_PROMPTS_MISSING, ROMFS_PROMPTS_FAILED, ROMFS_PROMPTS_SHAPE, ROMFS_PROMPTS_MODS,
ROMFS_PROMPTS_PATCHES };
const char *romfs_prompts(void);
int romfs_prompts_ready(const char *folder);
int romfs_prompts_off(const char *folder);
const char *romfs_overlay(size_t k, const char **file, uint64_t *size, int *is_added); // RomFS path of overlay k
void fs_trace(const char *what, const char *path); // TOTK_TRACE=fs: one line per open
+232 -50
View File
@@ -125,12 +125,30 @@ static void make_dirs(const char *p) {
if (!make_dir(buf)) rt_fatal("fs: cannot create directory %s", buf);
}
// Remove a directory tree; a link (reparse point, symbolic link) is removed as a link, never followed. 0 also when
// The first path remove_tree could not delete and the OS error code, for the message of a fatal.
static char remove_failed[1100];
static int remove_error;
static void remove_note(const char *path) {
if (*remove_failed) return;
snprintf(remove_failed, sizeof remove_failed, "%s", path);
remove_error = plat_fs_os_error();
}
// Remove a directory tree; a link (symbolic link, junction) is removed as a link, never followed. 0 also when
// nothing is there.
static int remove_tree(const char *path) {
int kind = plat_fs_kind(path);
if (kind == PLAT_FS_MISSING) return 0;
if (kind == PLAT_FS_FILE) return plat_fs_unlink(path) ? -1 : 0;
if (kind == PLAT_FS_FILE) {
if (!plat_fs_unlink(path)) return 0;
remove_note(path);
return -1;
}
if (kind == PLAT_FS_LINK) { // a link to a file goes by unlink, a link to a directory by rmdir
if (!plat_fs_unlink(path) || !plat_fs_rmdir(path)) return 0;
remove_note(path);
return -1;
}
int bad = 0;
if (kind == PLAT_FS_DIR) {
Listing l;
@@ -142,35 +160,56 @@ static int remove_tree(const char *path) {
}
listing_free(l.e, l.n);
}
return bad | (plat_fs_rmdir(path) ? -1 : 0);
if (plat_fs_rmdir(path)) {
remove_note(path);
bad |= -1;
}
return bad;
}
// remove_tree, tried again every 50 ms for up to 2 s while another process (the previous run still exiting, a virus
// scanner, a sync client) holds a file in the tree. The failing path and its OS error stay in remove_failed.
#define MOVE_TRIES 40 // 2 s, also move_dir's
static int remove_tree_retry(const char *path) {
for (int i = 0;; i++) {
*remove_failed = 0;
if (!remove_tree(path)) return 0;
if (i == MOVE_TRIES) return -1;
plat_sleep_ms(50);
}
}
// ---- 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 +222,10 @@ 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.
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 +313,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 +389,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,35 +402,96 @@ 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 (remove_tree_retry(work_root))
rt_fatal("fs: cannot delete the old save working copy: %s (OS error %d)", remove_failed, remove_error);
if (!strcmp(did, "nothing to recover"))
did = "deleted the last run's working copy (changes after its last Commit)";
}
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 +502,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 +1546,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 +1562,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 +1581,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) ==
@@ -1436,6 +1609,15 @@ static void probe_save_tx(const uint64_t *uid) {
CALL(S_UNMOUNT, U("ro"));
CHECK(RES(S_DELETE_FILE, U(a)) == 0 && RES(S_DELETE_FILE, U(b)) == 0 && tx_commit() == 0 &&
tx_read_committed("tx_a.sav") == -1, "Commit of deletions");
// a run that left read-only folders and files in the working copy (attributes another emulator's save carried):
// the next start deletes them and names the failing path if it cannot
char *ro_dir = join(work_root, "ro_dir"), *ro_file = join(ro_dir, "ro.sav");
CHECK(!plat_fs_mkdir(ro_dir) && plat_file_write(ro_file, "x", 1) && plat_file_set_readonly(ro_file, 1) &&
plat_file_set_readonly(ro_dir, 1), "read-only leftovers in the working copy");
probe_restart(uid);
CHECK(!exists(ro_dir), "the next start deleted a working copy with read-only folders");
free(ro_dir);
free(ro_file);
printf("save tx: uncommitted writes visible to the game and dropped by a restart and by Unmount, commits kept; a "
"crash at each of %d commit steps recovers to the old save %d times, the new one %d times, mixed %d "
"times; %d backups\n", steps, olds, news, mixed, backups);
+10 -1
View File
@@ -35,6 +35,8 @@ typedef struct { // the translator's a
uint8_t attr_type[32], frag_out_type[8]; // GFX_TYPE_*
uint16_t local[3]; uint32_t shared_bytes;
uint8_t depth_minus_w;
uint8_t mvprev; // VS: also write last frame's position (patch 0015,
// vk_mvprev.c); packed as bit 7 of the depth_minus_w byte
} GfxShaderAnswers;
typedef struct GfxShader GfxShader; // SPIR-V words + parsed reflection; owned by the cache
const GfxShader *gfx_shader_get(int stage, const uint8_t *control, const uint8_t *code, uint64_t code_size,
@@ -73,6 +75,12 @@ size_t gfx_shader_warm_list(GfxWarmEnt **out); // malloc'd; recorde
const GfxShader *gfx_shader_load_private(const uint8_t iid[32], int *owned);
void gfx_shader_release_private(const GfxShader *s);
const uint32_t *gfx_shader_spirv(const GfxShader *s, uint64_t *words);
// A shader of the runtime's own (SPIR-V and a .res.json text as the translator writes them), outside the cache and
// never freed; its key is the sha256 of both. NULL when the SPIR-V is malformed.
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json);
// vk_hdr.c: with TOTK_HDR=1, replaces a fragment shader's SPIR-V (malloc'd, *words long) by the HDR tonemap when it is
// the game's tonemap; gfx_shader.c calls it for every fragment shader entering the cache.
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words);
uint64_t gfx_shader_failures(void); // failed translations; feeds GfxStats.shader_failures
// Reflection: parsed once per shader from the translator's .res.json and the SPIR-V before it enters the cache.
enum { GFX_RES_UBO, GFX_RES_SSBO, GFX_RES_TEX, GFX_RES_IMG, GFX_RES_KINDS };
@@ -94,7 +102,8 @@ int gfx_shader_stage(const GfxShader *s); // GFX_STAGE_VS, GFX
int gfx_shader_resources(const GfxShader *s, int kind, const GfxShaderRes **res); // count of GFX_RES_* kind; array in *res
uint32_t gfx_shader_slot_mask(const GfxShader *s); // texture/image slots whose type its translation read
uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]); // Input Locations (VS attributes, FS varyings) as a mask
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]); // Output Locations (VS varyings, FS colour targets)
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]);
uint32_t gfx_shader_mvprev_banks(const GfxShader *s); // a last-frame variant: the banks (bit n: cn) its position reads // Output Locations (VS varyings, FS colour targets)
void gfx_shader_local_size(const GfxShader *s, uint32_t local[3]); // CS: the LocalSize execution mode; 1, 1, 1 otherwise
// ---- back end (gfx_null.c, vk_*.c). Called by the NVN front end (nvn_api_*.c, nvn_ext.c) on guest threads.
+66 -19
View File
@@ -23,9 +23,9 @@
#include "gfx.h"
#include "gfx_wsys.h"
#include "nvn_api.h"
#include "platform/platform.h"
#include "ui/menu.h"
#include "vk.h"
#include "platform/platform.h"
#define FRAMES 2
#define MAX_IMAGES 16
@@ -40,6 +40,7 @@ void vk_queue_unlock(void) { plat_rmutex_unlock(&g_ql); }
// ---- window state shared with the back end
static VkSurfaceKHR g_surface;
static volatile int32_t g_dirty; // the window was resized; swapchain stale
static volatile int32_t g_restart; // gfx_window_request_restart: closing the window starts the game again
static volatile int32_t g_focused = 1;
static volatile int32_t g_focus_msgs;
static PlatRMutex g_start_lock; // gfx_window_start / vsync start
@@ -47,25 +48,43 @@ static int g_started;
void gfx_window_mark_dirty(void) { plat_xchg32(&g_dirty, 1); }
// The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message.
// The window gained or lost the focus: the game hears it as nn::oe's focus state and a notification message. The line
// carries the local wall time, the only one in the log: when a GPU hang takes the whole system down, the log ends
// without an error, and this time set against the driver's entry in the Windows event log (nvlddmkm 153, Display 4101)
// tells whether an alt-tab or an overlay came before the hang or followed it.
void gfx_window_focus_changed(int in) {
plat_xchg32(&g_focused, in);
oe_set_focus(in); // pinned to 1 by TOTK_ASSUME_FOCUS inside
oe_push_message(OE_MSG_FOCUS_STATE_CHANGED);
plat_inc32(&g_focus_msgs);
rt_log("gfx: window %s the focus", in ? "gained" : "lost");
struct tm t;
plat_local_time(&t);
rt_log("gfx: window %s the focus at %04d-%02d-%02d %02d:%02d:%02d local time", in ? "gained" : "lost",
t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec);
}
// The window was closed: the process ends with it, as in MNG, once a save Commit still running in the background is
// done.
// done. After gfx_window_request_restart the executable then starts again with the same command line and environment
// (plat_restart), and the new process waits until this one is gone (main.c) before it moves the log and opens the
// RomFS and the saves.
void gfx_window_closed(void) {
rt_log("gfx: window closed; exiting");
int restart = plat_cas32(&g_restart, 0, 0) != 0;
rt_log("gfx: window closed; %s", restart ? "restarting" : "exiting");
fs_save_flush();
vk_pipeline_save();
prof_write(); // plat_exit runs no atexit handlers
if (restart) {
plat_setenv("TOTK_REEXEC", NULL); // the new process counts its own address-space retries (memory.c)
if (!plat_restart()) rt_log("gfx: the restart could not start the game again");
}
plat_exit(0);
}
void gfx_window_request_restart(void) {
plat_xchg32(&g_restart, 1);
gfx_window_request_close();
}
// TOTK_FULLSCREEN=1: borderless window over the monitor. TOTK_WINDOW_SIZE=<w>x<h>: client area of the window (the
// swapchain follows the window; the game image is scaled to it).
int gfx_cfg_fullscreen(void) { return cfg_bool("TOTK_FULLSCREEN"); }
@@ -93,6 +112,7 @@ double gfx_window_refresh_hz(void) { return plat_cas32(&g_rate_mhz, 0, 0) / 1000
static PlatRMutex g_pl;
static VkSwapchainKHR g_sc;
static VkFormat g_sc_fmt;
static int g_sc_hdr; // HDR10 (TOTK_HDR, vk_hdr.c): PQ code values in Rec. 2020
static VkExtent2D g_sc_ext;
static int g_scaler; // the last present: 0 the game image 1:1, 1 scaled by the blit, 2 by FSR
static uint32_t g_sc_n;
@@ -214,6 +234,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);
@@ -291,9 +312,9 @@ static int create_swapchain(void) {
if (nf > 64) nf = 64;
vkGetPhysicalDeviceSurfaceFormatsKHR(c->phys, g_surface, &nf, fmts);
if (!nf) return 0;
// UNORM: the blit writes the bytes the game wrote (no sRGB encode).
// UNORM: the blit writes the bytes the game wrote (no sRGB encode). TOTK_HDR: HDR10 when the surface offers it.
VkSurfaceFormatKHR sf = fmts[0];
int found = 0;
int hdr = vk_hdr_surface_format(fmts, nf, &sf), found = hdr;
for (uint32_t i = 0; i < nf && !found; i++)
if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; }
for (uint32_t i = 0; i < nf && !found; i++)
@@ -327,6 +348,9 @@ static int create_swapchain(void) {
}
if (old) vkDestroySwapchainKHR(c->device, old, NULL);
g_sc_fmt = sf.format;
g_sc_hdr = hdr;
vk_hdr_set_active(hdr);
if (hdr) vk_hdr_metadata(g_sc, 1);
g_sc_ext = ext;
g_sc_n = MAX_IMAGES;
vkGetSwapchainImagesKHR(c->device, g_sc, &g_sc_n, g_sc_img);
@@ -410,12 +434,14 @@ static int ensure_thumbs(void) {
}
g_th_ptr = info.pMappedData;
}
if (g_th_img && g_th_fmt != g_sc_fmt) {
// an HDR10 swapchain blits into RGBA8: its PQ codes, flat-looking but readable as bytes
VkFormat tf = g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt;
if (g_th_img && g_th_fmt != tf) {
vmaDestroyImage(c->vma, g_th_img, g_th_img_alloc); // the previous present's fence was waited for: idle
g_th_img = VK_NULL_HANDLE;
}
if (!g_th_img) {
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = g_sc_fmt,
VkImageCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = tf,
.extent = {FLIGHT_THUMB_W, FLIGHT_THUMB_H, 1}, .mipLevels = 1, .arrayLayers = 1,
.samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL,
.usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
@@ -427,7 +453,7 @@ static int ensure_thumbs(void) {
rt_log("gfx: flight thumbnails off: no %ux%u image", FLIGHT_THUMB_W, FLIGHT_THUMB_H);
return 0;
}
g_th_fmt = g_sc_fmt;
g_th_fmt = tf;
}
return 1;
}
@@ -501,7 +527,8 @@ static void save_capture(const char *path) {
uint8_t *px = malloc(n);
if (!px) return;
int bgra = g_sc_fmt == VK_FORMAT_B8G8R8A8_UNORM || g_sc_fmt == VK_FORMAT_B8G8R8A8_SRGB;
for (size_t i = 0; i < n; i += 4) {
if (g_sc_hdr) vk_hdr_to_srgb8(g_cap_ptr, px, n / 4, g_sc_fmt == VK_FORMAT_A2R10G10B10_UNORM_PACK32);
else for (size_t i = 0; i < n; i += 4) {
px[i] = g_cap_ptr[i + (bgra ? 2 : 0)];
px[i + 1] = g_cap_ptr[i + 1];
px[i + 2] = g_cap_ptr[i + (bgra ? 0 : 2)];
@@ -553,6 +580,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame
if (r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
vkResetFences(c->device, 1, &g_fence[slot]);
if (g_sc_hdr) vk_hdr_metadata(g_sc, 0); // a changed peak or white goes to the display
if (g_th_pending[slot]) { // that fence has signalled: the thumbnail its submission wrote is complete
g_th_tag[g_th_pending[slot] % (uint64_t)g_th_n] = g_th_pending[slot];
g_th_pending[slot] = 0;
@@ -567,7 +595,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
window_settings_live();
// The menu overlay: laid out before recording (its first frame starts the ImGui back end and uploads the font)
const char *menu_shot = NULL;
MenuTarget mt = {c->instance, c->phys, c->device, c->gfx_family, c->gfx_queue, g_sc_fmt, g_sc_ext.width,
MenuTarget mt = {c->instance, c->phys, c->device, c->gfx_family, c->gfx_queue,
g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt, g_sc_ext.width,
g_sc_ext.height, cw, ch, g_scaler};
int overlay = g_sc_view[idx] &&
menu_overlay_frame(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), &mt, &menu_shot);
@@ -595,38 +624,53 @@ 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), or on an HDR10 swapchain the PQ encode (vk_hdr.c; the adjustment is
// for SDR): a copy of the crop takes the game image's place below
VkImage graded = !general ? VK_NULL_HANDLE : g_sc_hdr ? vk_hdr_encode(cb, slot, src, fmt, rect)
: vk_color_record(cb, slot, src, fmt, rect);
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
VkImageLayout last = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
VkPipelineStageFlags2 last_stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
VkAccessFlags2 last_write = VK_ACCESS_2_TRANSFER_WRITE_BIT;
// HDR10: the menu goes into vk_hdr.c's RGBA8 image, which is then blended onto the swapchain image at the HUD's white
VkImageView menu_view = overlay && g_sc_hdr ? vk_hdr_ui_target(cb, g_sc_ext.width, g_sc_ext.height) : VK_NULL_HANDLE;
if (overlay && g_sc_hdr && !menu_view) overlay = 0;
if (overlay) {
sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, last_stage, last_write,
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT);
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = g_sc_view[idx],
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
.imageView = menu_view ? menu_view : g_sc_view[idx],
.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD, .storeOp = VK_ATTACHMENT_STORE_OP_STORE};
.loadOp = menu_view ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, g_sc_ext}, .layerCount = 1,
.colorAttachmentCount = 1, .pColorAttachments = &ca};
vkCmdBeginRendering(cb, &ri);
menu_overlay_draw(cb);
vkCmdEndRendering(cb);
if (menu_view) vk_hdr_ui_compose(cb, g_sc_view[idx], g_sc_fmt, g_sc_ext.width, g_sc_ext.height);
last = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
last_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
last_write = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
@@ -719,6 +763,9 @@ 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();
vk_hdr_release();
vk_contact_release();
free_capture();
free_thumbs();
for (int i = 0; i < FRAMES; i++) {
+101 -3
View File
@@ -86,6 +86,7 @@ struct GfxShader {
uint32_t in_mask, out_mask; // user Locations of Input / Output variables
GfxShaderIo in[32], out[32];
uint32_t local[3];
uint32_t mvprev_banks; // .res.json "mvprev_banks" (patch 0015), 0 none
};
static PlatMutex cache_lock = PLAT_MUTEX_INIT; // memory cache
@@ -589,6 +590,26 @@ static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int
s->reflection = (char *)res;
s->reflection_size = res_size;
reflect(s);
if (s->stage == GFX_STAGE_FS) vk_hdr_shader_swap(&s->spirv, &s->words); // the in-memory copy; the cache keeps its own
return s;
}
const GfxShader *gfx_shader_own(const uint32_t *spirv, uint64_t words, const char *res_json) {
if (words < 5 || spirv[0] != 0x07230203 || !spirv_complete(spirv, words) ||
!reflection_valid(res_json, strlen(res_json)))
return NULL;
GfxShader *s = calloc(1, sizeof *s);
if (!s) return NULL;
size_t rn = strlen(res_json);
uint8_t *buf = malloc(words * 4 + rn);
s->spirv = malloc(words * 4), s->reflection = malloc(rn + 1);
if (!buf || !s->spirv || !s->reflection) rt_fatal("gfx_shader: out of memory");
memcpy(buf, spirv, words * 4), memcpy(buf + words * 4, res_json, rn);
rt_sha256(buf, words * 4 + rn, s->key);
free(buf);
memcpy(s->spirv, spirv, words * 4), memcpy(s->reflection, res_json, rn + 1);
s->words = words, s->reflection_size = rn;
reflect(s);
return s;
}
@@ -758,18 +779,81 @@ out:
free(loc), free(packed), free(ttype), free(tcomps), free(pointee);
}
// Every gradient sample (OpImageSampleExplicitLod with Grad) whose two gradients are each built from two components gets
// them transposed, dx = (a, c) and dy = (b, d) from dx = (a, b) and dy = (c, d), by two new OpCompositeConstruct before
// it. TotK's shaders fill a TXD's gradient registers per coordinate (du/dx, du/dy, dv/dx, dv/dy), and the translator
// (Ryujinx, InstEmitTexture EmitTxd) builds dx = (first two registers), dy = (last two) = (du/dx, du/dy) and
// (dv/dx, dv/dy): a mip level chosen from the wrong derivatives, too sharp on terrain faces turned across the screen,
// which crawled in patches that followed the faces (1.4.3, the Depths). TOTK_TXD_TRANSPOSE=0 turns it off.
// Applied to the SPIR-V after it is loaded, so the shader cache is untouched.
enum { OP_COMPOSITE_CONSTRUCT = 80, OP_SAMPLE_EXPLICIT = 88, IO_GRAD = 4 };
static void patch_txd_transpose(GfxShader *s) {
static int on = -1;
static volatile int32_t n_logged;
if (on < 0) {
const char *e = cfg_get("TOTK_TXD_TRANSPOSE");
on = !(e && *e == '0');
}
uint32_t *w = s->spirv;
uint64_t n = s->words;
if (!on || n < 5) return;
uint32_t bound = w[3] < (1u << 20) ? w[3] : (1u << 20);
uint64_t *cc = calloc(bound, sizeof *cc); // word index of a 2-constituent OpCompositeConstruct defining the id
int found = 0;
if (!cc) return;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) goto out;
if (op == OP_COMPOSITE_CONSTRUCT && len == 5 && w[i + 2] < bound) cc[w[i + 2]] = i;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1])
found++;
i += len;
}
if (!found) goto out;
uint32_t *o = malloc((n + (uint64_t)found * 10) * sizeof *o);
if (!o) goto out;
uint64_t k = 0;
uint32_t next = w[3];
for (uint64_t i = 0; i < 5; i++) o[k++] = w[i];
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (op == OP_SAMPLE_EXPLICIT && len >= 8 && w[i + 5] == IO_GRAD && w[i + 6] < bound && w[i + 7] < bound &&
cc[w[i + 6]] && cc[w[i + 7]] && w[cc[w[i + 6]] + 1] == w[cc[w[i + 7]] + 1]) {
const uint32_t *dx = w + cc[w[i + 6]], *dy = w + cc[w[i + 7]]; // [hdr, type, id, a, b], [hdr, type, id, c, d]
uint32_t ndx = next++, ndy = next++;
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndx, o[k++] = dx[3], o[k++] = dy[3];
o[k++] = 5u << 16 | OP_COMPOSITE_CONSTRUCT, o[k++] = dx[1], o[k++] = ndy, o[k++] = dx[4], o[k++] = dy[4];
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
o[k - len + 6] = ndx, o[k - len + 7] = ndy;
} else {
for (uint32_t j = 0; j < len; j++) o[k++] = w[i + j];
}
i += len;
}
o[3] = next;
free(s->spirv);
s->spirv = o, s->words = k;
if (plat_inc32(&n_logged) <= 4) rt_log("gfx_shader: %d gradient samples now read their derivatives per coordinate", found);
out:
free(cc);
}
static void reflect(GfxShader *s) {
static const char *const kinds[GFX_RES_KINDS] = {"cbufs", "sbufs", "textures", "images"};
const char *js = s->reflection;
char stage[8];
json_str(js, js + strlen(js), "stage", stage, sizeof stage);
s->stage = !strcmp(stage, "cs") ? GFX_STAGE_CS : !strcmp(stage, "ps") ? GFX_STAGE_FS : GFX_STAGE_VS;
s->mvprev_banks = (uint32_t)json_int(js, js + strlen(js), "mvprev_banks", 0);
for (int k = 0; k < GFX_RES_KINDS; k++) {
int count = json_objects(js, kinds[k], NULL, s, k);
s->res[k] = calloc(count ? count : 1, sizeof *s->res[k]);
json_objects(js, kinds[k], add_res, s, k);
}
reflect_spirv(s);
patch_txd_transpose(s);
}
// ---- use masks (see the cache key at the top)
@@ -974,7 +1058,7 @@ static size_t pack_answers(uint8_t *o, const GfxShaderAnswers *a) {
memcpy(p, a->frag_out_type, 8), p += 8;
for (int i = 0; i < 3; i++) *p++ = (uint8_t)a->local[i], *p++ = (uint8_t)(a->local[i] >> 8);
put_u32(&p, a->shared_bytes);
*p++ = a->depth_minus_w;
*p++ = (uint8_t)(a->depth_minus_w | (a->mvprev ? 0x80 : 0));
return (size_t)(p - o);
}
@@ -1006,6 +1090,7 @@ static void mask_answers(const GfxShaderAnswers *a, const UseMask *m, GfxShaderA
if (m->fouts >> i & 1) o->frag_out_type[i] = a->frag_out_type[i];
memcpy(o->local, a->local, sizeof o->local);
o->shared_bytes = a->shared_bytes;
o->mvprev = a->mvprev; // another translation, not an answer the use mask covers
}
// The cache key of an input id under the current translator.
@@ -1085,6 +1170,7 @@ static size_t build_request(char *o0, const char *hex, const char *stage, const
if (!strcmp(stage, "ps") && (control[0x718] & 1)) o += sprintf(o, ",\"early_fragment_tests\":true");
int si = !strcmp(stage, "vs") ? 0 : !strcmp(stage, "cs") ? 2 : 1; // gfx_shader_set_host answers
if (!host_f64) o += sprintf(o, ",\"softfloat64\":true");
if (a && a->mvprev && !strcmp(stage, "vs")) o += sprintf(o, ",\"mvprev\":true");
if (host_sbuf) o += sprintf(o, ",\"sbuf_align\":%u", host_sbuf);
if (host_sg[si] != 32) o += sprintf(o, ",\"subgroup\":%u", host_sg[si]);
if (host_gather) o += sprintf(o, ",\"gather_bias\":%u", host_gather);
@@ -1287,8 +1373,18 @@ static const GfxShader *translate_one(const Req *r, int stage, const uint8_t *co
if (!req) return plat_file_delete(code_path), fail("out of memory", hex);
build_request(req, hex, r->stage_name, code_path, r->c1, r->c1_len, a, control);
int ok = svc_request(req, reply, sizeof reply);
// TOTK_XLATE_KEEP=1: a last-frame variant's job stays as <key>.job (one request line) with its code, to run the
// translator on by hand (totk_xlate --serve < <key>.job)
static int keep = -1;
if (keep < 0) keep = rt_knob("XLATE_KEEP", "0");
int kept = keep && a && a->mvprev;
if (kept) {
char job_path[PATH_CAP + 80];
snprintf(job_path, sizeof job_path, "%s/%s.job", cache_dir, hex);
write_file(job_path, req, strlen(req));
}
free(req);
plat_file_delete(code_path);
if (!kept) plat_file_delete(code_path);
if (!ok) return fail("translator unavailable", hex);
if (strncmp(reply, "ok ", 3)) {
plat_file_delete(spv_path), plat_file_delete(res_path);
@@ -1643,6 +1739,8 @@ uint32_t gfx_shader_inputs(const GfxShader *s, GfxShaderIo io[32]) {
return s->in_mask;
}
uint32_t gfx_shader_mvprev_banks(const GfxShader *s) { return s->mvprev_banks; }
uint32_t gfx_shader_outputs(const GfxShader *s, GfxShaderIo io[32]) {
if (io) memcpy(io, s->out, sizeof s->out);
return s->out_mask;
@@ -1732,7 +1830,7 @@ static void unpack_answers(const uint8_t *p, GfxShaderAnswers *a) {
memcpy(a->frag_out_type, p, 8), p += 8;
for (int i = 0; i < 3; i++) a->local[i] = (uint16_t)(p[0] | p[1] << 8), p += 2;
memcpy(&a->shared_bytes, p, 4), p += 4;
a->depth_minus_w = *p;
a->depth_minus_w = *p & 0x7f, a->mvprev = *p >> 7;
}
// Reads a record file's body (after its header) into *body; 0 when absent, and a file with another header is removed.
+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_V110 ? "v110" : RT_V143 ? "v143" : "v121",
RT_V110 ? "v121" : RT_V143 ? "v121" : "v143",
RT_V110 ? "v143" : RT_V143 ? "v110" : "v110"};
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;
+2
View File
@@ -131,6 +131,8 @@ static int present_and_check(Source *s, int first, int i, int *w_out, int *h_out
}
int probe_window(void) {
vk_hdr_off(); // the checks read 8-bit swapchain pixels
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");
+2
View File
@@ -64,6 +64,8 @@ double gfx_window_sdl_density(void) { return __atomic_load_n(&g_density_milli, _
SDL_Window *gfx_window_sdl(void) { return g_win; }
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg) { run_on_window(fn, arg); }
// ---- settings: size and fullscreen
static char g_applied_size[32];
static int g_applied_fs;
+23 -4
View File
@@ -279,6 +279,24 @@ static LRESULT CALLBACK wnd_proc(HWND h, UINT m, WPARAM w, LPARAM l) {
return DefWindowProcW(h, m, w, l);
}
// Brings the game window to the front. Windows refuses SetForegroundWindow to a process that was not the last to get
// input, which is the case for a game started by a frontend; attaching this thread's input queue to the foreground
// window's thread for the call is the documented way around it. The window is not left topmost.
static void raise_window(HWND h) {
HWND fg = GetForegroundWindow();
DWORD me = GetCurrentThreadId(), other = fg ? GetWindowThreadProcessId(fg, NULL) : 0;
int attached = other && other != me && AttachThreadInput(me, other, TRUE);
ShowWindow(h, IsIconic(h) ? SW_RESTORE : SW_SHOW);
BringWindowToTop(h);
SetForegroundWindow(h);
SetActiveWindow(h);
if (attached) AttachThreadInput(me, other, FALSE);
if (GetForegroundWindow() != h) { // still refused: a topmost toggle puts it above the others without keeping it there
SetWindowPos(h, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
SetWindowPos(h, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
}
static void window_main(void *arg) {
(void)arg;
HINSTANCE inst = GetModuleHandleW(NULL);
@@ -293,10 +311,10 @@ static void window_main(void *arg) {
int cw, ch, ow, oh;
cfg_window_size(&cw, &ch);
outer_size(NULL, cw, ch, &ow, &oh);
// The window opens behind the user's windows without taking focus: it shows the game's blank loading frames for a
// while, and must not cover what the player works on meanwhile. The game still counts itself in focus until the
// first WM_ACTIVATEAPP, so loading goes on, and keyboard input waits for the foreground (input_win.c). Its taskbar
// button flashes until it is brought up, and TOTK_FULLSCREEN waits for that first activation.
// The window opens behind the user's windows first and is then raised (raise_window): a game started by a frontend
// has no other way to reach it. The game counts itself in focus until the first WM_ACTIVATEAPP, so loading goes on,
// and keyboard input waits for the foreground (input_win.c). If Windows still refuses the foreground, the taskbar
// button flashes, and TOTK_FULLSCREEN waits for the first activation.
// TOTK_BACKGROUND_WINDOW=1 (test runs): no flashing, never fullscreen at start, and F11 keeps the window's place.
int background = g_background = cfg_get("TOTK_BACKGROUND_WINDOW") && atoi(cfg_get("TOTK_BACKGROUND_WINDOW"));
g_hwnd = CreateWindowExW(0, wc.lpszClassName, L"TotK Recomp", WND_STYLE, CW_USEDEFAULT, CW_USEDEFAULT, ow, oh, NULL,
@@ -315,6 +333,7 @@ static void window_main(void *arg) {
outer_size(g_hwnd, cw, ch, &ow, &oh);
SetWindowPos(g_hwnd, HWND_BOTTOM, 0, 0, ow, oh, SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!background) {
raise_window(g_hwnd); // a frontend's player may have no way to reach a window left behind
FLASHWINFO fw = {.cbSize = sizeof fw, .hwnd = g_hwnd, .dwFlags = FLASHW_TRAY | FLASHW_TIMERNOFG};
FlashWindowEx(&fw);
g_fs_pending = cfg_fullscreen();
+97
View File
@@ -0,0 +1,97 @@
// Generated by tools/build_shaders.py from runtime/shaders/hdr_encode.comp; do not edit.
// source sha256 08e1d94f9041b32d4183286db39941eec9cde41412babb197c0303fd64bb07a7
#pragma once
#include <stdint.h>
static const uint32_t hdr_encode_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000000dd, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0006000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x0000004c, 0x00060010, 0x00000004,
0x00000011, 0x00000008, 0x00000008, 0x00000001, 0x00040047, 0x0000004c, 0x0000000b, 0x0000001c,
0x00030047, 0x00000053, 0x00000002, 0x00050048, 0x00000053, 0x00000000, 0x00000023, 0x00000000,
0x00050048, 0x00000053, 0x00000001, 0x00000023, 0x00000008, 0x00050048, 0x00000053, 0x00000002,
0x00000023, 0x00000010, 0x00040047, 0x00000064, 0x00000021, 0x00000000, 0x00040047, 0x00000064,
0x00000022, 0x00000000, 0x00030047, 0x0000009c, 0x00000019, 0x00040047, 0x0000009c, 0x00000021,
0x00000001, 0x00040047, 0x0000009c, 0x00000022, 0x00000000, 0x00040047, 0x000000a7, 0x0000000b,
0x00000019, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006,
0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000003, 0x0004002b, 0x00000006, 0x00000015,
0x3d6147ae, 0x0004002b, 0x00000006, 0x0000001b, 0x4019999a, 0x0006002c, 0x00000007, 0x0000001c,
0x0000001b, 0x0000001b, 0x0000001b, 0x0004002b, 0x00000006, 0x0000001f, 0x3d25aee6, 0x0006002c,
0x00000007, 0x00000020, 0x0000001f, 0x0000001f, 0x0000001f, 0x00020014, 0x00000021, 0x00040017,
0x00000022, 0x00000021, 0x00000003, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000, 0x0004002b,
0x00000006, 0x0000002d, 0x3f800000, 0x0004002b, 0x00000006, 0x00000031, 0x3e232000, 0x0006002c,
0x00000007, 0x00000032, 0x00000031, 0x00000031, 0x00000031, 0x0004002b, 0x00000006, 0x00000034,
0x3f560000, 0x0004002b, 0x00000006, 0x00000035, 0x4196d000, 0x0004002b, 0x00000006, 0x0000003a,
0x41958000, 0x0004002b, 0x00000006, 0x00000040, 0x429db000, 0x0006002c, 0x00000007, 0x00000041,
0x00000040, 0x00000040, 0x00000040, 0x00040015, 0x00000045, 0x00000020, 0x00000001, 0x00040017,
0x00000046, 0x00000045, 0x00000002, 0x00040015, 0x00000049, 0x00000020, 0x00000000, 0x00040017,
0x0000004a, 0x00000049, 0x00000003, 0x00040020, 0x0000004b, 0x00000001, 0x0000004a, 0x0004003b,
0x0000004b, 0x0000004c, 0x00000001, 0x00040017, 0x0000004d, 0x00000049, 0x00000002, 0x00040017,
0x00000052, 0x00000006, 0x00000004, 0x0005001e, 0x00000053, 0x00000046, 0x00000046, 0x00000052,
0x00040020, 0x00000054, 0x00000009, 0x00000053, 0x0004003b, 0x00000054, 0x00000055, 0x00000009,
0x0004002b, 0x00000045, 0x00000056, 0x00000001, 0x00040020, 0x00000057, 0x00000009, 0x00000046,
0x00040017, 0x0000005a, 0x00000021, 0x00000002, 0x00090019, 0x00000061, 0x00000006, 0x00000001,
0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x00000062, 0x00000061,
0x00040020, 0x00000063, 0x00000000, 0x00000062, 0x0004003b, 0x00000063, 0x00000064, 0x00000000,
0x0004002b, 0x00000045, 0x00000067, 0x00000000, 0x0006002c, 0x00000007, 0x0000006e, 0x0000002c,
0x0000002c, 0x0000002c, 0x0004002b, 0x00000045, 0x00000070, 0x00000002, 0x0004002b, 0x00000049,
0x00000071, 0x00000002, 0x00040020, 0x00000072, 0x00000009, 0x00000006, 0x0004002b, 0x00000006,
0x00000075, 0x3f000000, 0x00040018, 0x00000080, 0x00000007, 0x00000003, 0x0004002b, 0x00000006,
0x00000081, 0x3f209d49, 0x0004002b, 0x00000006, 0x00000082, 0x3d8d844d, 0x0004002b, 0x00000006,
0x00000083, 0x3c86594b, 0x0006002c, 0x00000007, 0x00000084, 0x00000081, 0x00000082, 0x00000083,
0x0004002b, 0x00000006, 0x00000085, 0x3ea89a02, 0x0004002b, 0x00000006, 0x00000086, 0x3f6b645a,
0x0004002b, 0x00000006, 0x00000087, 0x3db43958, 0x0006002c, 0x00000007, 0x00000088, 0x00000085,
0x00000086, 0x00000087, 0x0004002b, 0x00000006, 0x00000089, 0x3d315b57, 0x0004002b, 0x00000006,
0x0000008a, 0x3c3ac711, 0x0004002b, 0x00000006, 0x0000008b, 0x3f65460b, 0x0006002c, 0x00000007,
0x0000008c, 0x00000089, 0x0000008a, 0x0000008b, 0x0006002c, 0x00000080, 0x0000008d, 0x00000084,
0x00000088, 0x0000008c, 0x0004002b, 0x00000049, 0x00000091, 0x00000000, 0x0004002b, 0x00000049,
0x00000095, 0x00000001, 0x00090019, 0x0000009a, 0x00000006, 0x00000001, 0x00000000, 0x00000000,
0x00000000, 0x00000002, 0x00000002, 0x00040020, 0x0000009b, 0x00000000, 0x0000009a, 0x0004003b,
0x0000009b, 0x0000009c, 0x00000000, 0x0004002b, 0x00000049, 0x000000a6, 0x00000008, 0x0006002c,
0x0000004a, 0x000000a7, 0x000000a6, 0x000000a6, 0x00000095, 0x0006002c, 0x00000007, 0x000000d1,
0x0000002d, 0x0000002d, 0x0000002d, 0x0006002c, 0x00000007, 0x000000d3, 0x00000015, 0x00000015,
0x00000015, 0x0006002c, 0x00000007, 0x000000d6, 0x00000034, 0x00000034, 0x00000034, 0x0004002b,
0x00000006, 0x000000d7, 0x3d9e8391, 0x0006002c, 0x00000007, 0x000000d8, 0x000000d7, 0x000000d7,
0x000000d7, 0x0004002b, 0x00000006, 0x000000d9, 0x3f72a76f, 0x0006002c, 0x00000007, 0x000000da,
0x000000d9, 0x000000d9, 0x000000d9, 0x0004002b, 0x00000006, 0x000000db, 0x38d1b717, 0x0006002c,
0x00000007, 0x000000dc, 0x000000db, 0x000000db, 0x000000db, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7, 0x000000a8, 0x00000000, 0x000300fb,
0x00000091, 0x000000a9, 0x000200f8, 0x000000a9, 0x0004003d, 0x0000004a, 0x0000004e, 0x0000004c,
0x0007004f, 0x0000004d, 0x0000004f, 0x0000004e, 0x0000004e, 0x00000000, 0x00000001, 0x0004007c,
0x00000046, 0x00000050, 0x0000004f, 0x00050041, 0x00000057, 0x00000058, 0x00000055, 0x00000056,
0x0004003d, 0x00000046, 0x00000059, 0x00000058, 0x000500af, 0x0000005a, 0x0000005b, 0x00000050,
0x00000059, 0x0004009a, 0x00000021, 0x0000005c, 0x0000005b, 0x000300f7, 0x0000005e, 0x00000000,
0x000400fa, 0x0000005c, 0x0000005d, 0x0000005e, 0x000200f8, 0x0000005d, 0x000200f9, 0x000000a8,
0x000200f8, 0x0000005e, 0x0004003d, 0x00000062, 0x00000065, 0x00000064, 0x00050041, 0x00000057,
0x00000068, 0x00000055, 0x00000067, 0x0004003d, 0x00000046, 0x00000069, 0x00000068, 0x00050080,
0x00000046, 0x0000006a, 0x00000050, 0x00000069, 0x00040064, 0x00000061, 0x0000006b, 0x00000065,
0x0007005f, 0x00000052, 0x0000006c, 0x0000006b, 0x0000006a, 0x00000002, 0x00000067, 0x0008004f,
0x00000007, 0x0000006d, 0x0000006c, 0x0000006c, 0x00000000, 0x00000001, 0x00000002, 0x0007000c,
0x00000007, 0x0000006f, 0x00000001, 0x00000028, 0x0000006d, 0x0000006e, 0x00060041, 0x00000072,
0x00000073, 0x00000055, 0x00000070, 0x00000071, 0x0004003d, 0x00000006, 0x00000074, 0x00000073,
0x000500ba, 0x00000021, 0x00000076, 0x00000074, 0x00000075, 0x000300f7, 0x00000078, 0x00000000,
0x000400fa, 0x00000076, 0x00000077, 0x00000078, 0x000200f8, 0x00000077, 0x0008000c, 0x00000007,
0x0000007c, 0x00000001, 0x0000002b, 0x0000006f, 0x0000006e, 0x000000d1, 0x00050085, 0x00000007,
0x000000b2, 0x0000007c, 0x000000d8, 0x00050081, 0x00000007, 0x000000b5, 0x0000007c, 0x000000d3,
0x00050085, 0x00000007, 0x000000b7, 0x000000b5, 0x000000da, 0x0007000c, 0x00000007, 0x000000b8,
0x00000001, 0x0000001a, 0x000000b7, 0x0000001c, 0x000500ba, 0x00000022, 0x000000ba, 0x0000007c,
0x00000020, 0x000600a9, 0x00000007, 0x000000bb, 0x000000ba, 0x000000b8, 0x000000b2, 0x000200f9,
0x00000078, 0x000200f8, 0x00000078, 0x000700f5, 0x00000007, 0x000000d0, 0x0000006f, 0x0000005e,
0x000000bb, 0x00000077, 0x00050091, 0x00000007, 0x0000008f, 0x0000008d, 0x000000d0, 0x0007000c,
0x00000007, 0x00000090, 0x00000001, 0x00000028, 0x0000008f, 0x0000006e, 0x00060041, 0x00000072,
0x00000092, 0x00000055, 0x00000070, 0x00000091, 0x0004003d, 0x00000006, 0x00000093, 0x00000092,
0x0005008e, 0x00000007, 0x00000094, 0x00000090, 0x00000093, 0x00060041, 0x00000072, 0x00000096,
0x00000055, 0x00000070, 0x00000095, 0x0004003d, 0x00000006, 0x00000097, 0x00000096, 0x00060050,
0x00000007, 0x00000098, 0x00000097, 0x00000097, 0x00000097, 0x0007000c, 0x00000007, 0x00000099,
0x00000001, 0x00000025, 0x00000094, 0x00000098, 0x0004003d, 0x0000009a, 0x0000009d, 0x0000009c,
0x00050085, 0x00000007, 0x000000c1, 0x00000099, 0x000000dc, 0x0008000c, 0x00000007, 0x000000c4,
0x00000001, 0x0000002b, 0x000000c1, 0x0000006e, 0x000000d1, 0x0007000c, 0x00000007, 0x000000c5,
0x00000001, 0x0000001a, 0x000000c4, 0x00000032, 0x0005008e, 0x00000007, 0x000000c7, 0x000000c5,
0x00000035, 0x00050081, 0x00000007, 0x000000c9, 0x000000d6, 0x000000c7, 0x0005008e, 0x00000007,
0x000000cb, 0x000000c5, 0x0000003a, 0x00050081, 0x00000007, 0x000000cd, 0x000000d1, 0x000000cb,
0x00050088, 0x00000007, 0x000000ce, 0x000000c9, 0x000000cd, 0x0007000c, 0x00000007, 0x000000cf,
0x00000001, 0x0000001a, 0x000000ce, 0x00000041, 0x00050051, 0x00000006, 0x000000a2, 0x000000cf,
0x00000000, 0x00050051, 0x00000006, 0x000000a3, 0x000000cf, 0x00000001, 0x00050051, 0x00000006,
0x000000a4, 0x000000cf, 0x00000002, 0x00070050, 0x00000052, 0x000000a5, 0x000000a2, 0x000000a3,
0x000000a4, 0x0000002d, 0x00040063, 0x0000009d, 0x00000050, 0x000000a5, 0x000200f9, 0x000000a8,
0x000200f8, 0x000000a8, 0x000100fd, 0x00010038,
};
+36
View File
@@ -0,0 +1,36 @@
// Generated by tools/build_shaders.py from runtime/shaders/hdr_fullscreen.vert; do not edit.
// source sha256 3474de7753260cdbc5df61bbd96074c566f98c9be15be4791ae7cbea32542b03
#pragma once
#include <stdint.h>
static const uint32_t hdr_fullscreen_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x0000002c, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0008000f, 0x00000000, 0x00000004, 0x6e69616d, 0x00000000, 0x00000009, 0x0000000c, 0x0000001d,
0x00040047, 0x00000009, 0x0000001e, 0x00000000, 0x00040047, 0x0000000c, 0x0000000b, 0x0000002a,
0x00030047, 0x0000001b, 0x00000002, 0x00050048, 0x0000001b, 0x00000000, 0x0000000b, 0x00000000,
0x00050048, 0x0000001b, 0x00000001, 0x0000000b, 0x00000001, 0x00050048, 0x0000001b, 0x00000002,
0x0000000b, 0x00000003, 0x00050048, 0x0000001b, 0x00000003, 0x0000000b, 0x00000004, 0x00020013,
0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000006, 0x00000020, 0x00040017,
0x00000007, 0x00000006, 0x00000002, 0x00040020, 0x00000008, 0x00000003, 0x00000007, 0x0004003b,
0x00000008, 0x00000009, 0x00000003, 0x00040015, 0x0000000a, 0x00000020, 0x00000001, 0x00040020,
0x0000000b, 0x00000001, 0x0000000a, 0x0004003b, 0x0000000b, 0x0000000c, 0x00000001, 0x0004002b,
0x0000000a, 0x0000000e, 0x00000001, 0x0004002b, 0x0000000a, 0x00000010, 0x00000002, 0x00040017,
0x00000017, 0x00000006, 0x00000004, 0x00040015, 0x00000018, 0x00000020, 0x00000000, 0x0004002b,
0x00000018, 0x00000019, 0x00000001, 0x0004001c, 0x0000001a, 0x00000006, 0x00000019, 0x0006001e,
0x0000001b, 0x00000017, 0x00000006, 0x0000001a, 0x0000001a, 0x00040020, 0x0000001c, 0x00000003,
0x0000001b, 0x0004003b, 0x0000001c, 0x0000001d, 0x00000003, 0x0004002b, 0x0000000a, 0x0000001e,
0x00000000, 0x0004002b, 0x00000006, 0x00000020, 0x40000000, 0x0004002b, 0x00000006, 0x00000022,
0x3f800000, 0x0004002b, 0x00000006, 0x00000025, 0x00000000, 0x00040020, 0x00000029, 0x00000003,
0x00000017, 0x0005002c, 0x00000007, 0x0000002b, 0x00000022, 0x00000022, 0x00050036, 0x00000002,
0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x0000000a, 0x0000000d,
0x0000000c, 0x000500c4, 0x0000000a, 0x0000000f, 0x0000000d, 0x0000000e, 0x000500c7, 0x0000000a,
0x00000011, 0x0000000f, 0x00000010, 0x0004006f, 0x00000006, 0x00000012, 0x00000011, 0x000500c7,
0x0000000a, 0x00000014, 0x0000000d, 0x00000010, 0x0004006f, 0x00000006, 0x00000015, 0x00000014,
0x00050050, 0x00000007, 0x00000016, 0x00000012, 0x00000015, 0x0003003e, 0x00000009, 0x00000016,
0x0004003d, 0x00000007, 0x0000001f, 0x00000009, 0x0005008e, 0x00000007, 0x00000021, 0x0000001f,
0x00000020, 0x00050083, 0x00000007, 0x00000024, 0x00000021, 0x0000002b, 0x00050051, 0x00000006,
0x00000026, 0x00000024, 0x00000000, 0x00050051, 0x00000006, 0x00000027, 0x00000024, 0x00000001,
0x00070050, 0x00000017, 0x00000028, 0x00000026, 0x00000027, 0x00000025, 0x00000022, 0x00050041,
0x00000029, 0x0000002a, 0x0000001d, 0x0000001e, 0x0003003e, 0x0000002a, 0x00000028, 0x000100fd,
0x00010038,
};
+290
View File
@@ -0,0 +1,290 @@
// Generated by tools/build_shaders.py from runtime/shaders/hdr_tonemap.frag; do not edit.
// source sha256 a09ae2b13d844bb6a136d2c3cf38b3ec571191775a4c2f0b43a586bd355381fa
#pragma once
#include <stdint.h>
static const uint32_t hdr_tonemap_spv[] = {
0x07230203, 0x00010300, 0x0008000b, 0x0000018a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000053, 0x00000181, 0x00030010,
0x00000004, 0x00000007, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004, 0x6e69616d,
0x00000000, 0x00050005, 0x0000000a, 0x76727563, 0x31662865, 0x0000003b, 0x00030005, 0x00000009,
0x00000078, 0x00050005, 0x00000010, 0x76727563, 0x76283365, 0x003b3366, 0x00030005, 0x0000000f,
0x00000078, 0x00050005, 0x00000014, 0x6378616d, 0x33667628, 0x0000003b, 0x00030005, 0x00000013,
0x00000063, 0x00030005, 0x00000016, 0x00003079, 0x00040005, 0x0000002e, 0x61726170, 0x0000006d,
0x00040005, 0x00000034, 0x61726170, 0x0000006d, 0x00040005, 0x00000039, 0x61726170, 0x0000006d,
0x00030005, 0x0000004b, 0x00000078, 0x00050005, 0x0000004f, 0x745f7066, 0x6263745f, 0x0000385f,
0x00030005, 0x00000053, 0x00007675, 0x00050005, 0x0000005c, 0x745f7066, 0x6263745f, 0x0000415f,
0x00030005, 0x00000064, 0x00000077, 0x00030005, 0x00000067, 0x00003143, 0x00040006, 0x00000067,
0x00000000, 0x00000076, 0x00040005, 0x00000069, 0x635f7066, 0x00000031, 0x00030005, 0x0000006f,
0x00000067, 0x00030005, 0x00000071, 0x00003343, 0x00040006, 0x00000071, 0x00000000, 0x00000076,
0x00040005, 0x00000073, 0x635f7066, 0x00000033, 0x00040005, 0x00000076, 0x656d6167, 0x00000000,
0x00060005, 0x00000079, 0x70707553, 0x4274726f, 0x65666675, 0x00000072, 0x00040006, 0x00000079,
0x00000000, 0x00646170, 0x00040006, 0x00000079, 0x00000001, 0x00726468, 0x00060005, 0x0000007b,
0x70707573, 0x5f74726f, 0x66667562, 0x00007265, 0x00040005, 0x00000089, 0x6b616570, 0x00000000,
0x00030005, 0x00000094, 0x006d756c, 0x00030005, 0x000000a6, 0x00726473, 0x00040005, 0x000000ab,
0x6c5f7962, 0x00006d75, 0x00040005, 0x000000b2, 0x61726170, 0x0000006d, 0x00030005, 0x000000bb,
0x00000063, 0x00040005, 0x000000bd, 0x61726170, 0x0000006d, 0x00030005, 0x000000c7, 0x00000073,
0x00040005, 0x000000c8, 0x61726170, 0x0000006d, 0x00030005, 0x000000cc, 0x0000006e, 0x00030005,
0x000000d1, 0x0000006d, 0x00040005, 0x000000d2, 0x61726170, 0x0000006d, 0x00030005, 0x000000d5,
0x00000064, 0x00040005, 0x000000e4, 0x64617267, 0x00000065, 0x00040005, 0x000000fa, 0x5f726473,
0x00000063, 0x00030005, 0x00000115, 0x00006d73, 0x00040005, 0x00000116, 0x61726170, 0x0000006d,
0x00030005, 0x00000119, 0x00006473, 0x00030005, 0x00000139, 0x00657568, 0x00030005, 0x0000013d,
0x00006379, 0x00030005, 0x00000144, 0x00007379, 0x00040005, 0x00000159, 0x61726170, 0x0000006d,
0x00030005, 0x00000160, 0x00706f74, 0x00040005, 0x00000161, 0x61726170, 0x0000006d, 0x00040005,
0x00000164, 0x65656e6b, 0x00000000, 0x00030005, 0x0000016c, 0x00000072, 0x00040005, 0x00000181,
0x6f6c6f63, 0x00000072, 0x00040047, 0x0000004f, 0x00000021, 0x00000001, 0x00040047, 0x0000004f,
0x00000022, 0x00000000, 0x00040047, 0x00000053, 0x0000001e, 0x00000000, 0x00040047, 0x0000005c,
0x00000021, 0x00000002, 0x00040047, 0x0000005c, 0x00000022, 0x00000000, 0x00040047, 0x00000066,
0x00000006, 0x00000010, 0x00030047, 0x00000067, 0x00000002, 0x00050048, 0x00000067, 0x00000000,
0x00000023, 0x00000000, 0x00040047, 0x00000069, 0x00000021, 0x00000003, 0x00040047, 0x00000069,
0x00000022, 0x00000000, 0x00040047, 0x00000070, 0x00000006, 0x00000010, 0x00030047, 0x00000071,
0x00000002, 0x00050048, 0x00000071, 0x00000000, 0x00000023, 0x00000000, 0x00040047, 0x00000073,
0x00000021, 0x00000004, 0x00040047, 0x00000073, 0x00000022, 0x00000000, 0x00040047, 0x00000078,
0x00000006, 0x00000010, 0x00030047, 0x00000079, 0x00000002, 0x00050048, 0x00000079, 0x00000000,
0x00000023, 0x00000000, 0x00050048, 0x00000079, 0x00000001, 0x00000023, 0x000005a0, 0x00040047,
0x0000007b, 0x00000021, 0x00000000, 0x00040047, 0x0000007b, 0x00000022, 0x00000000, 0x00040047,
0x00000181, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
0x00030016, 0x00000006, 0x00000020, 0x00040020, 0x00000007, 0x00000007, 0x00000006, 0x00040021,
0x00000008, 0x00000006, 0x00000007, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 0x00040020,
0x0000000d, 0x00000007, 0x0000000c, 0x00040021, 0x0000000e, 0x0000000c, 0x0000000d, 0x00040021,
0x00000012, 0x00000006, 0x0000000d, 0x0004002b, 0x00000006, 0x00000017, 0x3e28aeed, 0x0004002b,
0x00000006, 0x00000019, 0x3e3851ec, 0x00020014, 0x0000001a, 0x0004002b, 0x00000006, 0x0000001f,
0x3f800000, 0x0004002b, 0x00000006, 0x00000026, 0x3f55d445, 0x00040015, 0x0000002f, 0x00000020,
0x00000000, 0x0004002b, 0x0000002f, 0x00000030, 0x00000000, 0x0004002b, 0x0000002f, 0x00000035,
0x00000001, 0x0004002b, 0x0000002f, 0x0000003a, 0x00000002, 0x00090019, 0x0000004c, 0x00000006,
0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x0003001b, 0x0000004d,
0x0000004c, 0x00040020, 0x0000004e, 0x00000000, 0x0000004d, 0x0004003b, 0x0000004e, 0x0000004f,
0x00000000, 0x00040017, 0x00000051, 0x00000006, 0x00000004, 0x00040020, 0x00000052, 0x00000001,
0x00000051, 0x0004003b, 0x00000052, 0x00000053, 0x00000001, 0x00040017, 0x00000054, 0x00000006,
0x00000002, 0x0004002b, 0x00000006, 0x00000059, 0x461c4000, 0x0006002c, 0x0000000c, 0x0000005a,
0x00000059, 0x00000059, 0x00000059, 0x0004003b, 0x0000004e, 0x0000005c, 0x00000000, 0x00040020,
0x00000063, 0x00000007, 0x00000051, 0x0004002b, 0x0000002f, 0x00000065, 0x00001000, 0x0004001c,
0x00000066, 0x00000051, 0x00000065, 0x0003001e, 0x00000067, 0x00000066, 0x00040020, 0x00000068,
0x00000002, 0x00000067, 0x0004003b, 0x00000068, 0x00000069, 0x00000002, 0x00040015, 0x0000006a,
0x00000020, 0x00000001, 0x0004002b, 0x0000006a, 0x0000006b, 0x00000000, 0x00040020, 0x0000006c,
0x00000002, 0x00000051, 0x0004001c, 0x00000070, 0x00000051, 0x00000065, 0x0003001e, 0x00000071,
0x00000070, 0x00040020, 0x00000072, 0x00000002, 0x00000071, 0x0004003b, 0x00000072, 0x00000073,
0x00000002, 0x0004002b, 0x0000002f, 0x00000077, 0x0000005a, 0x0004001c, 0x00000078, 0x00000051,
0x00000077, 0x0004001e, 0x00000079, 0x00000078, 0x00000051, 0x00040020, 0x0000007a, 0x00000002,
0x00000079, 0x0004003b, 0x0000007a, 0x0000007b, 0x00000002, 0x0004002b, 0x0000006a, 0x0000007c,
0x00000001, 0x00040020, 0x0000007d, 0x00000002, 0x00000006, 0x0004002b, 0x00000006, 0x00000080,
0x00000000, 0x0004002b, 0x00000006, 0x00000097, 0x3e99096c, 0x0006002c, 0x0000000c, 0x000000c5,
0x00000080, 0x00000080, 0x00000080, 0x0004002b, 0x00000006, 0x000000d8, 0x3f2aaaab, 0x0006002c,
0x0000000c, 0x00000108, 0x0000001f, 0x0000001f, 0x0000001f, 0x0004002b, 0x00000006, 0x0000013f,
0x3e59b3d0, 0x0004002b, 0x00000006, 0x00000140, 0x3f371759, 0x0004002b, 0x00000006, 0x00000141,
0x3d93dd98, 0x0006002c, 0x0000000c, 0x00000142, 0x0000013f, 0x00000140, 0x00000141, 0x0004002b,
0x00000006, 0x0000014a, 0x3727c5ac, 0x0004002b, 0x00000006, 0x00000157, 0x3f000000, 0x0004002b,
0x00000006, 0x00000158, 0x3fc00000, 0x00040020, 0x00000180, 0x00000003, 0x00000051, 0x0004003b,
0x00000180, 0x00000181, 0x00000003, 0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003,
0x000200f8, 0x00000005, 0x0004003b, 0x0000000d, 0x0000004b, 0x00000007, 0x0004003b, 0x00000063,
0x00000064, 0x00000007, 0x0004003b, 0x00000063, 0x0000006f, 0x00000007, 0x0004003b, 0x00000007,
0x00000076, 0x00000007, 0x0004003b, 0x00000007, 0x00000082, 0x00000007, 0x0004003b, 0x00000007,
0x00000089, 0x00000007, 0x0004003b, 0x00000007, 0x0000008d, 0x00000007, 0x0004003b, 0x00000007,
0x00000094, 0x00000007, 0x0004003b, 0x00000007, 0x000000a6, 0x00000007, 0x0004003b, 0x0000000d,
0x000000ab, 0x00000007, 0x0004003b, 0x0000000d, 0x000000ae, 0x00000007, 0x0004003b, 0x00000007,
0x000000b2, 0x00000007, 0x0004003b, 0x0000000d, 0x000000bb, 0x00000007, 0x0004003b, 0x0000000d,
0x000000bd, 0x00000007, 0x0004003b, 0x00000007, 0x000000c7, 0x00000007, 0x0004003b, 0x0000000d,
0x000000c8, 0x00000007, 0x0004003b, 0x0000000d, 0x000000cc, 0x00000007, 0x0004003b, 0x00000007,
0x000000d1, 0x00000007, 0x0004003b, 0x0000000d, 0x000000d2, 0x00000007, 0x0004003b, 0x00000007,
0x000000d5, 0x00000007, 0x0004003b, 0x00000007, 0x000000e4, 0x00000007, 0x0004003b, 0x0000000d,
0x000000fa, 0x00000007, 0x0004003b, 0x0000000d, 0x000000fd, 0x00000007, 0x0004003b, 0x00000007,
0x00000115, 0x00000007, 0x0004003b, 0x0000000d, 0x00000116, 0x00000007, 0x0004003b, 0x00000007,
0x00000119, 0x00000007, 0x0004003b, 0x00000007, 0x00000139, 0x00000007, 0x0004003b, 0x00000007,
0x0000013d, 0x00000007, 0x0004003b, 0x00000007, 0x00000144, 0x00000007, 0x0004003b, 0x0000000d,
0x00000159, 0x00000007, 0x0004003b, 0x00000007, 0x00000160, 0x00000007, 0x0004003b, 0x0000000d,
0x00000161, 0x00000007, 0x0004003b, 0x00000007, 0x00000164, 0x00000007, 0x0004003b, 0x00000007,
0x0000016c, 0x00000007, 0x0004003d, 0x0000004d, 0x00000050, 0x0000004f, 0x0004003d, 0x00000051,
0x00000055, 0x00000053, 0x0007004f, 0x00000054, 0x00000056, 0x00000055, 0x00000055, 0x00000000,
0x00000001, 0x00050057, 0x00000051, 0x00000057, 0x00000050, 0x00000056, 0x0008004f, 0x0000000c,
0x00000058, 0x00000057, 0x00000057, 0x00000000, 0x00000001, 0x00000002, 0x0007000c, 0x0000000c,
0x0000005b, 0x00000001, 0x00000025, 0x00000058, 0x0000005a, 0x0004003d, 0x0000004d, 0x0000005d,
0x0000005c, 0x0004003d, 0x00000051, 0x0000005e, 0x00000053, 0x0007004f, 0x00000054, 0x0000005f,
0x0000005e, 0x0000005e, 0x00000000, 0x00000001, 0x00050057, 0x00000051, 0x00000060, 0x0000005d,
0x0000005f, 0x0008004f, 0x0000000c, 0x00000061, 0x00000060, 0x00000060, 0x00000000, 0x00000001,
0x00000002, 0x00050081, 0x0000000c, 0x00000062, 0x0000005b, 0x00000061, 0x0003003e, 0x0000004b,
0x00000062, 0x00060041, 0x0000006c, 0x0000006d, 0x00000069, 0x0000006b, 0x0000006b, 0x0004003d,
0x00000051, 0x0000006e, 0x0000006d, 0x0003003e, 0x00000064, 0x0000006e, 0x00060041, 0x0000006c,
0x00000074, 0x00000073, 0x0000006b, 0x0000006b, 0x0004003d, 0x00000051, 0x00000075, 0x00000074,
0x0003003e, 0x0000006f, 0x00000075, 0x00060041, 0x0000007d, 0x0000007e, 0x0000007b, 0x0000007c,
0x00000030, 0x0004003d, 0x00000006, 0x0000007f, 0x0000007e, 0x000500ba, 0x0000001a, 0x00000081,
0x0000007f, 0x00000080, 0x000300f7, 0x00000084, 0x00000000, 0x000400fa, 0x00000081, 0x00000083,
0x00000087, 0x000200f8, 0x00000083, 0x00060041, 0x0000007d, 0x00000085, 0x0000007b, 0x0000007c,
0x00000030, 0x0004003d, 0x00000006, 0x00000086, 0x00000085, 0x0003003e, 0x00000082, 0x00000086,
0x000200f9, 0x00000084, 0x000200f8, 0x00000087, 0x0003003e, 0x00000082, 0x0000001f, 0x000200f9,
0x00000084, 0x000200f8, 0x00000084, 0x0004003d, 0x00000006, 0x00000088, 0x00000082, 0x0003003e,
0x00000076, 0x00000088, 0x00060041, 0x0000007d, 0x0000008a, 0x0000007b, 0x0000007c, 0x00000035,
0x0004003d, 0x00000006, 0x0000008b, 0x0000008a, 0x000500ba, 0x0000001a, 0x0000008c, 0x0000008b,
0x0000001f, 0x000300f7, 0x0000008f, 0x00000000, 0x000400fa, 0x0000008c, 0x0000008e, 0x00000092,
0x000200f8, 0x0000008e, 0x00060041, 0x0000007d, 0x00000090, 0x0000007b, 0x0000007c, 0x00000035,
0x0004003d, 0x00000006, 0x00000091, 0x00000090, 0x0003003e, 0x0000008d, 0x00000091, 0x000200f9,
0x0000008f, 0x000200f8, 0x00000092, 0x0003003e, 0x0000008d, 0x0000001f, 0x000200f9, 0x0000008f,
0x000200f8, 0x0000008f, 0x0004003d, 0x00000006, 0x00000093, 0x0000008d, 0x0003003e, 0x00000089,
0x00000093, 0x00050041, 0x00000007, 0x00000095, 0x0000004b, 0x00000030, 0x0004003d, 0x00000006,
0x00000096, 0x00000095, 0x00050085, 0x00000006, 0x00000098, 0x00000096, 0x00000097, 0x00050041,
0x00000007, 0x00000099, 0x0000004b, 0x00000035, 0x0004003d, 0x00000006, 0x0000009a, 0x00000099,
0x00050041, 0x00000007, 0x0000009b, 0x00000064, 0x00000030, 0x0004003d, 0x00000006, 0x0000009c,
0x0000009b, 0x00050085, 0x00000006, 0x0000009d, 0x0000009a, 0x0000009c, 0x00050081, 0x00000006,
0x0000009e, 0x00000098, 0x0000009d, 0x00050041, 0x00000007, 0x0000009f, 0x0000004b, 0x0000003a,
0x0004003d, 0x00000006, 0x000000a0, 0x0000009f, 0x00050041, 0x00000007, 0x000000a1, 0x00000064,
0x00000035, 0x0004003d, 0x00000006, 0x000000a2, 0x000000a1, 0x00050085, 0x00000006, 0x000000a3,
0x000000a0, 0x000000a2, 0x00050081, 0x00000006, 0x000000a4, 0x0000009e, 0x000000a3, 0x0007000c,
0x00000006, 0x000000a5, 0x00000001, 0x00000028, 0x000000a4, 0x00000080, 0x0003003e, 0x00000094,
0x000000a5, 0x0004003d, 0x00000006, 0x000000a7, 0x00000094, 0x0004007f, 0x00000006, 0x000000a8,
0x000000a7, 0x0006000c, 0x00000006, 0x000000a9, 0x00000001, 0x0000001b, 0x000000a8, 0x00050083,
0x00000006, 0x000000aa, 0x0000001f, 0x000000a9, 0x0003003e, 0x000000a6, 0x000000aa, 0x0004003d,
0x00000006, 0x000000ac, 0x00000094, 0x000500ba, 0x0000001a, 0x000000ad, 0x000000ac, 0x00000080,
0x000300f7, 0x000000b0, 0x00000000, 0x000400fa, 0x000000ad, 0x000000af, 0x000000b8, 0x000200f8,
0x000000af, 0x0004003d, 0x0000000c, 0x000000b1, 0x0000004b, 0x0004003d, 0x00000006, 0x000000b3,
0x00000094, 0x0003003e, 0x000000b2, 0x000000b3, 0x00050039, 0x00000006, 0x000000b4, 0x0000000a,
0x000000b2, 0x0004003d, 0x00000006, 0x000000b5, 0x00000094, 0x00050088, 0x00000006, 0x000000b6,
0x000000b4, 0x000000b5, 0x0005008e, 0x0000000c, 0x000000b7, 0x000000b1, 0x000000b6, 0x0003003e,
0x000000ae, 0x000000b7, 0x000200f9, 0x000000b0, 0x000200f8, 0x000000b8, 0x0004003d, 0x0000000c,
0x000000b9, 0x0000004b, 0x0003003e, 0x000000ae, 0x000000b9, 0x000200f9, 0x000000b0, 0x000200f8,
0x000000b0, 0x0004003d, 0x0000000c, 0x000000ba, 0x000000ae, 0x0003003e, 0x000000ab, 0x000000ba,
0x0004003d, 0x0000000c, 0x000000bc, 0x000000ab, 0x0004003d, 0x0000000c, 0x000000be, 0x0000004b,
0x0003003e, 0x000000bd, 0x000000be, 0x00050039, 0x0000000c, 0x000000bf, 0x00000010, 0x000000bd,
0x0004003d, 0x00000006, 0x000000c0, 0x000000a6, 0x0004003d, 0x00000006, 0x000000c1, 0x000000a6,
0x00050085, 0x00000006, 0x000000c2, 0x000000c0, 0x000000c1, 0x00060050, 0x0000000c, 0x000000c3,
0x000000c2, 0x000000c2, 0x000000c2, 0x0008000c, 0x0000000c, 0x000000c4, 0x00000001, 0x0000002e,
0x000000bc, 0x000000bf, 0x000000c3, 0x0007000c, 0x0000000c, 0x000000c6, 0x00000001, 0x00000028,
0x000000c4, 0x000000c5, 0x0003003e, 0x000000bb, 0x000000c6, 0x0004003d, 0x0000000c, 0x000000c9,
0x000000bb, 0x0003003e, 0x000000c8, 0x000000c9, 0x00050039, 0x00000006, 0x000000ca, 0x00000014,
0x000000c8, 0x0007000c, 0x00000006, 0x000000cb, 0x00000001, 0x00000028, 0x000000ca, 0x0000001f,
0x0003003e, 0x000000c7, 0x000000cb, 0x0004003d, 0x0000000c, 0x000000cd, 0x000000bb, 0x0004003d,
0x00000006, 0x000000ce, 0x000000c7, 0x00060050, 0x0000000c, 0x000000cf, 0x000000ce, 0x000000ce,
0x000000ce, 0x00050088, 0x0000000c, 0x000000d0, 0x000000cd, 0x000000cf, 0x0003003e, 0x000000cc,
0x000000d0, 0x0004003d, 0x0000000c, 0x000000d3, 0x000000cc, 0x0003003e, 0x000000d2, 0x000000d3,
0x00050039, 0x00000006, 0x000000d4, 0x00000014, 0x000000d2, 0x0003003e, 0x000000d1, 0x000000d4,
0x00050041, 0x00000007, 0x000000d6, 0x000000cc, 0x00000030, 0x0004003d, 0x00000006, 0x000000d7,
0x000000d6, 0x00050085, 0x00000006, 0x000000d9, 0x000000d7, 0x000000d8, 0x00050041, 0x00000007,
0x000000da, 0x000000cc, 0x00000035, 0x0004003d, 0x00000006, 0x000000db, 0x000000da, 0x00050041,
0x00000007, 0x000000dc, 0x000000cc, 0x0000003a, 0x0004003d, 0x00000006, 0x000000dd, 0x000000dc,
0x00050081, 0x00000006, 0x000000de, 0x000000db, 0x000000dd, 0x00050041, 0x00000007, 0x000000df,
0x00000064, 0x0000003a, 0x0004003d, 0x00000006, 0x000000e0, 0x000000df, 0x00050085, 0x00000006,
0x000000e1, 0x000000de, 0x000000e0, 0x00050081, 0x00000006, 0x000000e2, 0x000000d9, 0x000000e1,
0x00050083, 0x00000006, 0x000000e3, 0x000000e2, 0x0000001f, 0x0003003e, 0x000000d5, 0x000000e3,
0x00050041, 0x00000007, 0x000000e5, 0x0000006f, 0x00000035, 0x0004003d, 0x00000006, 0x000000e6,
0x000000e5, 0x0004003d, 0x00000006, 0x000000e7, 0x000000d5, 0x0004003d, 0x00000006, 0x000000e8,
0x000000d5, 0x00050085, 0x00000006, 0x000000e9, 0x000000e7, 0x000000e8, 0x00050083, 0x00000006,
0x000000ea, 0x0000001f, 0x000000e9, 0x00050085, 0x00000006, 0x000000eb, 0x000000e6, 0x000000ea,
0x00050041, 0x00000007, 0x000000ec, 0x0000006f, 0x00000030, 0x0004003d, 0x00000006, 0x000000ed,
0x000000ec, 0x00050081, 0x00000006, 0x000000ee, 0x000000eb, 0x000000ed, 0x0003003e, 0x000000e4,
0x000000ee, 0x0004003d, 0x00000006, 0x000000ef, 0x000000c7, 0x0004003d, 0x0000000c, 0x000000f0,
0x000000cc, 0x0004003d, 0x00000006, 0x000000f1, 0x000000d1, 0x00060050, 0x0000000c, 0x000000f2,
0x000000f1, 0x000000f1, 0x000000f1, 0x00050083, 0x0000000c, 0x000000f3, 0x000000f0, 0x000000f2,
0x0004003d, 0x00000006, 0x000000f4, 0x000000e4, 0x0005008e, 0x0000000c, 0x000000f5, 0x000000f3,
0x000000f4, 0x0004003d, 0x00000006, 0x000000f6, 0x000000d1, 0x00060050, 0x0000000c, 0x000000f7,
0x000000f6, 0x000000f6, 0x000000f6, 0x00050081, 0x0000000c, 0x000000f8, 0x000000f5, 0x000000f7,
0x0005008e, 0x0000000c, 0x000000f9, 0x000000f8, 0x000000ef, 0x0003003e, 0x000000bb, 0x000000f9,
0x0004003d, 0x00000006, 0x000000fb, 0x00000094, 0x000500ba, 0x0000001a, 0x000000fc, 0x000000fb,
0x00000080, 0x000300f7, 0x000000ff, 0x00000000, 0x000400fa, 0x000000fc, 0x000000fe, 0x00000105,
0x000200f8, 0x000000fe, 0x0004003d, 0x0000000c, 0x00000100, 0x0000004b, 0x0004003d, 0x00000006,
0x00000101, 0x000000a6, 0x0004003d, 0x00000006, 0x00000102, 0x00000094, 0x00050088, 0x00000006,
0x00000103, 0x00000101, 0x00000102, 0x0005008e, 0x0000000c, 0x00000104, 0x00000100, 0x00000103,
0x0003003e, 0x000000fd, 0x00000104, 0x000200f9, 0x000000ff, 0x000200f8, 0x00000105, 0x0004003d,
0x0000000c, 0x00000106, 0x0000004b, 0x0003003e, 0x000000fd, 0x00000106, 0x000200f9, 0x000000ff,
0x000200f8, 0x000000ff, 0x0004003d, 0x0000000c, 0x00000107, 0x000000fd, 0x0004003d, 0x0000000c,
0x00000109, 0x0000004b, 0x0004007f, 0x0000000c, 0x0000010a, 0x00000109, 0x0006000c, 0x0000000c,
0x0000010b, 0x00000001, 0x0000001b, 0x0000010a, 0x00050083, 0x0000000c, 0x0000010c, 0x00000108,
0x0000010b, 0x0004003d, 0x00000006, 0x0000010d, 0x000000a6, 0x0004003d, 0x00000006, 0x0000010e,
0x000000a6, 0x00050085, 0x00000006, 0x0000010f, 0x0000010d, 0x0000010e, 0x00060050, 0x0000000c,
0x00000110, 0x0000010f, 0x0000010f, 0x0000010f, 0x0008000c, 0x0000000c, 0x00000111, 0x00000001,
0x0000002e, 0x00000107, 0x0000010c, 0x00000110, 0x00060050, 0x0000000c, 0x00000112, 0x00000080,
0x00000080, 0x00000080, 0x00060050, 0x0000000c, 0x00000113, 0x0000001f, 0x0000001f, 0x0000001f,
0x0008000c, 0x0000000c, 0x00000114, 0x00000001, 0x0000002b, 0x00000111, 0x00000112, 0x00000113,
0x0003003e, 0x000000fa, 0x00000114, 0x0004003d, 0x0000000c, 0x00000117, 0x000000fa, 0x0003003e,
0x00000116, 0x00000117, 0x00050039, 0x00000006, 0x00000118, 0x00000014, 0x00000116, 0x0003003e,
0x00000115, 0x00000118, 0x00050041, 0x00000007, 0x0000011a, 0x000000fa, 0x00000030, 0x0004003d,
0x00000006, 0x0000011b, 0x0000011a, 0x00050085, 0x00000006, 0x0000011c, 0x0000011b, 0x000000d8,
0x00050041, 0x00000007, 0x0000011d, 0x000000fa, 0x00000035, 0x0004003d, 0x00000006, 0x0000011e,
0x0000011d, 0x00050041, 0x00000007, 0x0000011f, 0x000000fa, 0x0000003a, 0x0004003d, 0x00000006,
0x00000120, 0x0000011f, 0x00050081, 0x00000006, 0x00000121, 0x0000011e, 0x00000120, 0x00050041,
0x00000007, 0x00000122, 0x00000064, 0x0000003a, 0x0004003d, 0x00000006, 0x00000123, 0x00000122,
0x00050085, 0x00000006, 0x00000124, 0x00000121, 0x00000123, 0x00050081, 0x00000006, 0x00000125,
0x0000011c, 0x00000124, 0x00050083, 0x00000006, 0x00000126, 0x00000125, 0x0000001f, 0x0003003e,
0x00000119, 0x00000126, 0x0004003d, 0x0000000c, 0x00000127, 0x000000fa, 0x0004003d, 0x00000006,
0x00000128, 0x00000115, 0x00060050, 0x0000000c, 0x00000129, 0x00000128, 0x00000128, 0x00000128,
0x00050083, 0x0000000c, 0x0000012a, 0x00000127, 0x00000129, 0x00050041, 0x00000007, 0x0000012b,
0x0000006f, 0x00000035, 0x0004003d, 0x00000006, 0x0000012c, 0x0000012b, 0x0004003d, 0x00000006,
0x0000012d, 0x00000119, 0x0004003d, 0x00000006, 0x0000012e, 0x00000119, 0x00050085, 0x00000006,
0x0000012f, 0x0000012d, 0x0000012e, 0x00050083, 0x00000006, 0x00000130, 0x0000001f, 0x0000012f,
0x00050085, 0x00000006, 0x00000131, 0x0000012c, 0x00000130, 0x00050041, 0x00000007, 0x00000132,
0x0000006f, 0x00000030, 0x0004003d, 0x00000006, 0x00000133, 0x00000132, 0x00050081, 0x00000006,
0x00000134, 0x00000131, 0x00000133, 0x0005008e, 0x0000000c, 0x00000135, 0x0000012a, 0x00000134,
0x0004003d, 0x00000006, 0x00000136, 0x00000115, 0x00060050, 0x0000000c, 0x00000137, 0x00000136,
0x00000136, 0x00000136, 0x00050081, 0x0000000c, 0x00000138, 0x00000135, 0x00000137, 0x0003003e,
0x000000fa, 0x00000138, 0x00060041, 0x0000007d, 0x0000013a, 0x0000007b, 0x0000007c, 0x0000003a,
0x0004003d, 0x00000006, 0x0000013b, 0x0000013a, 0x0008000c, 0x00000006, 0x0000013c, 0x00000001,
0x0000002b, 0x0000013b, 0x00000080, 0x0000001f, 0x0003003e, 0x00000139, 0x0000013c, 0x0004003d,
0x0000000c, 0x0000013e, 0x000000bb, 0x00050094, 0x00000006, 0x00000143, 0x0000013e, 0x00000142,
0x0003003e, 0x0000013d, 0x00000143, 0x0004003d, 0x0000000c, 0x00000145, 0x000000fa, 0x00050094,
0x00000006, 0x00000146, 0x00000145, 0x00000142, 0x0003003e, 0x00000144, 0x00000146, 0x0004003d,
0x00000006, 0x00000147, 0x00000139, 0x000500ba, 0x0000001a, 0x00000148, 0x00000147, 0x00000080,
0x0004003d, 0x00000006, 0x00000149, 0x00000144, 0x000500ba, 0x0000001a, 0x0000014b, 0x00000149,
0x0000014a, 0x000500a7, 0x0000001a, 0x0000014c, 0x00000148, 0x0000014b, 0x000300f7, 0x0000014e,
0x00000000, 0x000400fa, 0x0000014c, 0x0000014d, 0x0000014e, 0x000200f8, 0x0000014d, 0x0004003d,
0x0000000c, 0x0000014f, 0x000000bb, 0x0004003d, 0x0000000c, 0x00000150, 0x000000fa, 0x0007000c,
0x0000000c, 0x00000151, 0x00000001, 0x00000028, 0x00000150, 0x000000c5, 0x0004003d, 0x00000006,
0x00000152, 0x0000013d, 0x0004003d, 0x00000006, 0x00000153, 0x00000144, 0x00050088, 0x00000006,
0x00000154, 0x00000152, 0x00000153, 0x0005008e, 0x0000000c, 0x00000155, 0x00000151, 0x00000154,
0x0004003d, 0x00000006, 0x00000156, 0x00000139, 0x0004003d, 0x0000000c, 0x0000015a, 0x000000bb,
0x0003003e, 0x00000159, 0x0000015a, 0x00050039, 0x00000006, 0x0000015b, 0x00000014, 0x00000159,
0x0008000c, 0x00000006, 0x0000015c, 0x00000001, 0x00000031, 0x00000157, 0x00000158, 0x0000015b,
0x00050085, 0x00000006, 0x0000015d, 0x00000156, 0x0000015c, 0x00060050, 0x0000000c, 0x0000015e,
0x0000015d, 0x0000015d, 0x0000015d, 0x0008000c, 0x0000000c, 0x0000015f, 0x00000001, 0x0000002e,
0x0000014f, 0x00000155, 0x0000015e, 0x0003003e, 0x000000bb, 0x0000015f, 0x000200f9, 0x0000014e,
0x000200f8, 0x0000014e, 0x0004003d, 0x0000000c, 0x00000162, 0x000000bb, 0x0003003e, 0x00000161,
0x00000162, 0x00050039, 0x00000006, 0x00000163, 0x00000014, 0x00000161, 0x0003003e, 0x00000160,
0x00000163, 0x0004003d, 0x00000006, 0x00000165, 0x00000089, 0x00050085, 0x00000006, 0x00000166,
0x00000157, 0x00000165, 0x0003003e, 0x00000164, 0x00000166, 0x0004003d, 0x00000006, 0x00000167,
0x00000160, 0x0004003d, 0x00000006, 0x00000168, 0x00000164, 0x000500ba, 0x0000001a, 0x00000169,
0x00000167, 0x00000168, 0x000300f7, 0x0000016b, 0x00000000, 0x000400fa, 0x00000169, 0x0000016a,
0x0000016b, 0x000200f8, 0x0000016a, 0x0004003d, 0x00000006, 0x0000016d, 0x00000089, 0x0004003d,
0x00000006, 0x0000016e, 0x00000164, 0x00050083, 0x00000006, 0x0000016f, 0x0000016d, 0x0000016e,
0x0003003e, 0x0000016c, 0x0000016f, 0x0004003d, 0x00000006, 0x00000170, 0x00000164, 0x0004003d,
0x00000006, 0x00000171, 0x0000016c, 0x0004003d, 0x00000006, 0x00000172, 0x00000160, 0x0004003d,
0x00000006, 0x00000173, 0x00000164, 0x00050083, 0x00000006, 0x00000174, 0x00000172, 0x00000173,
0x0004007f, 0x00000006, 0x00000175, 0x00000174, 0x0004003d, 0x00000006, 0x00000176, 0x0000016c,
0x00050088, 0x00000006, 0x00000177, 0x00000175, 0x00000176, 0x0006000c, 0x00000006, 0x00000178,
0x00000001, 0x0000001b, 0x00000177, 0x00050083, 0x00000006, 0x00000179, 0x0000001f, 0x00000178,
0x00050085, 0x00000006, 0x0000017a, 0x00000171, 0x00000179, 0x00050081, 0x00000006, 0x0000017b,
0x00000170, 0x0000017a, 0x0004003d, 0x00000006, 0x0000017c, 0x00000160, 0x00050088, 0x00000006,
0x0000017d, 0x0000017b, 0x0000017c, 0x0004003d, 0x0000000c, 0x0000017e, 0x000000bb, 0x0005008e,
0x0000000c, 0x0000017f, 0x0000017e, 0x0000017d, 0x0003003e, 0x000000bb, 0x0000017f, 0x000200f9,
0x0000016b, 0x000200f8, 0x0000016b, 0x0004003d, 0x0000000c, 0x00000182, 0x000000bb, 0x0007000c,
0x0000000c, 0x00000183, 0x00000001, 0x00000028, 0x00000182, 0x000000c5, 0x0004003d, 0x00000006,
0x00000184, 0x00000076, 0x0005008e, 0x0000000c, 0x00000185, 0x00000183, 0x00000184, 0x00050051,
0x00000006, 0x00000186, 0x00000185, 0x00000000, 0x00050051, 0x00000006, 0x00000187, 0x00000185,
0x00000001, 0x00050051, 0x00000006, 0x00000188, 0x00000185, 0x00000002, 0x00070050, 0x00000051,
0x00000189, 0x00000186, 0x00000187, 0x00000188, 0x00000080, 0x0003003e, 0x00000181, 0x00000189,
0x000100fd, 0x00010038, 0x00050036, 0x00000006, 0x0000000a, 0x00000000, 0x00000008, 0x00030037,
0x00000007, 0x00000009, 0x000200f8, 0x0000000b, 0x0004003b, 0x00000007, 0x00000016, 0x00000007,
0x0004003b, 0x00000007, 0x0000001c, 0x00000007, 0x0003003e, 0x00000016, 0x00000017, 0x0004003d,
0x00000006, 0x00000018, 0x00000009, 0x000500bc, 0x0000001a, 0x0000001b, 0x00000018, 0x00000019,
0x000300f7, 0x0000001e, 0x00000000, 0x000400fa, 0x0000001b, 0x0000001d, 0x00000024, 0x000200f8,
0x0000001d, 0x0004003d, 0x00000006, 0x00000020, 0x00000009, 0x0004007f, 0x00000006, 0x00000021,
0x00000020, 0x0006000c, 0x00000006, 0x00000022, 0x00000001, 0x0000001b, 0x00000021, 0x00050083,
0x00000006, 0x00000023, 0x0000001f, 0x00000022, 0x0003003e, 0x0000001c, 0x00000023, 0x000200f9,
0x0000001e, 0x000200f8, 0x00000024, 0x0004003d, 0x00000006, 0x00000025, 0x00000016, 0x0004003d,
0x00000006, 0x00000027, 0x00000009, 0x00050083, 0x00000006, 0x00000028, 0x00000027, 0x00000019,
0x00050085, 0x00000006, 0x00000029, 0x00000026, 0x00000028, 0x00050081, 0x00000006, 0x0000002a,
0x00000025, 0x00000029, 0x0003003e, 0x0000001c, 0x0000002a, 0x000200f9, 0x0000001e, 0x000200f8,
0x0000001e, 0x0004003d, 0x00000006, 0x0000002b, 0x0000001c, 0x000200fe, 0x0000002b, 0x00010038,
0x00050036, 0x0000000c, 0x00000010, 0x00000000, 0x0000000e, 0x00030037, 0x0000000d, 0x0000000f,
0x000200f8, 0x00000011, 0x0004003b, 0x00000007, 0x0000002e, 0x00000007, 0x0004003b, 0x00000007,
0x00000034, 0x00000007, 0x0004003b, 0x00000007, 0x00000039, 0x00000007, 0x00050041, 0x00000007,
0x00000031, 0x0000000f, 0x00000030, 0x0004003d, 0x00000006, 0x00000032, 0x00000031, 0x0003003e,
0x0000002e, 0x00000032, 0x00050039, 0x00000006, 0x00000033, 0x0000000a, 0x0000002e, 0x00050041,
0x00000007, 0x00000036, 0x0000000f, 0x00000035, 0x0004003d, 0x00000006, 0x00000037, 0x00000036,
0x0003003e, 0x00000034, 0x00000037, 0x00050039, 0x00000006, 0x00000038, 0x0000000a, 0x00000034,
0x00050041, 0x00000007, 0x0000003b, 0x0000000f, 0x0000003a, 0x0004003d, 0x00000006, 0x0000003c,
0x0000003b, 0x0003003e, 0x00000039, 0x0000003c, 0x00050039, 0x00000006, 0x0000003d, 0x0000000a,
0x00000039, 0x00060050, 0x0000000c, 0x0000003e, 0x00000033, 0x00000038, 0x0000003d, 0x000200fe,
0x0000003e, 0x00010038, 0x00050036, 0x00000006, 0x00000014, 0x00000000, 0x00000012, 0x00030037,
0x0000000d, 0x00000013, 0x000200f8, 0x00000015, 0x00050041, 0x00000007, 0x00000041, 0x00000013,
0x00000030, 0x0004003d, 0x00000006, 0x00000042, 0x00000041, 0x00050041, 0x00000007, 0x00000043,
0x00000013, 0x00000035, 0x0004003d, 0x00000006, 0x00000044, 0x00000043, 0x00050041, 0x00000007,
0x00000045, 0x00000013, 0x0000003a, 0x0004003d, 0x00000006, 0x00000046, 0x00000045, 0x0007000c,
0x00000006, 0x00000047, 0x00000001, 0x00000028, 0x00000044, 0x00000046, 0x0007000c, 0x00000006,
0x00000048, 0x00000001, 0x00000028, 0x00000042, 0x00000047, 0x000200fe, 0x00000048, 0x00010038,
};
+73
View File
@@ -0,0 +1,73 @@
// Generated by tools/build_shaders.py from runtime/shaders/hdr_ui.frag; do not edit.
// source sha256 59cfbb1ab5393fdb743016cbf75a085ee8a05929c7ea67a69b4ab77f26a14303
#pragma once
#include <stdint.h>
static const uint32_t hdr_ui_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000000be, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x0000004f, 0x00000068, 0x00030010,
0x00000004, 0x00000007, 0x00040047, 0x0000004b, 0x00000021, 0x00000000, 0x00040047, 0x0000004b,
0x00000022, 0x00000000, 0x00040047, 0x0000004f, 0x0000001e, 0x00000000, 0x00040047, 0x00000068,
0x0000001e, 0x00000000, 0x00030047, 0x0000007b, 0x00000002, 0x00050048, 0x0000007b, 0x00000000,
0x00000023, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016,
0x00000006, 0x00000020, 0x00040017, 0x00000007, 0x00000006, 0x00000003, 0x0004002b, 0x00000006,
0x00000015, 0x3d6147ae, 0x0004002b, 0x00000006, 0x0000001b, 0x4019999a, 0x0006002c, 0x00000007,
0x0000001c, 0x0000001b, 0x0000001b, 0x0000001b, 0x0004002b, 0x00000006, 0x0000001f, 0x3d25aee6,
0x0006002c, 0x00000007, 0x00000020, 0x0000001f, 0x0000001f, 0x0000001f, 0x00020014, 0x00000021,
0x00040017, 0x00000022, 0x00000021, 0x00000003, 0x0004002b, 0x00000006, 0x0000002c, 0x00000000,
0x0004002b, 0x00000006, 0x0000002d, 0x3f800000, 0x0004002b, 0x00000006, 0x00000031, 0x3e232000,
0x0006002c, 0x00000007, 0x00000032, 0x00000031, 0x00000031, 0x00000031, 0x0004002b, 0x00000006,
0x00000034, 0x3f560000, 0x0004002b, 0x00000006, 0x00000035, 0x4196d000, 0x0004002b, 0x00000006,
0x0000003a, 0x41958000, 0x0004002b, 0x00000006, 0x00000040, 0x429db000, 0x0006002c, 0x00000007,
0x00000041, 0x00000040, 0x00000040, 0x00000040, 0x00040017, 0x00000045, 0x00000006, 0x00000004,
0x00090019, 0x00000048, 0x00000006, 0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x00000001,
0x00000000, 0x0003001b, 0x00000049, 0x00000048, 0x00040020, 0x0000004a, 0x00000000, 0x00000049,
0x0004003b, 0x0000004a, 0x0000004b, 0x00000000, 0x00040017, 0x0000004d, 0x00000006, 0x00000002,
0x00040020, 0x0000004e, 0x00000001, 0x0000004d, 0x0004003b, 0x0000004e, 0x0000004f, 0x00000001,
0x00040015, 0x00000052, 0x00000020, 0x00000000, 0x00040020, 0x00000067, 0x00000003, 0x00000045,
0x0004003b, 0x00000067, 0x00000068, 0x00000003, 0x00040018, 0x00000069, 0x00000007, 0x00000003,
0x0004002b, 0x00000006, 0x0000006a, 0x3f209d49, 0x0004002b, 0x00000006, 0x0000006b, 0x3d8d844d,
0x0004002b, 0x00000006, 0x0000006c, 0x3c86594b, 0x0006002c, 0x00000007, 0x0000006d, 0x0000006a,
0x0000006b, 0x0000006c, 0x0004002b, 0x00000006, 0x0000006e, 0x3ea89a02, 0x0004002b, 0x00000006,
0x0000006f, 0x3f6b645a, 0x0004002b, 0x00000006, 0x00000070, 0x3db43958, 0x0006002c, 0x00000007,
0x00000071, 0x0000006e, 0x0000006f, 0x00000070, 0x0004002b, 0x00000006, 0x00000072, 0x3d315b57,
0x0004002b, 0x00000006, 0x00000073, 0x3c3ac711, 0x0004002b, 0x00000006, 0x00000074, 0x3f65460b,
0x0006002c, 0x00000007, 0x00000075, 0x00000072, 0x00000073, 0x00000074, 0x0006002c, 0x00000069,
0x00000076, 0x0000006d, 0x00000071, 0x00000075, 0x0006002c, 0x00000007, 0x00000079, 0x0000002c,
0x0000002c, 0x0000002c, 0x0003001e, 0x0000007b, 0x00000045, 0x00040020, 0x0000007c, 0x00000009,
0x0000007b, 0x0004003b, 0x0000007c, 0x0000007d, 0x00000009, 0x00040015, 0x0000007e, 0x00000020,
0x00000001, 0x0004002b, 0x0000007e, 0x0000007f, 0x00000000, 0x0004002b, 0x00000052, 0x00000080,
0x00000000, 0x00040020, 0x00000081, 0x00000009, 0x00000006, 0x0006002c, 0x00000007, 0x000000b2,
0x0000002d, 0x0000002d, 0x0000002d, 0x0006002c, 0x00000007, 0x000000b4, 0x00000015, 0x00000015,
0x00000015, 0x0006002c, 0x00000007, 0x000000b7, 0x00000034, 0x00000034, 0x00000034, 0x0004002b,
0x00000006, 0x000000b8, 0x3d9e8391, 0x0006002c, 0x00000007, 0x000000b9, 0x000000b8, 0x000000b8,
0x000000b8, 0x0004002b, 0x00000006, 0x000000ba, 0x3f72a76f, 0x0006002c, 0x00000007, 0x000000bb,
0x000000ba, 0x000000ba, 0x000000ba, 0x0004002b, 0x00000006, 0x000000bc, 0x38d1b717, 0x0006002c,
0x00000007, 0x000000bd, 0x000000bc, 0x000000bc, 0x000000bc, 0x00050036, 0x00000002, 0x00000004,
0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x0004003d, 0x00000049, 0x0000004c, 0x0000004b,
0x0004003d, 0x0000004d, 0x00000050, 0x0000004f, 0x00050057, 0x00000045, 0x00000051, 0x0000004c,
0x00000050, 0x00050051, 0x00000006, 0x00000056, 0x00000051, 0x00000003, 0x000500bc, 0x00000021,
0x00000057, 0x00000056, 0x0000002c, 0x000300f7, 0x00000059, 0x00000000, 0x000400fa, 0x00000057,
0x00000058, 0x00000059, 0x000200f8, 0x00000058, 0x000100fc, 0x000200f8, 0x00000059, 0x0008004f,
0x00000007, 0x0000005d, 0x00000051, 0x00000051, 0x00000000, 0x00000001, 0x00000002, 0x00060050,
0x00000007, 0x00000060, 0x00000056, 0x00000056, 0x00000056, 0x00050088, 0x00000007, 0x00000061,
0x0000005d, 0x00000060, 0x0008000c, 0x00000007, 0x00000064, 0x00000001, 0x0000002b, 0x00000061,
0x00000079, 0x000000b2, 0x00050085, 0x00000007, 0x00000091, 0x00000064, 0x000000b9, 0x00050081,
0x00000007, 0x00000094, 0x00000064, 0x000000b4, 0x00050085, 0x00000007, 0x00000096, 0x00000094,
0x000000bb, 0x0007000c, 0x00000007, 0x00000097, 0x00000001, 0x0000001a, 0x00000096, 0x0000001c,
0x000500ba, 0x00000022, 0x00000099, 0x00000064, 0x00000020, 0x000600a9, 0x00000007, 0x0000009a,
0x00000099, 0x00000097, 0x00000091, 0x00050091, 0x00000007, 0x00000078, 0x00000076, 0x0000009a,
0x0007000c, 0x00000007, 0x0000007a, 0x00000001, 0x00000028, 0x00000078, 0x00000079, 0x00060041,
0x00000081, 0x00000082, 0x0000007d, 0x0000007f, 0x00000080, 0x0004003d, 0x00000006, 0x00000083,
0x00000082, 0x0005008e, 0x00000007, 0x00000084, 0x0000007a, 0x00000083, 0x00050085, 0x00000007,
0x000000a0, 0x00000084, 0x000000bd, 0x0008000c, 0x00000007, 0x000000a3, 0x00000001, 0x0000002b,
0x000000a0, 0x00000079, 0x000000b2, 0x0007000c, 0x00000007, 0x000000a4, 0x00000001, 0x0000001a,
0x000000a3, 0x00000032, 0x0005008e, 0x00000007, 0x000000a6, 0x000000a4, 0x00000035, 0x00050081,
0x00000007, 0x000000a8, 0x000000b7, 0x000000a6, 0x0005008e, 0x00000007, 0x000000aa, 0x000000a4,
0x0000003a, 0x00050081, 0x00000007, 0x000000ac, 0x000000b2, 0x000000aa, 0x00050088, 0x00000007,
0x000000ad, 0x000000a8, 0x000000ac, 0x0007000c, 0x00000007, 0x000000ae, 0x00000001, 0x0000001a,
0x000000ad, 0x00000041, 0x00050051, 0x00000006, 0x00000089, 0x000000ae, 0x00000000, 0x00050051,
0x00000006, 0x0000008a, 0x000000ae, 0x00000001, 0x00050051, 0x00000006, 0x0000008b, 0x000000ae,
0x00000002, 0x00070050, 0x00000045, 0x0000008c, 0x00000089, 0x0000008a, 0x0000008b, 0x00000056,
0x0003003e, 0x00000068, 0x0000008c, 0x000100fd, 0x00010038,
};
+22 -1
View File
@@ -57,6 +57,9 @@ typedef struct {
int have_gpu, have_vram, vram_exact;
uint64_t gpu_ns, gpu_parts, gpu_missed; // busy time, submission parts timed, parts left untimed
uint64_t vram_used, vram_budget;
int have_idle; // calibrated timestamps: the idle split below is filled
uint64_t idle_ns, starved_ns, queued_ns; // GPU idle between timed parts; of it, before the part was submitted; submit to start
uint64_t idle_gaps, max_gap_ns; // gaps over 0.5 ms; the longest
} PerfGpu;
extern void (*perf_gpu_stats)(PerfGpu *out);
// The performance overlay (perfmon.c): mode 0 off, 1 fps, 2 full. While it is on, perf_present hands every frame to
@@ -184,13 +187,16 @@ 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: 110 (1.1.0), 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_V110 (rt_game_version == 110) // 1.1.0: its own boot constants and data slots
#define RT_V143 (rt_game_version >= 143)
// ---- kernel (kernel.c)
typedef struct RtThread RtThread;
Cpu *kern_cpu(void); // current guest thread's Cpu, NULL on host threads
void kern_free_cores_note(void); // for crash reports: logs that free_cores = 1 lifted the
// virtual core gate, nothing when the gate is on
uint32_t kern_main_thread(Cpu **cpu); // main thread object and Cpu for the calling host thread
// (main.npdm: 1 MiB stack at the top of the stack region,
// priority 44, core 0); on the main thread: its handle
@@ -241,6 +247,13 @@ void account_uid(uint64_t out[2]); // the local user's n
// ---- movies (ov_movie.c)
void movie_tick(Cpu *c); // at every present, on the presenting guest thread: hands
// decoded movie frames over when due, signals their events
// ---- camera speed (camera_speed.c): TOTK_STICK_SENS
float cam_speed_wanted(void); // the factor TOTK_STICK_SENS asks for, 0.1 to 3
float cam_speed_applied(void); // the factor in effect: 1 until romfs.c serves the pack
int cam_pack_build(const void *in, size_t n, const void *zsdic, size_t zsdic_n, void **out, size_t *out_n);
// Pack/Actor/PlayerCamera.pack.zs with the camera speeds
// multiplied, malloc'd; 0 = factor 1 or cannot (logged)
void cam_pack_served(void); // romfs.c serves the multiplied pack
// ---- game resolution (resolution.c): TOTK_OUTPUT_RES, TOTK_RENDER_SCALE
uint64_t res_heap_extra_bytes(void); // added to the application pool (mem_init), 2 MiB steps
uint64_t res_heap_extra_kib(void); // added to the HeapDef texture heap and its parent; 0 = none
@@ -248,11 +261,16 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); //
// the grown heaps, malloc'd; 0 = cannot (logged)
void res_heapdef_served(void); // romfs.c serves the grown table
void res_init(void); // after rt_patches_init: the patch page and the sets
float res_dlss_lod_bias(void); // TOTK_DLSS upscaling: texture LOD bias (<= 0)
int res_dlss_mode(const char *mode); // TOTK_DLSS changed while running: new scene size
void res_fn_hit(uint64_t addr, uint64_t x0); // main.c fnprof_hit: catches the renderer at start
void res_present(Cpu *c); // nvnQueuePresentTexture: applies a pending scene size
void res_sizes(unsigned out[2], unsigned scene[2]); // the output and scene sizes in effect (the game's own
// until res_init switched the sets on)
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
@@ -293,6 +311,9 @@ void rt_boot(void); // boot.c
// process exit code.
int probe_cfg(void); // cfg_probe.c
int probe_dpi(void); // dpi_probe_win.c (Windows only)
int probe_mods(void); // mods_probe.c
void discord_init(void); // discord_rpc.c: the Rich Presence thread, game runs only
int probe_discord(void); // discord_rpc.c
int probe_loader(void), probe_kernel(void), probe_ipc(void), probe_services(void), probe_fs(void);
int probe_gfx_abi(void); // gfx_abi_probe.c
int probe_vkdevice(void); // vk_device.c
+1 -1
View File
@@ -121,7 +121,7 @@ __attribute__((noinline, noreturn, cold)) static void bad_entry(Cpu *c) {
}
// kern_host_track: the call counted in the thread's host depth. Not a tail call, so the host stack holds this frame
// while fn runs; longjmp out of fn (ExitThread) leaves the depth raised on a thread that ends.
// while fn runs; the jump out of fn (ExitThread) leaves the depth raised on a thread that ends.
__attribute__((noinline)) static void hostfn_tracked(Cpu *c, GuestFn fn) {
void *t = c->thread;
int *d = kern_host_depth(t);
+109 -17
View File
@@ -236,7 +236,56 @@ static float mouse_sens_get(void) {
}
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`.
// ---- walk key: the keyboard's stick samples scaled down while a key is held (TOTK_WALK_KEY, TOTK_WALK_SCALE)
// A keyboard key is all or nothing, so every keyboard-driven stick sample is +/-32767 and the game only ever sees a
// full tilt. Holding this key scales those two samples, which gives the half tilt a gamepad gets from its analog range,
// so the keyboard can walk as well as run. yuzu and eden do the same with their modifier and modifier_scale settings.
// Only the keyboard is scaled: the gamepad's axes and the mouse camera sample their own values.
static int walk_key = KEY_NONE; // TOTK_WALK_KEY as a KEY_*, KEY_NONE = off; live (cfg_version)
static float walk_scale = 0.51f; // TOTK_WALK_SCALE, 0.2 to 0.9; live (cfg_version)
static unsigned walk_seen = ~0u;
// The ini takes the key names the Controls tab writes; a hand-written line may spell them in any case.
static int name_matches(const char *name, const char *key) {
for (; *name && *key; name++, key++) {
unsigned char a = (unsigned char)*name, b = (unsigned char)*key;
if (a >= 'A' && a <= 'Z') a = (unsigned char)(a - 'A' + 'a');
if (b >= 'A' && b <= 'Z') b = (unsigned char)(b - 'A' + 'a');
if (a != b) return 0;
}
return *name == *key;
}
static void walk_get(void) {
unsigned ver = cfg_version();
if (ver == walk_seen) return;
walk_seen = ver;
const char *name = cfg_value("TOTK_WALK_KEY"); // "none" (the default) turns the key off
int key = KEY_NONE;
if (*name && !name_matches(name, "none")) {
for (int k = KEY_A; k < KEY_COUNT && key == KEY_NONE; k++)
if (input_key_name(k) && name_matches(name, input_key_name(k))) key = k;
if (key == KEY_NONE) rt_log("input: walk_key: unknown key '%s', the keyboard sticks keep their full tilt", name);
}
// 0.51 rather than 0.5: a keyboard sample is 32767, and 32767 * 0.5 = 16383 falls one short of stick_bits'
// STICK_DIR (16384), which would leave the game's stick-direction bits clear while the walk key is held. The value
// in the settings table carries that default; this code only rejects what the table's range does not allow.
const char *sv = cfg_value("TOTK_WALK_SCALE");
char *end;
double d = strtod(sv, &end);
double s = end == sv || *end || !(d >= 0.2 && d <= 0.9) ? strtod(cfg_meta("TOTK_WALK_SCALE")->def, NULL) : d;
if (key != walk_key || (float)s != walk_scale) {
walk_key = key;
walk_scale = (float)s;
rt_log("input: walk key %s, keyboard sticks at %.0f%% of their tilt (mouse and gamepad unchanged)",
key != KEY_NONE ? input_key_name(key) : "off", s * 100);
}
}
// The mouse camera as a right-stick sample, held for the whole presented frame. Under `lock`. The sample is divided by
// the camera speed factor the game runs with (camera_speed.c, cam_speed_applied), which multiplies every right-stick
// turn rate, so TOTK_MOUSE_SENS alone sets the mouse's speed; below 1 the sample stays within the stick's range, so the
// mouse's top speed drops with the factor.
static void mouse_camera(int32_t out[2]) {
static uint64_t frame = ~0ull, t0;
static int32_t held[2];
@@ -259,20 +308,23 @@ static void mouse_camera(int32_t out[2]) {
held[0] = held[1] = 0; // frames stopped: do not hold a deflection
}
}
out[0] = held[0];
out[1] = held[1];
double k = cam_speed_applied(), x = held[0] / k, y = held[1] / k, len = sqrt(x * x + y * y);
if (len > STICK_MAX) x *= STICK_MAX / len, y *= STICK_MAX / len;
out[0] = (int32_t)lround(x);
out[1] = (int32_t)lround(y);
}
static float stick_dead = -1; // TOTK_STICK_DEADZONE as a fraction of full deflection; live (cfg_version)
static float stick_dead[2] = {-1, -1}; // input_pad_deadzone of the left and right stick; live (cfg_version)
static unsigned stick_dead_seen;
static float stick_dead_get(void) {
static float stick_dead_get(int s) {
unsigned ver = cfg_version();
if (stick_dead >= 0 && ver == stick_dead_seen) return stick_dead;
stick_dead_seen = ver;
double v = cfg_double("TOTK_STICK_DEADZONE");
stick_dead = (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
return stick_dead;
if (stick_dead[0] < 0 || ver != stick_dead_seen) {
stick_dead_seen = ver;
stick_dead[0] = input_pad_deadzone(0);
stick_dead[1] = input_pad_deadzone(1);
}
return stick_dead[s];
}
// One gamepad stick through the dead zone; SDL's Y grows downwards, the Switch's upwards.
@@ -425,12 +477,26 @@ static int pad_read(uint64_t *b, int32_t sl[2], int32_t sr[2]) {
uint8_t pd[PAD_COUNT] = {0};
if (!pad_physical(pd)) return 0;
*b |= input_map_pad_buttons(pd, pad_layout_get());
float dead = stick_dead_get();
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, dead, sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, dead, sr);
stick(SDL_GAMEPAD_AXIS_LEFTX, SDL_GAMEPAD_AXIS_LEFTY, stick_dead_get(0), sl);
stick(SDL_GAMEPAD_AXIS_RIGHTX, SDL_GAMEPAD_AXIS_RIGHTY, stick_dead_get(1), sr);
return 1;
}
int input_pad_stick_percent(double pct[2]) {
int ok = 0;
pct[0] = pct[1] = 0;
plat_mutex_lock(&lock);
if (state == 1 && pad && SDL_GamepadConnected(pad)) { // the game's poll keeps the state fresh (SDL_UpdateGamepads)
pct[0] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_LEFTY));
pct[1] = input_stick_percent(SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTX),
SDL_GetGamepadAxis(pad, SDL_GAMEPAD_AXIS_RIGHTY));
ok = 1;
}
plat_mutex_unlock(&lock);
return ok;
}
static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
uint64_t b = 0;
int32_t sl[2] = {0, 0}, sr[2] = {0, 0};
@@ -456,9 +522,13 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
was_captured = cap;
b |= input_map_buttons(down);
int32_t k[2];
walk_get();
int walk = walk_key != KEY_NONE && down[walk_key];
input_map_stick(down, 0, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sl[0], &sl[1], k[0], k[1]);
input_map_stick(down, 1, k);
if (walk) k[0] = (int32_t)(k[0] * walk_scale), k[1] = (int32_t)(k[1] * walk_scale);
pick(&sr[0], &sr[1], k[0], k[1]);
mouse_camera(k);
pick(&sr[0], &sr[1], k[0], k[1]);
@@ -486,6 +556,12 @@ static void poll_merged(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx
// g_block: 0 off, 1 text entry open, 2 closed, waiting for the release. The cursor is released while it is open.
static volatile int32_t g_block;
static volatile int32_t g_menu; // the in-game menu, the same way (input_menu)
// When g_block and g_menu last went to 2 (plat_now_ns). The wait for the release ends after RELEASE_WAIT_NS even with
// something held: an input that never reads as released (a button another program holds down, a key whose release
// went to another window, a stick that rests outside its dead zone) would otherwise keep the game deaf until restart
// (#462, #494). A press that closed the menu is let go well within the wait; one still held after it is meant.
static volatile uint64_t g_block_closed, g_menu_closed;
#define RELEASE_WAIT_NS 1000000000ull
int input_cursor_allowed(void) {
const char *in = cfg_get("TOTK_INPUT");
@@ -493,6 +569,7 @@ int input_cursor_allowed(void) {
}
void input_block(int on) {
if (!on) __atomic_store_n(&g_block_closed, plat_now_ns(), __ATOMIC_RELEASE);
plat_xchg32(&g_block, on ? 1 : 2);
ikb_cursor_update();
}
@@ -500,20 +577,35 @@ void input_block(int on) {
// The in-game menu (ui/menu.cpp) blocks the game's input the same way: g_menu 0 closed, 1 open, 2 closed, waiting for
// the release. The cursor is released while it is open, for the menu's mouse.
void input_menu(int open) {
if (!open) __atomic_store_n(&g_menu_closed, plat_now_ns(), __ATOMIC_RELEASE);
plat_xchg32(&g_menu, open ? 1 : 2);
if (!open) ikb_menu_closed(); // the capture before Esc comes back
ikb_cursor_update();
}
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held.
static int blocked(volatile int32_t *flag, uint64_t buttons) {
// 1 while flag (g_block, g_menu) blocks the game's input; state 2 ends at the first poll with nothing held, or with
// something still held RELEASE_WAIT_NS after closed. A stick axis counts as held as well: with the walk key the
// keyboard's sticks move part way without setting a direction bit, so a gate that watched the buttons alone would let
// that movement through the moment the menu or the text entry closes.
static int blocked(volatile int32_t *flag, volatile uint64_t *closed, const char *what, uint64_t buttons,
const int32_t *axes) {
int32_t k = *flag;
return k && !(k == 2 && !buttons && plat_cas32(flag, 0, 2) == 2);
if (k != 2) return k != 0;
int held = buttons != 0;
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
if (held && plat_now_ns() - __atomic_load_n(closed, __ATOMIC_ACQUIRE) < RELEASE_WAIT_NS) return 1;
if (plat_cas32(flag, 0, 2) != 2) return *flag != 0; // reopened meanwhile
if (held)
rt_log("input: the %s closed 1 s ago and input is still held (buttons %#llx, sticks %d %d %d %d); the game "
"gets it from now on", what, (unsigned long long)buttons, axes[0], axes[1], axes[2], axes[3]);
return 0;
}
void input_poll(uint64_t *buttons, int32_t *lx, int32_t *ly, int32_t *rx, int32_t *ry) {
poll_merged(buttons, lx, ly, rx, ry);
int text = blocked(&g_block, *buttons), menu = blocked(&g_menu, *buttons);
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
int text = blocked(&g_block, &g_block_closed, "text entry", *buttons, axes),
menu = blocked(&g_menu, &g_menu_closed, "menu", *buttons, axes);
if (!text && !menu) return;
*buttons = 0; *lx = *ly = *rx = *ry = 0;
}
+4
View File
@@ -21,6 +21,10 @@ int input_overlay_gate(int *held, uint64_t *buttons); // L3+R3: 1 in the poll
int input_pad_read(uint64_t *buttons, int32_t *lx, int32_t *ly);
// The gamepad layout in effect (PAD_LAYOUT_*, input_map.h; TOTK_PAD_LAYOUT, live); -1 without a gamepad. Any thread.
int input_pad_layout(void);
// How far the open gamepad's left (pct[0]) and right (pct[1]) sticks are from the centre now, in percent of full
// deflection, before the dead zone: the menu shows it on the dead zone rows, so a drifting stick's resting value can be
// read off. 0 (and both 0) without a connected gamepad. Any thread.
int input_pad_stick_percent(double pct[2]);
// The label on a face button of the open gamepad (input_map.h PAD_SOUTH..PAD_NORTH, as SDL reports it); PAD_LABEL_NONE for another
// button, an unknown label or no gamepad. Any thread.
enum { PAD_LABEL_NONE, PAD_LABEL_A, PAD_LABEL_B, PAD_LABEL_X, PAD_LABEL_Y, PAD_LABEL_CROSS, PAD_LABEL_CIRCLE,
+22
View File
@@ -288,6 +288,18 @@ int input_pad_layout_pick(const char *setting, int east_labelled_a) {
return east_labelled_a ? PAD_LAYOUT_NINTENDO : PAD_LAYOUT_XBOX;
}
int input_prompts_pick(const char *prompts, const char *pad_layout) {
if (prompts && *prompts) return !strcasecmp(prompts, "xbox") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
return pad_layout && !strcasecmp(pad_layout, "xbox_prompts") ? PROMPTS_XBOX : PROMPTS_NINTENDO;
}
const char *input_prompts_folder(int prompts, const char *pad_layout) {
if (prompts != PROMPTS_XBOX) return NULL;
// The folders are read at start, before a pad is open: auto counts as Xbox, as for a pad without Nintendo labels.
// A Nintendo-labelled pad under auto plays by label too (its A is on the button labelled A), so it matches as well.
return input_pad_layout_pick(pad_layout, 0) == PAD_LAYOUT_NINTENDO ? "romfs-patches-xbox" : "romfs-patches-xbox-label";
}
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout) {
const PadBinding *t = input_map.pads[layout];
uint64_t b = 0;
@@ -334,6 +346,16 @@ void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]) {
out[1] = (int32_t)lrint(fmax(-1, fmin(1, fy * k)) * STICK_MAX);
}
float input_pad_deadzone(int stick) {
const char *own = stick ? "TOTK_RSTICK_DEADZONE" : "TOTK_LSTICK_DEADZONE", *set = cfg_get(own);
double v = cfg_double(set && *set ? own : "TOTK_STICK_DEADZONE");
return (float)(v < 0 ? 0 : v > 50 ? 50 : v) / 100.f;
}
double input_stick_percent(int32_t x, int32_t y) {
return sqrt((double)x * x + (double)y * y) * 100.0 / STICK_MAX;
}
// Minus and Plus held together open or close the menu. *held: latched from the poll where both go down until neither is
// down (the caller keeps it). Every poll of the latch reports neither to the game, so releasing one button first does not
// leak the other; Minus alone before Plus reaches the game unchanged. With the menu open the game sees a neutral pad.
+22 -4
View File
@@ -80,11 +80,23 @@ int input_map_bind(const char *button, const char *list);
// Applies the TOTK_BIND_<action> settings (cfg_get) over the defaults: at start, and again when one of them changes.
void input_map_configure(void);
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts" is the Nintendo layout with
// the A/B and X/Y prompts swapped (runtime/romfs.c overlay_init), so the buttons map as "nintendo". Anything else
// ("auto", NULL) picks Nintendo when the pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox
// otherwise.
// The layout for a TOTK_PAD_LAYOUT value: "nintendo" or "xbox" (any case); "xbox_prompts", 0.1.9's name for the Nintendo
// layout with the Xbox prompts, maps the buttons as "nintendo". Anything else ("auto", NULL) picks Nintendo when the
// pad's east button is labelled A (a Switch Pro Controller, a GuliKit) and Xbox otherwise.
int input_pad_layout_pick(const char *setting, int east_labelled_a);
// Button prompts (TOTK_PROMPTS): the controller whose buttons the game's prompts show. Nintendo: the game's own. Xbox:
// LB RB LT RT, Menu, View and a d-pad cross for the other buttons, and the face buttons named by the label of the
// button the pad layout presses (runtime/romfs.c overlay_init, tools/make_romfs_patches.py --prompts).
enum { PROMPTS_NINTENDO, PROMPTS_XBOX };
// The prompts for a TOTK_PROMPTS value ("nintendo" or "xbox", any case). Unset or empty: Xbox while the pad layout
// setting is "xbox_prompts", else Nintendo.
int input_prompts_pick(const char *prompts, const char *pad_layout);
// The folder next to romfs-index.bin that holds those prompts for a TOTK_PAD_LAYOUT value, NULL for the game's own. The
// Nintendo layout plays an Xbox pad by position, so its folder names the face buttons the other way round
// (romfs-patches-xbox: the right button does A and is labelled B); xbox and auto put every Switch button on the button
// labelled with its letter (auto picks the layout per pad by its labels), so they share romfs-patches-xbox-label.
const char *input_prompts_folder(int prompts, const char *pad_layout);
// down[pad] != 0 for the gamepad inputs held. Returns the Switch button bits (0..15) the layout's pad table presses.
uint64_t input_map_pad_buttons(const uint8_t down[PAD_COUNT], int layout);
@@ -100,6 +112,12 @@ void input_mouse_stick(int32_t dx, int32_t dy, double dt_s, float sens, int32_t
// 0..0.5): inside the circle the stick reads 0, and the rest of the travel is scaled to start at 0 at the edge, so a
// drifting stick rests at 0 and full deflection still reaches full. The result stays inside the unit square.
void input_pad_stick(int32_t x, int32_t y, float dead, int32_t out[2]);
// The dead zone of gamepad stick 0 (left) or 1 (right) for input_pad_stick, as a fraction of full deflection:
// TOTK_LSTICK_DEADZONE or TOTK_RSTICK_DEADZONE when set, else TOTK_STICK_DEADZONE (both sticks), percent clamped to 0..50.
float input_pad_deadzone(int stick);
// How far a stick (x, y in -32768..32767) is from the centre, in percent of full deflection (100 at an axis's end; a
// square gate's corner reads more).
double input_stick_percent(int32_t x, int32_t y);
// ---- the remapping rows' edits, on a copy of input_map. Pure.
// action's keys (table order) / its gamepad buttons in layout, at most max; returns how many.
+1
View File
@@ -11,6 +11,7 @@ extern "C" {
// gfx_window_sdl.c
SDL_Window *gfx_window_sdl(void); // the game window, NULL before gfx_window_start; any thread
double gfx_window_sdl_density(void); // pixels per window unit (mouse positions are in units); any thread
void gfx_window_sdl_run(void (*fn)(void *arg), void *arg); // runs fn on the window thread and waits; any thread
// input_sdl.c
void input_sdl_window(SDL_Window *w); // the game window was created
+2
View File
@@ -11,6 +11,7 @@
#include "audren.h"
#include "host.h"
#include "ipc.h"
#include "nfp.h"
#include "platform/platform.h"
#define CMIF_IN_MAGIC 0x49434653u // "SFCI"
@@ -25,6 +26,7 @@ enum { T_CLOSE = 2, T_REQUEST = 4, T_CONTROL = 5, T_REQUEST_CTX = 6, T_CONTROL_C
// Services sm: hands out. Anything else is a named fatal.
static const struct { const char *name; IpcHandler handler; } services[] = {
{"audren:u", audren_manager_handler},
{"nfp:user", nfp_user_manager_handler},
};
static volatile int32_t g_live;
+146 -89
View File
@@ -8,10 +8,6 @@
// Cpu. The SDK's nn::os runs as lifted code; only the kernel under it is the host. Every wait hangs on one global lock
// G with one condition variable per thread; wake order is the kernel's: lower priority value first, then first come.
// Semantics follow the public Horizon kernel (KConditionVariable, KAddressArbiter, KSynchronization).
#ifdef _WIN32
#define __USE_MINGW_SETJMP_NON_SEH // setjmp(b) = _setjmp(b, NULL): longjmp restores registers, no SEH unwinding
#endif
#include <setjmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -77,7 +73,7 @@ struct RtThread {
uint64_t cseq;
RtThread *cnext;
uint8_t *tlr, *tls;
jmp_buf exit_jmp; // the thread start; ExitThread longjmps to it
PlatJmp exit_jmp; // the thread start; ExitThread jumps back to it
// fibers
PlatFiber *base_fiber; // the host thread as a host fiber, after the first switch
Fiber *fiber; // current guest fiber, NULL on the thread's own context
@@ -296,7 +292,13 @@ static uint32_t block_inner(RtThread *t, int kind, int64_t ns) {
// and gives it up only where Horizon would switch threads (waits, sleep, yield, exit, blocking host calls); a free
// core goes to the best waiting thread. A waiter better than a holder asks one holder to give its core up (core_kick),
// which it does at its next SVC or indirect call (kern_checkpoint, rt_dispatch), as Horizon runs the better thread at
// once; guest code without either keeps the core until it reaches one. TOTK_FREE_CORES=1 turns the gate off.
// once; guest code without either keeps the core until it reaches one.
//
// free_cores = 1 (TOTK_FREE_CORES) turns the gate off, for experiments only: the game does not run that way. A thread
// that holds no core reports its ideal core to GetCurrentProcessorNumber, so the game's job workers all report core 0
// and run its per-core code at once: on 1.2.1 two of them add the same per-core NVN control memory, which retires the
// command handle the other just recorded, and CallCommands finds no recording at start (#212); 1.4.3 corrupts a heap
// in main+0x82e00 or hangs at the title menu. The start log and every crash report say so (kern_free_cores_note).
static RtThread *g_core_owner[NCORE], *g_core_wait;
static int g_cores_on = -1, g_held_max;
@@ -306,12 +308,22 @@ 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: WARNING: free_cores = 1 (TOTK_FREE_CORES) turns the virtual core gate off. This is an "
"experimental mode and the game crashes or hangs in it, usually at start. Set free_cores = 0 in "
"totk.ini to play");
diag_init();
ap_init();
}
return g_cores_on;
}
void kern_free_cores_note(void) {
if (!g_cores_on)
rt_log("fatal: free_cores = 1 was on: the game crashes in this experimental mode; set free_cores = 0 in "
"totk.ini before reporting this crash");
}
static int core_allowed(const RtThread *t, int c) { return (int)((t->mask >> c) & 1); }
// ---- asynchronous preemption (TOTK_ASYNC_PREEMPT, on unless 0)
@@ -403,6 +415,18 @@ static int ap_disabled(const RtThread *t) { return g_ap > 0 && ap_tlr(t)[0]; }
// in kern_ap_yield and jumps back to kern_ap_rip(), whose value it first stores in the return slot it reserved at
// entry, so a stack walk sees the interrupted function as its caller. No ret: the jump keeps call/return pairs
// balanced. The waiter never writes the owner's stack.
// An x86-64-v2 build (no __AVX__) saves XMM0-15 with SSE moves into the same 32-byte slots instead: its game code
// keeps nothing in the upper YMM halves, and the CPUs it is for have no VEX encoding, so a vmovdqa there faults with
// an illegal instruction the first time a core is interrupted.
#ifdef __AVX__
#define AP_VMOV "vmovdqa"
#define AP_V "%ymm"
#define AP_VZEROUPPER " vzeroupper\n"
#else
#define AP_VMOV "movdqa"
#define AP_V "%xmm"
#define AP_VZEROUPPER ""
#endif
#ifdef _WIN32
__asm__(".text\n"
".globl kern_ap_stub\n"
@@ -434,48 +458,48 @@ __asm__(".text\n"
".seh_setframe %rbp, 0\n"
".seh_endprologue\n"
" andq $-32, %rsp\n"
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
" vmovdqa %ymm0, 64(%rsp)\n"
" vmovdqa %ymm1, 96(%rsp)\n"
" vmovdqa %ymm2, 128(%rsp)\n"
" vmovdqa %ymm3, 160(%rsp)\n"
" vmovdqa %ymm4, 192(%rsp)\n"
" vmovdqa %ymm5, 224(%rsp)\n"
" vmovdqa %ymm6, 256(%rsp)\n"
" vmovdqa %ymm7, 288(%rsp)\n"
" vmovdqa %ymm8, 320(%rsp)\n"
" vmovdqa %ymm9, 352(%rsp)\n"
" vmovdqa %ymm10, 384(%rsp)\n"
" vmovdqa %ymm11, 416(%rsp)\n"
" vmovdqa %ymm12, 448(%rsp)\n"
" vmovdqa %ymm13, 480(%rsp)\n"
" vmovdqa %ymm14, 512(%rsp)\n"
" vmovdqa %ymm15, 544(%rsp)\n"
" subq $576, %rsp\n" // 0: shadow space, 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
" stmxcsr 32(%rsp)\n"
" movl $0x1f80, 36(%rsp)\n"
" ldmxcsr 36(%rsp)\n"
" cld\n"
" vzeroupper\n"
AP_VZEROUPPER
" call kern_ap_rip\n"
" movq %rax, 72(%rbp)\n" // the return slot: rbp + 8 pushes + pushfq
" call kern_ap_yield\n"
" ldmxcsr 32(%rsp)\n"
" vmovdqa 64(%rsp), %ymm0\n"
" vmovdqa 96(%rsp), %ymm1\n"
" vmovdqa 128(%rsp), %ymm2\n"
" vmovdqa 160(%rsp), %ymm3\n"
" vmovdqa 192(%rsp), %ymm4\n"
" vmovdqa 224(%rsp), %ymm5\n"
" vmovdqa 256(%rsp), %ymm6\n"
" vmovdqa 288(%rsp), %ymm7\n"
" vmovdqa 320(%rsp), %ymm8\n"
" vmovdqa 352(%rsp), %ymm9\n"
" vmovdqa 384(%rsp), %ymm10\n"
" vmovdqa 416(%rsp), %ymm11\n"
" vmovdqa 448(%rsp), %ymm12\n"
" vmovdqa 480(%rsp), %ymm13\n"
" vmovdqa 512(%rsp), %ymm14\n"
" vmovdqa 544(%rsp), %ymm15\n"
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" popq %r11\n"
@@ -513,48 +537,48 @@ __asm__(".text\n"
" pushq %rbp\n"
" movq %rsp, %rbp\n"
" andq $-32, %rsp\n"
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15
" vmovdqa %ymm0, 64(%rsp)\n"
" vmovdqa %ymm1, 96(%rsp)\n"
" vmovdqa %ymm2, 128(%rsp)\n"
" vmovdqa %ymm3, 160(%rsp)\n"
" vmovdqa %ymm4, 192(%rsp)\n"
" vmovdqa %ymm5, 224(%rsp)\n"
" vmovdqa %ymm6, 256(%rsp)\n"
" vmovdqa %ymm7, 288(%rsp)\n"
" vmovdqa %ymm8, 320(%rsp)\n"
" vmovdqa %ymm9, 352(%rsp)\n"
" vmovdqa %ymm10, 384(%rsp)\n"
" vmovdqa %ymm11, 416(%rsp)\n"
" vmovdqa %ymm12, 448(%rsp)\n"
" vmovdqa %ymm13, 480(%rsp)\n"
" vmovdqa %ymm14, 512(%rsp)\n"
" vmovdqa %ymm15, 544(%rsp)\n"
" subq $576, %rsp\n" // 32: MXCSR saved, 36: MXCSR default, 64: YMM0-15 (XMM0-15 without AVX)
" " AP_VMOV " " AP_V "0, 64(%rsp)\n"
" " AP_VMOV " " AP_V "1, 96(%rsp)\n"
" " AP_VMOV " " AP_V "2, 128(%rsp)\n"
" " AP_VMOV " " AP_V "3, 160(%rsp)\n"
" " AP_VMOV " " AP_V "4, 192(%rsp)\n"
" " AP_VMOV " " AP_V "5, 224(%rsp)\n"
" " AP_VMOV " " AP_V "6, 256(%rsp)\n"
" " AP_VMOV " " AP_V "7, 288(%rsp)\n"
" " AP_VMOV " " AP_V "8, 320(%rsp)\n"
" " AP_VMOV " " AP_V "9, 352(%rsp)\n"
" " AP_VMOV " " AP_V "10, 384(%rsp)\n"
" " AP_VMOV " " AP_V "11, 416(%rsp)\n"
" " AP_VMOV " " AP_V "12, 448(%rsp)\n"
" " AP_VMOV " " AP_V "13, 480(%rsp)\n"
" " AP_VMOV " " AP_V "14, 512(%rsp)\n"
" " AP_VMOV " " AP_V "15, 544(%rsp)\n"
" stmxcsr 32(%rsp)\n"
" movl $0x1f80, 36(%rsp)\n"
" ldmxcsr 36(%rsp)\n"
" cld\n"
" vzeroupper\n"
AP_VZEROUPPER
" call kern_ap_rip\n"
" movq %rax, 88(%rbp)\n" // the return slot: rbp + 10 pushes + pushfq
" call kern_ap_yield\n"
" ldmxcsr 32(%rsp)\n"
" vmovdqa 64(%rsp), %ymm0\n"
" vmovdqa 96(%rsp), %ymm1\n"
" vmovdqa 128(%rsp), %ymm2\n"
" vmovdqa 160(%rsp), %ymm3\n"
" vmovdqa 192(%rsp), %ymm4\n"
" vmovdqa 224(%rsp), %ymm5\n"
" vmovdqa 256(%rsp), %ymm6\n"
" vmovdqa 288(%rsp), %ymm7\n"
" vmovdqa 320(%rsp), %ymm8\n"
" vmovdqa 352(%rsp), %ymm9\n"
" vmovdqa 384(%rsp), %ymm10\n"
" vmovdqa 416(%rsp), %ymm11\n"
" vmovdqa 448(%rsp), %ymm12\n"
" vmovdqa 480(%rsp), %ymm13\n"
" vmovdqa 512(%rsp), %ymm14\n"
" vmovdqa 544(%rsp), %ymm15\n"
" " AP_VMOV " 64(%rsp), " AP_V "0\n"
" " AP_VMOV " 96(%rsp), " AP_V "1\n"
" " AP_VMOV " 128(%rsp), " AP_V "2\n"
" " AP_VMOV " 160(%rsp), " AP_V "3\n"
" " AP_VMOV " 192(%rsp), " AP_V "4\n"
" " AP_VMOV " 224(%rsp), " AP_V "5\n"
" " AP_VMOV " 256(%rsp), " AP_V "6\n"
" " AP_VMOV " 288(%rsp), " AP_V "7\n"
" " AP_VMOV " 320(%rsp), " AP_V "8\n"
" " AP_VMOV " 352(%rsp), " AP_V "9\n"
" " AP_VMOV " 384(%rsp), " AP_V "10\n"
" " AP_VMOV " 416(%rsp), " AP_V "11\n"
" " AP_VMOV " 448(%rsp), " AP_V "12\n"
" " AP_VMOV " 480(%rsp), " AP_V "13\n"
" " AP_VMOV " 512(%rsp), " AP_V "14\n"
" " AP_VMOV " 544(%rsp), " AP_V "15\n"
" movq %rbp, %rsp\n"
" popq %rbp\n"
" popq %r11\n"
@@ -1432,10 +1456,10 @@ static void thread_finish(RtThread *t) {
unlock();
}
// ExitThread: back to the thread start with a non-unwinding longjmp; no SEH unwinding through lifted frames.
// (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame register
// is offset, and the landing code then reads its locals through the wrong base.)
static __attribute__((noreturn)) void exit_now(RtThread *t) { longjmp(t->exit_jmp, 1); }
// ExitThread: back to the thread start with plat_jmp_go, which unwinds nothing: no SEH unwinding through lifted
// frames. (__builtin_setjmp is not usable: on Win64 LLVM saves the frame address, which is not rbp when the frame
// register is offset, and the landing code then reads its locals through the wrong base.)
static __attribute__((noreturn)) void exit_now(RtThread *t) { plat_jmp_go(&t->exit_jmp); }
// The calling host thread, for ap_try (TOTK_ASYNC_PREEMPT) and dg_sample (TOTK_CORE_DIAG).
static PlatIntr *ap_open_self(void) {
@@ -1457,7 +1481,7 @@ static void thread_main(void *p) {
unlock();
Cpu *c = t->cpu;
c->sp = t->stack_top & ~15ull;
if (!setjmp(t->exit_jmp)) {
if (!plat_jmp_save(&t->exit_jmp)) {
rt_call(c, t->entry, 1, t->arg);
char d[128];
rt_fatal("guest thread %#x: entry %s returned (Horizon threads end with ExitThread)", t->handle,
@@ -1517,7 +1541,7 @@ static void guest_main(void *p) {
void (*fn)(Cpu *) = (void (*)(Cpu *))p;
Cpu *c;
kern_main_thread(&c);
if (!setjmp(tcur->exit_jmp)) fn(c);
if (!plat_jmp_save(&tcur->exit_jmp)) fn(c);
thread_finish(tcur);
return;
}
@@ -1876,7 +1900,9 @@ int kern_svc(Cpu *c, uint32_t imm) {
if (type != 0) {
int32_t nv = val + 1;
if (type == 2) {
if (cnt <= 0) nv = nw > 0 ? val - 1 : val + 1;
// Waking all (count <= 0) with waiters subtracts 2, not 1 (mesosphere
// KAddressArbiter::SignalAndModifyByWaitingCountIfEqual; yuzu, LAS Recomp and nxnative agree).
if (cnt <= 0) nv = nw > 0 ? val - 2 : val + 1;
else nv = nw == 0 ? val + 1 : nw <= cnt ? val - 1 : val;
}
uint32_t e = (uint32_t)val;
@@ -1940,8 +1966,13 @@ int kern_svc(Cpu *c, uint32_t imm) {
int idx = -1;
lock();
g_sync_calls++;
// Horizon's order (mesosphere KSynchronizationObject::Wait): a signalled object, then timeout 0, then a pending
// CancelSynchronization. The cancel is for the next wait that would block: a poll or a wait on a signalled
// object leaves it pending. A cancel that wakes this wait (CancelSynchronization wakes it with R_CANCELLED; a
// signal wakes it with -1 to look again) ends it cancelled, signalled objects or not.
uint32_t woke = 0;
for (;;) {
if (t->cancel) {
if (woke == R_CANCELLED) {
t->cancel = 0;
r = R_CANCELLED;
break;
@@ -1957,6 +1988,15 @@ int kern_svc(Cpu *c, uint32_t imm) {
r = R_OK;
break;
}
if (ns == 0) {
r = R_TIMEOUT;
break;
}
if (t->cancel) {
t->cancel = 0;
r = R_CANCELLED;
break;
}
int64_t left = ns;
if (ns > 0) {
uint64_t spent = kern_now_ns() - start;
@@ -1968,7 +2008,8 @@ int kern_svc(Cpu *c, uint32_t imm) {
}
t->wobj = n ? hs[0] | (uint64_t)n << 32 : 0;
g_sync_blocks++;
if (block(t, W_SYNC, left) == R_TIMEOUT) {
woke = block(t, W_SYNC, left);
if (woke == R_TIMEOUT) {
r = R_TIMEOUT;
break;
}
@@ -1983,7 +2024,7 @@ int kern_svc(Cpu *c, uint32_t imm) {
RtThread *t = h_get((uint32_t)x[0], O_THREAD);
if (t) {
t->cancel = 1;
if (t->wkind == W_SYNC) wake(t, (uint32_t)-1);
if (t->wkind == W_SYNC) wake(t, R_CANCELLED);
}
unlock();
x[0] = t ? R_OK : R_BAD_HANDLE;
@@ -2044,10 +2085,19 @@ void kern_event_signal(uint32_t hw) {
// System events of host services: the SDK's own nn::os::CreateSystemEvent creates the kernel event (SVC 0x45) on a
// guest thread; the writable handle is kept, so any thread (host audio tick included) can signal it later.
// The guest may destroy such an event and attach the same SystemEventType again: the game does that with the nfp
// activate and deactivate events before each detection. nn::os::DestroySystemEvent zeroes the state byte at +0x28
// (0 not initialised, 1 event, 2 inter-process event; nn::os::TryWaitSystemEvent aborts on 0) and closes both
// handles, so a kept handle counts only while that byte is nonzero.
#define SE_STATE 0x28
static struct { uint64_t ev; uint32_t wh; } g_se[512];
static int g_nse;
static PlatMutex g_se_lock = PLAT_MUTEX_INIT;
static int se_live(uint64_t ev) {
return __atomic_load_n((const uint8_t *)(uintptr_t)(ev + SE_STATE), __ATOMIC_RELAXED) != 0;
}
static uint32_t se_find(uint64_t ev) {
plat_mutex_lock_shared(&g_se_lock);
uint32_t wh = 0;
@@ -2057,8 +2107,9 @@ static uint32_t se_find(uint64_t ev) {
return wh;
}
// A bound event the guest destroyed since is created again and its table entry takes the new writable handle.
void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
if (se_find(ev)) return;
if (se_find(ev) && se_live(ev)) return;
if (!tcur || !c) rt_fatal("kern_bind_system_event(%#llx) on a host thread", (unsigned long long)ev);
static uint64_t create, getw;
if (!create) {
@@ -2070,15 +2121,21 @@ void kern_bind_system_event(Cpu *c, uint64_t ev, int autoclear) {
if (r) rt_fatal("kern_bind_system_event: CreateSystemEvent(%#llx) failed %#x", (unsigned long long)ev, r);
uint32_t wh = (uint32_t)rt_call(c, getw, 1, ev);
plat_mutex_lock(&g_se_lock);
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
g_se[g_nse].ev = ev;
g_se[g_nse++].wh = wh;
int i = 0;
while (i < g_nse && g_se[i].ev != ev) i++;
if (i == g_nse) {
if (g_nse == (int)(sizeof g_se / sizeof *g_se)) rt_fatal("kern_bind_system_event: table full");
g_se[g_nse++].ev = ev;
}
g_se[i].wh = wh;
plat_mutex_unlock(&g_se_lock);
}
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread.
// An event not bound yet is bound on the spot with manual clear, which needs a guest thread. A bound event the guest
// destroyed is not signalled: its handles are closed and the kernel hands their numbers to new objects.
void kern_signal_system_event(uint64_t ev) {
uint32_t wh = se_find(ev);
if (wh && !se_live(ev)) return;
if (!wh) {
kern_bind_system_event(kern_cpu(), ev, 0);
wh = se_find(ev);
+40 -4
View File
@@ -305,9 +305,18 @@ static void test_arbiter(Cpu *c) {
CHECK(sig(c, 2, 5, 0) == 0 && g_arb == 6, "signal type 2, no waiter, count 0: value %d (want 6)", (int32_t)g_arb);
ArbJob jc = {.type = 2, .value = 6, .timeout = -1};
h = arb_spawn(c, &jc);
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 5, "signal type 2, 1 waiter, count -1: value %d (want 5)", (int32_t)g_arb);
// Waking all (count <= 0) with waiters subtracts 2 (mesosphere KAddressArbiter::SignalAndModifyByWaitingCountIfEqual).
CHECK(sig(c, 2, 6, -1) == 0 && g_arb == 4, "signal type 2, 1 waiter, count -1: value %d (want 4)", (int32_t)g_arb);
join(c, h);
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0, "wait type 2 results %#x %#x %#x", ja.r, jb.r, jc.r);
g_arb = 6;
ArbJob jd = {.type = 2, .value = 6, .timeout = -1}, je = jd;
uint32_t hd = arb_spawn(c, &jd), he = arb_spawn(c, &je);
CHECK(sig(c, 2, 6, 0) == 0 && g_arb == 4, "signal type 2, 2 waiters, count 0: value %d (want 4)", (int32_t)g_arb);
join(c, hd);
join(c, he);
CHECK(ja.r == 0 && jb.r == 0 && jc.r == 0 && jd.r == 0 && je.r == 0, "wait type 2 results %#x %#x %#x %#x %#x", ja.r,
jb.r, jc.r, jd.r, je.r);
g_arb = 5;
CHECK(sig(c, 2, 7, 1) == KERN_R_BAD_STATE, "signal type 2 with a different value");
// timeout, invalid type
uint64_t t0 = kern_now_ns();
@@ -320,11 +329,17 @@ static void test_arbiter(Cpu *c) {
}
// ---- events
typedef struct { uint32_t h, r, idx; int64_t timeout; volatile int delay, go; } EvJob;
// pre: a first wait on h0 with timeout t0 (result r0, index idx0) before the wait on h.
typedef struct { uint32_t h, r, idx, h0, r0, idx0; int64_t timeout, t0; volatile int delay, go; int pre; } EvJob;
static void event_waiter(Cpu *c) {
EvJob *j = arg_p(c, 0);
while (j->delay && !j->go) sleep_ms(c, 1);
if (j->pre) {
uint32_t h0[1] = {j->h0};
j->r0 = sv(c, 0x18, 0, ga(h0), 1, (uint64_t)j->t0);
j->idx0 = (uint32_t)c->x[1];
}
uint32_t hs[1] = {j->h};
j->r = sv(c, 0x18, 0, ga(hs), 1, (uint64_t)j->timeout);
j->idx = (uint32_t)c->x[1];
@@ -375,6 +390,26 @@ static void test_events(Cpu *c) {
join(c, h);
CHECK(g.r == KERN_R_CANCELLED, "wait after a cancel: %#x", g.r);
// A pending cancel is for the next WaitSynchronization that would block (mesosphere KSynchronizationObject::Wait:
// signalled objects first, then timeout 0, then the cancel): a poll times out and a wait on a signalled object
// succeeds, both leaving it pending; the next blocking wait ends cancelled. The blocking waits have a 500 ms
// timeout, so a cancel used up too early shows as a timeout instead of a hang.
CHECK(sv(c, 0x11, hw2, 0, 0, 0) == 0, "SignalEvent of the second event");
EvJob p = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr, .t0 = 0};
EvJob q = {.h = hr, .timeout = 500000000, .delay = 1, .pre = 1, .h0 = hr2, .t0 = -1};
EvJob *pq[2] = {&p, &q};
for (int i = 0; i < 2; i++) {
h = create(c, a_event_waiter, pq[i], 44, -2);
start(c, h);
CHECK(sv(c, 0x19, h, 0, 0, 0) == 0, "CancelSynchronization before the %s", i ? "signalled wait" : "poll");
pq[i]->go = 1;
join(c, h);
}
CHECK(p.r0 == KERN_R_TIMEOUT, "poll (timeout 0) with a cancel pending: %#x (want timeout)", p.r0);
CHECK(p.r == KERN_R_CANCELLED, "blocking wait after the poll: %#x (want cancelled)", p.r);
CHECK(q.r0 == 0 && q.idx0 == 0, "wait on a signalled event with a cancel pending: %#x (want success)", q.r0);
CHECK(q.r == KERN_R_CANCELLED, "blocking wait after the signalled wait: %#x (want cancelled)", q.r);
uint32_t sw = kern_event_new(), sr = kern_event_readable(sw);
CHECK(wait1(c, sr, 0) == KERN_R_TIMEOUT, "service event before the signal");
kern_event_signal(sw);
@@ -382,7 +417,8 @@ static void test_events(Cpu *c) {
uint32_t handles[] = {sr, sw, hr2, hw2, hr, hw};
for (unsigned i = 0; i < 6; i++) CHECK(sv(c, 0x16, handles[i], 0, 0, 0) == 0, "CloseHandle %#x", handles[i]);
CHECK(sv(c, 0x16, hr, 0, 0, 0) == KERN_R_BAD_HANDLE, "CloseHandle twice");
printf("events: signal, clear, reset, two handles, cancel while waiting and before, service events, 10 ms "
printf("events: signal, clear, reset, two handles, cancel while waiting and before, a pending cancel kept by a poll "
"and a signalled wait, service events, 10 ms "
"timeout after %.2f ms: %s\n", ms, failures == before ? "ok" : "FAIL");
}
+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.0, 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.0 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_110[] = {
{"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_V110 ? expect_110 : expect_121)
static int failures;
#define CHECK(cond, ...) do { if (!(cond)) { failures++; printf(" FAIL: " __VA_ARGS__); putchar('\n'); } } while (0)
+60 -7
View File
@@ -2,8 +2,10 @@
//
// totk.exe [--exefs DIR] [--romfs DIR] [--profile DIR] <mode>
// --selftest layout, dispatch table and host-function registry; needs no game files
// --probe-mods the mods folder scan, order and switches (mods.c); needs no game files
// --probe-loader load and relocate the modules, check them against the measured 1.2.1 values
// --probe-kernel, --probe-ipc, --probe-services, --probe-fs host probes
// --probe-discord the Rich Presence IPC exchange; skips without a Discord client; no game files
// --probe-dpi the process DPI awareness (per-monitor v2) and the primary monitor's size; no game files
// --probe-gfx-abi NVN object layouts against nvn.h's reserved sizes; needs no game files
// --probe-nvn, --probe-nvn-tex, --probe-window, --probe-shader, --probe-vkmem, --probe-vkexec graphics probes
@@ -152,8 +154,8 @@ static RtConfig cfg;
const RtConfig *rt_config(void) { return &cfg; }
// ---- runtime services the generated code calls (cpu.h)
volatile int g_fnprof = 0;
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0) { (void)addr, (void)lr, (void)x0; }
volatile int g_fnprof = 0; // resolution.c switches it on until the game sets its render size (res_fn_hit)
void fnprof_hit(uint64_t addr, uint64_t lr, uint64_t x0) { (void)lr, res_fn_hit(addr, x0); }
void rt_brk(Cpu *c, uint32_t imm, uint64_t pc) {
char at[128], lr[128];
@@ -183,7 +185,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 +200,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;
@@ -213,13 +219,24 @@ static void crash_handler(const PlatCrash *x) {
if (plat_xchg32(&busy, 1)) plat_exit(71);
char d[160];
uintptr_t rip = x->rip;
rt_logf("FATAL: %s (%#x) at host %p (totk+%#llx)", x->what, x->code, (void *)rip, (unsigned long long)x->offset);
rt_logf("FATAL: %s (%#x) at host %p (", x->what, x->code, (void *)rip);
if (x->module && !*x->module) rt_logf("outside every module)");
else rt_logf("%s+%#llx)", x->module ? x->module : "totk", (unsigned long long)x->offset);
if (x->access >= 0)
rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading",
(unsigned long long)x->addr);
rt_log_raw("\n", 1);
uint64_t f = lifted_function(rip);
uint64_t f = x->module ? 0 : lifted_function(rip);
if (f) rt_logf(" in lifted function f_%llx = %s\n", (unsigned long long)f, ldr_describe(f, d, sizeof d));
// abort and invalid parameter: where the host code called it; a fault in another module: the host calls that led
// into it
uintptr_t exe = plat_exe_base();
for (int i = 0; i < x->nframes; i++) {
uint64_t g = lifted_function(x->frames[i]);
rt_logf(" host frame %2d: totk+%#llx", i, (unsigned long long)(x->frames[i] - exe));
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++)
@@ -236,6 +253,7 @@ static void crash_handler(const PlatCrash *x) {
} else {
rt_logf(" not on a guest thread\n");
}
kern_free_cores_note();
fflush(stderr);
plat_exit(71);
}
@@ -249,11 +267,22 @@ static double now_s(void) { return (double)plat_now_ns() / 1e9; }
static volatile int host_probe_hits;
static void host_probe(Cpu *c) { host_probe_hits++; c->x[0] = 42; }
// The instruction set build_game.py --cpu compiled for: v3 (AVX2/FMA/BMI2, the default) or v2 (SSE4.2/POPCNT).
#ifndef RT_CPU_LEVEL
#define RT_CPU_LEVEL 3
#endif
static void check_cpu(void) {
__builtin_cpu_init();
#if RT_CPU_LEVEL >= 3
int ok = __builtin_cpu_supports("avx2") && __builtin_cpu_supports("fma") && __builtin_cpu_supports("bmi2");
printf("cpu: x86-64-v3 (AVX2, FMA, BMI2) %s\n", ok ? "present" : "MISSING");
CHECK(ok, "the lifted code is built for x86-64-v3");
#else
int ok = __builtin_cpu_supports("sse4.2") && __builtin_cpu_supports("popcnt");
printf("cpu: x86-64-v2 (SSE4.2, POPCNT) %s\n", ok ? "present" : "MISSING");
CHECK(ok, "the lifted code is built for x86-64-v2");
#endif
}
// Layout: ascending, no overlap, below 2^39, and inside the window mem_init reserved.
@@ -416,11 +445,27 @@ static int selftest(void) {
return failures ? 1 : 0;
}
// --probe-crash-call and --probe-crash-null crash on purpose, to check the report of a fault outside this executable.
// call: strlen in the C runtime library (ucrtbase.dll, libc.so.6) reads address 8, the way a driver faults inside a
// call the host made; the report names the library and the host frames that called it. null: a call through a null
// function pointer, which leaves rip at 0, outside every module. Both end with exit code 71 and a FATAL line in the
// log.
static size_t (*volatile probe_strlen)(const char *) = strlen;
static void (*volatile probe_null)(void);
static int probe_crash(int null) {
if (null) probe_null();
else printf("%zu\n", probe_strlen((const char *)(uintptr_t)8));
printf("probe-crash: still running, the crash did not happen\n");
return 1;
}
static int usage(const char *argv0) {
fprintf(stderr, "usage: %s [--exefs DIR] [--romfs DIR] [--profile DIR] [--selftest | --probe-loader | "
"--probe-cfg | --probe-dpi | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-cfg | --probe-dpi | --probe-mods | --probe-kernel | --probe-ipc | --probe-services | --probe-fs | --probe-gfx-abi | "
"--probe-nvn | --probe-nvn-tex | --probe-window | --probe-shader | --probe-vkmem | "
"--probe-vkexec | --probe-astc | --probe-movie | --prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
"--probe-vkexec | --probe-astc | --probe-movie | --probe-crash-call | --probe-crash-null | --probe-discord | "
"--prepare-shaders | --warm-shaders | --boot |--seconds S [--shot PNG]]\n", argv0);
return 2;
}
@@ -458,6 +503,9 @@ static void portable_paths(RtConfig *c) {
int main(int argc, char **argv) {
mem_init(); // first: claim the guest address space before anything else can land there; reads totk.ini
// A restart from the settings menu (gfx_present.c gfx_window_closed) starts this process while the one it replaces
// is still exiting: its log, RomFS and saves are free once it is gone.
if (plat_restart_wait(30000) < 0) rt_log("restart: the previous process still runs after 30 s; starting anyway");
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
plat_crash_hook(crash_handler);
@@ -500,15 +548,20 @@ int main(int argc, char **argv) {
log_open(!mode);
if (!mode) {
cfg.boot = cfg.seconds > 0; // --seconds starts the game like --boot, with its checkpoints
discord_init(); // the Rich Presence thread (discord_rpc.c); it reads totk.ini itself and idles while off
rt_boot();
return 0;
}
if (!strcmp(mode, "--selftest")) return selftest();
if (!strcmp(mode, "--probe-loader")) return probe_loader();
if (!strcmp(mode, "--probe-crash-call")) return probe_crash(0);
if (!strcmp(mode, "--probe-crash-null")) return probe_crash(1);
if (!strcmp(mode, "--probe-cfg")) return probe_cfg();
#ifdef _WIN32
if (!strcmp(mode, "--probe-dpi")) return probe_dpi(); // the Win32 DPI awareness of totk.manifest
#endif
if (!strcmp(mode, "--probe-mods")) return probe_mods();
if (!strcmp(mode, "--probe-discord")) return probe_discord();
if (!strcmp(mode, "--probe-kernel")) return probe_kernel();
if (!strcmp(mode, "--probe-ipc")) return probe_ipc();
if (!strcmp(mode, "--probe-services")) return probe_services();
+457
View File
@@ -0,0 +1,457 @@
// Player mods: the scan of the mods folder, its order and switches (mods.h).
//
// One subfolder per mod, as emulators take them. The RomFS root is the first of <mod>/romfs, <mod>/romfslite (TKMM's
// name for it when it bypasses Atmosphere's RomFS build), <mod>/atmosphere/contents/<title>/romfs,
// <mod>/contents/<title>/romfs and <mod>/<title>/romfs that exists; the ExeFS and cheats folders likewise. A mod is
// listed whatever it holds; what the port cannot use is its problem, shown in the menu and logged, and never stops the
// start. romfs.c serves the RomFS files of the enabled mods, the top one winning a path.
//
// The order and the switches are totk.ini keys (cfg.c): TOTK_MODS_ORDER lists names top first, separated by '|' (a
// Windows file name cannot hold '|'; a folder whose name does on Linux is listed as unusable); a folder it does not name
// goes below the named ones, by name. TOTK_MODS_DISABLED lists the switched-off names. The menu changes the list in
// place and writes both keys; the scan's state is kept to tell the menu a restart is due.
//
// ExeFS patch files: an .ips or .pchtxt file in the exefs folder or in atmosphere/exefs_patches/<name>/ is for the game
// when its build id is main's (an IPS file's name, a pchtxt file's @nsobid line); others are another version's and
// counted. The setup builds every mod's files into patch sets and writes build-cache/exefs.status next to totk.exe
// (tools/exefs_patches.py has the format): a file whose line is missing or has another content hash waits for the
// rebuild at start (cheats.c), and its mod stays off until then.
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "cfg.h"
#include "host.h"
#include "mods.h"
#include "platform/platform.h"
#define TITLE "0100F2C0115B6000"
#define MAX_MODS 512
static ModInfo *mods;
static int nmods;
static char dir[2048];
static char start_order[MAX_MODS][256]; // names in the scan's order and their switches, for mods_changed
static int start_enabled[MAX_MODS];
// 1 when a file lies anywhere under path.
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx);
static int has_file(const char *path) {
struct { const char *path; int found; } c = {path, 0};
plat_fs_list(path, has_file_cb, &c);
return c.found;
}
static int has_file_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
struct { const char *path; int found; } *c = ctx;
if (kind == PLAT_FS_FILE) {
c->found = 1;
} else if (kind == PLAT_FS_DIR) {
char sub[2100];
if (snprintf(sub, sizeof sub, "%s/%s", c->path, name) < (int)sizeof sub) c->found = has_file(sub);
}
return !c->found;
}
// The first of the emulator and Atmosphere places of <mod>/<part> that is a folder, into out ("" none).
static void find_root(const char *mod, const char *part, char *out, size_t cap) {
static const char *const forms[] = {"%s/%s", "%s/atmosphere/contents/" TITLE "/%s", "%s/contents/" TITLE "/%s",
"%s/" TITLE "/%s"};
for (size_t i = 0; i < sizeof forms / sizeof *forms; i++) {
if (snprintf(out, cap, forms[i], mod, part) < (int)cap && plat_dir_exists(out)) return;
}
out[0] = 0;
}
static int ends_with_ci(const char *s, const char *tail) {
size_t n = strlen(s), t = strlen(tail);
return n >= t && !strcasecmp(s + n - t, tail);
}
// What the ExeFS folder holds: MOD_EXEFS for patches (.ips, .pchtxt), MOD_SUBSDK for code modules (subsdk* plugins, a
// replaced main). main.npdm alone is the process's metadata, not code: it counts as neither.
static int exefs_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
int *k = ctx;
if (kind != PLAT_FS_FILE) return 1;
if (ends_with_ci(name, ".ips") || ends_with_ci(name, ".pchtxt")) *k |= MOD_EXEFS;
else if (!strncasecmp(name, "subsdk", 6) || !strcasecmp(name, "main")) *k |= MOD_SUBSDK;
return 1;
}
// MOD_CHEATS when the cheats folder holds an Atmosphere cheat file (<build id>.txt).
static int cheats_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
if (kind == PLAT_FS_FILE && ends_with_ci(name, ".txt")) *(int *)ctx |= MOD_CHEATS;
return 1;
}
// 1 when name is one of the '|'-separated names of list.
static int listed(const char *list, const char *name) {
size_t n = strlen(name);
for (const char *p = list; p && *p;) {
const char *bar = strchr(p, '|');
size_t len = bar ? (size_t)(bar - p) : strlen(p);
if (len == n && !memcmp(p, name, n)) return 1;
p = bar ? bar + 1 : NULL;
}
return 0;
}
// The position of name in the '|'-separated list, or -1.
static int list_pos(const char *list, const char *name) {
size_t n = strlen(name);
int k = 0;
for (const char *p = list; p && *p; k++) {
const char *bar = strchr(p, '|');
size_t len = bar ? (size_t)(bar - p) : strlen(p);
if (len == n && !memcmp(p, name, n)) return k;
p = bar ? bar + 1 : NULL;
}
return -1;
}
static const char *order_list; // TOTK_MODS_ORDER while sorting
static int by_order(const void *a, const void *b) {
const ModInfo *x = a, *y = b;
int px = list_pos(order_list, x->name), py = list_pos(order_list, y->name);
if (px >= 0 && py >= 0) return px - py;
if (px >= 0 || py >= 0) return px >= 0 ? -1 : 1;
return strcmp(x->name, y->name);
}
static const char *problem_name(ModProblem p) {
switch (p) {
case MOD_EMPTY: return "empty";
case MOD_PLUGIN: return "has a code module (subsdk or main), not supported; the whole mod stays off";
case MOD_EXEFS_LATER: return "ExeFS patches wait for a rebuild of the game code; the whole mod stays off";
case MOD_EXEFS_UNSUPPORTED: return "an ExeFS patch file cannot be applied; the whole mod stays off";
case MOD_EXEFS_OTHER: return "its ExeFS patches are for another game version";
case MOD_BAD_NAME: return "its folder name holds '|', which the order list uses; rename the folder";
default: return "ok";
}
}
// ---- ExeFS patch files
static uint32_t fnv(const void *p, size_t n) {
uint32_t h = 0x811C9DC5u;
for (size_t i = 0; i < n; i++) h = (h ^ ((const uint8_t *)p)[i]) * 0x01000193u;
return h;
}
const char *mods_main_build_id(void) {
static char bid[65];
static int read;
if (read) return bid;
read = 1;
char path[2100];
snprintf(path, sizeof path, "%s/main", rt_config()->exefs);
PlatFile *f = plat_file_open(path);
uint8_t head[0x60];
int ok = f && plat_file_pread(f, head, sizeof head, 0) == sizeof head && !memcmp(head, "NSO0", 4);
if (f) plat_file_close(f);
for (int i = 0; ok && i < 32; i++) snprintf(bid + 2 * i, 3, "%02x", head[0x40 + i]);
return bid;
}
// 1 when hex (any case; trailing zeros left out or not) names main's build id; also when main's is unknown.
static int same_build(const char *hex, size_t n) {
const char *bid = mods_main_build_id();
if (!*bid) return 1;
if (!n || n > 64) return 0;
for (size_t i = 0; i < 64; i++) {
char c = i < n ? hex[i] : '0';
if (c >= 'A' && c <= 'F') c = (char)(c - 'A' + 'a');
if (c != bid[i]) return 0;
}
return 1;
}
// build-cache/exefs.status next to totk.exe, read once; "" when there is none.
static const char *exefs_status(void) {
static char *text;
if (text) return text;
char path[2100];
uint64_t n = 0;
if (plat_exe_path(path, sizeof path)) {
char *cut = strrchr(path, '/'), *back = strrchr(path, '\\');
if (back > cut) cut = back;
if (cut) snprintf(cut + 1, sizeof path - (size_t)(cut + 1 - path), "build-cache/exefs.status");
text = (char *)plat_file_read(path, &n);
}
if (!text) text = calloc(1, 1);
return text;
}
// The status line of key: its kind, built-in sets and note, NUL-terminated copies; 0 when none.
static int status_line(const char *key, const char *content, char *kind, char *over, char *note) {
char want[20];
snprintf(want, sizeof want, "%s\t%s\t", key, content);
for (const char *line = exefs_status(); *line;) {
const char *end = strchr(line, '\n');
size_t len = end ? (size_t)(end - line) : strlen(line);
if (len > 18 && !memcmp(line, want, 18)) {
char copy[600];
snprintf(copy, sizeof copy, "%.*s", (int)(len < sizeof copy ? len : sizeof copy - 1), line + 18);
char *t1 = strchr(copy, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL;
if (!t1 || !t2) return 0;
*t1 = *t2 = 0;
char *r = strchr(t2 + 1, '\r');
if (r) *r = 0;
snprintf(kind, 16, "%s", copy);
snprintf(over, 256, "%s", strcmp(t1 + 1, "-") ? t1 + 1 : "");
snprintf(note, 256, "%s", t2 + 1);
return 1;
}
line = end ? end + 1 : line + len;
}
return 0;
}
typedef struct {
ModInfo *m;
const char *folder, *rel; // the folder listed and its path in the mod
int unsupported;
} ExefsWalk;
// One file of an exefs folder: its build, then its status line.
static int exefs_file_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
(void)size;
ExefsWalk *w = ctx;
ModInfo *m = w->m;
int ips = ends_with_ci(leaf, ".ips");
if (kind != PLAT_FS_FILE || (!ips && !ends_with_ci(leaf, ".pchtxt"))) return 1;
m->kind |= MOD_EXEFS;
char path[4300], rel[600];
snprintf(path, sizeof path, "%s/%s", w->folder, leaf);
snprintf(rel, sizeof rel, "%s/%s/%s", m->name, w->rel, leaf);
uint64_t n = 0;
char *data = (char *)plat_file_read(path, &n);
if (!data) {
rt_log("mods: %s: cannot read %s", m->name, path);
return 1;
}
int for_main;
if (ips) {
for_main = same_build(leaf, strlen(leaf) - 4);
} else {
char *id = NULL; // after "@nsobid-" at the start of a line
for (char *p = data; !id && p && data + n - p > 8;) {
if (!memcmp(p, "@nsobid-", 8)) id = p + 8;
p = memchr(p, '\n', (size_t)(data + n - p));
if (p) p++;
}
size_t len = 0;
while (id && id + len < data + n && isxdigit((unsigned char)id[len])) len++;
for_main = id && same_build(id, len);
}
char key[9], content[9], kind_s[16], over[256], note[256];
snprintf(key, sizeof key, "%08X", fnv(rel, strlen(rel)));
snprintf(content, sizeof content, "%08X", fnv(data, (size_t)n));
free(data);
if (!for_main) {
m->other_version++;
return 1;
}
if (!status_line(key, content, kind_s, over, note)) {
m->pending++;
m->pending_key = (m->pending_key ^ fnv(key, 8)) * 0x01000193u;
m->pending_key = (m->pending_key ^ fnv(content, 8)) * 0x01000193u;
return 1;
}
if (!strcmp(kind_s, "unsupported")) {
if (!w->unsupported) snprintf(m->note, sizeof m->note, "%s: %s", leaf, note);
w->unsupported = 1;
} else if (!strcmp(kind_s, "code") && m->nsets < (int)(sizeof m->sets / sizeof *m->sets)) {
m->sets[m->nsets++] = (uint32_t)strtoul(key, NULL, 16);
size_t k = strlen(m->overrides);
if (*over) snprintf(m->overrides + k, sizeof m->overrides - k, "%s%s", k ? "," : "", over);
} else if (!strcmp(kind_s, "other")) {
m->other_version++;
}
return 1;
}
// atmosphere/exefs_patches/<name>/ of a mod: each <name> folder's files.
static int exefs_patches_cb(const char *leaf, int kind, uint64_t size, void *ctx) {
(void)size;
ExefsWalk *w = ctx;
if (kind != PLAT_FS_DIR) return 1;
char folder[4300], rel[600];
snprintf(folder, sizeof folder, "%s/%s", w->folder, leaf);
snprintf(rel, sizeof rel, "atmosphere/exefs_patches/%s", leaf);
ExefsWalk sub = {w->m, folder, rel, 0};
plat_fs_list(folder, exefs_file_cb, &sub);
w->unsupported |= sub.unsupported;
return 1;
}
// The ExeFS patch files of mod m (folder path): 1 when one cannot be applied.
static int exefs_check(ModInfo *m, const char *path) {
ExefsWalk w = {m, m->exefs, m->exefs + strlen(path) + 1, 0};
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_file_cb, &w);
char patches[2200];
snprintf(patches, sizeof patches, "%s/atmosphere/exefs_patches", path);
ExefsWalk at = {m, patches, "", 0};
if (plat_dir_exists(patches)) plat_fs_list(patches, exefs_patches_cb, &at);
return w.unsupported || at.unsupported;
}
// One mod folder of the scan.
static int scan_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
const char *disabled = ctx;
if (kind != PLAT_FS_DIR) return 1;
if (nmods == MAX_MODS) {
rt_log("mods: more than %d folders in %s; the rest are left out", MAX_MODS, dir);
return 0;
}
ModInfo *m = &mods[nmods];
memset(m, 0, sizeof *m);
char path[2100];
if (snprintf(m->name, sizeof m->name, "%s", name) >= (int)sizeof m->name ||
snprintf(path, sizeof path, "%s/%s", dir, name) >= (int)sizeof path) {
rt_log("mods: %s: name too long, left out", name);
return 1;
}
find_root(path, "romfs", m->romfs, sizeof m->romfs);
if (!m->romfs[0]) find_root(path, "romfslite", m->romfs, sizeof m->romfs);
find_root(path, "exefs", m->exefs, sizeof m->exefs);
find_root(path, "cheats", m->cheats, sizeof m->cheats);
if (m->romfs[0] && has_file(m->romfs)) m->kind |= MOD_ROMFS;
if (m->exefs[0]) plat_fs_list(m->exefs, exefs_cb, &m->kind);
if (m->cheats[0]) plat_fs_list(m->cheats, cheats_cb, &m->kind);
int unsupported = exefs_check(m, path);
// a mod whose code part the port cannot use is off as a whole: its RomFS files may expect that code
m->problem = strchr(m->name, '|') ? MOD_BAD_NAME
: m->kind & MOD_SUBSDK ? MOD_PLUGIN
: unsupported ? MOD_EXEFS_UNSUPPORTED
: m->pending ? MOD_EXEFS_LATER
: m->nsets || m->kind & (MOD_ROMFS | MOD_CHEATS) ? MOD_OK
: m->kind & MOD_EXEFS ? MOD_EXEFS_OTHER
: MOD_EMPTY;
m->enabled = !listed(disabled, m->name);
nmods++;
return 1;
}
void mods_scan(void) {
static int done;
if (done) return;
done = 1;
const char *d = cfg_get("TOTK_MODS_DIR");
if (d && *d) {
snprintf(dir, sizeof dir, "%s", d);
} else {
char exe[2048];
if (!plat_exe_path(exe, sizeof exe)) return;
char *cut = strrchr(exe, '/'), *back = strrchr(exe, '\\');
if (back > cut) cut = back;
if (!cut) return;
*cut = 0;
snprintf(dir, sizeof dir, "%s/mods", exe);
}
if (!plat_dir_exists(dir)) {
rt_log("mods: no folder %s", dir);
return;
}
mods = calloc(MAX_MODS, sizeof *mods);
if (!mods) return;
plat_fs_list(dir, scan_cb, (void *)cfg_get("TOTK_MODS_DISABLED"));
order_list = cfg_get("TOTK_MODS_ORDER");
qsort(mods, nmods, sizeof *mods, by_order);
order_list = NULL;
for (int i = 0; i < nmods; i++) {
snprintf(start_order[i], sizeof start_order[i], "%s", mods[i].name);
start_enabled[i] = mods[i].enabled;
const ModInfo *m = &mods[i];
rt_log("mods: %s: %s,%s%s%s%s%s, %s%s%s", m->name, m->enabled ? "on" : "off", m->kind & MOD_ROMFS ? " romfs" : "",
m->kind & MOD_EXEFS ? " exefs" : "", m->kind & MOD_SUBSDK ? " subsdk" : "",
m->kind & MOD_CHEATS ? " cheats" : "", m->kind ? "" : " no files", problem_name(m->problem),
m->note[0] ? ": " : "", m->note);
}
rt_log("mods: %d folders in %s", nmods, dir);
}
void mods_exefs_switch(void) {
mods_scan();
for (int i = 0; i < nmods; i++) {
const ModInfo *m = &mods[i];
if (!start_enabled[i] || m->problem != MOD_OK || !m->nsets) continue;
for (int k = 0; k < m->nsets; k++) {
char name[32];
snprintf(name, sizeof name, "mod_%08x", m->sets[k]);
int set = rt_patch_set(name);
if (set >= 0) rt_patch_switch(set, 1);
else rt_log("mods: %s: patch set %s is not in this build", m->name, name);
}
char over[256];
snprintf(over, sizeof over, "%s", m->overrides);
for (char *s = strtok(over, ","); s; s = strtok(NULL, ",")) {
int set = rt_patch_set(s);
if (set >= 0) rt_patch_switch(set, 0);
rt_log("mods: %s replaces the port's patch set %s; it stays off", m->name, s);
}
rt_log("mods: %s: %d ExeFS patch files on", m->name, m->nsets);
}
}
int mods_exefs_pending(uint32_t *key) {
int n = 0;
for (int i = 0; i < nmods; i++) {
if (!start_enabled[i] || mods[i].problem != MOD_EXEFS_LATER) continue;
n += (int)mods[i].pending;
*key = (*key ^ mods[i].pending_key) * 0x01000193u;
}
return n;
}
const char *mods_dir(void) { return dir; }
int mods_count(void) { return nmods; }
ModInfo *mods_at(int i) { return i >= 0 && i < nmods ? &mods[i] : NULL; }
// TOTK_MODS_ORDER as every listed name top first; TOTK_MODS_DISABLED as the switched-off names. A name holding '|'
// would split into two; it is left out of both (its mod is unusable anyway).
static void write_keys(void) {
size_t cap = (size_t)nmods * 257 + 1;
char *order = calloc(cap, 1), *off = calloc(cap, 1);
if (order && off) {
for (int i = 0; i < nmods; i++) {
if (mods[i].problem == MOD_BAD_NAME) continue;
if (*order) strcat(order, "|");
strcat(order, mods[i].name);
if (!mods[i].enabled) {
if (*off) strcat(off, "|");
strcat(off, mods[i].name);
}
}
cfg_set("TOTK_MODS_ORDER", order);
cfg_set("TOTK_MODS_DISABLED", off);
}
free(order);
free(off);
}
void mods_set_enabled(int i, int on) {
if (i < 0 || i >= nmods) return;
mods[i].enabled = on != 0;
write_keys();
}
void mods_move(int i, int delta) {
int j = i + delta;
if (i < 0 || i >= nmods || j < 0 || j >= nmods || i == j) return;
ModInfo t = mods[i];
mods[i] = mods[j];
mods[j] = t;
write_keys();
}
int mods_changed(void) {
for (int i = 0; i < nmods; i++)
if (strcmp(mods[i].name, start_order[i]) || mods[i].enabled != start_enabled[i]) return 1;
return 0;
}
+57
View File
@@ -0,0 +1,57 @@
// Player mods (mods.c): one folder per mod under mods/ next to totk.exe (TOTK_MODS_DIR), unpacked as emulators take
// them: <mod>/romfs, <mod>/exefs and <mod>/cheats, the Atmosphere layout (atmosphere/contents/<title id>/romfs,
// atmosphere/exefs_patches/<name>/*.ips) or TKMM's merged output (romfs or romfslite, exefs, cheats). ExeFS patch
// files (.ips, .pchtxt) for the game's main are patch sets the setup builds into the game (tools/exefs_patches.py),
// switched with their mod (mods_exefs_switch). The list is scanned once at start (romfs.c overlay_init);
// its order and switches live in totk.ini [mods] (TOTK_MODS_ORDER: names top first, separated by '|';
// TOTK_MODS_DISABLED: names switched off). The top mod wins a file two mods replace.
#pragma once
#include <stdint.h>
enum { MOD_ROMFS = 1, MOD_EXEFS = 2, MOD_SUBSDK = 4, MOD_CHEATS = 8 }; // ModInfo.kind bits
typedef enum {
MOD_OK,
MOD_EMPTY, // no RomFS file, no ExeFS patch, no cheat file
MOD_PLUGIN, // has code modules (exefs/subsdk*, a replaced exefs/main): the port has no module loader, so the
// whole mod stays off (its RomFS files may expect that code)
MOD_EXEFS_LATER, // has ExeFS patches for this game's main that the game was built without: the rebuild at the
// next start builds them in; the whole mod stays off until then
MOD_EXEFS_UNSUPPORTED, // an ExeFS patch file the port cannot apply (note says why): the whole mod stays off
MOD_EXEFS_OTHER, // only ExeFS patches, all for another game version
MOD_BAD_NAME, // its folder name holds '|', the separator of the order keys
} ModProblem;
typedef struct {
char name[256]; // folder name, UTF-8
char romfs[2048]; // UTF-8 path of its RomFS root; "" none
char exefs[2048]; // UTF-8 path of its ExeFS folder; "" none
char cheats[2048]; // UTF-8 path of its cheats folder; "" none
int kind, enabled;
ModProblem problem;
uint32_t replaced, added, overridden; // romfs.c: files served (replacing / added), files a higher mod serves
uint32_t other_version; // files for another game version (RomFS *.Product.<n>.*, ExeFS patches
// for another build), not used
char overridden_by[256]; // the first higher mod that took one of its files
char note[256]; // MOD_EXEFS_UNSUPPORTED: why
uint32_t pending, pending_key; // ExeFS patch files the build lacks; FNV-1a of their names and contents
int nsets;
uint32_t sets[16]; // the built patch sets mod_<hash> of its ExeFS patch files
char overrides[256]; // the port's own patch sets its patches replace, comma separated
} ModInfo;
void mods_scan(void); // once; later calls return at once
const char *mods_dir(void); // the folder scanned (default: <dir of totk.exe>/mods)
int mods_count(void);
ModInfo *mods_at(int i); // 0 = top of the order; NULL out of range
// Menu edits (present path). They change the list mods_at shows and write TOTK_MODS_ORDER / TOTK_MODS_DISABLED with
// cfg_set; romfs.c used the start state, so they apply at the next start.
void mods_set_enabled(int i, int on);
void mods_move(int i, int delta); // -1 up, +1 down; nothing at the ends
int mods_changed(void); // 1 when the order or a switch differs from the start
// After rt_patches_init: the patch sets of the enabled mods' ExeFS patches on, the port's own sets they replace off.
void mods_exefs_switch(void);
// The ExeFS patch files of the enabled mods that the build lacks, their FNV-1a mixed into *key (cheats.c starts the
// rebuild for them).
int mods_exefs_pending(uint32_t *key);
// The hex of the game main's build id (64 digits), "" when exefs/main cannot be read.
const char *mods_main_build_id(void);
+130
View File
@@ -0,0 +1,130 @@
// --probe-mods: the mods folder scan, its order and switches (mods.c). Builds a mods folder of its own next to totk.exe,
// scans it and removes it. Needs no game files.
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "mods.h"
#include "platform/platform.h"
static int fails;
#define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL mods: %s\n", what); } } while (0)
#define EQ(a, b) ((a) && !strcmp((a), (b)))
// root/rel as a file of a few bytes (rel ends in '/': a folder), with every folder above it.
static void put(const char *root, const char *rel) {
char p[2048];
snprintf(p, sizeof p, "%s/%s", root, rel);
char *slash = strrchr(p, '/');
*slash = 0;
plat_file_mkdir_p(p);
*slash = '/';
if (slash[1]) plat_file_write(p, "probe", 5);
}
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx);
static void remove_tree(const char *dir) {
plat_fs_list(dir, remove_cb, (void *)dir);
plat_fs_rmdir(dir);
}
static int remove_cb(const char *name, int kind, uint64_t size, void *ctx) {
(void)size;
char sub[2048];
snprintf(sub, sizeof sub, "%s/%s", (const char *)ctx, name);
if (kind == PLAT_FS_DIR) remove_tree(sub);
else plat_fs_unlink(sub);
return 1;
}
// s ends in tail, either path separator matching either.
static int ends_with(const char *s, const char *tail) {
size_t n = strlen(s), t = strlen(tail);
if (n < t) return 0;
for (size_t i = 0; i < t; i++) {
char a = s[n - t + i], b = tail[i];
if (a == '\\') a = '/';
if (b == '\\') b = '/';
if (a != b) return 0;
}
return 1;
}
int probe_mods(void) {
char root[1024];
if (!plat_exe_path(root, sizeof root)) return 1;
char *cut = strrchr(root, '/'), *back = strrchr(root, '\\');
if (back > cut) cut = back;
if (!cut) return 1;
strcpy(cut + 1, "mods-probe");
remove_tree(root);
plat_file_mkdir_p(root);
static const char *const files[] = {
"A/romfs/x.bin", "B/atmosphere/contents/0100F2C0115B6000/romfs/x.bin",
"C/contents/0100F2C0115B6000/romfs/y.bin", "D/exefs/main.pchtxt", "E/exefs/subsdk9", "F/romfs/",
"G/romfs/z.bin", "G/exefs/1234.ips", "Ultra \xD0\x96/romfs/u.bin",
"H/romfs/h.bin", "H/exefs/subsdk9", "I/romfs/i.bin", "I/exefs/main", "J/cheats/0123456789ABCDEF.txt",
"K/romfslite/k.bin", "L/romfs/l.bin", "L/cheats/0123456789ABCDEF.txt", "L/exefs/main.npdm",
};
for (size_t i = 0; i < sizeof files / sizeof *files; i++) put(root, files[i]);
// an IPS file for this game's main (any file counts as one when main cannot be read), in Atmosphere's layout
char ips[160];
snprintf(ips, sizeof ips, "M/atmosphere/exefs_patches/x/%s.ips", *mods_main_build_id() ? mods_main_build_id() : "0");
put(root, ips);
cfg_set("TOTK_MODS_DIR", root);
cfg_set("TOTK_MODS_ORDER", "B|A|Gone");
cfg_set("TOTK_MODS_DISABLED", "C");
mods_scan();
static const char *const order[] = {"B", "A", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"Ultra \xD0\x96"};
enum { N = sizeof order / sizeof *order };
CHECK(mods_count() == N, "fourteen mod folders");
int in_order = mods_count() == N;
for (int i = 0; in_order && i < N; i++) in_order = EQ(mods_at(i)->name, order[i]);
CHECK(in_order, "order: mods_order first, the rest by name");
if (mods_count() == N && in_order) {
ModInfo *a = mods_at(1), *b = mods_at(0), *c = mods_at(2), *d = mods_at(3), *e = mods_at(4), *f = mods_at(5),
*g = mods_at(6), *hm = mods_at(7), *im = mods_at(8), *jm = mods_at(9), *km = mods_at(10),
*lm = mods_at(11), *mm = mods_at(12), *u = mods_at(13);
CHECK(hm->kind == (MOD_ROMFS | MOD_SUBSDK) && hm->problem == MOD_PLUGIN, "RomFS + subsdk plugin: unusable");
CHECK(im->kind == (MOD_ROMFS | MOD_SUBSDK) && im->problem == MOD_PLUGIN, "RomFS + replaced exefs/main");
CHECK(ends_with(b->romfs, "B/atmosphere/contents/0100F2C0115B6000/romfs"), "Atmosphere RomFS root");
CHECK(ends_with(c->romfs, "C/contents/0100F2C0115B6000/romfs"), "contents RomFS root");
CHECK(ends_with(a->romfs, "A/romfs") && a->kind == MOD_ROMFS && a->problem == MOD_OK, "plain RomFS mod");
CHECK(!c->enabled && a->enabled && b->enabled && g->enabled && u->enabled, "mods_disabled switches C off");
CHECK(d->kind == MOD_EXEFS && d->problem == MOD_EXEFS_OTHER && d->other_version == 1,
"ExeFS patch mod without a build id: another version's");
CHECK(e->problem == MOD_PLUGIN, "subsdk-only mod is a plugin mod");
CHECK(f->problem == MOD_EMPTY, "empty RomFS folder");
CHECK(g->kind == (MOD_ROMFS | MOD_EXEFS) && g->problem == MOD_OK && g->other_version == 1,
"RomFS + another version's ExeFS patch: the RomFS is used");
CHECK(mm->kind == MOD_EXEFS && mm->problem == MOD_EXEFS_LATER && mm->pending == 1,
"an ExeFS patch for main the build lacks waits for the rebuild");
CHECK(jm->kind == MOD_CHEATS && jm->problem == MOD_OK && ends_with(jm->cheats, "J/cheats"),
"cheats-only mod");
CHECK(km->kind == MOD_ROMFS && km->problem == MOD_OK && ends_with(km->romfs, "K/romfslite"), "TKMM romfslite");
CHECK(lm->kind == (MOD_ROMFS | MOD_CHEATS) && lm->problem == MOD_OK, "RomFS + cheats, main.npdm is no code");
CHECK(u->kind == MOD_ROMFS && ends_with(u->romfs, "Ultra \xD0\x96/romfs"), "Unicode folder name");
CHECK(!mods_changed(), "nothing changed after the scan");
mods_move(1, -1);
CHECK(EQ(mods_at(0)->name, "A") && EQ(mods_at(1)->name, "B"), "move up");
CHECK(EQ(cfg_get("TOTK_MODS_ORDER"), "A|B|C|D|E|F|G|H|I|J|K|L|M|Ultra \xD0\x96"),
"order written without the gone name");
mods_move(0, -1);
CHECK(EQ(mods_at(0)->name, "A"), "move up at the top does nothing");
mods_move(N - 1, 1);
CHECK(EQ(mods_at(N - 1)->name, "Ultra \xD0\x96"), "move down at the bottom does nothing");
mods_set_enabled(2, 1);
const char *dis = cfg_get("TOTK_MODS_DISABLED");
CHECK(mods_at(2)->enabled && (!dis || !*dis), "switching C on empties mods_disabled");
mods_set_enabled(0, 0);
CHECK(EQ(cfg_get("TOTK_MODS_DISABLED"), "A"), "switching A off writes it");
CHECK(mods_changed(), "changes seen");
}
remove_tree(root);
printf("mods: %s\n", fails ? "FAIL" : "ok");
return fails ? 1 : 0;
}
+230
View File
@@ -0,0 +1,230 @@
// Horizon OS nfp:user IPC service implementation (nn::nfp::detail::IUserManager & IUser).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "amiibo.h"
#include "host.h"
#include "ipc.h"
#include "kernel.h"
#include "nfp.h"
// Simulated NFC device handle: ID 1
static const uint8_t g_device_handle[8] = { 1, 0, 0, 0, 0, 0, 0, 0 };
typedef struct {
uint32_t activate_event; // writable handle
uint32_t deactivate_event; // writable handle
uint32_t avail_event; // writable handle
} NfpUserState;
static void nfp_user_close(void *p) {
NfpUserState *st = p;
if (st) {
if (st->activate_event) kern_handle_close(st->activate_event);
if (st->deactivate_event) kern_handle_close(st->deactivate_event);
if (st->avail_event) kern_handle_close(st->avail_event);
free(st);
}
rt_log("nfp: IUser session closed");
}
// Handler for nn::nfp::detail::IUser
static void nfp_user_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
NfpUserState *st = (NfpUserState *)s->state;
switch (rq->cmd) {
case 0: { // InitializeNfp(u64 aruid, u64 m_pid, pid, buffer<unknown, 5>)
rt_log("nfp: Initialize");
rs->result = 0;
return;
}
case 1: { // FinalizeNfp()
rt_log("nfp: Finalize");
amiibo_stop_detection();
amiibo_unmount();
rs->result = 0;
return;
}
case 2: { // ListDevicesNfp() -> (s32 total_out, buffer<DeviceHandle, 0xa>)
int32_t count = amiibo_is_device_present() ? 1 : 0;
IPC_OUT(rs, count);
if (count > 0 && rq->nb > 0 && rq->b[0].size >= sizeof(g_device_handle)) {
void *dst = ipc_guest(rq->b[0].addr, sizeof(g_device_handle));
memcpy(dst, g_device_handle, sizeof(g_device_handle));
}
rs->result = 0;
return;
}
case 3: { // StartDetectionNfp(DeviceHandle)
amiibo_start_detection();
if (amiibo_has_tag() && st && st->activate_event) {
kern_event_signal(st->activate_event);
}
rs->result = 0;
return;
}
case 4: { // StopDetectionNfp(DeviceHandle)
amiibo_stop_detection();
rs->result = 0;
return;
}
case 5: { // MountNfp(DeviceHandle, u32 ModelType, u32 MountTarget)
if (!amiibo_has_tag()) {
rs->result = 0x8073u; // Device/Tag not found
return;
}
amiibo_mount();
rs->result = 0;
return;
}
case 6: { // UnmountNfp(DeviceHandle)
amiibo_unmount();
if (st && st->deactivate_event) {
kern_event_signal(st->deactivate_event);
}
rs->result = 0;
return;
}
case 7: { // OpenApplicationAreaNfp(DeviceHandle, u32 access_id)
if (!amiibo_is_mounted()) {
rs->result = 0x927Fu; // not mounted
return;
}
rs->result = 0;
return;
}
case 8: { // GetApplicationAreaNfp(DeviceHandle) -> (u32 size, buffer<data, 0x6>)
if (!amiibo_is_mounted()) {
rs->result = 0x927Fu;
return;
}
uint32_t area_size = 0xD8;
if (rq->nb > 0 && rq->b[0].size > 0) {
void *dst = ipc_guest(rq->b[0].addr, rq->b[0].size);
area_size = (uint32_t)amiibo_get_application_area(dst, (uint32_t)rq->b[0].size);
}
IPC_OUT(rs, area_size);
rs->result = 0;
return;
}
case 13: { // GetTagInfoNfp(DeviceHandle) -> buffer<TagInfo, 0x1a>
if (!amiibo_has_tag() && !amiibo_is_mounted()) {
rs->result = 0x8073u;
return;
}
if (rq->nb > 0 && rq->b[0].size >= 0x58) {
void *dst = ipc_guest(rq->b[0].addr, 0x58);
amiibo_get_tag_info(dst);
}
rs->result = 0;
return;
}
case 14: { // GetRegisterInfoNfp(DeviceHandle) -> buffer<RegisterInfo, 0x1a>
if (!amiibo_is_mounted()) {
rs->result = 0x927Fu;
return;
}
if (rq->nb > 0 && rq->b[0].size >= 0x100) {
void *dst = ipc_guest(rq->b[0].addr, 0x100);
amiibo_get_register_info(dst);
}
rs->result = 0;
return;
}
case 15: { // GetCommonInfoNfp(DeviceHandle) -> buffer<CommonInfo, 0x1a>
if (!amiibo_is_mounted()) {
rs->result = 0x927Fu;
return;
}
if (rq->nb > 0 && rq->b[0].size >= 0x40) {
void *dst = ipc_guest(rq->b[0].addr, 0x40);
amiibo_get_common_info(dst);
}
rs->result = 0;
return;
}
case 16: { // GetModelInfoNfp(DeviceHandle) -> buffer<ModelInfo, 0x1a>
if (!amiibo_is_mounted()) {
rs->result = 0x927Fu;
return;
}
if (rq->nb > 0 && rq->b[0].size >= 0x40) {
void *dst = ipc_guest(rq->b[0].addr, 0x40);
amiibo_get_model_info(dst);
}
rs->result = 0;
return;
}
case 17: { // GetActivateEventHandleNfp(DeviceHandle) -> handle<copy>
if (st && st->activate_event) {
rs->copy[rs->ncopy++] = kern_event_readable(st->activate_event);
}
rs->result = 0;
return;
}
case 18: { // GetDeactivateEventHandleNfp(DeviceHandle) -> handle<copy>
if (st && st->deactivate_event) {
rs->copy[rs->ncopy++] = kern_event_readable(st->deactivate_event);
}
rs->result = 0;
return;
}
case 19: { // GetStateNfp() -> u32 state
uint32_t state = 1; // Initialized
IPC_OUT(rs, state);
rs->result = 0;
return;
}
case 20: { // GetDeviceStateNfp(DeviceHandle) -> u32 device_state
uint32_t dev_state = (uint32_t)amiibo_get_device_state();
IPC_OUT(rs, dev_state);
rs->result = 0;
return;
}
case 21: { // GetNpadIdNfp(DeviceHandle) -> u32 npad_id
uint32_t npad_id = 0; // Handheld / Player 1
IPC_OUT(rs, npad_id);
rs->result = 0;
return;
}
case 22: { // GetApplicationAreaSizeNfp(DeviceHandle) -> u32 size
uint32_t size = 0xD8;
IPC_OUT(rs, size);
rs->result = 0;
return;
}
case 23: { // GetAvailabilityChangeEventHandleNfp() -> handle<copy>
if (st && st->avail_event) {
rs->copy[rs->ncopy++] = kern_event_readable(st->avail_event);
}
rs->result = 0;
return;
}
default:
ipc_unimplemented(s, rq);
}
}
// Handler for nn::nfp::detail::IUserManager (nfp:user)
void nfp_user_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs) {
switch (rq->cmd) {
case 0: { // CreateUserInterface() -> move handle to IUser
NfpUserState *st = calloc(1, sizeof(*st));
if (!st) rt_fatal("nfp: failed to allocate user state");
st->activate_event = kern_event_new();
st->deactivate_event = kern_event_new();
st->avail_event = kern_event_new();
// Signal availability immediately so game detects reader
kern_event_signal(st->avail_event);
uint32_t user_session = ipc_session_new("nfp:IUser", nfp_user_handler, st, nfp_user_close);
rs->move[rs->nmove++] = user_session;
rs->result = 0;
rt_log("nfp: created IUser session handle %#x", user_session);
return;
}
default:
ipc_unimplemented(s, rq);
}
}
+14
View File
@@ -0,0 +1,14 @@
// Nintendo Figurine Platform (nfp:user) Horizon OS IPC service emulation.
#pragma once
#include "ipc.h"
#ifdef __cplusplus
extern "C" {
#endif
// sm: handler for "nfp:user" (nn::nfp::detail::IUserManager)
void nfp_user_manager_handler(IpcSession *s, const IpcRequest *rq, IpcResponse *rs);
#ifdef __cplusplus
}
#endif
+114 -4
View File
@@ -75,14 +75,118 @@ static void put_hdr(uint8_t *p, uint32_t op, uint32_t size) {
memcpy(p, &h, sizeof h);
}
// 1.4.3 initializes and finalizes command buffers about 2000 times a second while a scene loads and several times a
// frame in play, and its two largest per-frame recordings take 1-2 MiB. Freeing the recording buffer at Finalize made
// every new recording grow again from 64 KiB through up to five reallocations and copies, the last ones fresh 1-2 MiB
// blocks the heap takes from the system: new commit and zero-filled pages twice a frame. Those growths were most of the
// process's new commit requests, so when the system ran short for a moment (a Depths load at render scale 200 on 16 GB)
// a 1 MiB growth was the allocation that failed. Finalized buffers therefore wait in a small pool for the next
// recording, and a failed allocation waits for memory instead of ending the game (realloc_wait).
#define POOL_SLOTS 16
#define POOL_MAX_BYTES ((size_t)4 << 20) // a larger buffer (a recording that inlined a big one) goes back to the heap
static PlatMutex pool_lock = PLAT_MUTEX_INIT;
static struct { uint8_t *p; size_t size; } pool[POOL_SLOTS];
static uint32_t npool;
// The largest pooled buffer and its size, or NULL: the largest, so the big per-frame recordings stop growing once the
// pool holds enough buffers of their size.
static uint8_t *pool_take(size_t *size) {
uint8_t *p = NULL;
plat_mutex_lock(&pool_lock);
if (npool) {
uint32_t best = 0;
for (uint32_t i = 1; i < npool; i++)
if (pool[i].size > pool[best].size) best = i;
p = pool[best].p;
*size = pool[best].size;
pool[best] = pool[--npool];
}
plat_mutex_unlock(&pool_lock);
return p;
}
// Keeps a finalized buffer when the pool has room or holds a smaller one (which is freed instead).
static void pool_put(uint8_t *p, size_t size) {
if (!p) return;
if (size <= POOL_MAX_BYTES) {
plat_mutex_lock(&pool_lock);
if (npool < POOL_SLOTS) {
pool[npool].p = p;
pool[npool++].size = size;
p = NULL;
} else {
uint32_t small = 0;
for (uint32_t i = 1; i < npool; i++)
if (pool[i].size < pool[small].size) small = i;
if (pool[small].size < size) {
uint8_t *drop = pool[small].p;
pool[small].p = p;
pool[small].size = size;
p = drop;
}
}
plat_mutex_unlock(&pool_lock);
}
free(p);
}
static void pool_drain(void) {
plat_mutex_lock(&pool_lock);
while (npool) free(pool[--npool].p);
plat_mutex_unlock(&pool_lock);
}
// realloc that waits out a short commit shortage: Windows refuses commit beyond the current limit while it grows a
// system-managed page file, so the same request succeeds a moment later, and a stall of some frames is better than
// ending the game. Frees the pooled buffers first. NULL only after about 30 s without memory; the caller ends the game.
// Never called with a lock held.
static void *realloc_wait(void *old, size_t n, const char *what) {
void *p = realloc(old, n);
if (p) return p;
pool_drain();
static uint32_t reports;
int report = __atomic_fetch_add(&reports, 1, __ATOMIC_RELAXED) < 8;
if (report) {
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_log("nvn: %s of %zu bytes failed (memory commit: this process %.0f MB, the system %.0f of %.0f MB); waiting "
"for memory", what, n, proc, used, limit);
}
uint64_t t0 = plat_now_ms();
while (plat_now_ms() - t0 < 30000) {
plat_sleep_ms(10);
if ((p = realloc(old, n))) {
if (report)
rt_log("nvn: %s of %zu bytes succeeded after %llu ms", what, n,
(unsigned long long)(plat_now_ms() - t0));
return p;
}
}
return NULL;
}
static void cmd_reserve(cmd_t *o, size_t need) {
if (o->seg && o->cur + need + OP_RESERVE <= o->end) return;
if (!o->seg) { // the first record since Initialize: a pooled buffer when one waits
size_t size = 0;
uint8_t *p = pool_take(&size);
if (p) {
o->seg = o->cur = p;
o->end = p + size;
if (need + OP_RESERVE <= size) return;
}
}
size_t used = o->seg ? (size_t)(o->cur - o->seg) : 0;
size_t cap = o->seg ? (size_t)(o->end - o->seg) : 0;
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);
uint8_t *b = realloc_wait(o->seg, cap, "command recording buffer");
// Still failing after the wait: the system has no commit left (the fatal report states it), or 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;
@@ -230,7 +334,7 @@ static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
uint8_t *out = malloc(sizeof(copy_prefix) + n);
uint8_t *out = realloc_wait(NULL, sizeof(copy_prefix) + n, "command copy");
if (!out) rt_fatal("nvn: command copy allocation");
memcpy(out + sizeof(copy_prefix), p, n);
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
@@ -312,6 +416,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);
@@ -323,7 +433,7 @@ NVN_API(nvnCommandBufferInitialize) {
// void nvnCommandBufferFinalize(NVNcommandBuffer *cmd): handles outlive their recorder
NVN_API(nvnCommandBufferFinalize) {
cmd_t *o = cmd_of(c);
free(o->seg);
pool_put(o->seg, (size_t)(o->end - o->seg));
o->seg = o->cur = o->end = NULL;
o->magic = 0;
cb_set_disabled(o, CB_NO_COMMAND | CB_NO_CONTROL, 0);
+8 -3
View File
@@ -197,6 +197,7 @@ NVN_API(nvnTextureInitialize) {
o->magic = NVN_TEX_MAGIC;
o->d = s->d;
o->pool = s->pool;
o->pool_flags = s->pool ? (uint16_t)s->pool->flags : 0;
o->off = s->off;
o->has_layout = s->has_layout;
memcpy(o->layout, s->layout, 8);
@@ -323,8 +324,11 @@ NVN_API(nvnTextureReadTexelsStrided) { read_texels(c, 1); }
NVN_API(nvnTextureWriteTexels) { write_texels(c, 0); }
NVN_API(nvnTextureWriteTexelsStrided) { write_texels(c, 1); }
// void nvnTexture{Flush,Invalidate}Texels(const NVNtexture *t, const NVNtextureView *v, const NVNcopyRegion *r): texel
// access is coherent
NVN_API(nvnTextureFlushTexels) { (void)c; }
// access is coherent; flush marks the storage as written for re-upload
NVN_API(nvnTextureFlushTexels) {
const tex_t *t = arg_p(c, 0);
if (t && t->mem && !nvn_gpu_only((uintptr_t)t->mem)) nvn_guest_written((uintptr_t)t->mem, t->L.size);
}
NVN_API(nvnTextureInvalidateTexels) { (void)c; }
// ---------- Samplers
@@ -633,7 +637,7 @@ NVN_API(nvnWindowSetCrop) {
// ---------- Present pacing
// The vsync thread ticks at a fixed 60 Hz whatever the monitor does, so one game frame lasts `interval` ticks. With
// the real-time frame step (frame_rate.c) game speed does not depend on the interval, so a frame is shown when it is
// ready, at most at the frame limit (TOTK_FPS_LIMIT: 30 to 360 per second or the monitor's rate): holding it to the
// ready, at most at the frame limit (TOTK_FPS_LIMIT: 30 to 240 per second or the monitor's rate): holding it to the
// 2nd or 3rd tick would only add latency and 50 ms frames.
#define VSYNC_HZ 60ull
static uint64_t present_period(const win_t *o) {
@@ -695,6 +699,7 @@ NVN_API(nvnQueuePresentTexture) {
win_t *o = arg_p(c, 1);
int idx = (int)arg_x(c, 2);
movie_tick(c); // ov_movie.c: hand over decoded movie frames that are due; needs the core, so before the wait
res_present(c); // resolution.c: a DLSS mode change sets the game's render size again, between frames
NVN_BLOCKING();
if (o->magic != NVN_WIN_MAGIC || idx < 0 || idx >= o->ntex || idx >= 4 || !o->tex[idx])
rt_fatal("nvn: QueuePresentTexture of image %d on window %p", idx, (void *)o);
+3
View File
@@ -48,6 +48,9 @@ typedef struct {
} tex_builder_t; FITS(tex_builder_t, NVNtextureBuilder);
typedef struct {
uint32_t magic; nvn_texdesc d; uint8_t has_layout, layout[8];
uint16_t pool_flags; // the pool's flags at Initialize (in the padding): a game may finalize and free
// the pool object while the back end still looks the texture up, and the
// driver keeps no pointer to it either
nvn_texlayout L; uint8_t *mem; pool_t *pool; int64_t off;
uint64_t serial; // Initialization serial: the back end uses it to find its texture in the host table (the game may copy the object by value, so object pointers cannot be stored)
} tex_t; FITS(tex_t, NVNtexture);
+108 -21
View File
@@ -12,6 +12,7 @@
#include <string.h>
#include <time.h>
#include "amiibo.h"
#include "cfg.h"
#include "fs.h"
#include "gfx.h"
@@ -25,6 +26,7 @@
#define CHILD_ENV "TOTK_PROBE_SERVICES_CHILD"
#define TRY_WAIT_SYSTEM_EVENT "_ZN2nn2os18TryWaitSystemEventEPNS0_15SystemEventTypeE"
#define DESTROY_SYSTEM_EVENT "_ZN2nn2os18DestroySystemEventEPNS0_15SystemEventTypeE"
typedef struct { const char *key, *name; int fatal; uint64_t addr; int bound, calls; } Ov;
static Ov ovs[] = {
@@ -348,6 +350,13 @@ static int event_works(uint64_t ev, int autoclear) {
return !before && after && again == !autoclear;
}
// A manual-clear system event an override signalled when binding it: set, and still set on a second wait.
static int event_set(uint64_t ev) {
static uint64_t try_wait;
if (!try_wait && !(try_wait = ldr_symbol(TRY_WAIT_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", TRY_WAIT_SYSTEM_EVENT);
return (uint8_t)rt_call(gc, try_wait, 1, ev) && (uint8_t)rt_call(gc, try_wait, 1, ev);
}
static void probe_applet(void) {
uint8_t *report = guest_alloc(0x100);
uint64_t uid[2] = {1, 2};
@@ -357,21 +366,65 @@ static void probe_applet(void) {
"AddInternetConnectionStatus");
CHECK(CALL("nfp.Initialize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 1, "nfp initialize");
uint8_t handles[64];
int32_t count = 7;
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0x8073 && count == 0, "nfp ListDevices");
int32_t count = 0;
CHECK((uint32_t)CALL("nfp.ListDevices", U(handles), U(&count), 4) == 0 && count == 1, "nfp ListDevices");
uint64_t ev = U(guest_alloc(0x100));
CHECK(CALL("nfp.AttachAvailabilityChangeEvent", ev) == 0, "AttachAvailabilityChangeEvent");
int nfp_event = event_works(ev, 0);
CHECK(nfp_event, "the nfp availability event does not work as a manual-clear system event");
// The emulated reader is there from the start (amiibo.c), so the availability event is set when it is attached.
int nfp_event = event_set(ev);
CHECK(nfp_event, "the nfp availability event is not set for the emulated reader, or clears itself");
CHECK((uint32_t)CALL("nfp.GetDeviceState", U(handles)) == 0, "nfp device state");
static const char *const none[] = {"nfp.Mount", "nfp.GetNpadId", "nfp.AttachActivateEvent", "nfp.AttachDeactivateEvent",
"nfp.StartDetection", "nfp.StopDetection", "nfp.GetTagInfo", "nfp.GetModelInfo"};
static const char *const none[] = {"nfp.Mount", "nfp.GetTagInfo", "nfp.GetModelInfo"};
uint8_t out[0x100];
for (size_t i = 0; i < sizeof none / sizeof *none; i++) {
int first_is_handle = !strcmp(none[i], "nfp.Mount") || strstr(none[i], "Detection");
int first_is_handle = !strcmp(none[i], "nfp.Mount");
uint64_t r = first_is_handle ? CALL(none[i], U(handles), 0, 0) : CALL(none[i], U(out), U(handles));
CHECK((uint32_t)r == 0x8073, "%s: %#x, not device-not-found", none[i], (uint32_t)r);
}
CHECK((uint32_t)CALL("nfp.GetNpadId", U(out), U(handles)) == 0, "nfp GetNpadId");
uint64_t activate = U(guest_alloc(0x100)), deactivate = U(guest_alloc(0x100));
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent");
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent");
CHECK(event_works(activate, 0), "the nfp activate event is set without an amiibo on the reader");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection");
CHECK((uint32_t)CALL("nfp.StopDetection", U(handles)) == 0, "nfp StopDetection");
// The game attaches again before each detection: DestroySystemEvent on the attached event, then Attach* on the
// same SystemEventType. Both events must work again afterwards.
static uint64_t destroy;
if (!destroy && !(destroy = ldr_symbol(DESTROY_SYSTEM_EVENT))) rt_fatal("probe-services: no %s", DESTROY_SYSTEM_EVENT);
rt_call(gc, destroy, 1, activate);
rt_call(gc, destroy, 1, deactivate);
CHECK(CALL("nfp.AttachActivateEvent", activate, U(handles)) == 0, "nfp AttachActivateEvent again");
CHECK(CALL("nfp.AttachDeactivateEvent", deactivate, U(handles)) == 0, "nfp AttachDeactivateEvent again");
int reattached = event_works(activate, 0) && event_works(deactivate, 0);
CHECK(reattached, "the nfp events do not work after DestroySystemEvent and a second Attach");
CHECK((uint32_t)CALL("nfp.StartDetection", U(handles)) == 0, "nfp StartDetection again");
// An amiibo's NTAG215 header: UID with its two check bytes, static lock 0F E0, capability container F1 10 FF EE.
// The loader takes it and refuses the same file with a plain NDEF container (E1).
static const uint8_t head[16] = {0x04, 0x11, 0x22, 0x88 ^ 0x04 ^ 0x11 ^ 0x22, 0x33, 0x44, 0x55, 0x66,
0x33 ^ 0x44 ^ 0x55 ^ 0x66, 0x48, 0x0F, 0xE0, 0xF1, 0x10, 0xFF, 0xEE};
uint8_t dump[AMIIBO_DUMP_SIZE] = {0};
memcpy(dump, head, sizeof head);
// Link (Archer): character 0x0100, variant 0, figure type 0, model number 0x0353, series 9, format 02
static const uint8_t id[8] = {0x01, 0x00, 0x00, 0x00, 0x03, 0x53, 0x09, 0x02};
memcpy(dump + 0x54, id, sizeof id);
const char *dump_path = "probe-amiibo.bin";
int amiibo_ok = plat_file_write(dump_path, dump, sizeof dump) && amiibo_load_file(dump_path);
// TagInfo carries the 7-byte UID without its check byte; ModelInfo has the series at 3, the model number
// little-endian at 4 and the figure type at 6
uint8_t ti[0x58], mi[0x40];
amiibo_get_tag_info(ti);
amiibo_get_model_info(mi);
static const uint8_t want_uid[7] = {0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
static const uint8_t want_model[7] = {0x01, 0x00, 0x00, 0x09, 0x53, 0x03, 0x00};
CHECK(amiibo_ok && !memcmp(ti, want_uid, 7) && ti[0x0A] == 7, "amiibo TagInfo UID %02x %02x %02x %02x %02x %02x %02x",
ti[0], ti[1], ti[2], ti[3], ti[4], ti[5], ti[6]);
CHECK(amiibo_ok && !memcmp(mi, want_model, 7), "amiibo ModelInfo %02x %02x %02x %02x %02x %02x %02x", mi[0], mi[1],
mi[2], mi[3], mi[4], mi[5], mi[6]);
dump[0x0C] = 0xE1;
amiibo_ok = amiibo_ok && plat_file_write(dump_path, dump, sizeof dump) && !amiibo_load_file(dump_path);
plat_fs_unlink(dump_path);
CHECK(amiibo_ok, "the amiibo loader refuses a dump with the amiibo capability container, or takes a plain NDEF one");
CHECK(CALL("nfp.Finalize", 0) == 0 && (uint32_t)CALL("nfp.GetState", 0) == 0, "nfp finalize");
CALL("err.ShowError", 0x2a8);
CALL("nndiagStartup", 0);
@@ -380,8 +433,9 @@ static void probe_applet(void) {
CALL("err.InitializeContextWriterApi", 0);
CALL("pctl.Initialize", 0);
CALL("diag.InitializeApplicationAbortObserver", 0);
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, no NFC device, "
"availability event (bound through the SDK's CreateSystemEvent) %s\n", nfp_event ? "ok" : "FAILED");
printf("applet/prepo/nfp/err: launch parameter none, volume balance, play reports local, an NFC reader without "
"an amiibo, availability event (bound through the SDK's CreateSystemEvent) %s, activate and deactivate events "
"destroyed and attached again %s\n", nfp_event ? "ok" : "FAILED", reattached ? "ok" : "FAILED");
}
static void probe_account(void) {
@@ -811,17 +865,23 @@ static void probe_fonts(void) {
(unsigned long long)size, path);
}
// The host catalog (ui_text.c): every menu id has its own English and Russian text, the software keyboard keeps its
// game-language rows, and menu_text follows TOTK_MENU_LANG.
// The host catalog (ui_text.c): every menu id has its own English, Russian, Brazilian Portuguese and Simplified
// Chinese text, the software keyboard keeps its game-language rows, and menu_text follows TOTK_MENU_LANG.
static void probe_menu_text(void) {
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko"};
static const char *const langs[] = {"en", "ja", "fr", "de", "it", "es", "nl", "pt", "ru", "zh-Hans", "zh-Hant", "ko",
"pt-BR"};
for (int id = UI_MENU_FIRST; id <= UI_MENU_LAST_ID; id++) {
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id), "menu text id %d lacks en or ru", id);
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id), "menu text id %d is empty", id);
CHECK(ui_text_defined("en", (UiTextId)id) && ui_text_defined("ru", (UiTextId)id) &&
ui_text_defined("pt-BR", (UiTextId)id), "menu text id %d lacks en, ru or pt-BR", id);
CHECK(*ui_text_in("en", (UiTextId)id) && *ui_text_in("ru", (UiTextId)id) && *ui_text_in("pt-BR", (UiTextId)id),
"menu text id %d is empty", id);
// the description pane keeps the whole text and a note after it (option_rows.cpp desc_set)
CHECK(strlen(ui_text_in("en", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES, "menu text id %d is too long for the "
"description pane", id);
strlen(ui_text_in("ru", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("pt-BR", (UiTextId)id)) + 256 < UI_DESC_BYTES &&
strlen(ui_text_in("zh-Hans", (UiTextId)id)) + 256 < UI_DESC_BYTES,
"menu text id %d is too long for the description pane", id);
CHECK(ui_text_defined("zh-Hans", (UiTextId)id), "menu text id %d lacks Simplified Chinese", id);
}
CHECK(!ui_text_defined("en", UI_TEXT_COUNT) && !*ui_text_in("ru", UI_TEXT_COUNT), "an id past the catalog gives text");
// other languages fall back to English for the menu, and keep their own keyboard texts
@@ -829,22 +889,49 @@ static void probe_menu_text(void) {
CHECK(!strcmp(ui_text_in("ru", UI_SWKBD_OK), "ОК") && !strcmp(ui_text_in("de", UI_SWKBD_CANCEL), "Abbrechen") &&
!strcmp(ui_text_in("en", UI_SWKBD_OK), "OK"), "keyboard texts changed");
// language names: the same string in every row
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_KO; id++)
for (int id = UI_MENU_LANGNAME_EN; id <= UI_MENU_LANGNAME_ES_419; id++)
for (size_t i = 1; i < sizeof langs / sizeof *langs; i++)
CHECK(!strcmp(ui_text_in(langs[i], (UiTextId)id), ui_text_in("en", (UiTextId)id)), "language name %d differs in %s", id, langs[i]);
CHECK(!strcmp(ui_text_in("en", UI_MENU_LANGNAME_RU), "Русский"), "Russian is not shown as Russian");
// game language list: pt-BR only where the build ships USpt; the Switch variants are entries of their own
{
UiGameLang gl[16];
int n121 = ui_text_game_langs(121, gl, 16), has[16] = {0};
for (int i = 0; i < n121; i++) CHECK(strcmp(gl[i].code, "pt-BR"), "1.2.1 game language list offers pt-BR");
int n143 = ui_text_game_langs(143, gl, 16), pt = 0;
for (int i = 0; i < n143; i++) {
pt |= !strcmp(gl[i].code, "pt-BR");
for (int j = 0; j < i; j++) CHECK(strcmp(gl[i].code, gl[j].code), "duplicate game language %s", gl[i].code);
}
CHECK(pt && n143 == n121 + 1, "1.4.3 game language list lacks pt-BR");
static const char *const want[] = {"en-GB", "fr-CA", "es-419"};
for (int w = 0; w < 3; w++)
for (int i = 0; i < n143; i++) has[w] |= !strcmp(gl[i].code, want[w]);
CHECK(has[0] && has[1] && has[2], "game language list lacks en-GB, fr-CA or es-419");
}
// the overlay formats keep their conversions
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f"),
CHECK(strstr(ui_text_in("ru", UI_MENU_OV_GAME_PCT), "%.0f%%") && strstr(ui_text_in("ru", UI_MENU_OV_MAX_MS), "%.1f") &&
strstr(ui_text_in("ru", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("ru", UI_MENU_OV_LOW01), "%.0f"),
"overlay format lost its conversion");
CHECK(strstr(ui_text_in("pt-BR", UI_MENU_OV_GAME_PCT), "%.0f%%") &&
strstr(ui_text_in("pt-BR", UI_MENU_OV_MAX_MS), "%.1f") &&
strstr(ui_text_in("pt-BR", UI_MENU_OV_LOW1), "%.0f") && strstr(ui_text_in("zh-Hans", UI_MENU_OV_LOW01), "%.0f"), "pt-BR overlay format lost its conversion");
// a Brazilian game language gets the Brazilian keyboard row, Portugal's Portuguese keeps its own
CHECK(!strcmp(ui_text_lang("pt-BR"), "pt-BR") && !strcmp(ui_text_lang("pt"), "pt") &&
strcmp(ui_text_in("pt-BR", UI_SWKBD_PROMPT), ui_text_in("pt", UI_SWKBD_PROMPT)), "pt-BR keyboard row not used");
// menu_text follows the setting live
cfg_set("TOTK_MENU_LANG", "ru");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("ru", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=ru");
cfg_set("TOTK_MENU_LANG", "pt-BR");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("pt-BR", UI_MENU_CONTINUE), "menu_text ignores MENU_LANG=pt-BR");
cfg_set("TOTK_MENU_LANG", "en");
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in("en", UI_MENU_CONTINUE), "menu_text did not follow MENU_LANG=en");
cfg_set("TOTK_MENU_LANG", "zh-Hans");
CHECK(menu_text(UI_MENU_TITLE) == ui_text_in("zh-Hans", UI_MENU_TITLE), "menu_text ignores MENU_LANG=zh-Hans");
cfg_set("TOTK_MENU_LANG", "");
const char *sys = plat_ui_language();
int ru = !strncmp(sys, "ru", 2);
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(ru ? "ru" : "en", UI_MENU_CONTINUE), "menu_text auto ignores the system language");
const char *sys_lang = ui_text_lang(plat_ui_language());
const char *auto_lang = ui_text_defined(sys_lang, UI_MENU_TITLE) ? sys_lang : "en";
CHECK(menu_text(UI_MENU_CONTINUE) == ui_text_in(auto_lang, UI_MENU_CONTINUE), "menu_text auto ignores the system language");
}
static void probe_main(Cpu *c) {
+112 -13
View File
@@ -19,6 +19,7 @@
#include <string.h>
#include <time.h>
#include "amiibo.h"
#include "cfg.h"
#include "fs.h"
#include "host.h"
@@ -266,26 +267,124 @@ static void ov_report_save(Cpu *c) { // Result PlayReport::Save() and Save(con
}
static void ov_result_ok(Cpu *c) { ret_x(c, 0); } // RequestImmediateTransmission, AddInternetConnectionStatus
// nn::nfp: no NFC reader
// nn::nfp: NFC reader & amiibo support
static volatile int32_t nfp_state;
static void ov_nfp_initialize(Cpu *c) { plat_xchg32(&nfp_state, 1); ret_x(c, 0); }
static void ov_nfp_finalize(Cpu *c) { plat_xchg32(&nfp_state, 0); ret_x(c, 0); }
static void ov_nfp_finalize(Cpu *c) {
plat_xchg32(&nfp_state, 0);
amiibo_stop_detection();
amiibo_unmount();
ret_x(c, 0);
}
static void ov_nfp_state(Cpu *c) { ret_x(c, (uint64_t)nfp_state); }
// Result ListDevices(DeviceHandle *out, int *count, int max)
static void ov_nfp_list(Cpu *c) {
void *out = arg_p(c, 0);
int32_t *count = arg_p(c, 1);
if (count) *count = 0;
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
int32_t max = (int32_t)arg_x(c, 2);
if (!amiibo_is_device_present()) {
if (count) *count = 0;
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
return;
}
if (out && max > 0) {
static const uint8_t dev[8] = { 1, 0, 0, 0, 0, 0, 0, 0 };
memcpy(out, dev, sizeof dev);
}
if (count) *count = 1;
ret_x(c, 0);
}
// Result AttachAvailabilityChangeEvent(os::SystemEventType *event)
static void ov_nfp_availability(Cpu *c) {
uint64_t ev = arg_x(c, 0);
if (!ev) rt_fatal("nn::nfp::AttachAvailabilityChangeEvent: null event");
kern_bind_system_event(c, ev, 0);
kern_signal_system_event(ev);
ret_x(c, 0);
}
// DeviceState GetDeviceState(const DeviceHandle &)
static void ov_nfp_device_state(Cpu *c) {
(void)c;
ret_x(c, (uint64_t)amiibo_get_device_state());
}
// Result Mount(const DeviceHandle &, ModelType, MountTarget)
static void ov_nfp_mount(Cpu *c) {
(void)c;
if (!amiibo_has_tag()) {
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
return;
}
amiibo_mount();
ret_x(c, 0);
}
// Result GetNpadId(u32 *npad_id, const DeviceHandle &)
static void ov_nfp_get_npad_id(Cpu *c) {
uint32_t *npad = arg_p(c, 0);
if (npad) *npad = 0; // Handheld / Player 1
ret_x(c, 0);
}
// Result AttachActivateEvent(os::SystemEventType *event, const DeviceHandle &)
static void ov_nfp_attach_activate(Cpu *c) {
uint64_t ev = arg_x(c, 0);
if (ev) {
kern_bind_system_event(c, ev, 0);
if (amiibo_has_tag()) kern_signal_system_event(ev);
}
ret_x(c, 0);
}
// Result AttachDeactivateEvent(os::SystemEventType *event, const DeviceHandle &)
static void ov_nfp_attach_deactivate(Cpu *c) {
uint64_t ev = arg_x(c, 0);
if (ev) {
kern_bind_system_event(c, ev, 0);
}
ret_x(c, 0);
}
// Result StartDetection(const DeviceHandle &)
static void ov_nfp_start_detection(Cpu *c) {
(void)c;
amiibo_start_detection();
ret_x(c, 0);
}
// Result StopDetection(const DeviceHandle &)
static void ov_nfp_stop_detection(Cpu *c) {
(void)c;
amiibo_stop_detection();
ret_x(c, 0);
}
// Result GetTagInfo(TagInfo *out, const DeviceHandle &)
static void ov_nfp_get_tag_info(Cpu *c) {
void *out = arg_p(c, 0);
if (!amiibo_has_tag() && !amiibo_is_mounted()) {
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
return;
}
if (out) amiibo_get_tag_info(out);
ret_x(c, 0);
}
// Result GetModelInfo(ModelInfo *out, const DeviceHandle &)
static void ov_nfp_get_model_info(Cpu *c) {
void *out = arg_p(c, 0);
if (!amiibo_is_mounted() && !amiibo_has_tag()) {
ret_x(c, NFP_R_DEVICE_NOT_FOUND);
return;
}
if (out) amiibo_get_model_info(out);
ret_x(c, 0);
}
static void ov_nfp_no_device(Cpu *c) { ret_x(c, NFP_R_DEVICE_NOT_FOUND); }
static void ov_nfp_device_state(Cpu *c) { ret_x(c, 0); } // DeviceState GetDeviceState(const DeviceHandle &): none
// void err::ShowError(Result): the game's error dialog, logged
static void ov_show_error(Cpu *c) {
@@ -795,14 +894,14 @@ HOST_OVERRIDE("_ZN2nn3nfp8GetStateEv", ov_nfp_state);
HOST_OVERRIDE("_ZN2nn3nfp11ListDevicesEPNS0_12DeviceHandleEPii", ov_nfp_list);
HOST_OVERRIDE("_ZN2nn3nfp29AttachAvailabilityChangeEventEPNS_2os15SystemEventTypeE", ov_nfp_availability);
HOST_OVERRIDE("_ZN2nn3nfp14GetDeviceStateERKNS0_12DeviceHandleE", ov_nfp_device_state);
HOST_OVERRIDE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE", ov_nfp_no_device);
HOST_OVERRIDE("_ZN2nn3nfp5MountERKNS0_12DeviceHandleENS0_9ModelTypeENS0_11MountTargetE", ov_nfp_mount);
HOST_OVERRIDE("_ZN2nn3nfp9GetNpadIdEPjRKNS0_12DeviceHandleE", ov_nfp_get_npad_id);
HOST_OVERRIDE("_ZN2nn3nfp19AttachActivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_attach_activate);
HOST_OVERRIDE("_ZN2nn3nfp21AttachDeactivateEventEPNS_2os15SystemEventTypeERKNS0_12DeviceHandleE", ov_nfp_attach_deactivate);
HOST_OVERRIDE("_ZN2nn3nfp14StartDetectionERKNS0_12DeviceHandleE", ov_nfp_start_detection);
HOST_OVERRIDE("_ZN2nn3nfp13StopDetectionERKNS0_12DeviceHandleE", ov_nfp_stop_detection);
HOST_OVERRIDE("_ZN2nn3nfp10GetTagInfoEPNS0_7TagInfoERKNS0_12DeviceHandleE", ov_nfp_get_tag_info);
HOST_OVERRIDE("_ZN2nn3nfp12GetModelInfoEPNS0_9ModelInfoERKNS0_12DeviceHandleE", ov_nfp_get_model_info);
HOST_OVERRIDE("_ZN2nn3err9ShowErrorENS_6ResultE", ov_show_error);
HOST_OVERRIDE("nndiagStartup", ov_diag_startup);
+8
View File
@@ -183,6 +183,9 @@ static uint64_t ft_frame(uint64_t frame, uint64_t now, uint64_t d) {
// process memory, and which side holds the frame back. pace: the frame waited for pacing (vsync, the frame limiter) at
// least 15% of the window; gpu: the GPU was busy for 80% of the frame interval or more (without timestamps: threads
// waited for fences for half the interval or more); cpu otherwise.
// With calibrated timestamps a third line splits the GPU's idle time between timed parts: the part was submitted only
// after the previous one had ended (the GPU waited for the CPU's replay or the game), or it was already queued (untimed
// work such as uploads and presents ran there, or a wait held it).
static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share, uint64_t n) {
PerfGpu g;
memset(&g, 0, sizeof g);
@@ -205,6 +208,11 @@ static void perf_gpu_line(uint64_t frame, double interval_ms, double pace_share,
rt_log("perf: frame %llu: %s; fence wait per frame: render thread %.2f ms, other threads %.2f ms; %s; ram working set "
"%.0f MiB, private %.0f MiB; bound: %s",
(unsigned long long)frame, gs, rw, ow, vs, ws, priv, bound);
if (g.have_gpu && g.have_idle)
rt_log("perf: frame %llu: gpu idle per frame %.2f ms between timed parts: %.2f ms waiting for the next submission, "
"%.2f ms with it queued (untimed work or waits); %.1f gaps over 0.5 ms, longest %.2f ms; submit to start avg %.2f ms",
(unsigned long long)frame, g.idle_ns / 1e6 / n, g.starved_ns / 1e6 / n, (g.idle_ns - g.starved_ns) / 1e6 / n,
(double)g.idle_gaps / n, g.max_gap_ns / 1e6, g.gpu_parts ? g.queued_ns / 1e6 / g.gpu_parts : 0.0);
}
int rt_ab(void) {
+36
View File
@@ -32,6 +32,14 @@ static float g_ring[PERF_HISTORY];
static uint64_t g_ring_at[PERF_HISTORY]; // when each frame ended
static int g_ring_n, g_ring_pos;
// The lows: a histogram of the frame times (perf.c's 0.25 ms bins) over the last LOW_SLICES seconds, kept as one slice
// per second so the oldest second drops out whole. Per frame: one increment. The percentiles are read twice a second.
#define LOW_BINS 1024
#define LOW_SLICES 10
static uint16_t g_low_hist[LOW_SLICES][LOW_BINS];
static int g_low_cur;
static uint64_t g_low_t0;
// The per-frame counters, summed since t0 (the overlay's lines are averages over half a second).
static struct {
uint64_t t0, frames, gpu0;
@@ -47,6 +55,14 @@ void perfmon_frame(uint64_t now, uint64_t interval_ns, const uint64_t *wait_ns,
g_ring_at[g_ring_pos] = now;
g_ring_pos = (g_ring_pos + 1) % PERF_HISTORY;
if (g_ring_n < PERF_HISTORY) g_ring_n++;
if (now - g_low_t0 >= 1000000000ull) { // the next second: its slice starts empty
g_low_cur = (g_low_cur + 1) % LOW_SLICES;
memset(g_low_hist[g_low_cur], 0, sizeof g_low_hist[0]);
g_low_t0 = now;
}
uint64_t bin = interval_ns / 250000ull;
uint16_t *h = &g_low_hist[g_low_cur][bin < LOW_BINS ? bin : LOW_BINS - 1];
if (*h < 65535) (*h)++;
}
g_win.frames++;
g_win.replay += replay_ns / 1e6;
@@ -76,9 +92,27 @@ static void frames_summary(PerfStats *s, uint64_t now) { // (lock held)
}
}
// The fps of the slowest 1% and 0.1% of the frames in the window (the bin's upper edge, as perf.c's summary does); -1
// when the window holds too few frames. (lock held)
static void lows_summary(PerfStats *s) {
uint32_t total = 0;
for (int k = 0; k < LOW_SLICES; k++)
for (int b = 0; b < LOW_BINS; b++) total += g_low_hist[k][b];
s->low1_fps = s->low01_fps = -1;
if (total < 100) return;
uint32_t cut1 = (total + 99) / 100, cut01 = (total + 999) / 1000, acc = 0;
for (int b = LOW_BINS - 1; b >= 0 && s->low1_fps < 0; b--) {
for (int k = 0; k < LOW_SLICES; k++) acc += g_low_hist[k][b];
float fps = 1000.0f / ((b + 1) * 0.25f);
if (s->low01_fps < 0 && acc >= cut01) s->low01_fps = fps;
if (acc >= cut1) s->low1_fps = fps;
}
}
// Every half second: the window's sums as per-frame averages into g_stats, and a new window. (lock held)
static void counters_summary(uint64_t now) {
if (now - g_win.t0 < 500000000ull) return;
lows_summary(&g_stats);
uint64_t gpu = perf_gpu_busy_total();
if (g_win.t0 && g_win.frames) {
double n = (double)g_win.frames;
@@ -342,6 +376,8 @@ void perfmon_mode(int mode) {
}
if (!mode) { // the next start begins a new graph and a new counter window
g_ring_n = g_ring_pos = 0;
memset(g_low_hist, 0, sizeof g_low_hist);
g_low_t0 = 0;
memset(&g_win, 0, sizeof g_win);
}
plat_mutex_unlock(&g_lock);
+2
View File
@@ -17,6 +17,8 @@ extern "C" {
typedef struct {
// the game's frames (presents), over the last second
float fps, frame_ms_avg, frame_ms_max;
// fps of the slowest 1% and 0.1% of the frames over the last ten seconds (-1: too few frames yet)
float low1_fps, low01_fps;
float frame_ms[PERF_HISTORY]; // oldest first
// CPU: the whole machine and this process, percent of all logical processors
char cpu_name[64];
+201 -5
View File
@@ -9,6 +9,7 @@
#include <linux/fs.h>
#include <linux/futex.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <signal.h>
@@ -21,9 +22,11 @@
#include <sys/prctl.h>
#include <sys/random.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/un.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <time.h>
@@ -915,8 +918,9 @@ uint64_t plat_mem_written(uint64_t addr, uint64_t n, uint64_t *pages, uint64_t m
}
// One handler for the synchronous faults. A SIGSEGV whose page-fault error code has the write bit set goes to the
// write-fault hook first (memory.c thaws a frozen region); a fault in this executable's code then goes to the crash
// hook (main.c); anything else gets the default action. Runs on the thread's signal stack.
// write-fault hook first (memory.c thaws a frozen region); any other fault, in this executable or in a library (Mesa,
// SDL), then goes to the crash hook (main.c), which reports it and ends the process. Without a crash hook the fault
// gets the default action. Runs on the thread's signal stack.
static int (*fault_hook)(uint64_t addr);
static void (*crash_fn)(const PlatCrash *c);
@@ -926,10 +930,17 @@ static void fault_handler(int sig, siginfo_t *si, void *uc) {
if (sig == SIGSEGV && fault_hook && (err & 2) && fault_hook((uint64_t)(uintptr_t)si->si_addr)) return;
uintptr_t rip = (uintptr_t)u->uc_mcontext.gregs[REG_RIP];
Dl_info at, self;
if (crash_fn && dladdr((void *)rip, &at) && dladdr((void *)plat_crash_hook, &self) && at.dli_fbase == self.dli_fbase) {
if (crash_fn) {
int found = dladdr((void *)rip, &at) && at.dli_fname;
PlatCrash c = {sig == SIGSEGV ? "access violation" : sig == SIGBUS ? "bus error"
: sig == SIGILL ? "illegal instruction" : "arithmetic exception",
(uint32_t)sig, rip, rip - (uintptr_t)self.dli_fbase, -1, 0};
(uint32_t)sig, rip, found ? rip - (uintptr_t)at.dli_fbase : rip, -1, 0};
if (!found) {
c.module = "";
} else if (!dladdr((void *)plat_crash_hook, &self) || at.dli_fbase != self.dli_fbase) {
const char *n = strrchr(at.dli_fname, '/');
c.module = n ? n + 1 : at.dli_fname;
}
if (sig == SIGSEGV || sig == SIGBUS)
c.access = (err & 16) ? 8 : (err & 2) ? 1 : 0, c.addr = (uint64_t)(uintptr_t)si->si_addr;
crash_fn(&c);
@@ -1037,6 +1048,8 @@ const void *plat_file_map(PlatFile *f) {
return v == MAP_FAILED ? NULL : v;
}
int plat_fs_os_error(void) { return errno; }
static int fs_err(int e) {
switch (e) {
case ENOENT: case ENOTDIR: return PLAT_FS_NOT_FOUND;
@@ -1094,6 +1107,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 +1191,16 @@ 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. noinline: a fiber
// may resume on another thread (platform.h), and a caller that inlined this could keep the address of tid it computed
// before a plat_fiber_switch; as a call it reads the slot of the thread it runs on.
__attribute__((noinline)) 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) {
@@ -1234,6 +1262,8 @@ int plat_os_version(char *out, size_t cap) {
return snprintf(out, cap, "%s (Linux %s)", name, u.release) < (int)cap;
}
int plat_is_wine(void) { return 0; }
int plat_stderr_path(char *out, size_t cap) {
struct stat st;
if (fstat(2, &st) || !S_ISREG(st.st_mode)) return 0;
@@ -1635,3 +1665,169 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
while (k < 0 && errno == EINTR);
return (long)k;
}
// ---- local IPC (platform.h): Discord's Rich Presence Unix domain socket
struct PlatIpc { int fd; };
PlatIpc *plat_ipc_connect(const char *name) {
const char *dirs[] = {getenv("DISCORD_IPC_PATH"), getenv("XDG_RUNTIME_DIR"), getenv("TMPDIR"), "/tmp"};
for (size_t d = 0; d < sizeof dirs / sizeof *dirs; d++) {
if (!dirs[d] || !*dirs[d]) continue;
char path[sizeof ((struct sockaddr_un *)0)->sun_path];
if ((size_t)snprintf(path, sizeof path, "%s/%s", dirs[d], name) >= sizeof path) continue;
int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
if (fd < 0) return NULL;
struct sockaddr_un sa = {0};
sa.sun_family = AF_UNIX;
strcpy(sa.sun_path, path);
if (connect(fd, (struct sockaddr *)&sa, sizeof sa) == 0) {
PlatIpc *p = malloc(sizeof *p);
if (!p) {
close(fd);
return NULL;
}
p->fd = fd;
return p;
}
close(fd); // nothing listens at this path: the next directory may have it
}
return NULL;
}
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
struct pollfd pf = {.fd = p->fd, .events = POLLIN};
int r = poll(&pf, 1, (int)ms);
if (r <= 0) return r < 0 && errno != EINTR ? -1 : 0;
ssize_t got = recv(p->fd, buf, n, 0);
return got > 0 ? (int)got : -1; // 0 bytes: the peer closed the socket
}
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
const uint8_t *at = buf;
while (n) {
ssize_t put = send(p->fd, at, n, MSG_NOSIGNAL); // a closed peer is an error, not a SIG_PIPE
if (put <= 0) {
if (put < 0 && errno == EINTR) continue;
return 0;
}
at += put, n -= put;
}
return 1;
}
void plat_ipc_close(PlatIpc *p) {
if (!p) return;
close(p->fd);
free(p);
}
// ---- restart
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and start time (field 22 of /proc/<pid>/stat,
// clock ticks after boot): a process that has only reused the id is not waited for. Waiting keeps the new process off
// the old one's files (the log it moves at start, the saves) until the old one is gone. The new process gets no
// descriptor past stderr (glibc 2.34's closefrom file action), and the default signal mask and dispositions, whatever
// the calling thread had.
#define RESTART_WAIT "TOTK_RESTART_WAIT"
// The start time of process pid (field 22 of its /proc stat line), 0 when it is gone.
static unsigned long long process_start(int pid) {
char path[64], buf[1024];
snprintf(path, sizeof path, "/proc/%d/stat", pid);
FILE *f = fopen(path, "r");
if (!f) return 0;
size_t n = fread(buf, 1, sizeof buf - 1, f);
fclose(f);
buf[n] = 0;
char *p = strrchr(buf, ')'); // the command name may hold spaces
unsigned long long start = 0;
if (!p || sscanf(p + 2, "%*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %*u %*u %*d %*d %*d %*d %*d %*d %llu",
&start) != 1)
return 0;
return start;
}
// glibc 2.34 added posix_spawn_file_actions_addclosefrom_np, and the release links against the Steam Runtime's 2.31: a
// weak reference, used when the running glibc has it. Without it the child inherits the open descriptors.
extern int posix_spawn_file_actions_addclosefrom_np(posix_spawn_file_actions_t *fa, int from) __attribute__((weak));
static void spawn_close_from(posix_spawn_file_actions_t *fa, int from) {
if (posix_spawn_file_actions_addclosefrom_np) posix_spawn_file_actions_addclosefrom_np(fa, from);
}
int plat_restart(void) {
// the arguments, NUL-separated, from /proc/self/cmdline
static char args[16384];
char *argv[256];
int argc = 0;
FILE *f = fopen("/proc/self/cmdline", "rb");
size_t n = f ? fread(args, 1, sizeof args - 1, f) : 0;
if (f) fclose(f);
args[n] = 0;
for (size_t i = 0; i < n && argc < 255; i += strlen(args + i) + 1) argv[argc++] = args + i;
argv[argc] = NULL;
char exe[PATH_MAX], wait[64];
if (!argc || !plat_exe_path(exe, sizeof exe)) return 0;
snprintf(wait, sizeof wait, "%d.%llu", (int)getpid(), process_start((int)getpid()));
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
spawn_close_from(&fa, 3);
posix_spawnattr_t at;
posix_spawnattr_init(&at);
sigset_t none, all;
sigemptyset(&none);
sigfillset(&all);
posix_spawnattr_setsigmask(&at, &none);
posix_spawnattr_setsigdefault(&at, &all);
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF);
setenv(RESTART_WAIT, wait, 1);
pid_t pid;
int e = posix_spawn(&pid, exe, &fa, &at, argv, environ);
unsetenv(RESTART_WAIT);
posix_spawnattr_destroy(&at);
posix_spawn_file_actions_destroy(&fa);
return e == 0;
}
int plat_spawn(const char *const *argv, const char *cwd) {
posix_spawn_file_actions_t fa;
posix_spawn_file_actions_init(&fa);
spawn_close_from(&fa, 3);
posix_spawn_file_actions_addchdir_np(&fa, cwd);
posix_spawnattr_t at;
posix_spawnattr_init(&at);
sigset_t none, all;
sigemptyset(&none);
sigfillset(&all);
posix_spawnattr_setsigmask(&at, &none);
posix_spawnattr_setsigdefault(&at, &all);
posix_spawnattr_setflags(&at, POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSID);
pid_t pid;
int e = posix_spawn(&pid, argv[0], &fa, &at, (char *const *)argv, environ);
posix_spawnattr_destroy(&at);
posix_spawn_file_actions_destroy(&fa);
return e == 0;
}
int plat_restart_wait(uint32_t timeout_ms) {
const char *v = getenv(RESTART_WAIT);
if (!v) return 0;
int pid = 0;
unsigned long long start = 0;
int ok = sscanf(v, "%d.%llu", &pid, &start) == 2 && pid > 0 && start;
unsetenv(RESTART_WAIT); // the processes this one starts do not wait again
if (!ok) return 0;
// The descriptor is taken first: a start time that still matches then proves it is that process, not a new one with
// the id. It reads as ready once the process has exited, reaped or not.
int fd = (int)syscall(SYS_pidfd_open, pid, 0);
if (fd < 0) return 0; // already gone
int r = 0;
if (process_start(pid) == start) {
struct pollfd p = {fd, POLLIN, 0};
do r = poll(&p, 1, (int)timeout_ms);
while (r < 0 && errno == EINTR);
r = r > 0 ? 1 : -1;
}
close(fd);
return r;
}
Loaded 100 of 191 files, more files were not shown because too many files have changed in this diff. Show more