142 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
FaserDocs 86a9ea6d53 Read installer sources once as UTF-8 in the catalog test 2026-11-01 15:54:57 +01: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
113 changed files with 8151 additions and 946 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.
+10 -3
View File
@@ -17,19 +17,24 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md).
- Native code, not emulation
- 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
- 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), 21:9 and 32:9 (ultrawide) game resolutions, with the camera and the interface in that shape
- 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
- 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/tester-guide.md](docs/tester-guide.md), "Mods")
([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
@@ -44,7 +49,9 @@ Requirements: Windows 10 or later, an x86-64 CPU with SSE4.2 (AVX2/FMA machines
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.
### Linux
+1 -1
View File
@@ -1 +1 @@
0.3.1
0.3.7
+11 -5
View File
@@ -84,7 +84,7 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- 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.
- 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
@@ -99,8 +99,9 @@ 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
@@ -117,8 +118,13 @@ Every guest thread is a host thread. TotK assumes Horizon's scheduler: three app
- 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; motion vectors are camera-only, reprojected from the scene depth through this and last frame's
view-projection of the scene camera block (constant bank c4). `fxaa` switches the game's own FXAA pass.
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`).
+6 -1
View File
@@ -95,7 +95,12 @@ of them.
```
`--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.
`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`
+12 -3
View File
@@ -1,6 +1,6 @@
# 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 ([tester-guide.md](tester-guide.md), "Mods"). Open the menu (F1) and switch the cheats on the Cheats tab. The switches are saved under `[cheats]` in `totk.ini`.
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
@@ -12,7 +12,16 @@ The port reads Atmosphère cheat files, the format Switch homebrew and the emula
| 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: `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.
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.
@@ -39,7 +48,7 @@ Standard Atmosphère: a `[name]` line starts a cheat, `{name}` a master cheat th
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. Renaming a mod's folder resets its cheats' switches to off.
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
+1 -1
View File
@@ -94,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_LSTICK_DEADZONE` and `TOTK_RSTICK_DEADZONE` (the *Left/Right stick dead zone* rows 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.
`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`.
+33 -6
View File
@@ -9,19 +9,21 @@ 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.1.0, 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
@@ -44,7 +46,7 @@ loads the saves 1.4.3 wrote.
## Setup steps
1. **Check** before writing anything: the files open with the keys, the update is 1.1.0, 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 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.
@@ -71,7 +73,13 @@ loads the saves 1.4.3 wrote.
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. **Clean up** the temp folder, except the generated C and the objects (about 1.3 GB), and write
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`).
@@ -149,11 +157,23 @@ 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. Building the release also needs `make` and `pkg-config`. The bundled Tcl/Tk 9.0.4 is rebuilt 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:
@@ -201,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.
+4
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
+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.
+8 -28
View File
@@ -39,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.
@@ -54,34 +58,10 @@ The line `vk: using gpu N: <name>` says which one ran.
## Mods
Mods made for emulators go into `mods/` next to `totk.exe`, one folder per mod, unpacked as they come:
`mods/<name>/romfs/...` (the Atmosphere layout `mods/<name>/atmosphere/contents/0100F2C0115B6000/romfs` works
too). The game loads them at the next start. The Mods tab of the in-game menu (F1) lists them: switch a mod off, or move
it up or down. When two mods change the same file, the higher one wins.
- RomFS mods (textures, models, text, gameplay files) work.
- Mods packaged for TKMM (`.tkcl`, "TKMM only") and mods that change the same files: merge them in TKMM and set an
export location to an empty folder inside `mods/` (for example `mods/TKMM`). That folder is then one mod here.
- Files made for another game version (names with `.Product.<number>.` that this version does not have) are left out;
the Mods tab counts them. Look for the mod's release for your game version.
- The resource size table (`System/Resource/ResourceSizeTable.Product.<n>...rsizetable.zs`), which tells the game how
much memory each file needs, is rebuilt at every start with mods enabled: the tables the mods ship are merged and
the entries of files a mod made larger or added are raised. The log line `RomFS: resource size table: ...` says
how many. A mod's table for another game version still counts for that mod's own files.
- Mods that patch the game code (`exefs/*.ips`, `exefs/*.pchtxt`, or `atmosphere/exefs_patches/<name>/*.ips`) work
when the patches are for this game version and change only code: the game rebuilds itself with them at the next
start (the setup's rebuild window, a few minutes) and switches them with the mod. Patches for another version are
counted and left out; a file that writes the game's data, or that the port cannot express, keeps the whole mod off
and the Mods tab says why. A patch over one of the port's own patches turns that built-in patch off while the mod
is on (the Mods tab names it).
- Mods with their own code (`exefs/subsdk*` or a replaced `exefs/main`, such as UltraCam) never load, their RomFS files
included.
- Cheats: an Atmosphère cheat file goes into a mod's `cheats` folder (`mods/<name>/cheats/<build id>.txt`, the
build id `277178B7DBA1B6D4` for 1.4.3, `9B4E43650501A4D4` for 1.2.1). The Cheats tab of the menu switches them.
Cheats that write memory work at the next start; for cheats that change the game's code the game opens the setup's
rebuild window at start and comes back after it (a few minutes). The port ships no cheat files. Details: [cheats.md](cheats.md).
The log names every mod at the start (`mods: <name>: ...`) and how many of its files it serves.
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 -1
View File
@@ -4,7 +4,7 @@
[--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 seven 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.
+10 -4
View File
@@ -1,5 +1,5 @@
"""Command line: python -m installer --base FILE --update FILE --keys FILE --lang CODE --dest DIR [--firmware DIR]
[--root DIR] [--temp DIR] [--verify]
[--root DIR] [--temp DIR] [--verify] [--dlss]
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
@@ -7,6 +7,9 @@ Traditional Chinese. On a folder with totk-install.json (resume after a cancel o
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
@@ -19,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}
@@ -47,8 +50,8 @@ 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='the update NSP: 1.1.0, 1.2.1 or 1.4.3')
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')
@@ -58,6 +61,8 @@ def main(argv=None):
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
@@ -79,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])
+12 -5
View File
@@ -225,7 +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')
@@ -288,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(),
firmware=self.vars['firmware'].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()
@@ -303,9 +311,8 @@ 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)
@@ -537,7 +544,7 @@ class App:
# ---- self test ---------------------------------------------------------------------------------------------------
SAMPLE_START = {'type': 'start', 'release': 'selftest', 'mode': 'install', 'dest': 'Z:/selftest/TotK', 'log': 'D:\\log.txt',
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup']}
'steps': ['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup']}
SAMPLE_RUN = [
SAMPLE_START,
{'type': 'plan', 'run': ['check', 'fonts', 'extract'], 'skip': {}},
+4 -1
View File
@@ -27,7 +27,7 @@ TEXT = Path(__file__).resolve().parent / 'text'
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', 'cleanup')
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
@@ -137,6 +137,7 @@ class Form:
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."""
@@ -187,6 +188,8 @@ class Form:
cmd += ['--temp', str(temp)]
if verify_hashes:
cmd.append('--verify')
if self.dlss:
cmd.append('--dlss')
return cmd
+2 -1
View File
@@ -90,7 +90,8 @@ class Layout:
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 / 'build_game.py', self.tools / 'prepare_shaders.py',
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',
+198 -21
View File
@@ -1,4 +1,4 @@
"""The seven setup steps of docs/installer.md: check, fonts, 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); fonts
runs the vendored extract_shared_fonts.py on the firmware folder; lift, compile and prepare run the bundled Python on
@@ -7,18 +7,20 @@ recomp/gen.py, tools/build_game.py, tools/make_romfs_patches.py and tools/prepar
with the bundled clang (hostcheck.c).
Install folder: exefs/, romfs/ (romfs.bin, index.json), profile/, shared-fonts/ (Chinese and Korean text), totk.exe
and its DLLs, romfs-index.bin, romfs-patches/, romfs-patches-xbox/, romfs-patches-xbox-label/, totk_xlate.exe, the
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), 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.
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
@@ -28,6 +30,7 @@ import subprocess
import sys
import threading
import time
import urllib.request
from pathlib import Path
from . import ini
@@ -36,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)
@@ -174,6 +178,12 @@ 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 no version, and that install is 1.2.1: name it here rather than leaning on SUPPORTED's order."""
@@ -184,6 +194,25 @@ 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')
@@ -349,6 +378,11 @@ class Check(Step):
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)
@@ -427,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:
@@ -515,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
@@ -636,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'):
@@ -648,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
@@ -659,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
@@ -681,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}
@@ -702,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-'
@@ -768,9 +865,12 @@ class Lift(Step):
transient = True
def key(self, 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', *cheat_files(ctx),
*exefs_files(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'
@@ -835,7 +935,7 @@ def runtime_key(ctx):
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')]
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])
@@ -899,9 +999,11 @@ SKIPPED = re.compile(r'prepare-shaders: (\d+) variants skipped: the graphics dri
# 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):
@@ -1014,7 +1116,82 @@ class Prepare(Step):
return {'xlate_sha256': file_sha256(xlate)}
# ---- 7. 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'
@@ -1032,7 +1209,7 @@ class Cleanup(Step):
rep.progress('cleanup', 'temp', force=True)
for stale in dest.glob('update-merge-*'):
remove(stale)
for name in ('check', 'shader_list.tsv', 'shared-fonts'): # gen/ and build/ stay for the next rebuild
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)
@@ -1082,4 +1259,4 @@ def romfs_problem(romfs, expected):
def all_steps():
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Cleanup()]
return [Check(), Fonts(), Extract(), Lift(), Compile(), Prepare(), Dlss(), Cleanup()]
+17 -7
View File
@@ -5,12 +5,15 @@
"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",
@@ -30,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",
@@ -51,20 +55,22 @@
"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.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.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 file.",
"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}.",
@@ -93,13 +99,15 @@
"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. Choose the firmware folder (Contents/registered) to install them; without it 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",
@@ -108,8 +116,8 @@
"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.1.0 or 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",
@@ -121,8 +129,10 @@
"ui.pick_firmware": "Choose the Switch firmware folder (Contents/registered)",
"ui.pick_dest": "Choose the install folder",
"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.type_game": "Game files (NSP, XCI)",
"ui.type_nsp": "NSP files",
"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",
+17 -7
View File
@@ -5,12 +5,15 @@
"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",
@@ -30,6 +33,7 @@
"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",
@@ -51,20 +55,22 @@
"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 ou NSP.",
"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.",
"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}.",
@@ -93,13 +99,15 @@
"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, que o instalador não extrai; parte do texto pode não aparecer.",
"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",
@@ -108,8 +116,8 @@
"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 ou XCI)",
"ui.update": "Atualização 1.1.0, 1.2.1 ou 1.4.3 (NSP)",
"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",
@@ -121,8 +129,10 @@
"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.type_game": "Arquivos de jogo (NSP, XCI)",
"ui.type_nsp": "Arquivos NSP",
"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",
+17 -7
View File
@@ -5,12 +5,15 @@
"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": "Копирование общих шрифтов в папку установки",
@@ -30,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": "Удаление временных файлов",
@@ -51,20 +55,22 @@
"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.",
"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.base_is_update": "Файл базовой игры — это обновление. Выберите здесь NSP или XCI базовой игры (версия 1.0.0), а этот файл укажите как обновление.",
"check.base_combined": "Файл базовой игры содержит несколько пакетов (игра, обновление или DLC в одном файле). Выберите дамп, в котором только базовая игра.",
"check.update_not_nsp": "Обновление должно быть файлом NSP.",
"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}.",
@@ -93,13 +99,15 @@
"prepare.pack_failed": "Не удалось упаковать кэш шейдеров (код выхода {code}). Подробности: {log}",
"cleanup.ini_failed": "Не удаётся записать totk.ini: {detail}",
"warn.lang_needs_fonts": "Для языка «{lang}» нужны системные шрифты Switch. Выберите папку прошивки (Contents/registered), чтобы установить их; без неё часть текста может не отображаться.",
"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": "Пересборка игры под ваши моды",
@@ -108,8 +116,8 @@
"ui.intro": "Собирает готовую к запуску папку с Tears of the Kingdom из ваших игровых файлов. Занимает много времени и выполняется один раз.",
"ui.notice": "Папка, которую создаёт установка, содержит игру. Она предназначена только для личного использования: не передавайте и не выкладывайте её.",
"ui.keys_private": "Файл ключей читается на месте. Его содержимое не показывается, не копируется и не сохраняется.",
"ui.base": "Базовая игра (NSP или XCI)",
"ui.update": "Обновление 1.1.0 или 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": "Язык игры",
@@ -121,8 +129,10 @@
"ui.pick_firmware": "Выберите папку прошивки Switch (Contents/registered)",
"ui.pick_dest": "Выберите папку установки",
"ui.firmware_help": "Для текста на китайском и корейском: папка Contents/registered прошивки, например из Ryujinx или Yuzu. Установка скопирует из неё системные шрифты Switch в папку установки.",
"ui.type_game": "Файлы игры (NSP, XCI)",
"ui.type_nsp": "Файлы NSP",
"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": "Начать",
+17 -7
View File
@@ -5,12 +5,15 @@
"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": "正在将共享字体复制到安装文件夹",
@@ -30,6 +33,7 @@
"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": "删除临时文件",
@@ -51,20 +55,22 @@
"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 游戏文件。",
"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 文件。",
"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}。",
@@ -93,13 +99,15 @@
"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),或参阅 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": "正在为你的模组重新构建游戏",
@@ -108,8 +116,8 @@
"ui.intro": "用你自己的游戏文件构建一个可以游玩的《王国之泪》文件夹。耗时较长,只需运行一次。",
"ui.notice": "此安装程序构建的文件夹包含游戏本身,仅供你个人使用;请勿分享或上传。",
"ui.keys_private": "密钥文件只在原位置读取,其内容绝不会被显示、复制或保存。",
"ui.base": "游戏本体(NSP 或 XCI)",
"ui.update": "1.1.0、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": "游戏语言",
@@ -121,8 +129,10 @@
"ui.pick_firmware": "选择 Switch 固件文件夹(Contents/registered)",
"ui.pick_dest": "选择安装文件夹",
"ui.firmware_help": "用于中文、韩文和繁体中文文本:固件的 Contents/registered 文件夹,例如来自 Ryujinx 或 Yuzu。安装程序会从中把 Switch 系统字体复制到安装文件夹。",
"ui.type_game": "游戏文件(NSP、XCI)",
"ui.type_nsp": "NSP 文件",
"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": "开始",
+39 -11
View File
@@ -558,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
@@ -581,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()
@@ -632,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:
@@ -705,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'])
+11
View File
@@ -82,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
+45 -22
View File
@@ -29,9 +29,10 @@ static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
return false;
}
// NTAG215 Capability Container (CC) at page 3 (offset 0x0C).
// Byte 0x0C must be 0xE1 (NDEF Magic / Type 2 Tag CC container byte).
if (data[0x0C] != 0xE1) {
// 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;
}
@@ -55,6 +56,12 @@ static bool amiibo_validate_dump(const uint8_t *data, uint64_t size) {
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) {
@@ -68,15 +75,15 @@ static void parse_dump_info(const char *path) {
snprintf(g_info.filename, sizeof(g_info.filename), "%s", name);
}
// UID: bytes 0..6 (7 bytes) or up to 9
memcpy(g_info.uuid, g_dump, 7);
copy_uid(g_info.uuid, g_dump);
g_info.uuid_len = 7;
// Amiibo ID at offset 0x54 (8 bytes)
// 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.series_id = g_dump[0x57];
g_info.nfp_type = g_dump[0x57];
g_info.numbering_id = ((uint16_t)g_dump[0x58] << 8) | g_dump[0x59];
g_info.nfp_type = g_dump[0x5A];
g_info.series_id = g_dump[0x5A];
g_info.version = g_dump[0x5B];
}
@@ -106,17 +113,25 @@ bool amiibo_is_device_present(void) {
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;
rt_log("amiibo: start detection");
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;
rt_log("amiibo: stop detection");
if (g_detect_starts <= 3) rt_log("amiibo: stop detection");
plat_mutex_unlock(&g_amiibo_lock);
}
@@ -175,6 +190,15 @@ AmiiboDeviceState amiibo_get_device_state(void) {
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;
@@ -303,7 +327,7 @@ void amiibo_get_tag_info(void *out_tag_info) {
uint8_t *out = (uint8_t *)out_tag_info;
memset(out, 0, 0x58);
// UUID: offset 0x00, 10 bytes max
memcpy(out + 0x00, g_dump, 7);
copy_uid(out, g_dump);
out[0x0A] = 7; // UUID length
// Protocol at offset 0x20: TypeA = 1
*(uint32_t *)(out + 0x20) = 1;
@@ -316,15 +340,14 @@ 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);
// Character ID at 0x00 (3 bytes)
memcpy(out + 0x00, g_dump + 0x54, 3);
// Series ID at 0x03 (1 byte)
out[0x03] = g_dump[0x57];
// Numbering ID at 0x04 (2 bytes, big-endian from 0x58)
out[0x04] = g_dump[0x58];
out[0x05] = g_dump[0x59];
// NFP Type at 0x06 (1 byte)
out[0x06] = g_dump[0x5A];
// 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);
}
@@ -333,7 +356,7 @@ void amiibo_get_common_info(void *out_common_info) {
uint8_t *out = (uint8_t *)out_common_info;
memset(out, 0, 0x40);
// Last write date: 0x00..0x03
out[0x00] = 0x07; out[0x01] = 0xE7; // 2023
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;
@@ -351,7 +374,7 @@ void amiibo_get_register_info(void *out_register_info) {
// Mii CharInfo at 0x00..0x57 (dummy valid mii)
out[0x00] = 0x03; // version 3
// First write date at 0x58..0x5B
out[0x58] = 0x07; out[0x59] = 0xE7;
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";
+3
View File
@@ -51,6 +51,9 @@ 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);
+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
+28 -17
View File
@@ -30,21 +30,23 @@ 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", "AMIIBO_DIR", "ANISO", "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", "COLOR_BRIGHTNESS", "COLOR_CONTRAST", "COLOR_GAMMA",
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DLAA", "DLAA_JITTER", "DLAA_MV", "DLSS", "DUMP", "DUMP_BUF", "DUMP_DIR",
"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",
"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",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PATCH_OUTPUT", "PATCH_SCENE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
"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", "PROMPTS", "PUSH_SKIP", "PUSH_STAGE", "RDOC", "RDOC_DIR", "RDOC_DLL", "REALTIME", "REC_END", "REEXEC",
"RENDER_SCALE", "REPLAY_ASYNC",
@@ -53,9 +55,9 @@ static const char *const known[] = {
"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", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"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",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_PROCS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
"ZSTD_CHECK", "ZSTD_NATIVE",
};
@@ -69,14 +71,15 @@ static const struct { const char *key, *section; } sections[] = {
{"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"}, {"SHADOW_RES", "display"},
{"DLAA", "display"}, {"DLSS", "display"}, {"FXAA", "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"}, {"PROMPTS", "input"}, {"STICK_DEADZONE", "input"}, {"LSTICK_DEADZONE", "input"}, {"RSTICK_DEADZONE", "input"},
{"STICK_SENS", "input"},
@@ -95,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"},
{"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
@@ -111,7 +115,9 @@ 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},
{"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance", RESTART}, {"FXAA", "1", "0|1", 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
@@ -120,9 +126,9 @@ static const CfgMeta metas[] = {
{"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_UI_NITS", "203", "80..500", LIVE}, {"HDR_HUE", "80", "0..100", LIVE},
{"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART},
{"SHADOW_RES", "game", "game|512|768|1024|1536|2048", 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},
@@ -135,7 +141,7 @@ static const CfgMeta metas[] = {
{"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", LIVE},
{"PROMPTS", "nintendo", "nintendo|xbox", RESTART},
@@ -145,16 +151,20 @@ static const CfgMeta metas[] = {
{"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},
@@ -167,6 +177,7 @@ static const CfgMeta metas[] = {
// diagnostics
{"TRACE", "", NULL, RESTART}, {"FLIGHT", "0", "0|1", RESTART}, {"FLIGHT_SPIKE_MS", "100", "0..60000", 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},
};
+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
}
+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
};
+156 -113
View File
@@ -1,119 +1,162 @@
// Generated by tools/build_shaders.py from runtime/shaders/dlaa_mv.comp; do not edit.
// source sha256 bfa9b6d9e0f9d5cfd4382fa5080fa46f007a1a4e2b886b8fecd9adba11767686
// source sha256 e96a6d2d0cec26c47a76280363077c5982919bedc984f20ae9f1d185dc6ae674
#pragma once
#include <stdint.h>
static const uint32_t dlaa_mv_spv[] = {
0x07230203, 0x00010600, 0x0008000b, 0x00000131, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
0x07230203, 0x00010300, 0x000d000b, 0x0000019a, 0x00000000, 0x00020011, 0x00000001, 0x00020011,
0x00000031, 0x0006000b, 0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e,
0x00000000, 0x00000001, 0x000b000f, 0x00000005, 0x00000004, 0x6e69616d, 0x00000000, 0x00000036,
0x00000043, 0x0000005d, 0x00000089, 0x000000a0, 0x000000e7, 0x00060010, 0x00000004, 0x00000011,
0x00000008, 0x00000008, 0x00000001, 0x00030003, 0x00000002, 0x000001c2, 0x00040005, 0x00000004,
0x6e69616d, 0x00000000, 0x00080005, 0x00000036, 0x475f6c67, 0x61626f6c, 0x766e496c, 0x7461636f,
0x496e6f69, 0x00000044, 0x00030005, 0x00000041, 0x00000050, 0x00050006, 0x00000041, 0x00000000,
0x657a6973, 0x00000000, 0x00050006, 0x00000041, 0x00000001, 0x7474696a, 0x00007265, 0x00030005,
0x00000043, 0x00006370, 0x00050005, 0x0000005d, 0x74706564, 0x65745f68, 0x00000078, 0x00030005,
0x00000087, 0x00727543, 0x00040006, 0x00000087, 0x00000000, 0x00000072, 0x00030005, 0x00000089,
0x00727563, 0x00040005, 0x0000009e, 0x76657250, 0x00000000, 0x00040006, 0x0000009e, 0x00000000,
0x00000072, 0x00040005, 0x000000a0, 0x76657270, 0x00000000, 0x00040005, 0x000000e7, 0x6f5f766d,
0x00007475, 0x00040047, 0x00000036, 0x0000000b, 0x0000001c, 0x00030047, 0x00000041, 0x00000002,
0x00050048, 0x00000041, 0x00000000, 0x00000023, 0x00000000, 0x00050048, 0x00000041, 0x00000001,
0x00000023, 0x00000008, 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, 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, 0x0004001e, 0x00000041, 0x00000040, 0x00000040, 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, 0x0005002c, 0x00000040, 0x00000130, 0x00000067, 0x00000067,
0x00050036, 0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 0x000300f7,
0x000000f0, 0x00000000, 0x000300fb, 0x0000003c, 0x000000f1, 0x000200f8, 0x000000f1, 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, 0x000000f1, 0x00000053, 0x0000004b, 0x000300f7, 0x00000056,
0x00000000, 0x000400fa, 0x00000054, 0x00000055, 0x00000056, 0x000200f8, 0x00000055, 0x000200f9,
0x000000f0, 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, 0x00000130,
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, 0x00000110, 0x00000091, 0x00000094, 0x00000097, 0x0000009a,
0x00040054, 0x00000009, 0x00000111, 0x00000110, 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, 0x0000012c, 0x000000a3,
0x000000a6, 0x000000a9, 0x000000ac, 0x00040054, 0x00000009, 0x0000012d, 0x0000012c, 0x0006000c,
0x00000009, 0x000000b0, 0x00000001, 0x00000022, 0x00000111, 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, 0x0000012d, 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,
0x0000012f, 0x000000b4, 0x000000bb, 0x000000e4, 0x000000c9, 0x000200f9, 0x000000bc, 0x000200f8,
0x000000bc, 0x000700f5, 0x00000040, 0x0000012e, 0x000000b4, 0x00000056, 0x0000012f, 0x000000ca,
0x0004003d, 0x000000e5, 0x000000e8, 0x000000e7, 0x00050051, 0x00000006, 0x000000eb, 0x0000012e,
0x00000000, 0x00050051, 0x00000006, 0x000000ec, 0x0000012e, 0x00000001, 0x00070050, 0x00000007,
0x000000ed, 0x000000eb, 0x000000ec, 0x00000016, 0x00000016, 0x00040063, 0x000000e8, 0x0000003a,
0x000000ed, 0x000200f9, 0x000000f0, 0x000200f8, 0x000000f0, 0x000100fd, 0x00010038,
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,
};
+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;
}
}
+48 -5
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,7 +160,23 @@ 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
@@ -190,7 +224,6 @@ static int exists(const char *p) { return plat_fs_kind(p) != PLAT_FS_MISSING; }
// Rename a directory, durably; retried every 50 ms, tries times in all, while another process (a virus scanner, the
// indexer) holds a file in it. 0 on success.
#define MOVE_TRIES 40 // 2 s
static int move_dir(const char *from, const char *to, int tries) {
for (int i = 0; i < tries; i++) {
int e = plat_fs_rename(from, to, 1);
@@ -446,7 +479,8 @@ static void save_recover(void) {
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)";
}
@@ -1575,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);
+7 -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,9 @@ 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);
@@ -97,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.
+100 -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
@@ -593,6 +594,25 @@ static GfxShader *disk_load_from(const uint8_t key[32], const uint8_t *iid, int
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;
}
// The pack, then the loose files (a newer translation of a key the pack holds a bad copy of); NULL when neither has a
// valid pair (the caller translates again and writes loose files).
static GfxShader *disk_load(const uint8_t key[32], const uint8_t *iid) {
@@ -759,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)
@@ -975,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);
}
@@ -1007,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.
@@ -1086,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);
@@ -1288,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);
@@ -1644,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;
@@ -1733,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.
+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();
+267 -122
View File
@@ -1,12 +1,38 @@
// Generated by tools/build_shaders.py from runtime/shaders/hdr_tonemap.frag; do not edit.
// source sha256 59af32c0b20e9783c36779cdd379db02b52c0652cc1916ebca1c62a2b38df71e
// source sha256 a09ae2b13d844bb6a136d2c3cf38b3ec571191775a4c2f0b43a586bd355381fa
#pragma once
#include <stdint.h>
static const uint32_t hdr_tonemap_spv[] = {
0x07230203, 0x00010300, 0x000d000b, 0x000001bf, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x07230203, 0x00010300, 0x0008000b, 0x0000018a, 0x00000000, 0x00020011, 0x00000001, 0x0006000b,
0x00000001, 0x4c534c47, 0x6474732e, 0x3035342e, 0x00000000, 0x0003000e, 0x00000000, 0x00000001,
0x0007000f, 0x00000004, 0x00000004, 0x6e69616d, 0x00000000, 0x00000053, 0x0000011c, 0x00030010,
0x00000004, 0x00000007, 0x00040047, 0x0000004f, 0x00000021, 0x00000001, 0x00040047, 0x0000004f,
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,
@@ -17,129 +43,248 @@ static const uint32_t hdr_tonemap_spv[] = {
0x00000006, 0x00000010, 0x00030047, 0x00000079, 0x00000002, 0x00050048, 0x00000079, 0x00000000,
0x00000023, 0x00000000, 0x00050048, 0x00000079, 0x00000001, 0x00000023, 0x000005a0, 0x00040047,
0x0000007b, 0x00000021, 0x00000000, 0x00040047, 0x0000007b, 0x00000022, 0x00000000, 0x00040047,
0x0000011c, 0x0000001e, 0x00000000, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002,
0x00030016, 0x00000006, 0x00000020, 0x00040017, 0x0000000c, 0x00000006, 0x00000003, 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, 0x00090019, 0x0000004c, 0x00000006,
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, 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, 0x0004002b, 0x00000006, 0x000000ff, 0x3f000000, 0x00040020,
0x0000011b, 0x00000003, 0x00000051, 0x0004003b, 0x0000011b, 0x0000011c, 0x00000003, 0x00050036,
0x00000002, 0x00000004, 0x00000000, 0x00000003, 0x000200f8, 0x00000005, 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, 0x00050057, 0x00000051, 0x00000060,
0x0000005d, 0x00000056, 0x0008004f, 0x0000000c, 0x00000061, 0x00000060, 0x00000060, 0x00000000,
0x00000001, 0x00000002, 0x00050081, 0x0000000c, 0x00000062, 0x0000005b, 0x00000061, 0x00060041,
0x0000006c, 0x0000006d, 0x00000069, 0x0000006b, 0x0000006b, 0x0004003d, 0x00000051, 0x0000006e,
0x0000006d, 0x00060041, 0x0000006c, 0x00000074, 0x00000073, 0x0000006b, 0x0000006b, 0x0004003d,
0x00000051, 0x00000075, 0x00000074, 0x00060041, 0x0000007d, 0x0000007e, 0x0000007b, 0x0000007c,
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, 0x000600a9, 0x00000006, 0x000001bd, 0x00000081, 0x0000007f, 0x0000001f,
0x00060041, 0x0000007d, 0x0000008a, 0x0000007b, 0x0000007c, 0x00000035, 0x0004003d, 0x00000006,
0x0000008b, 0x0000008a, 0x000500ba, 0x0000001a, 0x0000008c, 0x0000008b, 0x0000001f, 0x000600a9,
0x00000006, 0x000001be, 0x0000008c, 0x0000008b, 0x0000001f, 0x00050051, 0x00000006, 0x00000096,
0x00000062, 0x00000000, 0x00050085, 0x00000006, 0x00000098, 0x00000096, 0x00000097, 0x00050051,
0x00000006, 0x0000009a, 0x00000062, 0x00000001, 0x00050051, 0x00000006, 0x0000009c, 0x0000006e,
0x00000000, 0x00050085, 0x00000006, 0x0000009d, 0x0000009a, 0x0000009c, 0x00050081, 0x00000006,
0x0000009e, 0x00000098, 0x0000009d, 0x00050051, 0x00000006, 0x000000a0, 0x00000062, 0x00000002,
0x00050051, 0x00000006, 0x000000a2, 0x0000006e, 0x00000001, 0x00050085, 0x00000006, 0x000000a3,
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, 0x0004007f, 0x00000006,
0x000000a8, 0x000000a5, 0x0006000c, 0x00000006, 0x000000a9, 0x00000001, 0x0000001b, 0x000000a8,
0x00050083, 0x00000006, 0x000000aa, 0x0000001f, 0x000000a9, 0x000500ba, 0x0000001a, 0x000000ad,
0x000000a5, 0x00000080, 0x000300f7, 0x000000b0, 0x00000000, 0x000400fa, 0x000000ad, 0x000000af,
0x000000b8, 0x000200f8, 0x000000af, 0x000500bc, 0x0000001a, 0x0000012a, 0x000000a5, 0x00000019,
0x000300f7, 0x00000136, 0x00000000, 0x000400fa, 0x0000012a, 0x0000012b, 0x00000130, 0x000200f8,
0x0000012b, 0x000200f9, 0x00000136, 0x000200f8, 0x00000130, 0x00050083, 0x00000006, 0x00000133,
0x000000a5, 0x00000019, 0x00050085, 0x00000006, 0x00000134, 0x00000026, 0x00000133, 0x00050081,
0x00000006, 0x00000135, 0x00000017, 0x00000134, 0x000200f9, 0x00000136, 0x000200f8, 0x00000136,
0x000700f5, 0x00000006, 0x000001b7, 0x000000aa, 0x0000012b, 0x00000135, 0x00000130, 0x00050088,
0x00000006, 0x000000b6, 0x000001b7, 0x000000a5, 0x0005008e, 0x0000000c, 0x000000b7, 0x00000062,
0x000000b6, 0x000200f9, 0x000000b0, 0x000200f8, 0x000000b8, 0x000200f9, 0x000000b0, 0x000200f8,
0x000000b0, 0x000700f5, 0x0000000c, 0x000001b8, 0x000000b7, 0x00000136, 0x00000062, 0x000000b8,
0x000500bc, 0x0000001a, 0x0000014c, 0x00000096, 0x00000019, 0x000300f7, 0x00000158, 0x00000000,
0x000400fa, 0x0000014c, 0x0000014d, 0x00000152, 0x000200f8, 0x0000014d, 0x0004007f, 0x00000006,
0x0000014f, 0x00000096, 0x0006000c, 0x00000006, 0x00000150, 0x00000001, 0x0000001b, 0x0000014f,
0x00050083, 0x00000006, 0x00000151, 0x0000001f, 0x00000150, 0x000200f9, 0x00000158, 0x000200f8,
0x00000152, 0x00050083, 0x00000006, 0x00000155, 0x00000096, 0x00000019, 0x00050085, 0x00000006,
0x00000156, 0x00000026, 0x00000155, 0x00050081, 0x00000006, 0x00000157, 0x00000017, 0x00000156,
0x000200f9, 0x00000158, 0x000200f8, 0x00000158, 0x000700f5, 0x00000006, 0x000001b9, 0x00000151,
0x0000014d, 0x00000157, 0x00000152, 0x000500bc, 0x0000001a, 0x0000015f, 0x0000009a, 0x00000019,
0x000300f7, 0x0000016b, 0x00000000, 0x000400fa, 0x0000015f, 0x00000160, 0x00000165, 0x000200f8,
0x00000160, 0x0004007f, 0x00000006, 0x00000162, 0x0000009a, 0x0006000c, 0x00000006, 0x00000163,
0x00000001, 0x0000001b, 0x00000162, 0x00050083, 0x00000006, 0x00000164, 0x0000001f, 0x00000163,
0x000200f9, 0x0000016b, 0x000200f8, 0x00000165, 0x00050083, 0x00000006, 0x00000168, 0x0000009a,
0x00000019, 0x00050085, 0x00000006, 0x00000169, 0x00000026, 0x00000168, 0x00050081, 0x00000006,
0x0000016a, 0x00000017, 0x00000169, 0x000200f9, 0x0000016b, 0x000200f8, 0x0000016b, 0x000700f5,
0x00000006, 0x000001ba, 0x00000164, 0x00000160, 0x0000016a, 0x00000165, 0x000500bc, 0x0000001a,
0x00000172, 0x000000a0, 0x00000019, 0x000300f7, 0x0000017e, 0x00000000, 0x000400fa, 0x00000172,
0x00000173, 0x00000178, 0x000200f8, 0x00000173, 0x0004007f, 0x00000006, 0x00000175, 0x000000a0,
0x0006000c, 0x00000006, 0x00000176, 0x00000001, 0x0000001b, 0x00000175, 0x00050083, 0x00000006,
0x00000177, 0x0000001f, 0x00000176, 0x000200f9, 0x0000017e, 0x000200f8, 0x00000178, 0x00050083,
0x00000006, 0x0000017b, 0x000000a0, 0x00000019, 0x00050085, 0x00000006, 0x0000017c, 0x00000026,
0x0000017b, 0x00050081, 0x00000006, 0x0000017d, 0x00000017, 0x0000017c, 0x000200f9, 0x0000017e,
0x000200f8, 0x0000017e, 0x000700f5, 0x00000006, 0x000001bb, 0x00000177, 0x00000173, 0x0000017d,
0x00000178, 0x00060050, 0x0000000c, 0x00000146, 0x000001b9, 0x000001ba, 0x000001bb, 0x00050085,
0x00000006, 0x000000c2, 0x000000aa, 0x000000aa, 0x00060050, 0x0000000c, 0x000000c3, 0x000000c2,
0x000000c2, 0x000000c2, 0x0008000c, 0x0000000c, 0x000000c4, 0x00000001, 0x0000002e, 0x000001b8,
0x00000146, 0x000000c3, 0x0007000c, 0x0000000c, 0x000000c6, 0x00000001, 0x00000028, 0x000000c4,
0x000000c5, 0x00050051, 0x00000006, 0x00000183, 0x000000c6, 0x00000000, 0x00050051, 0x00000006,
0x00000185, 0x000000c6, 0x00000001, 0x00050051, 0x00000006, 0x00000187, 0x000000c6, 0x00000002,
0x0007000c, 0x00000006, 0x00000188, 0x00000001, 0x00000028, 0x00000185, 0x00000187, 0x0007000c,
0x00000006, 0x00000189, 0x00000001, 0x00000028, 0x00000183, 0x00000188, 0x0007000c, 0x00000006,
0x000000cb, 0x00000001, 0x00000028, 0x00000189, 0x0000001f, 0x00060050, 0x0000000c, 0x000000cf,
0x000000cb, 0x000000cb, 0x000000cb, 0x00050088, 0x0000000c, 0x000000d0, 0x000000c6, 0x000000cf,
0x00050051, 0x00000006, 0x0000018d, 0x000000d0, 0x00000000, 0x00050051, 0x00000006, 0x0000018f,
0x000000d0, 0x00000001, 0x00050051, 0x00000006, 0x00000191, 0x000000d0, 0x00000002, 0x0007000c,
0x00000006, 0x00000192, 0x00000001, 0x00000028, 0x0000018f, 0x00000191, 0x0007000c, 0x00000006,
0x00000193, 0x00000001, 0x00000028, 0x0000018d, 0x00000192, 0x00050085, 0x00000006, 0x000000d9,
0x0000018d, 0x000000d8, 0x00050081, 0x00000006, 0x000000de, 0x0000018f, 0x00000191, 0x00050051,
0x00000006, 0x000000e0, 0x0000006e, 0x00000002, 0x00050085, 0x00000006, 0x000000e1, 0x000000de,
0x000000e0, 0x00050081, 0x00000006, 0x000000e2, 0x000000d9, 0x000000e1, 0x00050083, 0x00000006,
0x000000e3, 0x000000e2, 0x0000001f, 0x00050051, 0x00000006, 0x000000e6, 0x00000075, 0x00000001,
0x00050085, 0x00000006, 0x000000e9, 0x000000e3, 0x000000e3, 0x00050083, 0x00000006, 0x000000ea,
0x0000001f, 0x000000e9, 0x00050085, 0x00000006, 0x000000eb, 0x000000e6, 0x000000ea, 0x00050051,
0x00000006, 0x000000ed, 0x00000075, 0x00000000, 0x00050081, 0x00000006, 0x000000ee, 0x000000eb,
0x000000ed, 0x00060050, 0x0000000c, 0x000000f2, 0x00000193, 0x00000193, 0x00000193, 0x00050083,
0x0000000c, 0x000000f3, 0x000000d0, 0x000000f2, 0x0005008e, 0x0000000c, 0x000000f5, 0x000000f3,
0x000000ee, 0x00050081, 0x0000000c, 0x000000f8, 0x000000f5, 0x000000f2, 0x0005008e, 0x0000000c,
0x000000f9, 0x000000f8, 0x000000cb, 0x00050051, 0x00000006, 0x00000197, 0x000000f9, 0x00000000,
0x00050051, 0x00000006, 0x00000199, 0x000000f9, 0x00000001, 0x00050051, 0x00000006, 0x0000019b,
0x000000f9, 0x00000002, 0x0007000c, 0x00000006, 0x0000019c, 0x00000001, 0x00000028, 0x00000199,
0x0000019b, 0x0007000c, 0x00000006, 0x0000019d, 0x00000001, 0x00000028, 0x00000197, 0x0000019c,
0x00050085, 0x00000006, 0x00000101, 0x000000ff, 0x000001be, 0x000500ba, 0x0000001a, 0x00000104,
0x0000019d, 0x00000101, 0x000300f7, 0x00000106, 0x00000000, 0x000400fa, 0x00000104, 0x00000105,
0x00000106, 0x000200f8, 0x00000105, 0x00050083, 0x00000006, 0x0000010a, 0x000001be, 0x00000101,
0x00050083, 0x00000006, 0x0000010f, 0x0000019d, 0x00000101, 0x0004007f, 0x00000006, 0x00000110,
0x0000010f, 0x00050088, 0x00000006, 0x00000112, 0x00000110, 0x0000010a, 0x0006000c, 0x00000006,
0x00000113, 0x00000001, 0x0000001b, 0x00000112, 0x00050083, 0x00000006, 0x00000114, 0x0000001f,
0x00000113, 0x00050085, 0x00000006, 0x00000115, 0x0000010a, 0x00000114, 0x00050081, 0x00000006,
0x00000116, 0x00000101, 0x00000115, 0x00050088, 0x00000006, 0x00000118, 0x00000116, 0x0000019d,
0x0005008e, 0x0000000c, 0x0000011a, 0x000000f9, 0x00000118, 0x000200f9, 0x00000106, 0x000200f8,
0x00000106, 0x000700f5, 0x0000000c, 0x000001bc, 0x000000f9, 0x0000017e, 0x0000011a, 0x00000105,
0x0007000c, 0x0000000c, 0x0000011e, 0x00000001, 0x00000028, 0x000001bc, 0x000000c5, 0x0005008e,
0x0000000c, 0x00000120, 0x0000011e, 0x000001bd, 0x00050051, 0x00000006, 0x00000121, 0x00000120,
0x00000000, 0x00050051, 0x00000006, 0x00000122, 0x00000120, 0x00000001, 0x00050051, 0x00000006,
0x00000123, 0x00000120, 0x00000002, 0x00070050, 0x00000051, 0x00000124, 0x00000121, 0x00000122,
0x00000123, 0x00000080, 0x0003003e, 0x0000011c, 0x00000124, 0x000100fd, 0x00010038,
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,
};
+5
View File
@@ -262,6 +262,9 @@ int res_heapdef_build(const void *in, size_t n, void **out, size_t *out_n); //
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)
@@ -309,6 +312,8 @@ void rt_boot(void); // boot.c
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
+23 -6
View File
@@ -556,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");
@@ -563,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();
}
@@ -570,25 +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. 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, uint64_t buttons, const int32_t *axes) {
// 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;
if (k != 2) return k != 0;
int held = buttons != 0;
for (int i = 0; i < 4; i++) held |= axes[i] != 0;
return k && !(k == 2 && !held && plat_cas32(flag, 0, 2) == 2);
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);
const int32_t axes[4] = {*lx, *ly, *rx, *ry};
int text = blocked(&g_block, *buttons, axes), menu = blocked(&g_menu, *buttons, axes);
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;
}
+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
+122 -77
View File
@@ -415,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"
@@ -446,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"
@@ -525,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"
@@ -1888,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;
@@ -1952,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;
@@ -1969,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;
@@ -1980,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;
}
@@ -1995,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;
@@ -2056,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;
@@ -2069,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) {
@@ -2082,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");
}
+33 -7
View File
@@ -5,6 +5,7 @@
// --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
@@ -153,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];
@@ -218,16 +219,21 @@ 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));
for (int i = 0; i < x->nframes; i++) { // abort and invalid parameter: where the host code called it
// 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] - (rip - x->offset)));
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);
}
@@ -439,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-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;
}
@@ -526,16 +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();
+2
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);
@@ -698,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);
+62 -4
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[] = {
@@ -386,6 +388,43 @@ static void probe_applet(void) {
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);
@@ -395,7 +434,8 @@ static void probe_applet(void) {
CALL("pctl.Initialize", 0);
CALL("diag.InitializeApplicationAbortObserver", 0);
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\n", nfp_event ? "ok" : "FAILED");
"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) {
@@ -849,15 +889,33 @@ 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_PT_BR; 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"), "pt-BR overlay format lost its conversion");
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");
+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];
+72 -4
View File
@@ -22,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>
@@ -916,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);
@@ -927,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);
@@ -1038,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;
@@ -1654,6 +1666,62 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
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
+27 -9
View File
@@ -218,19 +218,25 @@ void plat_mem_fault_hook(int (*fn)(uint64_t addr));
// replaces this process with it (Linux). Environment changes made before the call are passed on.
_Noreturn void plat_reexec(void);
// A crash in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
// overflow) goes to fn, which reports it and must end the process. Faults in other modules (drivers) are left to the
// OS. The write-fault hook above runs first. On Windows abort() (a failed assert, std::terminate) and an invalid
// parameter to a C runtime function go there too, with the host frames in this executable: the C runtime ends the
// process for both with 0xc0000409 and no exception.
// A crash goes to fn, which reports it and must end the process. The write-fault hook above runs first.
// - A fault in this executable's own code (an access violation, an illegal instruction, a division by zero, a stack
// overflow) is reported as it happens.
// - A fault in another module (a GPU driver, a system library) is reported only when nothing handles it: on Windows
// from the unhandled-exception filter, with the return addresses in this executable on the faulting thread's stack,
// so the report shows which host call ran into the driver; on Linux from the signal handler, without frames. Drivers
// that raise and catch their own exceptions never reach fn.
// - On Windows abort() (a failed assert, std::terminate) and an invalid parameter to a C runtime function go there
// too, with the host frames in this executable: the C runtime ends the process for both with 0xc0000409 and no
// exception.
typedef struct {
const char *what; // "access violation", "illegal instruction", ...
uint32_t code; // the Windows exception code or the signal number
uintptr_t rip, offset; // the faulting instruction, and its offset in the executable
uintptr_t rip, offset; // the faulting instruction, and its offset in its module
int access; // the data access that faulted: 0 read, 1 write, 8 execute; -1 when not an access
uint64_t addr; // its address when access >= 0
int nframes; // abort and invalid parameter: return addresses in this executable, innermost first
uintptr_t frames[16]; // (rip is frames[0]); 0 for a fault, whose rip is exact
int nframes; // return addresses in this executable, innermost first: for abort and invalid parameter
uintptr_t frames[16]; // (rip is frames[0]) and for a fault in another module; 0 for a fault in this one
const char *module; // the module holding rip: NULL for this executable, "" for none (a jump to address 0)
} PlatCrash;
void plat_crash_hook(void (*fn)(const PlatCrash *c));
int plat_mem_read(uint64_t addr, void *out, size_t n); // copies n bytes, 0 instead of faulting on unmapped memory
@@ -316,8 +322,9 @@ enum { PLAT_FS_OK, PLAT_FS_NOT_FOUND, PLAT_FS_EXISTS, PLAT_FS_NOT_EMPTY, PLAT_FS
enum { PLAT_FS_MISSING = -1, PLAT_FS_FILE, PLAT_FS_DIR, PLAT_FS_LINK }; // plat_fs_kind; LINK: a symbolic link or
// reparse point, never followed
int plat_fs_kind(const char *path);
int plat_fs_os_error(void); // the OS error code (GetLastError, errno) of this thread's last failed plat_fs_* call
int plat_fs_mkdir(const char *path); // PLAT_FS_EXISTS when something is there
int plat_fs_rmdir(const char *path); // an empty directory, or a link to one
int plat_fs_rmdir(const char *path); // an empty directory (a read-only one too), or a link to one
int plat_fs_unlink(const char *path); // a file or a link; a read-only one too
// Renames a file or directory; never replaces (PLAT_FS_EXISTS). durable: the change is on the disk at return.
int plat_fs_rename(const char *from, const char *to, int durable);
@@ -412,6 +419,17 @@ void plat_child_stop(PlatChild *c); // kills the child and
int plat_child_write(PlatChild *c, const void *data, size_t n); // 1 when all n bytes were written
long plat_child_read(PlatChild *c, void *buf, size_t cap); // bytes read (at least 1); 0 or negative: pipe broke
// ---- local IPC: a stream connection to another program of this machine, one listen location per name (Discord's
// Rich Presence, discord_rpc.c). On Windows the named pipe \\.\pipe\<name>; on Linux the Unix domain socket
// <dir>/<name> with <dir> the first of $DISCORD_IPC_PATH, $XDG_RUNTIME_DIR, $TMPDIR and /tmp that has one.
typedef struct PlatIpc PlatIpc;
PlatIpc *plat_ipc_connect(const char *name); // NULL when nothing listens under that name
// Up to n bytes, waiting at most ms for the first of them: bytes read, 0 none in time, -1 the connection broke.
// The rest of what the peer wrote may still be in its buffer: a caller that needs a whole frame loops.
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms);
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n); // 1 when all n bytes were written, 0 the connection broke
void plat_ipc_close(PlatIpc *p);
// ---- restart: this program again, in a new process that starts once this one is gone
// Starts this executable again with the same command line, environment and working folder, and no handle of this
// process. 1 when it started; the caller then exits.
+152 -5
View File
@@ -665,6 +665,76 @@ __attribute__((noinline)) static void crash_from_runtime(const char *what, uint3
crash_fn(&c);
}
// The name a DLL records for itself in its export directory ("amdvlk64.dll"). Read from its mapped headers, not through
// the loader, whose lock another thread may hold when the process crashes.
static const char *image_name(const char *base) {
const IMAGE_NT_HEADERS64 *nt = (const void *)(base + ((const IMAGE_DOS_HEADER *)base)->e_lfanew);
const IMAGE_DATA_DIRECTORY *d = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!d->VirtualAddress || !d->Size) return "unnamed module";
return base + ((const IMAGE_EXPORT_DIRECTORY *)(base + d->VirtualAddress))->Name;
}
// The return addresses in [lo, lo + size) on the crashed thread's stack, innermost first: unwinds the faulting context
// with the x64 unwind tables; a function without a table entry (a leaf, or address 0 after a call through a null
// pointer) has its return address on top of the stack. The walk stops at return address 0 (the thread's first frame)
// and when the stack pointer leaves the thread's stack, so a damaged stack ends it instead of faulting.
static int frames_in(const CONTEXT *at, uintptr_t lo, uintptr_t size, uintptr_t *out, int max) {
CONTEXT c = *at;
ULONG_PTR low, high;
GetCurrentThreadStackLimits(&low, &high);
int n = 0;
for (int i = 0; i < 256 && n < max && (c.Rip || !i) && c.Rsp >= low && c.Rsp + 8 <= high; i++) {
DWORD64 img;
RUNTIME_FUNCTION *fe = RtlLookupFunctionEntry(c.Rip, &img, NULL);
if (fe) {
void *data;
DWORD64 frame;
RtlVirtualUnwind(UNW_FLAG_NHANDLER, img, c.Rip, fe, &c, &data, &frame, NULL);
} else {
c.Rip = *(const DWORD64 *)c.Rsp;
c.Rsp += 8;
}
if (c.Rip - lo < size) out[n++] = (uintptr_t)c.Rip;
}
return n;
}
// The last-chance filter: an exception no handler took, in any module. crash_veh reports faults in this executable as
// they happen; this one catches the rest, which used to end the process with no line in the log: a fault in a GPU
// driver's own thread or in a driver call (vkQueueSubmit, vkAllocateMemory), a C++ exception a library let escape, a
// call through a null pointer (rip 0, outside every module). Windows calls it only after every handler declined, so
// the exceptions drivers raise and catch themselves never get here; it costs nothing until a crash.
static LONG WINAPI crash_filter(EXCEPTION_POINTERS *ep) {
const EXCEPTION_RECORD *er = ep->ExceptionRecord;
DWORD code = er->ExceptionCode;
uintptr_t rip = (uintptr_t)ep->ContextRecord->Rip;
PlatCrash c = {code == EXCEPTION_ACCESS_VIOLATION ? "access violation"
: code == EXCEPTION_ILLEGAL_INSTRUCTION ? "illegal instruction"
: code == EXCEPTION_INT_DIVIDE_BY_ZERO ? "integer division by zero"
: code == EXCEPTION_PRIV_INSTRUCTION ? "privileged instruction"
: code == EXCEPTION_STACK_OVERFLOW ? "stack overflow"
: code == EXCEPTION_IN_PAGE_ERROR ? "in-page error"
: code == EXCEPTION_BREAKPOINT ? "breakpoint"
: code == 0xe06d7363u ? "C++ exception"
: code == 0xc0000374u ? "heap corruption" // STATUS_HEAP_CORRUPTION
: "unhandled exception",
(uint32_t)code, rip, 0, -1, 0};
if (code == EXCEPTION_ACCESS_VIOLATION && er->NumberParameters >= 2)
c.access = (int)er->ExceptionInformation[0], c.addr = er->ExceptionInformation[1];
void *base = NULL;
RtlPcToFileHeader((void *)rip, &base);
MODULEINFO mi;
uintptr_t exe = (uintptr_t)GetModuleHandleW(NULL);
c.offset = rip - (uintptr_t)base;
if (!base) c.module = "";
else if ((uintptr_t)base != exe) c.module = image_name(base);
if (c.module && GetModuleInformation(GetCurrentProcess(), (HMODULE)exe, &mi, sizeof mi))
c.nframes = frames_in(ep->ContextRecord, exe, mi.SizeOfImage, c.frames,
(int)(sizeof c.frames / sizeof *c.frames));
crash_fn(&c);
return EXCEPTION_CONTINUE_SEARCH;
}
static void abort_signal(int sig) { crash_from_runtime("abort", (uint32_t)sig); }
static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wchar_t *file, unsigned line,
@@ -676,6 +746,7 @@ static void invalid_parameter(const wchar_t *expr, const wchar_t *func, const wc
void plat_crash_hook(void (*fn)(const PlatCrash *c)) {
crash_fn = fn;
AddVectoredExceptionHandler(0, crash_veh);
SetUnhandledExceptionFilter(crash_filter); // replaces the C runtime's filter; crash_fn never returns
signal(SIGABRT, abort_signal);
_set_invalid_parameter_handler(invalid_parameter);
}
@@ -866,7 +937,11 @@ const void *plat_file_map(PlatFile *f) {
return v;
}
static __thread DWORD fs_last_error;
int plat_fs_os_error(void) { return (int)fs_last_error; }
static int fs_err(DWORD e) {
fs_last_error = e;
switch (e) {
case ERROR_FILE_NOT_FOUND: case ERROR_PATH_NOT_FOUND: case ERROR_DIRECTORY: return PLAT_FS_NOT_FOUND;
case ERROR_FILE_EXISTS: case ERROR_ALREADY_EXISTS: return PLAT_FS_EXISTS;
@@ -876,12 +951,28 @@ static int fs_err(DWORD e) {
}
}
// A symbolic link or junction is a link: removed as a link, never followed. Any other reparse point (OneDrive's cloud
// placeholders, deduplicated files) is an ordinary file or directory whose content the file system fetches on access.
static int fs_kind_of(const WIN32_FIND_DATAW *fd) {
DWORD a = fd->dwFileAttributes;
if (a & FILE_ATTRIBUTE_REPARSE_POINT && (fd->dwReserved0 == IO_REPARSE_TAG_SYMLINK || fd->dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT))
return PLAT_FS_LINK;
return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
}
int plat_fs_kind(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_MISSING;
DWORD a = GetFileAttributesW(w);
if (a == INVALID_FILE_ATTRIBUTES) return PLAT_FS_MISSING;
return a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
if (!(a & FILE_ATTRIBUTE_REPARSE_POINT)) return a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR : PLAT_FS_FILE;
// the reparse tag comes with the find data only; FindFirstFileW is asked only here, since it fails on a path
// with a trailing separator and reads wildcards
WIN32_FIND_DATAW fd;
HANDLE h = FindFirstFileW(w, &fd);
if (h == INVALID_HANDLE_VALUE) return PLAT_FS_LINK;
FindClose(h);
return fs_kind_of(&fd);
}
int plat_fs_mkdir(const char *path) {
@@ -893,7 +984,15 @@ int plat_fs_mkdir(const char *path) {
int plat_fs_rmdir(const char *path) {
wchar_t w[WPATH];
if (!widen(path, w, WPATH)) return PLAT_FS_ERROR;
return RemoveDirectoryW(w) ? PLAT_FS_OK : fs_err(GetLastError());
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
DWORD e = GetLastError(), a = GetFileAttributesW(w);
if (e == ERROR_ACCESS_DENIED && a != INVALID_FILE_ATTRIBUTES && (a & FILE_ATTRIBUTE_READONLY)) {
// a read-only directory (copied over from another emulator's save folder) refuses RemoveDirectoryW
SetFileAttributesW(w, a & ~(DWORD)FILE_ATTRIBUTE_READONLY);
if (RemoveDirectoryW(w)) return PLAT_FS_OK;
e = GetLastError();
}
return fs_err(e);
}
int plat_fs_unlink(const char *path) {
@@ -1038,9 +1137,7 @@ int plat_fs_list(const char *dir, int (*cb)(const char *name, int kind, uint64_t
if (!wcscmp(fd.cFileName, L".") || !wcscmp(fd.cFileName, L"..")) continue;
char name[1024];
if (!WideCharToMultiByte(CP_UTF8, 0, fd.cFileName, -1, name, sizeof name, NULL, NULL)) continue;
DWORD a = fd.dwFileAttributes;
int kind = a & FILE_ATTRIBUTE_REPARSE_POINT ? PLAT_FS_LINK : a & FILE_ATTRIBUTE_DIRECTORY ? PLAT_FS_DIR
: PLAT_FS_FILE;
int kind = fs_kind_of(&fd);
if (!cb(name, kind, (uint64_t)fd.nFileSizeHigh << 32 | fd.nFileSizeLow, ctx)) break;
} while (FindNextFileW(h, &fd));
FindClose(h);
@@ -1527,6 +1624,56 @@ long plat_child_read(PlatChild *c, void *buf, size_t cap) {
return (long)got;
}
// ---- local IPC (platform.h): Discord's Rich Presence named pipe
struct PlatIpc { HANDLE h; };
PlatIpc *plat_ipc_connect(const char *name) {
wchar_t wname[128], path[152];
if (!widen(name, wname, 128)) return NULL;
swprintf(path, 152, L"\\\\.\\pipe\\%ls", wname);
HANDLE h = CreateFileW(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return NULL; // no listener, or the pipe is busy: the caller retries later
PlatIpc *p = malloc(sizeof *p);
if (!p) {
CloseHandle(h);
return NULL;
}
p->h = h;
return p;
}
int plat_ipc_read(PlatIpc *p, void *buf, size_t n, uint32_t ms) {
uint64_t deadline = plat_now_ms() + ms;
for (;;) {
DWORD have = 0;
if (!PeekNamedPipe(p->h, NULL, 0, NULL, &have, NULL)) return -1; // the writer closed the pipe
if (have) {
DWORD got = 0;
if (!ReadFile(p->h, buf, have < n ? have : (DWORD)n, &got, NULL) || !got) return -1;
return (int)got;
}
if (plat_now_ms() >= deadline) return 0;
plat_sleep_ms(5);
}
}
int plat_ipc_write(PlatIpc *p, const void *buf, size_t n) {
const uint8_t *at = buf;
while (n) {
DWORD put = 0;
if (!WriteFile(p->h, at, n > (1u << 30) ? 1u << 30 : (DWORD)n, &put, NULL) || !put) return 0;
at += put, n -= put;
}
return 1;
}
void plat_ipc_close(PlatIpc *p) {
if (!p) return;
CloseHandle(p->h);
free(p);
}
// ---- restart
// TOTK_RESTART_WAIT names the process that called plat_restart, as its id and creation time: 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,
+139 -17
View File
@@ -27,6 +27,9 @@
// rewritten table (res_heapdef_build) in place of the original; memory.c adds the amount to the application pool, so
// no other heap shrinks. Without the rewritten table (a RomFS that lacks it, a format this code does not know) the
// patch sets stay off and the game keeps its own sizes.
#ifndef _WIN32
#include <dirent.h>
#endif
#include <math.h>
#include <stdint.h>
#include <stdio.h>
@@ -36,6 +39,7 @@
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
#define BASE_W 1920
#define BASE_H 1080
@@ -52,9 +56,70 @@ static struct {
int heapdef_ok; // romfs.c serves the grown HeapDef
int on; // res_init switched the patch sets on
unsigned shadow; // fixed sun shadow map size (TOTK_SHADOW_RES), 0: the game's own
double dlss_k; // TOTK_DLSS quality/balanced/performance: the scene's share of the output, else 0
double dlss_k; // TOTK_DLSS quality/balanced/performance/ultra_performance: the scene's share of the output, else 0
double base_k; // the scene's share without a DLSS mode (render_scale, or the game's 5/6)
unsigned hw, hh; // the largest scene a DLSS mode change can ask for: the heap is sized for it
int live; // 1.4.3 with DLSS able to run: the DLSS mode can change while the game runs
} R;
// The scene size is decided before the device exists, so NGX cannot be asked yet. NGX loads DLSS from nvngx_dlss.dll
// next to totk.exe on Windows, libnvidia-ngx-dlss.so.<version> next to totk on Linux (vk_dlaa.c vk_dlaa_init):
// without that file DLSS never runs, and a DLSS mode would leave the game's plain scaling at 50..67 % of the output.
static int dlss_dll(void) {
#ifdef _WIN32
char exe[1024], path[1024 + 32];
if (!plat_exe_path(exe, sizeof exe)) return 1;
char *last = plat_last_sep(exe);
if (!last) return 1;
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
return plat_file_exists(path);
#else
char exe[1024];
if (!plat_exe_path(exe, sizeof exe)) return 1;
char *last = plat_last_sep(exe);
if (!last) return 1;
*last = 0;
DIR *d = opendir(exe);
if (!d) return 1;
int found = 0;
for (struct dirent *e; !found && (e = readdir(d));)
found = !strncmp(e->d_name, "libnvidia-ngx-dlss.so.", 22);
closedir(d);
return found;
#endif
}
// The scene size for a share k of the output, within SCENE_MAX per side.
static void scene_for(double k, unsigned *w, unsigned *h) {
double sw = R.ow * k, sh = R.oh * k;
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
*w = (unsigned)(sw * fit + 0.5);
*h = (unsigned)(sh * fit + 0.5);
}
// The scene's share of the output for a TOTK_DLSS mode; 0 for dlaa (the scene keeps base_k), while DLSS/DLAA is off
// (the mode the menu still holds would shrink the scene for the plain upscaler) and where DLSS cannot run (dlss_dll).
static double dlss_share(const char *m) {
static const struct { const char *name; double k; } dlss[] = {
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}, {"ultra_performance", 1.0 / 3.0}};
static int dll = -1;
if (!cfg_bool("TOTK_DLAA")) return 0;
for (int i = 0; i < (int)(sizeof dlss / sizeof dlss[0]); i++)
if (!strcmp(m, dlss[i].name)) {
if (dll < 0 && !(dll = dlss_dll())) {
#ifdef _WIN32
rt_log("resolution: DLSS %s needs nvngx_dlss.dll next to totk.exe, which is missing: the scene follows "
"render_scale", m);
#else
rt_log("resolution: DLSS %s needs libnvidia-ngx-dlss.so.<version> next to totk, which is missing: the "
"scene follows render_scale", m);
#endif
}
return dll ? dlss[i].k : 0;
}
return 0;
}
static void decide(void) {
if (R.decided) return;
R.decided = 1;
@@ -71,16 +136,7 @@ static void decide(void) {
}
const char *s = cfg_value("TOTK_RENDER_SCALE");
double k = 5.0 / 6.0;
const char *m = cfg_value("TOTK_DLSS"); // a DLSS mode other than dlaa sets the scene size
static const struct { const char *name; double k; } dlss[] = {
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}};
for (int i = 0; i < 3; i++)
if (!strcmp(m, dlss[i].name)) R.dlss_k = dlss[i].k;
if (R.dlss_k) {
k = R.dlss_k;
R.scene_set = 1;
rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, k * 100);
} else if (strcmp(s, "game")) {
if (strcmp(s, "game")) {
char *end;
double pct = strtod(s, &end);
if (end != s && !*end && pct >= 25 && pct <= 400) {
@@ -90,11 +146,20 @@ static void decide(void) {
rt_log("resolution: TOTK_RENDER_SCALE=%s is not game or a percentage 25..400; game", s);
}
}
double sw = R.ow * k, sh = R.oh * k;
double fit = fmin(1.0, fmin(SCENE_MAX / sw, SCENE_MAX / sh));
if (fit < 1.0) R.scene_set = 1;
R.sw = (unsigned)(sw * fit + 0.5);
R.sh = (unsigned)(sh * fit + 0.5);
// A DLSS mode other than dlaa sets the scene size instead. On 1.4.3 with DLSS able to run (DLAA on, the DLL there)
// the mode can change while the game runs (res_dlss_mode): the scene patch is then always on and the heap is sized
// for the largest scene the modes ask for. Everyone else keeps the game's own scene size and its dynamic resolution
// unless render_scale or a DLSS mode asks for another, and a heap sized for that scene alone.
R.base_k = k;
R.live = RT_V143 && cfg_bool("TOTK_DLAA") && dlss_dll();
const char *m = cfg_value("TOTK_DLSS");
R.dlss_k = dlss_share(m);
if (R.dlss_k) rt_log("resolution: DLSS %s: the scene at %.0f %% of the output (render_scale ignored)", m, R.dlss_k * 100);
double sk = R.dlss_k ? R.dlss_k : k;
if (R.live || R.dlss_k || R.ow * sk > SCENE_MAX || R.oh * sk > SCENE_MAX) R.scene_set = 1;
scene_for(sk, &R.sw, &R.sh);
if (R.live) scene_for(fmax(k, 2.0 / 3.0), &R.hw, &R.hh);
else R.hw = R.sw, R.hh = R.sh;
const char *sr = cfg_value("TOTK_SHADOW_RES");
if (strcmp(sr, "game")) {
unsigned n = (unsigned)atoi(sr);
@@ -117,7 +182,7 @@ static int bigger(void) {
uint64_t res_heap_extra_kib(void) {
if (!bigger()) return 0;
double grow = fmax(0.0, (double)R.ow * R.oh / (BASE_W * BASE_H) - 1.0) +
fmax(0.0, (double)R.sw * R.sh / (SCENE_W * SCENE_H) - 1.0);
fmax(0.0, (double)R.hw * R.hh / (SCENE_W * SCENE_H) - 1.0);
uint64_t kib = grow > 0 ? (uint64_t)ceil(DTA_KIB * (1.0 + grow) / 1024.0) * 1024 - DTA_KIB : 0;
return kib + ((shadow_kib() + 1023) & ~1023ull); // whole MiB
}
@@ -282,6 +347,7 @@ void res_init(void) {
rt_patch_switch(output, R.ow != BASE_W || R.oh != BASE_H);
rt_patch_switch(scene, scene_on);
R.on = 1;
if (R.live) g_fnprof = 1; // until the game sets its render size: res_fn_hit, or res_present gives up
// Another shape than 16:9 (more than 1% off): the camera takes the output's ratio (recomp/patches-v121.txt and
// patches-v143.txt, set aspect), so a 21:9 output shows more of the world at the sides instead of a stretched 16:9 view. The HUD's layouts
// for that shape come from romfs-patches-<w>x<h> (romfs.c overlay_init, tools/make_romfs_patches.py --aspect).
@@ -352,6 +418,62 @@ void res_wanted(unsigned out[2]) {
out[0] = R.ow, out[1] = R.oh;
}
// A TOTK_DLSS change while the game runs (vk_dlaa.c, at a present): the new scene size goes to the patch page, where
// the game reads it when it sets its render size (main+0x585e48). 1.4.3 does that once, at start: its renderer setup
// (main+0x7000e67028) calls main+0x585bd0(renderer, force), which picks the console mode's sizes and flags the render
// targets for re-creation. The setup call is caught through the function-entry hook (res_fn_hit, on only until then),
// and the next nvnQueuePresentTexture on the game's thread calls it again with force set (res_present), between frames
// as at start. The heap was sized for the largest scene a mode asks for. 1 when the size changed.
#define SET_RENDER_SIZE 0x7000585bd0ull
static uint64_t renderer; // x0 of the game's own call of SET_RENDER_SIZE
static volatile int size_pending; // the page holds a size the game has not set yet
void res_fn_hit(uint64_t addr, uint64_t x0) {
if (addr != SET_RENDER_SIZE) return;
renderer = x0;
g_fnprof = 0;
rt_log("resolution: renderer at %#llx; the scene size can change while the game runs", (unsigned long long)x0);
}
void res_present(Cpu *c) {
// The hook runs on every guest function entry while it waits: should the game set its render size before
// res_init or never, it stops after 1800 presents (half a minute at 60 fps) and modes change at the next start.
static unsigned waited;
if (g_fnprof && !renderer && ++waited == 1800) {
g_fnprof = 0;
rt_log("resolution: the game's render size setup was not seen; DLSS modes change at the next start");
}
if (!size_pending || !renderer) return;
size_pending = 0;
Cpu keep = *c; // the guest's registers as at the present call: the setup runs as if it never happened
rt_call(c, SET_RENDER_SIZE, 2, (uint64_t)renderer, (uint64_t)1);
memcpy(c->x, keep.x, sizeof c->x);
memcpy(c->v, keep.v, sizeof c->v);
c->nzcv = keep.nzcv, c->fpsr = keep.fpsr;
rt_log("resolution: the game set the scene size %ux%u", R.sw, R.sh);
}
int res_dlss_mode(const char *m) {
decide();
double k = dlss_share(m);
if (k == R.dlss_k || !R.on) return 0;
if (!RT_V143 || !renderer) {
rt_log("resolution: DLSS %s takes effect at the next start (the game set its render size already)", m);
return 0;
}
R.dlss_k = k;
unsigned w, h;
scene_for(k ? k : R.base_k, &w, &h);
if (w == R.sw && h == R.sh) return 0;
double s = RT_V143 ? (double)R.ow / BASE_W : 1.0;
float sc[2] = {(float)(w / s), (float)(h / s)};
memcpy((uint8_t *)rt_patch_page() + 16, sc, sizeof sc);
R.sw = w, R.sh = h;
size_pending = 1;
rt_log("resolution: DLSS %s: scene %ux%u", m, w, h);
return 1;
}
void res_sizes(unsigned out[2], unsigned scene[2]) {
decide();
out[0] = R.on ? R.ow : BASE_W, out[1] = R.on ? R.oh : BASE_H;
+23
View File
@@ -6,14 +6,26 @@
// The camera block is TotK's scene uniform block (bound as c4 in the scene's vertex shaders): vec4 rows 3-6 are the
// view-projection, rows of a row-major 4x4 with OpenGL clip depth (-w..w). The replay's viewports have negative
// heights, so window y grows downwards from ndc y = +1, and depth is z_ndc * 0.5 + 0.5.
// With pc.bias > 0 the pass also writes DLSS's bias mask (vk_dlaa.c, "characters"): G-buffer target 0 carries an object
// class in green, above 64/255 for characters, creatures and what they hold (Link 253, others 125, held items 160;
// terrain 0, foliage 32). A pixel gets pc.bias when a character covers it now, or covered the place its surface came
// from last frame (the previous class mask, read through the motion vector, 3x3): there the history holds a moving
// character that the camera-only vector does not follow, which is the trail. chars_out keeps this frame's mask for the
// next; chars_prev is last frame's (pc.prev_ok 0: none).
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D depth_tex;
layout(binding = 1, rg16f) uniform writeonly image2D mv_out;
layout(binding = 2, std140) uniform Cur { vec4 r[16]; } cur;
layout(binding = 3, std140) uniform Prev { vec4 r[16]; } prev;
layout(binding = 4) uniform sampler2D ids_tex;
layout(binding = 5, r8) uniform writeonly image2D mask_out;
layout(binding = 6, r8) uniform readonly image2D chars_prev;
layout(binding = 7, r8) uniform writeonly image2D chars_out;
layout(push_constant) uniform P {
vec2 size; // render size in pixels
vec2 jitter; // this frame's jitter, pixels, as the scene was drawn (window x right, y down)
float bias; // the mask value for character pixels; 0: no mask (bindings 4-7 unused)
float prev_ok; // 1: chars_prev holds last frame's class mask at this size
} pc;
mat4 vp(vec4 a, vec4 b, vec4 c, vec4 d) { return transpose(mat4(a, b, c, d)); }
@@ -37,4 +49,15 @@ void main() {
}
}
imageStore(mv_out, p, vec4(mv, 0.0, 0.0));
if (pc.bias <= 0.0) return;
float ch = texelFetch(ids_tex, p, 0).g > 64.0 / 255.0 ? 1.0 : 0.0;
imageStore(chars_out, p, vec4(ch));
float was = 0.0;
if (pc.prev_ok > 0.5) {
ivec2 q = ivec2(floor(win + mv)), hi = ivec2(pc.size) - 1;
for (int dy = -1; dy <= 1; dy++)
for (int dx = -1; dx <= 1; dx++)
was = max(was, imageLoad(chars_prev, clamp(q + ivec2(dx, dy), ivec2(0), hi)).r);
}
imageStore(mask_out, p, vec4(max(ch, was) * pc.bias));
}
+14 -1
View File
@@ -6,6 +6,10 @@
// the game's up to PIVOT and a straight line with its slope above, so mid-greys look as in SDR and highlights keep
// going; the grade works on the colour divided by its largest channel above 1; a roll-off on the largest channel stops
// at the display's peak. The output is relative to the HUD's white (1.0), which the present maps to TOTK_HDR_UI_NITS.
// Hue correction (hdr.z, TOTK_HDR_HUE): 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 curve keeps
// their ratios, so they stay saturated. From the picture's white upwards the colour moves towards the game's own SDR
// result at the HDR luminance, by up to hdr.z (RenoDX's TotK mod has the same control).
// Approach after RenoDX's TotK mod by souperman9 and Musa (github.com/clshortfuse/renodx, src/games/totk, MIT): its
// "extended vanilla" curve.
// Bindings are those of the game's translated shader: vk_hdr.c rewrites them from the original's names.
@@ -14,7 +18,7 @@ layout(location = 0) out vec4 color;
layout(set = 0, binding = 0, std140) uniform SupportBuffer {
vec4 pad[90];
vec4 hdr; // byte 1440 (vk_exec_draw.c support_fill): game white / HUD white, peak / game white
vec4 hdr; // byte 1440 (vk_exec_draw.c support_fill): game white / HUD white, peak / game white, hue correction
} support_buffer;
layout(set = 0, binding = 1) uniform sampler2D fp_t_tcb_8;
layout(set = 0, binding = 2) uniform sampler2D fp_t_tcb_A;
@@ -51,6 +55,15 @@ void main() {
float grade = g.y * (1.0 - d * d) + g.x;
c = s * ((n - m) * grade + m);
// the game's own SDR result: the same blend clamped to 0..1, graded
vec3 sdr_c = clamp(mix(lum > 0.0 ? x * (sdr / lum) : x, vec3(1.0) - exp(-x), sdr * sdr), 0.0, 1.0);
float sm = maxc(sdr_c), sd = sdr_c.r * 0.6666667 + (sdr_c.g + sdr_c.b) * w.z - 1.0;
sdr_c = (sdr_c - sm) * (g.y * (1.0 - sd * sd) + g.x) + sm;
float hue = clamp(support_buffer.hdr.z, 0.0, 1.0);
const vec3 Y = vec3(0.2126, 0.7152, 0.0722); // Rec. 709 luminance
float yc = dot(c, Y), ys = dot(sdr_c, Y);
if (hue > 0.0 && ys > 1e-5) c = mix(c, max(sdr_c, vec3(0.0)) * (yc / ys), hue * smoothstep(0.5, 1.5, maxc(c)));
// roll-off of the largest channel from half the peak to the peak
float top = maxc(c), knee = 0.5 * peak;
if (top > knee) {
+58 -11
View File
@@ -59,16 +59,60 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
return FN(fv)(DNAN);
}
// ---- FMA flavours. FMA_S / FMA_V: fast paths, host MXCSR is round-to-nearest there (simd_directed() is checked first).
// FMA_X: scalar FMA for the slow paths, any host rounding mode.
// Without hardware FMA (Ivy Bridge, no -mfma) clang turns __builtin_elementwise_fma into a call to the C runtime's
// fmaf/fma for every lane, which is very slow and keeps the thread outside generated code. For float the product of
// two floats is exact in a double (24 x 24 = 48 bits), so the fused op is done in double. A plain
// (float)(x * y + z) rounds twice and can differ by one ulp; rounding the double sum to odd first (Boldo-Melquiond)
// makes the final float rounding exact: TwoSum gives the error e of s = fl(p + z), and when e != 0 and s has an even
// last bit, s is moved one ulp toward the exact value (the odd neighbour). Inf/NaN lanes are left alone (e is NaN
// there, so the ordered compares below are false). Double has no wider type: it keeps the C runtime's fma.
#if defined(__FMA__) || defined(__aarch64__) || NB == 64
#define FMA_S(x, y, z) __builtin_elementwise_fma(x, y, z)
#define FMA_V(x, y, z) __builtin_elementwise_fma(x, y, z)
#define FMA_X(x, y, z) __builtin_elementwise_fma(x, y, z)
#else
typedef double simd_vf64x4 __attribute__((vector_size(32)));
typedef uint64_t simd_vu64x4 __attribute__((vector_size(32)));
SI float fma_rn_s(float x, float y, float z) {
double p = (double)x * (double)y, zd = (double)z, s = p + zd;
double bb = s - p, e = (p - (s - bb)) + (zd - bb); // TwoSum: s + e == p + zd exactly
uint64_t u;
memcpy(&u, &s, sizeof u);
if (((e < 0.0) | (e > 0.0)) && !(u & 1)) u += (e > 0.0) == (s > 0.0) ? 1 : (uint64_t)-1;
memcpy(&s, &u, sizeof s);
return (float)s;
}
SI vf32 fma_rn_v(vf32 x, vf32 y, vf32 z) {
simd_vf64x4 p = CVT(x, simd_vf64x4) * CVT(y, simd_vf64x4), zd = CVT(z, simd_vf64x4), s = p + zd;
simd_vf64x4 bb = s - p, e = (p - (s - bb)) + (zd - bb);
simd_vu64x4 u = (simd_vu64x4)s;
simd_vu64x4 fix = (simd_vu64x4)((e < 0.0) | (e > 0.0)) & (simd_vu64x4)((u & 1) == 0);
simd_vu64x4 up = (simd_vu64x4)((e > 0.0) == (s > 0.0));
u += fix & ((up & 1) | ~up); // +1 toward the exact value, i.e. +1 if e and s share a sign, else -1
return CVT((simd_vf64x4)u, vf32);
}
// Any rounding mode: directed modes are exact with a plain double (same-direction double rounding is harmless).
SI float fma_any_s(float x, float y, float z) {
if (__builtin_expect(_mm_getcsr() & 0x6000u, 0)) return (float)((double)x * (double)y + (double)z);
return fma_rn_s(x, y, z);
}
#define FMA_S(x, y, z) fma_rn_s(x, y, z)
#define FMA_V(x, y, z) fma_rn_v(x, y, z)
#define FMA_X(x, y, z) fma_any_s(x, y, z)
#endif
// The arithmetic itself, for scalars (KK = K) and vectors (KK = VK). MADD: a + b*d (a = addend).
// RECPS/RSQRTS get their first operand already negated (FPNeg happens before NaN processing).
#define FCALC(op, a, b, d, KK) \
#define FCALC(op, a, b, d, KK, FMA) \
((op) == FOP_ADD ? (a) + (b) : (op) == FOP_SUB ? (a) - (b) \
: (op) == FOP_MUL || (op) == FOP_MULX ? (a) * (b) : (op) == FOP_NMUL ? -((a) * (b)) \
: (op) == FOP_DIV ? (a) / (b) : (op) == FOP_ABD ? __builtin_elementwise_abs((a) - (b)) \
: (op) == FOP_SQRT ? __builtin_elementwise_sqrt(a) \
: (op) == FOP_MADD ? __builtin_elementwise_fma(b, d, a) \
: (op) == FOP_RECPS ? __builtin_elementwise_fma(a, b, KK(2)) \
: (op) == FOP_RSQRTS ? __builtin_elementwise_fma(a, b, KK(3)) * KK(0.5) : (a))
: (op) == FOP_MADD ? FMA(b, d, a) \
: (op) == FOP_RECPS ? FMA(a, b, KK(2)) \
: (op) == FOP_RSQRTS ? FMA(a, b, KK(3)) * KK(0.5) : (a))
// Lanes whose fast result needs fixing: NaN results (NaN operand or invalid op), RSQRTS overflow.
#define FBAD(op, r) ((op) == FOP_RSQRTS ? ((r) != (r)) | (__builtin_elementwise_abs(r) == K(INFINITY)) : (r) != (r))
@@ -76,9 +120,9 @@ SN T FN(nan3)(Cpu *c, T a, T b, T d) {
// (3 + a*b) / 2 with one rounding (a already negated): halve a normal operand first, so a product beyond
// the format's range (which the fast path rounds to inf, or to the largest normal when rounding down) is exact.
SN T FN(rsqrts)(T a, T b) {
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a * K(0.5), b, K(1.5));
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return __builtin_elementwise_fma(a, b * K(0.5), K(1.5));
return __builtin_elementwise_fma(a, b, K(3)) * K(0.5);
if (__builtin_elementwise_abs(a) >= FN(fv)(2 * MINN)) return FMA_X(a * K(0.5), b, K(1.5));
if (__builtin_elementwise_abs(b) >= FN(fv)(2 * MINN)) return FMA_X(a, b * K(0.5), K(1.5));
return FMA_X(a, b, K(3)) * K(0.5);
}
// ARM result for a lane flagged by FBAD (operands already flushed).
@@ -136,13 +180,13 @@ SN T FN(slow)(Cpu *c, int op, T a, T b, T d) {
if (FMINMAX(op)) return FN(minmax)(c, op, a, b);
unsigned old = simd_rm_enter(fpcr);
volatile T va = a, vb = b, vd = d;
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K);
volatile T vr = op == FOP_RSQRTS ? FN(rsqrts)(va, vb) : FCALC(op, va, vb, vd, K, FMA_X);
T r = vr;
if (FBAD(op, r)) vr = FN(fix)(c, op, a, b, d), r = vr;
if ((fpcr & FPCR_FZ) && (FN(fb)(r) & ~SGN) <= MINN && !FN(iszero)(FN(fb)(r))) {
if ((FN(fb)(r) & ~SGN) == MINN) {
simd_rm_rz();
volatile T vz = FCALC(op, va, vb, vd, K);
volatile T vz = FCALC(op, va, vb, vd, K, FMA_X);
if ((FN(fb)(vz) & ~SGN) < MINN) r = FN(fv)(FN(fb)(r) & SGN);
} else {
r = FN(fv)(FN(fb)(r) & SGN);
@@ -172,7 +216,7 @@ SI T FN(op)(Cpu *c, int op, T a, T b, T d) {
if (c->fpcr & FPCR_FZ) a = FN(flush)(a), b = FN(flush)(b);
return FN(minmax)(c, op, a, b);
}
T r = FCALC(op, a, b, d, K);
T r = FCALC(op, a, b, d, K, FMA_S);
if (__builtin_expect(op == FOP_RSQRTS ? FREDO(op, r) : !FOK(r), 0)) r = FN(redo)(c, op, a, b, d, r);
return r;
}
@@ -207,7 +251,7 @@ SI VT FN(vop)(Cpu *c, int op, VT a, VT b, VT d) {
r = (VT)FN(vsel)(a < b, x, FN(vsel)(a == b, x | y, y));
return r;
}
r = FCALC(op, a, b, d, VK);
r = FCALC(op, a, b, d, VK, FMA_V);
if (__builtin_expect(op == FOP_RSQRTS ? __builtin_reduce_or(FREDO(op, r)) != 0 : __builtin_reduce_and(FOK(r)) == 0, 0))
return FN(vslow)(c, op, a, b, d);
return r;
@@ -363,3 +407,6 @@ SI T FN(recpx)(Cpu *c, T a) {
#undef MINN
#undef VK
#undef K
#undef FMA_S
#undef FMA_V
#undef FMA_X
+40 -11
View File
@@ -17,6 +17,7 @@
extern "C" {
#include "amiibo.h"
#include "cfg.h"
#include "folder_pick.h"
#include "platform/platform.h"
#include "ui_text.h"
}
@@ -30,7 +31,6 @@ struct FileEntry {
static const char kDefaultAmiiboDir[] = "./amiibo";
static std::string g_current_dir = kDefaultAmiiboDir;
static char g_dir_input[512] = "./amiibo";
static std::vector<FileEntry> g_entries;
static int g_selected_entry = -1;
// The last action: a catalog id and the file or folder it names, formatted when drawn (the menu language can change)
@@ -61,10 +61,24 @@ static bool iequals(std::string_view a, std::string_view b) {
static void set_current_dir(const std::string &path) {
g_current_dir = path;
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
cfg_set("TOTK_AMIIBO_DIR", g_current_dir.c_str());
}
// The host's folder dialog (folder_pick.h) answers on its own thread; the page takes the answer when it draws.
static PlatMutex g_pick_lock = PLAT_MUTEX_INIT;
static bool g_pick_open;
static bool g_pick_ready;
static std::string g_pick_path;
static void on_folder_picked(const char *path, void *ctx) {
(void)ctx;
plat_mutex_lock(&g_pick_lock);
g_pick_ready = path != NULL;
if (path) g_pick_path = path;
g_pick_open = false;
plat_mutex_unlock(&g_pick_lock);
}
static void refresh_dir(void) {
g_entries.clear();
g_selected_entry = -1;
@@ -122,11 +136,21 @@ void tab_amiibo(float scale) {
} else {
g_current_dir = kDefaultAmiiboDir;
}
snprintf(g_dir_input, sizeof(g_dir_input), "%s", g_current_dir.c_str());
refresh_dir();
s_inited = true;
}
plat_mutex_lock(&g_pick_lock);
bool picking = g_pick_open;
std::string picked;
if (g_pick_ready) picked.swap(g_pick_path);
g_pick_ready = false;
plat_mutex_unlock(&g_pick_lock);
if (!picked.empty()) {
set_current_dir(picked);
refresh_dir();
}
// Two panes: Left (Directory Browser & Tag List), Right (Amiibo Tag Details & Controls)
const float content_h = ImGui::GetContentRegionAvail().y - opt_bottom(); // Continue and Quit stay visible
const float left_w = ImGui::GetContentRegionAvail().x * 0.58f;
@@ -136,17 +160,17 @@ void tab_amiibo(float scale) {
ImGui::BeginChild("##amiibo_explorer", ImVec2(left_w, content_h), true);
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_DIR));
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
if (ImGui::InputText("##amiibo_dir_input", g_dir_input, sizeof(g_dir_input), ImGuiInputTextFlags_EnterReturnsTrue)) {
set_current_dir(g_dir_input);
refresh_dir();
}
ImGui::TextWrapped("%s", g_current_dir.c_str());
ImGui::Spacing();
ImGui::BeginDisabled(picking);
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_OPEN))) {
set_current_dir(g_dir_input);
refresh_dir();
plat_mutex_lock(&g_pick_lock);
g_pick_open = true;
plat_mutex_unlock(&g_pick_lock);
folder_pick(g_current_dir.c_str(), on_folder_picked, NULL);
}
ImGui::EndDisabled();
ImGui::SameLine();
if (ImGui::Button(menu_text(UI_MENU_AMIIBO_DEFAULT_DIR))) {
set_current_dir(kDefaultAmiiboDir);
@@ -161,6 +185,11 @@ void tab_amiibo(float scale) {
ImGui::TextDisabled("%s", menu_text(UI_MENU_AMIIBO_PICK));
ImGui::BeginChild("##file_list", ImVec2(0, 0), true);
if (g_status_id == UI_MENU_AMIIBO_NO_DIR) {
char missing[512];
snprintf(missing, sizeof missing, menu_text(UI_MENU_AMIIBO_NO_DIR), g_current_dir.c_str());
ImGui::TextWrapped("%s", missing);
}
for (size_t i = 0; i < g_entries.size(); i++) {
const auto &e = g_entries[i];
std::string label = e.is_dir ? e.name + "/" : e.name;
@@ -198,7 +227,7 @@ void tab_amiibo(float scale) {
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1.0f), "%s", menu_text(UI_MENU_AMIIBO_READER));
ImGui::Separator();
AmiiboDeviceState state = amiibo_get_device_state();
AmiiboDeviceState state = amiibo_get_reader_state();
UiTextId state_id = UI_MENU_AMIIBO_STATE_IDLE;
ImVec4 state_col = ImVec4(0.8f, 0.8f, 0.8f, 1.0f);
switch (state) {
+73 -37
View File
@@ -346,6 +346,12 @@ static int fonts_up(float scale) {
ImFontGlyphRangesBuilder b;
b.AddText(ui_text_in("en", kCjk[i].name));
ui_text_visit(kCjk[i].lang, add_row_glyphs, &b);
// Text the menu shows that no catalog row holds: a mod's cheat names and its folder name,
// both read from disk at runtime. Their glyphs would otherwise fall back to the missing-glyph box.
for (int k = 0; k < cheats_count(); k++)
if (const CheatInfo *ci = cheats_info(k); ci && ci->name) b.AddText(ci->name);
for (int k = 0; k < mods_count(); k++)
if (const ModInfo *mi = mods_at(k); mi) b.AddText(mi->name);
b.BuildRanges(&ranges[i]);
ImFontConfig mc;
mc.MergeMode = true;
@@ -485,13 +491,15 @@ static void row_window_size(void) {
}
// The game resolution (resolution.c): 16:9 sizes from its smallest, 1280x720 (for weak GPUs), up, the common 21:9 and
// 32:9 monitor sizes and the 16:10 screens of the Steam Deck, handhelds and laptops (the camera and the HUD follow
// 32:9 monitor sizes, the 16:10 screens of the Steam Deck, handhelds and laptops and the 3:2 screens of laptops and
// tablets (Surface, Framework; the camera and the HUD follow
// another shape, resolution.c res_init; the installer lays the HUD out for each, installer/steps.py ASPECT_SIZES), and
// the current value when it is another size.
static void row_output_res(void) {
static const char *const vals[] = {"1280x720", "1600x900", "1920x1080", "2560x1440", "3200x1800", "3840x2160",
"2560x1080", "3440x1440", "3840x1600", "5120x2160", "3840x1080", "5120x1440",
"1280x800", "1680x1050", "1920x1200", "2560x1600"};
"1280x800", "1680x1050", "1920x1200", "2560x1600",
"1920x1280", "2160x1440", "2256x1504", "2880x1920", "3000x2000"};
enum { N = sizeof vals / sizeof *vals };
char shown[N][32];
const char *names[N];
@@ -518,7 +526,7 @@ static void row_render_scale(void) {
}
static void row_shadow_res(void) {
static const char *const vals[] = {"game", "512", "768", "1024", "1536", "2048"};
static const char *const vals[] = {"game", "256", "512", "768", "1024", "1536", "2048"};
enum { N = sizeof vals / sizeof *vals };
const char *names[N];
names[0] = menu_text(UI_MENU_SHADOW_RES_GAME);
@@ -551,27 +559,25 @@ static void row_gpu(void) {
opt_choice_list("TOTK_GPU_INDEX", UI_MENU_GPU, vals, names, g_gpu_n + 1, UI_MENU_GPU_DESC);
}
// The game language: one entry per language TotK ships (a Switch code that names it), each under its own name. The
// Switch's Portuguese is not among them.
// The game language: one entry per Mals language file the running game build ships (ui_text_game_langs), each under
// its own name. A code that names no entry exactly (older "en", "fr-CA" on a build without it) selects the first entry
// of the same catalog language.
static void row_game_lang(void) {
static const struct { const char *code; UiTextId name; } langs[] = {
{"en-US", UI_MENU_LANGNAME_EN}, {"ja", UI_MENU_LANGNAME_JA}, {"fr", UI_MENU_LANGNAME_FR},
{"de", UI_MENU_LANGNAME_DE}, {"it", UI_MENU_LANGNAME_IT}, {"es", UI_MENU_LANGNAME_ES},
{"nl", UI_MENU_LANGNAME_NL}, {"ru", UI_MENU_LANGNAME_RU},
{"zh-Hans", UI_MENU_LANGNAME_ZH_HANS}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT}, {"ko", UI_MENU_LANGNAME_KO},
};
enum { N = sizeof langs / sizeof *langs + 1 };
const char *vals[N], *names[N];
UiGameLang langs[16];
int n = ui_text_game_langs(rt_game_version, langs, 16);
const char *vals[17], *names[17];
vals[0] = "";
names[0] = menu_text(UI_MENU_GAME_LANG_AUTO);
const char *now = cfg_value("TOTK_LANG");
int cur = *now ? -1 : 0;
for (int i = 1; i < N; i++) {
for (int i = 1; i <= n; i++) {
vals[i] = langs[i - 1].code;
names[i] = menu_text(langs[i - 1].name);
if (*now && !strcmp(ui_text_lang(now), ui_text_lang(vals[i]))) cur = i; // en-GB is English too
if (*now && !strcmp(now, vals[i])) cur = i;
}
opt_choice_list("TOTK_LANG", UI_MENU_GAME_LANG, vals, names, N, UI_MENU_GAME_LANG_DESC, cur);
for (int i = 1; i <= n && *now && cur < 0; i++)
if (!strcmp(ui_text_lang(now), ui_text_lang(vals[i]))) cur = i;
opt_choice_list("TOTK_LANG", UI_MENU_GAME_LANG, vals, names, n + 1, UI_MENU_GAME_LANG_DESC, cur);
}
// The frame limit (frame_rate.c): unset, the older fps60 = 0 still means 30. The monitor choice shows the rate it gives.
@@ -672,8 +678,13 @@ static void tab_screen(float scale) {
opt_slider("TOTK_CONTACT_SHADOWS", UI_MENU_CONTACT_SHADOWS, 0, 100, UI_MENU_CONTACT_SHADOWS_DESC, 10, "%.0f %%");
opt_toggle("TOTK_DLAA", UI_MENU_DLAA, UI_MENU_DLAA_DESC);
static const UiTextId dlss[] = {UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY, UI_MENU_DLSS_BALANCED,
UI_MENU_DLSS_PERFORMANCE};
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 4, UI_MENU_DLSS_DESC);
UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA};
opt_choice("TOTK_DLSS", UI_MENU_DLSS, dlss, 5, UI_MENU_DLSS_DESC);
static const UiTextId preset[] = {UI_MENU_DLSS_PRESET_DEFAULT, UI_MENU_DLSS_PRESET_K, UI_MENU_DLSS_PRESET_J};
opt_choice("TOTK_DLSS_PRESET", UI_MENU_DLSS_PRESET, preset, 3, UI_MENU_DLSS_PRESET_DESC);
static const UiTextId chars[] = {UI_MENU_DLSS_CHARS_OFF, UI_MENU_DLSS_CHARS_LIGHT, UI_MENU_DLSS_CHARS_MEDIUM,
UI_MENU_DLSS_CHARS_FULL};
opt_choice("TOTK_DLSS_CHAR_MASK", UI_MENU_DLSS_CHARS, chars, 4, UI_MENU_DLSS_CHARS_DESC);
opt_toggle("TOTK_FXAA", UI_MENU_FXAA, UI_MENU_FXAA_DESC);
rows_color();
opt_group(UI_MENU_HDR_GROUP);
@@ -681,6 +692,7 @@ static void tab_screen(float scale) {
opt_slider("TOTK_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f");
opt_slider("TOTK_HDR_GAME_NITS", UI_MENU_HDR_GAME, 80, 500, UI_MENU_HDR_GAME_DESC, 5, "%.0f");
opt_slider("TOTK_HDR_UI_NITS", UI_MENU_HDR_UI, 80, 500, UI_MENU_HDR_UI_DESC, 5, "%.0f");
opt_slider("TOTK_HDR_HUE", UI_MENU_HDR_HUE, 0, 100, UI_MENU_HDR_HUE_DESC, 5, "%.0f %%");
opt_end();
}
@@ -697,6 +709,8 @@ static void tab_game(float scale) {
static void tab_sound(float scale) {
opt_begin("##sound", scale);
opt_slider("TOTK_VOLUME", UI_MENU_VOLUME, 0, 100, UI_MENU_VOLUME_DESC, 5, "%.0f %%");
static const UiTextId channels[] = {UI_MENU_CHANNELS_AUTO, UI_MENU_CHANNELS_STEREO, UI_MENU_CHANNELS_51};
opt_choice("TOTK_AUDIO_CHANNELS", UI_MENU_AUDIO_CHANNELS, channels, 3, UI_MENU_AUDIO_CHANNELS_DESC);
opt_end();
}
@@ -822,8 +836,13 @@ static void tab_controls(float scale) {
opt_slider("TOTK_RUMBLE_STRENGTH", UI_MENU_RUMBLE_STRENGTH, 0, 300, UI_MENU_RUMBLE_STRENGTH_DESC, 10, "%.0f %%");
opt_slider("TOTK_RUMBLE_CURVE", UI_MENU_RUMBLE_CURVE, 0.2f, 1.5f, UI_MENU_RUMBLE_CURVE_DESC, 0.05f, "%.2f");
opt_toggle("TOTK_RUMBLE_TRIGGERS", UI_MENU_RUMBLE_TRIGGERS, UI_MENU_RUMBLE_TRIGGERS_DESC);
// One row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With a gamepad connected each
// row shows its stick's deflection now, before the dead zone: a drifting stick's resting value, to set it above.
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
row_walk_key();
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
// Analog sticks: one dead zone row per stick, falling back to TOTK_STICK_DEADZONE (both sticks) while unset. With
// a gamepad connected each row shows its stick's deflection now, before the dead zone: a drifting stick's resting
// value, to set it above.
opt_group(UI_MENU_GROUP_STICKS);
double now[2];
bool pad = input_pad_stick_percent(now) != 0;
char stick_note[2][64];
@@ -834,9 +853,6 @@ static void tab_controls(float scale) {
opt_slider("TOTK_RSTICK_DEADZONE", UI_MENU_RSTICK_DEADZONE, 0, 50, UI_MENU_STICK_DEADZONE_DESC, 1, "%.0f %%",
pad ? stick_note[1] : nullptr, "TOTK_STICK_DEADZONE");
opt_slider("TOTK_STICK_SENS", UI_MENU_STICK_SENS, 0.1f, 3, UI_MENU_STICK_SENS_DESC, 0.05f, "%.2f");
opt_slider("TOTK_MOUSE_SENS", UI_MENU_MOUSE_SENS, 0, 10, UI_MENU_MOUSE_SENS_DESC, 0.1f, "%.1f");
row_walk_key();
opt_slider("TOTK_WALK_SCALE", UI_MENU_WALK_SCALE, 0.2f, 0.9f, UI_MENU_WALK_SCALE_DESC, 0.01f, "%.2f");
binds_rows(scale);
opt_end();
binds_after_page();
@@ -849,6 +865,7 @@ static void tab_menu(float scale) {
static const UiTextId langs[] = {UI_MENU_MENU_LANG_AUTO, UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_RU,
UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_ZH_HANS};
opt_choice("TOTK_MENU_LANG", UI_MENU_MENU_LANG, langs, sizeof langs / sizeof *langs, UI_MENU_MENU_LANG_DESC);
opt_toggle("TOTK_DISCORD_RPC", UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC);
opt_end();
}
@@ -934,7 +951,8 @@ static void tab_advanced(float scale) {
opt_choice("TOTK_MEMORY", UI_MENU_MEMORY, mem, 4, UI_MENU_MEMORY_DESC);
opt_toggle("TOTK_TEX_ALIAS", UI_MENU_TEX_ALIAS, UI_MENU_TEX_ALIAS_DESC);
opt_slider("TOTK_TEX_VRAM_PRESSURE", UI_MENU_TEX_VRAM_PRESSURE, 0, 100, UI_MENU_TEX_VRAM_PRESSURE_DESC, 1, "%.0f%%");
opt_toggle("TOTK_ASTC_BC", UI_MENU_ASTC_BC, UI_MENU_ASTC_BC_DESC);
static const UiTextId astc_bc[] = {UI_MENU_AUTO, UI_MENU_OFF, UI_MENU_ON}; // unset, 0, 1
opt_choice("TOTK_ASTC_BC", UI_MENU_ASTC_BC, astc_bc, 3, UI_MENU_ASTC_BC_DESC);
opt_group(UI_MENU_GROUP_FIXES);
opt_toggle("TOTK_LOD_GUARD", UI_MENU_LOD_GUARD, UI_MENU_LOD_GUARD_DESC);
@@ -960,6 +978,7 @@ static void tab_advanced(float scale) {
row_set_or_not("TOTK_TRACE", UI_MENU_TRACE, UI_MENU_TRACE_DESC, "perf");
opt_toggle("TOTK_FLIGHT", UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC);
opt_slider("TOTK_FLIGHT_SPIKE_MS", UI_MENU_FLIGHT_SPIKE_MS, 20, 1000, UI_MENU_FLIGHT_SPIKE_MS_DESC, 10, "%.0f");
opt_choice("TOTK_PASS_CRUMBS", UI_MENU_PASS_CRUMBS, dyn, 3, UI_MENU_PASS_CRUMBS_DESC); // vk_crumbs.c pass marks
static char rec_path[600]; // the recording goes beside the log
if (!rec_path[0]) {
char dir[512];
@@ -1370,17 +1389,30 @@ static void perf_full_draw(const MenuTarget *t, float scale) {
ImGui::SetWindowFontScale(0.72f);
const ImVec4 gpu_c(0.55f, 0.83f, 0.31f, 1), cpu_c(0.18f, 0.59f, 0.80f, 1), ram_c(0.80f, 0.45f, 0.70f, 1),
fps_c(0.95f, 0.95f, 0.95f, 1), wait_c(0.93f, 0.76f, 0.33f, 1), pic_c(0.62f, 0.70f, 0.95f, 1);
// the port version and the game update first, so a screenshot in a bug report carries both; formatted once per
// menu language
static char ver[96];
static const char *ver_fmt;
if (ver_fmt != menu_text(UI_MENU_VERSION)) {
ver_fmt = menu_text(UI_MENU_VERSION);
char game[16];
snprintf(game, sizeof game, "%d.%d.%d", rt_game_version / 100, rt_game_version / 10 % 10, rt_game_version % 10);
snprintf(ver, sizeof ver, ver_fmt, rt_port_version, game);
}
ImGui::TextDisabled("%s", ver);
if (ImGui::BeginTable("##perft", 4, ImGuiTableFlags_SizingFixedFit)) {
static const UiTextId labels[] = {UI_MENU_OV_GPU, UI_MENU_OV_VRAM, UI_MENU_OV_CPU, UI_MENU_OV_RAM};
static const PerfCol c0[] = {{UI_MENU_OV_FPS, 888}};
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {UI_MENU_OV_FRAME_MS, 888.8f}};
static const PerfCol c2[] = {{UI_MENU_OV_FMT_TEMP, 188}, {UI_MENU_OV_FMT_GB_OF, 888.8f}, {UI_MENU_OV_MAX_MS, 8888.8f}};
static const UiTextId labels[] = {UI_MENU_OV_GPU, UI_MENU_OV_VRAM, UI_MENU_OV_CPU, UI_MENU_OV_RAM,
UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL};
static const PerfCol c1[] = {{UI_MENU_OV_FMT_PCT, 100}, {UI_MENU_OV_FMT_GB, 888.8f}, {UI_MENU_OV_FMT_FPS, 888},
{UI_MENU_OV_FRAME_MS, 888.8f}};
static const PerfCol c2[] = {{UI_MENU_OV_FMT_TEMP, 188}, {UI_MENU_OV_FMT_GB_OF, 888.8f}, {UI_MENU_OV_LOW1, 888},
{UI_MENU_OV_MAX_MS, 8888.8f}};
static const PerfCol c3[] = {{UI_MENU_OV_FMT_POWER, 888}, {UI_MENU_OV_FMT_MHZ, 8888}, {UI_MENU_OV_GAME_PCT, 100},
{UI_MENU_OV_GAME_GB, 888.8f}};
perf_column("##c0", c0, 1, labels, 4);
perf_column("##c1", c1, 3);
perf_column("##c2", c2, 3);
perf_column("##c3", c3, 4);
{UI_MENU_OV_GAME_GB, 888.8f}, {UI_MENU_OV_LOW01, 888}};
perf_column("##c0", nullptr, 0, labels, 6);
perf_column("##c1", c1, 4);
perf_column("##c2", c2, 4);
perf_column("##c3", c3, 5);
perf_label(UI_MENU_OV_GPU, gpu_c);
perf_cell(UI_MENU_OV_FMT_PCT, s.gpu_pct);
perf_cell(UI_MENU_OV_FMT_TEMP, s.gpu_temp_c);
@@ -1397,12 +1429,16 @@ static void perf_full_draw(const MenuTarget *t, float scale) {
perf_cell(UI_MENU_OV_FMT_GB, perf_gb(s.ram_used_mb));
perf_cell(UI_MENU_OV_FMT_GB_OF, perf_gb(s.ram_total_mb));
perf_cell(UI_MENU_OV_GAME_GB, perf_gb(s.ram_game_mb));
ImGui::TableNextColumn(); // the frame row: no label, the rate in its place
char b[64];
snprintf(b, sizeof b, menu_text(UI_MENU_OV_FPS), s.fps);
ImGui::TextColored(fps_c, "%s", b);
ImGui::TableNextRow(ImGuiTableRowFlags_None, 6 * scale); // a gap between the machine and the frames
// the frames: the rate over the last second with the lows over the last ten seconds, then the frame times
perf_label(UI_MENU_OV_FPS_LABEL, fps_c);
perf_cell(UI_MENU_OV_FMT_FPS, s.fps);
perf_cell(UI_MENU_OV_LOW1, s.low1_fps);
perf_cell(UI_MENU_OV_LOW01, s.low01_fps);
perf_label(UI_MENU_OV_FRAME_LABEL, fps_c);
perf_cell(UI_MENU_OV_FRAME_MS, s.frame_ms_avg);
perf_cell(UI_MENU_OV_MAX_MS, s.frame_ms_max);
ImGui::TableNextColumn();
ImGui::EndTable();
}
float top = s.frame_ms_max > 50 ? s.frame_ms_max : 50;
+6 -2
View File
@@ -353,7 +353,10 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
const char *set = cfg_get(key);
const char *src = fallback && !(set && *set) ? fallback : key; // unset: show the value in effect, the fallback's
const CfgMeta *m = cfg_meta(src);
double v = cfg_double(src), def = m ? strtod(m->def, NULL) : lo;
// A key without a table default of its own (the per-stick dead zones) defaults to its fallback: the "default" mark
// follows the fallback's default, not 0, even once the key is set.
if (fallback && m && !m->def[0]) m = cfg_meta(fallback);
double v = cfg_double(src), def = m && m->def[0] ? strtod(m->def, NULL) : lo;
char shown[48];
snprintf(shown, sizeof shown, fmt, v);
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
@@ -367,7 +370,8 @@ bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId de
char out[32];
if (step >= 1) snprintf(out, sizeof out, "%d", (int)lround(n));
else snprintf(out, sizeof out, "%g", round(n / step) * step);
apply(key, out);
// Landing on the fallback's value unsets the key (its line leaves totk.ini), so the row follows the fallback again.
apply(key, fallback && fabs(n - cfg_double(fallback)) < step * 0.01 ? "" : out);
return true;
}
+2 -1
View File
@@ -61,7 +61,8 @@ void opt_group_text(const char *title);
void opt_text_str(const char *text, bool warn = false);
// lo..hi in steps of step (left and right held for a second go ten times as fast); fmt formats the value ("%.0f %%").
// note: a short line under the value in place of the "default" mark (NULL: none). fallback: the setting that applies
// while key is unset; the row shows its value then, and a change sets key.
// while key is unset; the row shows its value then, and a change sets key. A key with no table default of its own takes
// the "default" mark from the fallback's default, and a step onto the fallback's value unsets key again.
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step = 1,
const char *fmt = "%g", const char *note = nullptr, const char *fallback = nullptr);
+164 -33
View File
@@ -18,13 +18,23 @@ typedef struct { const char *lang; const char *const *text; } Row;
[UI_MENU_LANGNAME_DE] = "Deutsch", [UI_MENU_LANGNAME_IT] = "Italiano", [UI_MENU_LANGNAME_ES] = "Español", \
[UI_MENU_LANGNAME_NL] = "Nederlands", [UI_MENU_LANGNAME_RU] = "Русский", \
[UI_MENU_LANGNAME_ZH_HANS] = "简体中文", [UI_MENU_LANGNAME_ZH_HANT] = "繁體中文", [UI_MENU_LANGNAME_KO] = "한국어", \
[UI_MENU_LANGNAME_PT_BR] = "Português (Brasil)"
[UI_MENU_LANGNAME_PT_BR] = "Português (Brasil)", [UI_MENU_LANGNAME_EN_GB] = "English (UK)", \
[UI_MENU_LANGNAME_FR_CA] = "Français (Canada)", [UI_MENU_LANGNAME_ES_419] = "Español (Latinoamérica)"
// SWKBD: TITLE, PROMPT, DEFAULT_NAME (at most 9 UTF-16 units, no '@' '%' '\'), OK, CANCEL, PAD_HINT.
#define SWKBD(title, prompt, name, ok, cancel, hint) \
[UI_SWKBD_TITLE] = title, [UI_SWKBD_PROMPT] = prompt, [UI_SWKBD_DEFAULT_NAME] = name, [UI_SWKBD_OK] = ok, \
[UI_SWKBD_CANCEL] = cancel, [UI_SWKBD_PAD_HINT] = hint, LANG_NAMES
// The DLSS model file and the executable, as the player sees them on this platform.
#ifdef _WIN32
#define DLSS_FILE "nvngx_dlss.dll"
#define EXE_FILE "totk.exe"
#else
#define DLSS_FILE "libnvidia-ngx-dlss.so"
#define EXE_FILE "totk"
#endif
static const char *const text_en[UI_TEXT_COUNT] = {
SWKBD("Text entry", "Enter a name", "Horse", "OK", "Cancel", "Controller: A = OK, B = Cancel"),
@@ -55,7 +65,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_OUTPUT_RES] = "Game resolution",
[UI_MENU_OUTPUT_RES_DESC] = "The size the game draws its picture and interface in. Above 1920 x 1080 the game needs "
"more video memory and GPU time. A wider size (21:9) widens the camera's view and moves "
"the HUD to the screen's corners; a taller one (16:10, Steam Deck) moves it to the top "
"the HUD to the screen's corners; a taller one (16:10, 3:2) moves it to the top "
"and bottom edges.",
[UI_MENU_RENDER_SCALE] = "3D render scale",
[UI_MENU_RENDER_SCALE_DESC] = "Size of the 3D scene relative to the game resolution; the game's FSR scales it to "
@@ -74,7 +84,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES] = "Shadow map size",
[UI_MENU_SHADOW_RES_DESC] = "Size of the sun's shadow map. The game's own size changes with the view, down to about 400 "
"near the ground, so leaf shadows look blocky and jump; a fixed size keeps them still and "
"sharper. Above 1024 the shadows cost more GPU time and video memory.",
"sharper. Above 1024 the shadows cost more GPU time and video memory; 256 is coarser than the "
"game's own and is for integrated GPUs short of memory bandwidth.",
[UI_MENU_SHADOW_RES_GAME] = "Game (changes)",
[UI_MENU_ANISO] = "Texture filtering",
[UI_MENU_ANISO_DESC] = "Anisotropic filtering of surface textures: ground, roads and walls seen at a shallow angle stay sharp into the distance instead of blurring. Game: the level the game asks for, as on the Switch.",
@@ -108,6 +119,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"bright room.",
[UI_MENU_HDR_UI] = "HUD and menu brightness",
[UI_MENU_HDR_UI_DESC] = "Nits of the HUD's and this menu's white.",
[UI_MENU_HDR_HUE] = "Highlight hue correction",
[UI_MENU_HDR_HUE_DESC] = "Bright colours such as fire and sparks turn towards white as in the game's own SDR picture, "
"matching the light they cast; 0 keeps them fully saturated.",
[UI_MENU_SHADOW_SCALE] = "Shadow resolution",
[UI_MENU_SHADOW_SCALE_DESC] = "Draws the sun's shadow maps this many times larger on each side than the game's 960x960: "
"sharper shadow edges and leaf shadows. Takes effect at the next start.",
@@ -122,15 +136,28 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"away. Spreads the shadow map over more ground: raise Shadow resolution with it. "
"Takes effect at the next start.",
[UI_MENU_DLAA] = "NVIDIA DLSS / DLAA",
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS for the 3D scene (NVIDIA RTX GPUs; nvngx_dlss.dll next to totk.exe): smoother edges "
"and less shimmer. The HUD is not touched. The mode below sets the resolution it works from. Takes effect "
"after a restart.",
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS for the 3D scene (NVIDIA RTX GPUs; " DLSS_FILE " next to " EXE_FILE "): smoother "
"edges and less shimmer. The HUD is not touched. The mode below sets the resolution it works "
"from. Takes effect after a restart.",
[UI_MENU_DLSS_PRESET] = "DLSS model",
[UI_MENU_DLSS_PRESET_DESC] = "Which DLSS model runs. Automatic follows NVIDIA's advice per mode: K for DLAA, Quality "
"and Balanced, M for Performance, L for Ultra Performance. J: slightly less ghosting "
"behind moving characters, a little more flicker. K: the sharpest model in every mode. "
"Applies at once.",
[UI_MENU_DLSS_PRESET_DEFAULT] = "Automatic", [UI_MENU_DLSS_PRESET_K] = "K (sharpest)",
[UI_MENU_DLSS_PRESET_J] = "J (less ghosting)",
[UI_MENU_DLSS_CHARS] = "DLSS character trails",
[UI_MENU_DLSS_CHARS_DESC] = "Reduces the faint trails behind moving characters, animals and what they hold: DLSS takes "
"less of the previous frames there. Stronger removes more of the trails and leaves those "
"edges a little less smooth. Applies at once.",
[UI_MENU_DLSS_CHARS_OFF] = "Off", [UI_MENU_DLSS_CHARS_LIGHT] = "Light", [UI_MENU_DLSS_CHARS_MEDIUM] = "Medium",
[UI_MENU_DLSS_CHARS_FULL] = "Full",
[UI_MENU_DLSS] = "DLSS mode",
[UI_MENU_DLSS_DESC] = "DLAA draws the 3D scene at full resolution. Quality, Balanced and Performance draw it at 67, 58 "
"and 50 % and let DLSS scale it up, for more frames per second; they replace the 3D render "
"scale. Takes effect after a restart.",
[UI_MENU_DLSS_DESC] = "DLAA draws the 3D scene at full resolution. Quality, Balanced, Performance and Ultra "
"Performance draw it at 67, 58, 50 and 33 % and let DLSS scale it up, which saves GPU time; "
"they replace the 3D render scale. Takes effect at once.",
[UI_MENU_DLSS_DLAA] = "DLAA (native)", [UI_MENU_DLSS_QUALITY] = "Quality", [UI_MENU_DLSS_BALANCED] = "Balanced",
[UI_MENU_DLSS_PERFORMANCE] = "Performance",
[UI_MENU_DLSS_PERFORMANCE] = "Performance", [UI_MENU_DLSS_ULTRA] = "Ultra Performance",
[UI_MENU_FXAA] = "Game anti-aliasing (FXAA)",
[UI_MENU_FXAA_DESC] = "The game's own FXAA edge smoothing. It blurs fine detail a little; with DLAA on it is not needed.",
@@ -190,6 +217,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"at 60 FPS.",
[UI_MENU_VOLUME] = "Volume", [UI_MENU_VOLUME_DESC] = "Master volume of the game.",
[UI_MENU_AUDIO_CHANNELS] = "Speakers",
[UI_MENU_AUDIO_CHANNELS_DESC] = "Stereo or 5.1 surround output. Automatic picks 5.1 when the Windows speaker setup "
"has six or more channels. Applies after a restart.",
[UI_MENU_CHANNELS_AUTO] = "Automatic", [UI_MENU_CHANNELS_STEREO] = "Stereo", [UI_MENU_CHANNELS_51] = "5.1 surround",
[UI_MENU_MOTION] = "Motion controls", [UI_MENU_MOTION_DESC] = "Aiming with the controller's gyroscope.",
[UI_MENU_RUMBLE_STRENGTH] = "Vibration strength",
@@ -226,6 +257,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_RESTART_CONFIRM] = "Restart the game now? Progress since your last save is lost.",
[UI_MENU_RESTART_YES] = "Restart",
[UI_MENU_RESTART_SAVING] = "The game is saving: Restart is possible in a moment.",
[UI_MENU_GROUP_STICKS] = "Analog sticks",
[UI_MENU_LSTICK_DEADZONE] = "Left stick dead zone", [UI_MENU_RSTICK_DEADZONE] = "Right stick dead zone",
[UI_MENU_STICK_DEADZONE_DESC] = "How far a gamepad stick moves before the game sees it. Raise it if the character or the camera moves on its own: let go of the stick, read \"stick now\" and set the dead zone a few percent above it.",
[UI_MENU_STICK_DEADZONE_NOW] = "stick now: %.0f %%",
@@ -320,6 +352,9 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_AMIIBO_STATUS] = "Status: %s",
[UI_MENU_MENU_LANG] = "Menu language", [UI_MENU_MENU_LANG_DESC] = "Language of this menu and the overlay.",
[UI_MENU_MENU_LANG_AUTO] = "Automatic (from Windows)",
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
[UI_MENU_DISCORD_RPC_DESC] = "Shows \"Playing TotK Recomp\" with the play time in your Discord status while the "
"game runs. discord_app_id and discord_image in totk.ini choose the application.",
[UI_MENU_ADV_WARNING] = "These settings are for testing and troubleshooting. Change them only when you know why.",
[UI_MENU_GROUP_SCHED] = "Scheduling", [UI_MENU_GROUP_SHADERS] = "Shaders and pipelines",
@@ -357,7 +392,7 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_VERIFY] = "Verify",
[UI_MENU_TEX_ALIAS] = "Texture aliasing", [UI_MENU_TEX_ALIAS_DESC] = "A new texture created over a live texture's memory starts with that content instead of empty.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Texture memory limit", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Percent of video memory above which textures the game released are dropped (they come back from storage when used again). 0 keeps them.",
[UI_MENU_ASTC_BC] = "Compress ASTC to BC3", [UI_MENU_ASTC_BC_DESC] = "With the GPU decoder: re-encodes decoded ASTC textures to BC3, a quarter of the video memory. For GPUs with 4 GB or less; textures look slightly blockier.",
[UI_MENU_ASTC_BC] = "Compress ASTC to BC3", [UI_MENU_ASTC_BC_DESC] = "With the GPU decoder: re-encodes decoded ASTC textures to BC3, a quarter of the video memory. Textures look slightly blockier. Auto: on for GPUs with 4 GB of video memory or less.",
[UI_MENU_LOD_GUARD] = "LOD guard", [UI_MENU_LOD_GUARD_DESC] = "Prevents objects flashing in the wrong place or a flat far field for a frame, by waiting for the game to finish rewriting level-of-detail data before the GPU reads it.",
[UI_MENU_FIX_C0] = "Constant buffer fix",
[UI_MENU_FIX_C0_DESC] = "Fills the driver-reserved constant bank c0 (storage sizes, dispatch size, point size, base instance) instead of reading zeros.",
@@ -388,6 +423,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Keeps a short history and saves it when a frame freezes.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Freeze threshold (ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Frame time that counts as a freeze.",
[UI_MENU_PASS_CRUMBS] = "GPU pass marks",
[UI_MENU_PASS_CRUMBS_DESC] = "After a GPU crash, the log names the render pass that was running. Automatic: AMD only. On: also NVIDIA, at a small cost.",
[UI_MENU_INPUT_RECORD] = "Input recording",
[UI_MENU_INPUT_RECORD_DESC] = "Writes your inputs to a file for bug reports.",
[UI_MENU_OPEN_LOG] = "Open log folder", [UI_MENU_OPEN_LOG_DESC] = "Shows the folder with the log files.",
@@ -400,6 +437,8 @@ static const char *const text_en[UI_TEXT_COUNT] = {
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f ms", [UI_MENU_OV_GAME_PCT] = "game %.0f%%",
[UI_MENU_OV_MAX_MS] = "max %.1f ms",
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Frame", [UI_MENU_OV_FMT_FPS] = "%.0f",
[UI_MENU_OV_LOW1] = "1%% low %.0f", [UI_MENU_OV_LOW01] = "0.1%% low %.0f",
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "CPU", [UI_MENU_OV_RAM] = "RAM", [UI_MENU_OV_VRAM] = "VRAM",
[UI_MENU_OV_TEMP] = "Temperature", [UI_MENU_OV_POWER] = "Power", [UI_MENU_OV_CLOCK] = "Clock",
[UI_MENU_OV_NONE] = "-",
@@ -479,7 +518,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_OUTPUT_RES] = "Разрешение игры",
[UI_MENU_OUTPUT_RES_DESC] = "Размер, в котором игра рисует картинку и интерфейс. Выше 1920 × 1080 игре нужно больше "
"видеопамяти и времени GPU. Более широкий размер (21:9) расширяет обзор камеры и "
"переносит интерфейс в углы экрана, более высокий (16:10, Steam Deck) переносит его к "
"переносит интерфейс в углы экрана, более высокий (16:10, 3:2) переносит его к "
"верхнему и нижнему краям.",
[UI_MENU_RENDER_SCALE] = "Масштаб 3D-отрисовки",
[UI_MENU_RENDER_SCALE_DESC] = "Размер 3D-сцены относительно разрешения игры; FSR игры доводит её до этого размера. "
@@ -498,7 +537,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES] = "Размер карты теней",
[UI_MENU_SHADOW_RES_DESC] = "Размер карты теней от солнца. Собственный размер игры меняется вместе с видом, у земли до "
"примерно 400, поэтому тени листвы выглядят квадратными и дёргаются; постоянный размер делает "
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти.",
"их ровными и чётче. Выше 1024 тени требуют больше времени GPU и видеопамяти; 256 грубее "
"собственного размера игры и предназначен для встроенных GPU с нехваткой пропускной способности памяти.",
[UI_MENU_SHADOW_RES_GAME] = "Как в игре (меняется)",
[UI_MENU_ANISO] = "Фильтрация текстур",
[UI_MENU_ANISO_DESC] = "Анизотропная фильтрация текстур поверхностей: земля, дороги и стены под острым углом остаются чёткими вдали, а не размываются. Игра: уровень, который запрашивает игра, как на Switch.",
@@ -533,6 +573,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
"поднимите.",
[UI_MENU_HDR_UI] = "Яркость интерфейса и меню",
[UI_MENU_HDR_UI_DESC] = "Ниты белого цвета интерфейса игры и этого меню.",
[UI_MENU_HDR_HUE] = "Коррекция оттенка бликов",
[UI_MENU_HDR_HUE_DESC] = "Яркие цвета (огонь, искры) уходят к белому, как в SDR-картинке игры, и совпадают со светом, "
"который они дают; 0 оставляет их полностью насыщенными.",
[UI_MENU_SHADOW_SCALE] = "Разрешение теней",
[UI_MENU_SHADOW_SCALE_DESC] = "Карты теней от солнца рисуются во столько раз больше по каждой стороне, чем 960x960 "
"самой игры: края теней и тени листвы чётче. Действует после перезапуска.",
@@ -547,15 +590,28 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
"Карта теней растягивается на большую площадь, поэтому вместе с этим поднимайте "
"разрешение теней. Действует после перезапуска.",
[UI_MENU_DLAA] = "NVIDIA DLSS / DLAA",
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS для 3D-сцены (видеокарты NVIDIA RTX; nvngx_dlss.dll рядом с totk.exe): ровнее края и "
"меньше мерцания. Интерфейс не затрагивается. Режим ниже задаёт разрешение, с которым он работает. "
"Действует после перезапуска.",
[UI_MENU_DLAA_DESC] = "NVIDIA DLSS для 3D-сцены (видеокарты NVIDIA RTX; " DLSS_FILE " рядом с " EXE_FILE "): ровнее "
"края и меньше мерцания. Интерфейс не затрагивается. Режим ниже задаёт разрешение, с которым он "
"работает. Действует после перезапуска.",
[UI_MENU_DLSS_PRESET] = "Модель DLSS",
[UI_MENU_DLSS_PRESET_DESC] = "Какая модель DLSS работает. «Автоматически» следует совету NVIDIA для каждого режима: K "
"для DLAA, «Качества» и «Баланса», M для «Производительности», L для «Макс. "
"производительности». J: чуть меньше шлейфов за движущимися персонажами, немного больше "
"мерцания. K: самая чёткая модель во всех режимах. Действует сразу.",
[UI_MENU_DLSS_PRESET_DEFAULT] = "Автоматически", [UI_MENU_DLSS_PRESET_K] = "K (самая чёткая)",
[UI_MENU_DLSS_PRESET_J] = "J (меньше шлейфов)",
[UI_MENU_DLSS_CHARS] = "Шлейфы персонажей в DLSS",
[UI_MENU_DLSS_CHARS_DESC] = "Убирает слабые шлейфы за движущимися персонажами, животными и тем, что они держат: DLSS "
"берёт там меньше от прошлых кадров. Чем сильнее, тем меньше шлейфов и тем чуть менее "
"гладкие края. Действует сразу.",
[UI_MENU_DLSS_CHARS_OFF] = "Выкл.", [UI_MENU_DLSS_CHARS_LIGHT] = "Слабо", [UI_MENU_DLSS_CHARS_MEDIUM] = "Средне",
[UI_MENU_DLSS_CHARS_FULL] = "Полностью",
[UI_MENU_DLSS] = "Режим DLSS",
[UI_MENU_DLSS_DESC] = "DLAA рисует 3D-сцену в полном разрешении. «Качество», «Баланс» и «Производительность» рисуют её "
"в 67, 58 и 50 % и масштабируют через DLSS ради большего числа кадров; они заменяют масштаб "
"3D-отрисовки. Действует после перезапуска.",
[UI_MENU_DLSS_DESC] = "DLAA рисует 3D-сцену в полном разрешении. «Качество», «Баланс», «Производительность» и "
"«Макс. производительность» рисуют её в 67, 58, 50 и 33 % и масштабируют через DLSS, что "
"экономит время видеокарты; они заменяют масштаб 3D-отрисовки. Действует сразу.",
[UI_MENU_DLSS_DLAA] = "DLAA (родное)", [UI_MENU_DLSS_QUALITY] = "Качество", [UI_MENU_DLSS_BALANCED] = "Баланс",
[UI_MENU_DLSS_PERFORMANCE] = "Производительность",
[UI_MENU_DLSS_PERFORMANCE] = "Производительность", [UI_MENU_DLSS_ULTRA] = "Макс. производительность",
[UI_MENU_FXAA] = "Сглаживание игры (FXAA)",
[UI_MENU_FXAA_DESC] = "Собственное сглаживание FXAA. Слегка размывает мелкие детали; с DLAA оно не нужно.",
@@ -612,6 +668,10 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_MODS_QUICKMENU_DESC] = "Исправление порта: чёрный фон меню оружия, щитов и материалов при 60 FPS.",
[UI_MENU_VOLUME] = "Громкость", [UI_MENU_VOLUME_DESC] = "Общая громкость игры.",
[UI_MENU_AUDIO_CHANNELS] = "Динамики",
[UI_MENU_AUDIO_CHANNELS_DESC] = "Стерео или объёмный звук 5.1. Автоматически выбирается 5.1, если в Windows "
"настроено шесть каналов или больше. Применяется после перезапуска.",
[UI_MENU_CHANNELS_AUTO] = "Автоматически", [UI_MENU_CHANNELS_STEREO] = "Стерео", [UI_MENU_CHANNELS_51] = "Объёмный 5.1",
[UI_MENU_MOTION] = "Управление движением", [UI_MENU_MOTION_DESC] = "Прицеливание гироскопом контроллера.",
[UI_MENU_RUMBLE_STRENGTH] = "Сила вибрации",
@@ -648,6 +708,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_RESTART_CONFIRM] = "Перезапустить игру сейчас? Прогресс после последнего сохранения будет потерян.",
[UI_MENU_RESTART_YES] = "Перезапустить",
[UI_MENU_RESTART_SAVING] = "Игра сохраняется: перезапуск будет доступен через мгновение.",
[UI_MENU_GROUP_STICKS] = "Стики",
[UI_MENU_LSTICK_DEADZONE] = "Мёртвая зона левого стика", [UI_MENU_RSTICK_DEADZONE] = "Мёртвая зона правого стика",
[UI_MENU_STICK_DEADZONE_DESC] = "Насколько нужно отклонить стик геймпада, чтобы игра это заметила. Увеличьте, если персонаж или камера двигаются сами: отпустите стик, посмотрите «стик сейчас» и поставьте мёртвую зону на несколько процентов выше.",
[UI_MENU_STICK_DEADZONE_NOW] = "стик сейчас: %.0f %%",
@@ -746,6 +807,9 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_AMIIBO_STATUS] = "Состояние: %s",
[UI_MENU_MENU_LANG] = "Язык меню", [UI_MENU_MENU_LANG_DESC] = "Язык этого меню и индикатора.",
[UI_MENU_MENU_LANG_AUTO] = "Автоматически (из Windows)",
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
[UI_MENU_DISCORD_RPC_DESC] = "Пока игра запущена, показывает «Играю в TotK Recomp» со временем игры в статусе "
"Discord. Приложение выбирают discord_app_id и discord_image в totk.ini.",
[UI_MENU_ADV_WARNING] = "Эти настройки нужны для тестов и поиска неполадок. Меняйте их, только если знаете зачем.",
[UI_MENU_GROUP_SCHED] = "Планирование", [UI_MENU_GROUP_SHADERS] = "Шейдеры и конвейеры",
@@ -783,7 +847,7 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_VERIFY] = "Проверка",
[UI_MENU_TEX_ALIAS] = "Наследование содержимого текстур", [UI_MENU_TEX_ALIAS_DESC] = "Новая текстура поверх памяти живой текстуры получает её содержимое, а не пустую.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Предел памяти текстур", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Процент видеопамяти, выше которого освобождённые игрой текстуры удаляются (при повторном использовании загружаются снова). 0 — не удалять.",
[UI_MENU_ASTC_BC] = "Сжатие ASTC в BC3", [UI_MENU_ASTC_BC_DESC] = "С декодером на GPU: декодированные текстуры ASTC пересжимаются в BC3 и занимают вчетверо меньше видеопамяти. Для видеокарт с 4 ГБ и меньше; текстуры чуть более блочные.",
[UI_MENU_ASTC_BC] = "Сжатие ASTC в BC3", [UI_MENU_ASTC_BC_DESC] = "С декодером на GPU: декодированные текстуры ASTC пересжимаются в BC3 и занимают вчетверо меньше видеопамяти. Текстуры чуть более блочные. Авто: включено на видеокартах с 4 ГБ видеопамяти и меньше.",
[UI_MENU_LOD_GUARD] = "Защита LOD", [UI_MENU_LOD_GUARD_DESC] = "Не даёт объектам мелькать не на своём месте, а дальнему плану — на кадр становиться плоским: ждёт, пока игра допишет данные уровней детализации, и только потом их читает GPU.",
[UI_MENU_FIX_C0] = "Исправление константного буфера",
[UI_MENU_FIX_C0_DESC] = "Заполняет зарезервированный драйвером банк констант c0 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
@@ -816,6 +880,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Хранит короткую историю и сохраняет её, когда кадр подвисает.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Порог подвисания (мс)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Время кадра, которое считается подвисанием.",
[UI_MENU_PASS_CRUMBS] = "Метки проходов GPU",
[UI_MENU_PASS_CRUMBS_DESC] = "После сбоя GPU журнал называет проход отрисовки, который выполнялся. Автоматически: только AMD. Вкл.: также NVIDIA, с небольшой нагрузкой.",
[UI_MENU_INPUT_RECORD] = "Запись ввода",
[UI_MENU_INPUT_RECORD_DESC] = "Пишет ваш ввод в файл для отчёта об ошибке.",
[UI_MENU_OPEN_LOG] = "Открыть папку журналов", [UI_MENU_OPEN_LOG_DESC] = "Показывает папку с файлами журнала.",
@@ -828,6 +894,8 @@ static const char *const text_ru[UI_TEXT_COUNT] = {
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f мс", [UI_MENU_OV_GAME_PCT] = "игра %.0f%%",
[UI_MENU_OV_MAX_MS] = "макс. %.1f мс",
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Кадр", [UI_MENU_OV_FMT_FPS] = "%.0f",
[UI_MENU_OV_LOW1] = "1%% худших %.0f", [UI_MENU_OV_LOW01] = "0.1%% худших %.0f",
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "ЦП", [UI_MENU_OV_RAM] = "ОЗУ", [UI_MENU_OV_VRAM] = "Видеопамять",
[UI_MENU_OV_TEMP] = "Температура", [UI_MENU_OV_POWER] = "Мощность", [UI_MENU_OV_CLOCK] = "Частота",
[UI_MENU_OV_NONE] = "-",
@@ -937,7 +1005,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_OUTPUT_RES] = "Resolução do jogo",
[UI_MENU_OUTPUT_RES_DESC] = "O tamanho em que o jogo desenha a imagem e a interface. Acima de 1920 x 1080 o jogo "
"precisa de mais memória de vídeo e tempo de GPU. Um tamanho mais largo (21:9) amplia a "
"visão da câmera e leva o HUD para os cantos da tela; um mais alto (16:10, Steam Deck) "
"visão da câmera e leva o HUD para os cantos da tela; um mais alto (16:10, 3:2) "
"leva-o para as bordas de cima e de baixo.",
[UI_MENU_RENDER_SCALE] = "Escala da renderização 3D",
[UI_MENU_RENDER_SCALE_DESC] = "Tamanho da cena 3D em relação à resolução do jogo; o FSR do jogo a amplia até esse "
@@ -998,6 +1066,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT_AUTO] = "Automático", [UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo (A à direita)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox (A embaixo)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "em uso: %s",
[UI_MENU_GROUP_STICKS] = "Analógicos",
[UI_MENU_LSTICK_DEADZONE] = "Zona morta do analógico esquerdo",
[UI_MENU_RSTICK_DEADZONE] = "Zona morta do analógico direito",
[UI_MENU_STICK_DEADZONE_DESC] = "Quanto um analógico do controle precisa se mover antes que o jogo perceba. Aumente "
@@ -1091,6 +1160,10 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_AMIIBO_STATUS] = "Estado: %s",
[UI_MENU_MENU_LANG] = "Idioma do menu", [UI_MENU_MENU_LANG_DESC] = "Idioma deste menu e do indicador.",
[UI_MENU_MENU_LANG_AUTO] = "Automático (do Windows)",
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
[UI_MENU_DISCORD_RPC_DESC] = "Mostra \"Jogando TotK Recomp\" com o tempo de jogo no seu status do Discord "
"enquanto o jogo roda. discord_app_id e discord_image no totk.ini escolhem a "
"aplicação.",
[UI_MENU_ADV_WARNING] = "Estas configurações são para testes e solução de problemas. Altere-as só quando souber "
"por quê.",
@@ -1131,7 +1204,7 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_MEMORY_VERIFY] = "Verificar",
[UI_MENU_TEX_ALIAS] = "Aliasing de texturas", [UI_MENU_TEX_ALIAS_DESC] = "Uma textura nova criada sobre a memória de uma textura viva começa com esse conteúdo em vez de vazia.",
[UI_MENU_TEX_VRAM_PRESSURE] = "Limite de memória de texturas", [UI_MENU_TEX_VRAM_PRESSURE_DESC] = "Porcentagem da memória de vídeo acima da qual as texturas liberadas pelo jogo são descartadas (voltam do armazenamento quando usadas de novo). 0 mantém todas.",
[UI_MENU_ASTC_BC] = "Comprimir ASTC em BC3", [UI_MENU_ASTC_BC_DESC] = "Com o decodificador da GPU: recodifica as texturas ASTC decodificadas em BC3, que ocupa um quarto da memória de vídeo. Para GPUs com 4 GB ou menos; as texturas ficam um pouco mais quadriculadas.",
[UI_MENU_ASTC_BC] = "Comprimir ASTC em BC3", [UI_MENU_ASTC_BC_DESC] = "Com o decodificador da GPU: recodifica as texturas ASTC decodificadas em BC3, que ocupa um quarto da memória de vídeo. As texturas ficam um pouco mais quadriculadas. Automático: ligado em GPUs com 4 GB de memória de vídeo ou menos.",
[UI_MENU_LOD_GUARD] = "Proteção de LOD", [UI_MENU_LOD_GUARD_DESC] = "Evita que objetos pisquem no lugar errado ou que o plano distante fique achatado por um quadro, esperando o jogo terminar de reescrever os dados de nível de detalhe antes que a GPU os leia.",
[UI_MENU_FIX_C0] = "Correção do buffer de constantes",
[UI_MENU_FIX_C0_DESC] = "Preenche o banco de constantes c0 reservado ao driver (tamanhos de armazenamento, tamanho do dispatch, tamanho do ponto, instância base) em vez de ler zeros.",
@@ -1163,6 +1236,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "Guarda um histórico curto e o salva quando um quadro trava.",
[UI_MENU_FLIGHT_SPIKE_MS] = "Limite de travamento (ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "Tempo de quadro que conta como travamento.",
[UI_MENU_PASS_CRUMBS] = "Marcas de passagem da GPU",
[UI_MENU_PASS_CRUMBS_DESC] = "Após uma falha da GPU, o log indica a passagem de renderização que estava em execução. Automático: só AMD. Ligado: também NVIDIA, com um pequeno custo.",
[UI_MENU_INPUT_RECORD] = "Gravação de comandos",
[UI_MENU_INPUT_RECORD_DESC] = "Grava os seus comandos num arquivo para relatórios de bug.",
[UI_MENU_OPEN_LOG] = "Abrir pasta de logs", [UI_MENU_OPEN_LOG_DESC] = "Mostra a pasta com os arquivos de log.",
@@ -1175,6 +1250,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f ms", [UI_MENU_OV_GAME_PCT] = "jogo %.0f%%",
[UI_MENU_OV_MAX_MS] = "máx. %.1f ms",
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Quadro", [UI_MENU_OV_FMT_FPS] = "%.0f",
[UI_MENU_OV_LOW1] = "1%% piores %.0f", [UI_MENU_OV_LOW01] = "0.1%% piores %.0f",
[UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "CPU", [UI_MENU_OV_RAM] = "RAM", [UI_MENU_OV_VRAM] = "VRAM",
[UI_MENU_OV_TEMP] = "Temperatura", [UI_MENU_OV_POWER] = "Consumo", [UI_MENU_OV_CLOCK] = "Frequência",
[UI_MENU_OV_NONE] = "-",
@@ -1196,7 +1273,8 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_SHADOW_RES_DESC] = "Tamanho do mapa de sombras do sol. O tamanho do jogo muda com a vista, até cerca de "
"400 perto do chão, então as sombras das folhas ficam quadriculadas e pulam; um "
"tamanho fixo as deixa paradas e mais nítidas. Acima de 1024 as sombras custam mais "
"tempo de GPU e memória de vídeo.",
"tempo de GPU e memória de vídeo; 256 é mais grosseiro que o do jogo e serve para GPUs "
"integradas com pouca largura de banda de memória.",
[UI_MENU_SHADOW_RES_GAME] = "Do jogo (varia)",
[UI_MENU_ANISO] = "Filtragem de texturas",
[UI_MENU_ANISO_DESC] = "Filtragem anisotrópica das texturas de superfície: chão, estradas e paredes vistos em "
@@ -1268,6 +1346,9 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
[UI_MENU_WALK_SCALE_DESC] = "A inclinação dos analógicos do teclado enquanto a tecla de andar está segura. Metade "
"é andar, a inclinação total é correr.",
[UI_MENU_OV_SCALE_DOWN] = "bilinear (o FSR só amplia)",
[UI_MENU_HDR_HUE] = "Correção de matiz dos realces",
[UI_MENU_HDR_HUE_DESC] = "Cores fortes como fogo e faíscas vão para o branco como na imagem SDR do próprio jogo e "
"combinam com a luz que emitem; 0 as mantém totalmente saturadas.",
[UI_MENU_SHADOW_SCALE] = "Resolução das sombras",
[UI_MENU_SHADOW_SCALE_DESC] = "Desenha os mapas de sombra do sol essa quantidade de vezes maiores em cada lado "
"que os 960x960 do jogo: bordas de sombra e sombras de folhas mais nítidas. Vale no "
@@ -1288,13 +1369,26 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = {
"mais suaves e menos cintilação. O HUD não é afetado. O modo abaixo define a resolução de "
"onde ele parte. Vale após reiniciar.",
[UI_MENU_DLSS] = "Modo do DLSS",
[UI_MENU_DLSS_DESC] = "DLAA desenha a cena 3D na resolução cheia. Qualidade, Equilibrado e Desempenho a desenham "
"em 67, 58 e 50 % e deixam o DLSS ampliá-la, para mais quadros por segundo; eles substituem "
"a escala 3D. Vale após reiniciar.",
[UI_MENU_DLSS_DESC] = "DLAA desenha a cena 3D na resolução cheia. Qualidade, Equilibrado, Desempenho e Ultra "
"Desempenho a desenham em 67, 58, 50 e 33 % e deixam o DLSS ampliá-la, o que poupa tempo da "
"GPU; eles substituem a escala 3D. Vale na hora.",
[UI_MENU_DLSS_DLAA] = "DLAA (nativo)",
[UI_MENU_DLSS_QUALITY] = "Qualidade",
[UI_MENU_DLSS_BALANCED] = "Equilibrado",
[UI_MENU_DLSS_PERFORMANCE] = "Desempenho",
[UI_MENU_DLSS_PERFORMANCE] = "Desempenho", [UI_MENU_DLSS_ULTRA] = "Ultra Desempenho",
[UI_MENU_DLSS_PRESET] = "Modelo do DLSS",
[UI_MENU_DLSS_PRESET_DESC] = "Qual modelo do DLSS roda. Automático segue a recomendação da NVIDIA para cada modo: K "
"para DLAA, Qualidade e Equilibrado, M para Desempenho, L para Ultra Desempenho. J: um "
"pouco menos de rastro atrás de personagens em movimento, um pouco mais de cintilação. K: "
"o modelo mais nítido em todos os modos. Vale na hora.",
[UI_MENU_DLSS_PRESET_DEFAULT] = "Automático", [UI_MENU_DLSS_PRESET_K] = "K (mais nítido)",
[UI_MENU_DLSS_PRESET_J] = "J (menos rastro)",
[UI_MENU_DLSS_CHARS] = "Rastros de personagens no DLSS",
[UI_MENU_DLSS_CHARS_DESC] = "Reduz os rastros fracos atrás de personagens, animais e do que eles seguram em movimento: "
"ali o DLSS usa menos dos quadros anteriores. Mais forte remove mais rastros e deixa essas "
"bordas um pouco menos suaves. Vale na hora.",
[UI_MENU_DLSS_CHARS_OFF] = "Desligado", [UI_MENU_DLSS_CHARS_LIGHT] = "Leve", [UI_MENU_DLSS_CHARS_MEDIUM] = "Médio",
[UI_MENU_DLSS_CHARS_FULL] = "Total",
[UI_MENU_FXAA] = "Antisserrilhado do jogo (FXAA)",
[UI_MENU_FXAA_DESC] = "A suavização de bordas FXAA do próprio jogo. Borra um pouco os detalhes finos; com o DLAA "
"ligado não é necessária.",
@@ -1407,7 +1501,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_OUTPUT_RES_DESC] = "游戏绘制画面和界面的尺寸。"
"高于 1920 x 1080 会占用更多显存和 GPU 时间。"
"更宽的尺寸(21:9)会扩大视野,并把界面移到屏幕四角;"
"更高的尺寸(16:10、Steam Deck)则移到上下边缘。",
"更高的尺寸(16:10、3:2)则移到上下边缘。",
[UI_MENU_RENDER_SCALE] = "3D 渲染比例",
[UI_MENU_RENDER_SCALE_DESC] = "3D 场景相对于游戏分辨率的尺寸;"
"游戏的 FSR 会将其缩放到该尺寸。"
@@ -1497,6 +1591,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_PAD_LAYOUT_NINTENDO] = "Nintendo(A 在右)",
[UI_MENU_PAD_LAYOUT_XBOX] = "Xbox(A 在下)",
[UI_MENU_PAD_LAYOUT_IN_USE] = "使用中:%s",
[UI_MENU_GROUP_STICKS] = "摇杆",
[UI_MENU_LSTICK_DEADZONE] = "左摇杆死区", [UI_MENU_RSTICK_DEADZONE] = "右摇杆死区",
[UI_MENU_STICK_DEADZONE_DESC] = "手柄摇杆要推动多远游戏才会响应。如果角色或视角会自己移动,"
"请调高此值:松开摇杆,查看“当前摇杆”,把死区设得比它高几个百分点。",
@@ -1644,6 +1739,9 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_MENU_LANG] = "菜单语言",
[UI_MENU_MENU_LANG_DESC] = "本菜单和性能浮层的语言。",
[UI_MENU_MENU_LANG_AUTO] = "自动(跟随 Windows)",
[UI_MENU_DISCORD_RPC] = "Discord Rich Presence",
[UI_MENU_DISCORD_RPC_DESC] = "游戏运行时在 Discord 状态中显示“正在玩 TotK Recomp”及游戏时长。totk.ini 中的 "
"discord_app_id 和 discord_image 选择应用。",
[UI_MENU_ADV_WARNING] = "这些设置用于测试和排查问题,请在了解原因后再修改。",
[UI_MENU_GROUP_SCHED] = "调度",
[UI_MENU_GROUP_SHADERS] = "着色器和管线",
@@ -1702,8 +1800,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
"再次用到时从存储重新加载。0 表示不丢弃。",
[UI_MENU_ASTC_BC] = "ASTC 压缩为 BC3",
[UI_MENU_ASTC_BC_DESC] = "配合 GPU 解码器使用:把解码后的 ASTC 纹理重新编码为 BC3,"
"显存占用只有四分之一。适用于 4 GB 及以下的显卡;"
"纹理会出现轻微色块。",
"显存占用只有四分之一。纹理会出现轻微色块。"
"自动:显存 4 GB 及以下的显卡上开启。",
[UI_MENU_LOD_GUARD] = "LOD 保护",
[UI_MENU_LOD_GUARD_DESC] = "等待游戏改写完细节层次数据后再让 GPU 读取,"
"避免物体错位闪现,或远景有一帧变平。",
@@ -1743,6 +1841,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_FLIGHT_DESC] = "保留一小段历史记录,在画面卡住时保存。",
[UI_MENU_FLIGHT_SPIKE_MS] = "卡顿阈值(ms)",
[UI_MENU_FLIGHT_SPIKE_MS_DESC] = "算作卡顿的帧时间。",
[UI_MENU_PASS_CRUMBS] = "GPU 渲染通道标记",
[UI_MENU_PASS_CRUMBS_DESC] = "GPU 崩溃后,日志会写出当时正在运行的渲染通道。自动:仅 AMD。开:NVIDIA 也启用,有少量开销。",
[UI_MENU_INPUT_RECORD] = "输入录制",
[UI_MENU_INPUT_RECORD_DESC] = "把你的输入写入文件,用于反馈问题。",
[UI_MENU_OPEN_LOG] = "打开日志文件夹",
@@ -1760,6 +1860,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_OV_FRAME_MS] = "%.1f ms",
[UI_MENU_OV_GAME_PCT] = "游戏 %.0f%%",
[UI_MENU_OV_MAX_MS] = "最长 %.1f ms",
[UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "帧时间", [UI_MENU_OV_FMT_FPS] = "%.0f",
[UI_MENU_OV_LOW1] = "1%% 低 %.0f", [UI_MENU_OV_LOW01] = "0.1%% 低 %.0f",
[UI_MENU_OV_GPU] = "GPU",
[UI_MENU_OV_CPU] = "CPU",
[UI_MENU_OV_RAM] = "内存",
@@ -1795,7 +1897,7 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_OV_SCALE_BILINEAR] = "双线性",
[UI_MENU_SHADOW_RES] = "阴影贴图尺寸",
[UI_MENU_SHADOW_RES_DESC] = "太阳阴影贴图的尺寸。游戏自身的尺寸随视角变化,靠近地面时降到约 400,因此树叶阴影显得块状并且跳动;固定尺寸能让阴影稳定、更清晰。高于 1024 时阴影会占用更多 "
"GPU 时间和显存。",
"GPU 时间和显存;256 比游戏自身的尺寸更粗糙,适合显存带宽不足的集成显卡。",
[UI_MENU_SHADOW_RES_GAME] = "游戏默认(会变化)",
[UI_MENU_ANISO] = "纹理过滤",
[UI_MENU_ANISO_DESC] = "表面纹理的各向异性过滤:以小角度看到的地面、道路和墙壁在远处依然清晰,而不会模糊。游戏:使用游戏请求的级别,与 Switch 上相同。",
@@ -1835,6 +1937,8 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_WALK_SCALE] = "步行幅度",
[UI_MENU_WALK_SCALE_DESC] = "按住步行键时键盘摇杆的推动幅度。一半为走路,推满为奔跑。",
[UI_MENU_OV_SCALE_DOWN] = "双线性(FSR 只能放大)",
[UI_MENU_HDR_HUE] = "高光色调校正",
[UI_MENU_HDR_HUE_DESC] = "火焰、火花等明亮色彩像游戏自身的 SDR 画面一样趋向白色,与它们发出的光一致;0 保持完全饱和。",
[UI_MENU_SHADOW_SCALE] = "阴影分辨率",
[UI_MENU_SHADOW_SCALE_DESC] = "将太阳阴影贴图的每条边绘制为游戏 960x960 的若干倍:阴影边缘和树叶阴影更清晰。下次启动时生效。",
[UI_MENU_SHADOW_RANGE] = "阴影级联范围",
@@ -1847,11 +1951,22 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = {
[UI_MENU_DLAA_DESC] = "用于 3D 场景的 NVIDIA DLSS(NVIDIA RTX 显卡;nvngx_dlss.dll 放在 totk.exe 旁):边缘更平滑,闪烁更少。HUD "
"不受影响。下方的模式决定它的工作分辨率。重启后生效。",
[UI_MENU_DLSS] = "DLSS 模式",
[UI_MENU_DLSS_DESC] = "DLAA 以全分辨率绘制 3D 场景。质量、平衡和性能分别以 67%、58% 和 50% 绘制,再由 DLSS 放大,以获得更高帧率;它们会取代 3D 渲染比例。重启后生效。",
[UI_MENU_DLSS_DESC] = "DLAA 以全分辨率绘制 3D 场景。质量、平衡、性能和超级性能分别以 67%、58%、50% 和 33% 绘制,再由 DLSS 放大,以节省 GPU 时间;它们会取代 3D 渲染比例。立即生效。",
[UI_MENU_DLSS_DLAA] = "DLAA(原生)",
[UI_MENU_DLSS_QUALITY] = "质量",
[UI_MENU_DLSS_BALANCED] = "平衡",
[UI_MENU_DLSS_PERFORMANCE] = "性能",
[UI_MENU_DLSS_ULTRA] = "超级性能",
[UI_MENU_DLSS_PRESET] = "DLSS 模型",
[UI_MENU_DLSS_PRESET_DESC] = "运行哪个 DLSS 模型。自动按 NVIDIA 对各模式的建议:DLAA、质量和平衡用 K,性能用 M,超级性能用 L。"
"J:移动角色身后的拖影略少,闪烁略多。K:所有模式都用最清晰的模型。立即生效。",
[UI_MENU_DLSS_PRESET_DEFAULT] = "自动", [UI_MENU_DLSS_PRESET_K] = "K(最清晰)",
[UI_MENU_DLSS_PRESET_J] = "J(拖影更少)",
[UI_MENU_DLSS_CHARS] = "DLSS 角色拖影",
[UI_MENU_DLSS_CHARS_DESC] = "减少移动中的角色、动物及其手持物品身后的淡淡拖影:DLSS 在这些位置少用之前的帧。越强拖影越少,"
"这些边缘也略欠平滑。立即生效。",
[UI_MENU_DLSS_CHARS_OFF] = "关", [UI_MENU_DLSS_CHARS_LIGHT] = "弱", [UI_MENU_DLSS_CHARS_MEDIUM] = "中",
[UI_MENU_DLSS_CHARS_FULL] = "强",
[UI_MENU_FXAA] = "游戏抗锯齿(FXAA)",
[UI_MENU_FXAA_DESC] = "游戏自身的 FXAA 边缘平滑。会让细节略微模糊;开启 DLAA 时不需要。",
[UI_MENU_TAB_CHEATS] = "金手指",
@@ -1906,6 +2021,22 @@ const char *ui_text_lang(const char *code) {
return code;
}
// Every game language: the Mals files EUen/USen, USfr/EUfr, USes/EUes ship in both builds; USpt only from 1.4.3.
int ui_text_game_langs(int game_version, UiGameLang *out, int max) {
static const struct { const char *code; UiTextId name; int since; } all[] = {
{"en-US", UI_MENU_LANGNAME_EN, 0}, {"en-GB", UI_MENU_LANGNAME_EN_GB, 0}, {"ja", UI_MENU_LANGNAME_JA, 0},
{"fr", UI_MENU_LANGNAME_FR, 0}, {"fr-CA", UI_MENU_LANGNAME_FR_CA, 0}, {"de", UI_MENU_LANGNAME_DE, 0},
{"it", UI_MENU_LANGNAME_IT, 0}, {"es", UI_MENU_LANGNAME_ES, 0}, {"es-419", UI_MENU_LANGNAME_ES_419, 0},
{"pt-BR", UI_MENU_LANGNAME_PT_BR, 143}, {"nl", UI_MENU_LANGNAME_NL, 0}, {"ru", UI_MENU_LANGNAME_RU, 0},
{"zh-Hans", UI_MENU_LANGNAME_ZH_HANS, 0}, {"zh-Hant", UI_MENU_LANGNAME_ZH_HANT, 0},
{"ko", UI_MENU_LANGNAME_KO, 0},
};
int n = 0;
for (size_t i = 0; i < sizeof all / sizeof *all && n < max; i++)
if (game_version >= all[i].since) out[n++] = (UiGameLang){all[i].code, all[i].name};
return n;
}
static const Row *find_row(const char *lang) {
for (size_t i = 0; i < sizeof rows / sizeof *rows; i++)
if (!strcmp(rows[i].lang, lang)) return &rows[i];
+18 -4
View File
@@ -48,13 +48,15 @@ typedef enum {
UI_MENU_COLOR_SATURATION, UI_MENU_COLOR_SATURATION_DESC, UI_MENU_COLOR_GAMMA, UI_MENU_COLOR_GAMMA_DESC,
UI_MENU_HDR_GROUP, // group heading over the HDR rows
UI_MENU_HDR, UI_MENU_HDR_DESC, UI_MENU_HDR_PEAK, UI_MENU_HDR_PEAK_DESC, UI_MENU_HDR_GAME, UI_MENU_HDR_GAME_DESC,
UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC,
UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC, UI_MENU_HDR_HUE, UI_MENU_HDR_HUE_DESC,
UI_MENU_SHADOW_SCALE, UI_MENU_SHADOW_SCALE_DESC,
UI_MENU_SHADOW_RANGE, UI_MENU_SHADOW_RANGE_DESC,
UI_MENU_CONTACT_SHADOWS, UI_MENU_CONTACT_SHADOWS_DESC,
UI_MENU_PCSS, UI_MENU_PCSS_DESC,
UI_MENU_DLAA, UI_MENU_DLAA_DESC, UI_MENU_DLSS, UI_MENU_DLSS_DESC, UI_MENU_DLSS_DLAA, UI_MENU_DLSS_QUALITY,
UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_FXAA, UI_MENU_FXAA_DESC,
UI_MENU_DLSS_BALANCED, UI_MENU_DLSS_PERFORMANCE, UI_MENU_DLSS_ULTRA, UI_MENU_DLSS_PRESET, UI_MENU_DLSS_PRESET_DESC,
UI_MENU_DLSS_PRESET_DEFAULT, UI_MENU_DLSS_PRESET_K, UI_MENU_DLSS_PRESET_J, UI_MENU_DLSS_CHARS, UI_MENU_DLSS_CHARS_DESC,
UI_MENU_DLSS_CHARS_OFF, UI_MENU_DLSS_CHARS_LIGHT, UI_MENU_DLSS_CHARS_MEDIUM, UI_MENU_DLSS_CHARS_FULL, UI_MENU_FXAA, UI_MENU_FXAA_DESC,
// Game
UI_MENU_FPS_LIMIT, UI_MENU_FPS_LIMIT_DESC, UI_MENU_FPS_30, UI_MENU_FPS_40, UI_MENU_FPS_45, UI_MENU_FPS_60, UI_MENU_FPS_90,
@@ -88,6 +90,7 @@ typedef enum {
// Sound
UI_MENU_VOLUME, UI_MENU_VOLUME_DESC,
UI_MENU_AUDIO_CHANNELS, UI_MENU_AUDIO_CHANNELS_DESC, UI_MENU_CHANNELS_AUTO, UI_MENU_CHANNELS_STEREO, UI_MENU_CHANNELS_51,
// Controls
UI_MENU_MOTION, UI_MENU_MOTION_DESC,
@@ -102,7 +105,7 @@ typedef enum {
UI_MENU_PROMPTS_OFF_MODS, UI_MENU_PROMPTS_OFF_PATCHES,
// "Restart now" under the prompts while they change after restart, and its confirmation (Cancel: UI_MENU_RESET_NO)
UI_MENU_RESTART, UI_MENU_RESTART_DESC, UI_MENU_RESTART_CONFIRM, UI_MENU_RESTART_YES, UI_MENU_RESTART_SAVING,
UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
UI_MENU_GROUP_STICKS, UI_MENU_LSTICK_DEADZONE, UI_MENU_RSTICK_DEADZONE, UI_MENU_STICK_DEADZONE_DESC,
UI_MENU_STICK_DEADZONE_NOW, UI_MENU_STICK_SENS, UI_MENU_STICK_SENS_DESC,
UI_MENU_MOUSE_SENS, UI_MENU_MOUSE_SENS_DESC,
UI_MENU_WALK_KEY, UI_MENU_WALK_KEY_DESC, UI_MENU_WALK_KEY_TAKEN, UI_MENU_WALK_SCALE, UI_MENU_WALK_SCALE_DESC,
@@ -150,11 +153,13 @@ typedef enum {
// Menu tab
UI_MENU_MENU_LANG, UI_MENU_MENU_LANG_DESC, UI_MENU_MENU_LANG_AUTO,
UI_MENU_DISCORD_RPC, UI_MENU_DISCORD_RPC_DESC,
// Language names, each in its own language and the same in every row (game language and menu language lists)
UI_MENU_LANGNAME_EN, UI_MENU_LANGNAME_JA, UI_MENU_LANGNAME_FR, UI_MENU_LANGNAME_DE, UI_MENU_LANGNAME_IT,
UI_MENU_LANGNAME_ES, UI_MENU_LANGNAME_NL, UI_MENU_LANGNAME_RU, UI_MENU_LANGNAME_ZH_HANS,
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO, UI_MENU_LANGNAME_PT_BR,
UI_MENU_LANGNAME_ZH_HANT, UI_MENU_LANGNAME_KO, UI_MENU_LANGNAME_PT_BR, UI_MENU_LANGNAME_EN_GB,
UI_MENU_LANGNAME_FR_CA, UI_MENU_LANGNAME_ES_419,
// Advanced
UI_MENU_ADV_WARNING,
@@ -194,6 +199,7 @@ typedef enum {
UI_MENU_TRACE, UI_MENU_TRACE_DESC,
UI_MENU_FLIGHT, UI_MENU_FLIGHT_DESC,
UI_MENU_FLIGHT_SPIKE_MS, UI_MENU_FLIGHT_SPIKE_MS_DESC,
UI_MENU_PASS_CRUMBS, UI_MENU_PASS_CRUMBS_DESC,
UI_MENU_INPUT_RECORD, UI_MENU_INPUT_RECORD_DESC,
UI_MENU_OPEN_LOG, UI_MENU_OPEN_LOG_DESC,
UI_MENU_COPY_INFO, UI_MENU_COPY_INFO_DESC, UI_MENU_COPY_INFO_DONE,
@@ -202,6 +208,8 @@ typedef enum {
// Performance overlay
UI_MENU_OV_FPS, UI_MENU_OV_FRAME_MS, UI_MENU_OV_GAME_PCT, UI_MENU_OV_MAX_MS,
UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL, UI_MENU_OV_FMT_FPS, // the frame rows: their labels, the rate
UI_MENU_OV_LOW1, UI_MENU_OV_LOW01, // printf: the fps of the slowest 1% / 0.1% of the frames, no unit (the overlay column stays narrow)
UI_MENU_OV_GPU, UI_MENU_OV_CPU, UI_MENU_OV_RAM, UI_MENU_OV_VRAM, UI_MENU_OV_TEMP, UI_MENU_OV_POWER,
UI_MENU_OV_CLOCK, UI_MENU_OV_NONE,
UI_MENU_OV_REPLAY, UI_MENU_OV_GPU_TIME, UI_MENU_OV_PIPE_WAIT, UI_MENU_OV_SHADER_WAIT, UI_MENU_OV_TEX_UPLOADS,
@@ -245,6 +253,12 @@ int ui_text_defined(const char *lang, UiTextId id);
// The catalog language for a Switch language code ("en-GB" -> "en", "zh-CN" -> "zh-Hans", ...).
const char *ui_text_lang(const char *switch_code);
// The game languages the F1 menu offers for a game build (rt_game_version): one entry per Mals language file the build
// ships, each with the Switch code TOTK_LANG takes and the id of its name. 1.4.3 adds Brazilian Portuguese (USpt).
// Fills out (room for max entries) and returns the count.
typedef struct { const char *code; UiTextId name; } UiGameLang;
int ui_text_game_langs(int game_version, UiGameLang *out, int max);
// Menu text: ui_text_in(menu language, id). The menu language is TOTK_MENU_LANG ("en", "ru", "pt-BR", "zh-Hans");
// unset, the system UI language when the catalog has menu text for it (English, Russian, Brazilian Portuguese,
// Simplified Chinese), else English. Follows a live change of the setting.
+52
View File
@@ -33,6 +33,7 @@ typedef struct VkCtx {
int has_custom_border_color, has_eds, has_eds2, has_eds3, has_vertex_input_dynamic_state,
has_conditional_rendering, has_transform_feedback, has_provoking_vertex, has_device_fault, has_memory_budget,
has_robustness2, has_buffer_device_address, has_fragment_shader_interlock, has_buffer_marker,
has_nv_checkpoints, // VK_NV_device_diagnostic_checkpoints (TOTK_PASS_CRUMBS=1, vk_crumbs.c)
has_astc_ldr, // textureCompressionASTC_LDR (enabled when reported)
has_calibrated, // VK_KHR/EXT_calibrated_timestamps (GPU idle split in the perf line)
has_hdr_metadata; // VK_EXT_hdr_metadata (vk_hdr.c vk_hdr_metadata)
@@ -87,6 +88,45 @@ void vk_crumbs_release(void);
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n);
void vk_crumbs_report(void); // logs the two codes and what they name
// ---- pass marks (vk_crumbs.c): the replay (vk_exec.c vk_exec_pass_begin / vk_exec_pass_end, vk_exec_draw.c
// op_dispatch) marks where each render pass and dispatch starts and where it ends, so that a device loss names the
// pass the GPU was in. A mark is a number; a ring of VK_PASS_MARKS records keeps what it was. With
// VK_AMD_buffer_marker the GPU writes the number of the last mark it started (TOP_OF_PIPE, before vkCmdBeginRendering
// or the dispatch) and of the last it finished (BOTTOM_OF_PIPE, after vkCmdEndRendering or the dispatch) into the
// breadcrumb buffer; with VK_NV_device_diagnostic_checkpoints the same two points are checkpoints, read with
// vkGetQueueCheckpointData(2)NV after the loss. TOTK_PASS_CRUMBS: unset, AMD buffer marks only; 1, also NVIDIA
// checkpoints (the extension is enabled at device creation); 0, none.
#define VK_PASS_MARKS 4096 // power of two
enum { VK_MARK_PASS = 1, VK_MARK_DISPATCH };
typedef struct {
uint32_t id; // the mark this record belongs to (0: never written)
uint8_t kind; // VK_MARK_*
uint8_t ncolor; // colour attachments
uint32_t step; // index of the pass, or of the dispatch, within its submission, from 0
uint32_t w, h, d; // render area and layers; a dispatch: group counts (0 0 0: indirect)
uint32_t draws; // draws recorded in the pass; UINT32_MAX until its end is recorded
uint64_t fence; // the submission: the timeline value it signals
VkFormat color[8], depth; // attachment formats (VK_FORMAT_UNDEFINED: none)
const GfxShader *first[2], *last[2];// vertex and fragment shader of the first and last draw; a dispatch: first[0]
} VkPassMark;
// Pass marks wanted and possible (TOTK_PASS_CRUMBS and the extensions): set by vk_crumbs_init.
extern int vk_pass_marks_on;
// vk_device.c create_device, before vkCreateDevice: 1 when TOTK_PASS_CRUMBS asks for NVIDIA checkpoints.
int vk_pass_marks_want_nv(void);
// The next mark: records its start into cb (through the recorder thread when it owns cb) and returns its ring record,
// id, kind and fence set, for the caller to fill. NULL when pass marks are off. Replay thread only.
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence);
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m); // records the end of mark m into cb
// The recorder thread's side of both (vk_rec.c): the marker and checkpoint commands themselves.
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end);
// AMD only: the last mark the GPU started and the last it finished (0: none); 0 without buffer marks.
int vk_pass_marks_read(uint32_t *started, uint32_t *finished);
const VkPassMark *vk_pass_mark_record(uint32_t id); // the ring record of mark id; NULL once a later mark took its slot
// The report's text for a started and a finished mark, three lines without a trailing newline: which marks (source:
// the extension they came from), then each with its ring record. vk_pass_marks_report logs it; the probe checks it.
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size);
void vk_pass_marks_report(void); // vk_device_fault_report: the AMD words and the NVIDIA checkpoints, formatted
// ---- window and present (gfx_present.c). Host threads only.
// vk_present_image: blits the crop rectangle {x, y, w, h} of src (NULL, or w/h <= 0: the whole w x h image), scaled
// to the swapchain extent, into the acquired swapchain image and presents it. src is in layout, which is
@@ -215,9 +255,20 @@ void vk_mem_gpu_wrote(const VkMemSpan *sp, uint64_t size);
void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence);
void vk_usnap_end(uint64_t fence);
int vk_usnap_find(uint64_t addr, uint64_t n, uint64_t align, VkDescriptorBufferInfo *out);
const uint8_t *vk_usnap_map(void); // a host copy of the current arena, at the offsets vk_usnap_find gives
const uint8_t *vk_usnap_bytes(uint64_t addr, uint64_t n); // the copy of [addr, addr+n) as of the submit, NULL: none
void vk_usnap_drop(uint64_t addr, uint64_t n); // the replay makes the GPU write [addr, addr+n) of guest memory
uint64_t vk_usnap_gen(void); // changes whenever a binding may resolve differently
void vk_usnap_shutdown(void); // vk_exec_draw_shutdown: frees the arenas
// vk_mvprev.c: last frame's copy of the scene's object (c7) and bone (c5) blocks, for per-object motion vectors.
// vk_mvprev_step: the distance of the game's other copy of a vertex-stage block the snapshot also copies, 0: none.
// vk_mvprev_find: last frame's block of that object (prev, its address; how, MVPREV_*), 0: none.
enum { MVPREV_NONE, MVPREV_SELF, MVPREV_SIDE, MVPREV_NEARER };
uint64_t vk_mvprev_step(int stage, int index, uint64_t size);
int vk_mvprev_on(void);
void vk_mvprev_want(uint32_t index_mask); // also keep these binding indices' last-frame copies (bit n: index n) // DLAA on and TOTK_DLAA_OBJMV not 0, as of the last present
int vk_mvprev_find(int index, uint64_t addr, uint64_t size, uint64_t *prev, int *how);
void vk_mvprev_frame(void); // each present, beside vk_dlaa_frame
void vk_mem_mirror_begin(void);
int vk_mem_mirror_end(VkCommandBuffer cb, uint64_t fence);
int vk_mem_mirror_pending(void);
@@ -350,6 +401,7 @@ uint32_t vk_texture_slots_gen(void);
// its storage address is created, dropped, released or taken back into use. Unlocked; read it before the lookup it
// guards (a lookup may create an entry). 0 when the slot was never registered: nothing is kept under 0.
uint64_t vk_texture_slot_token(int id);
void vk_sampler_dlss_bias(float bias); // texture LOD bias added for DLSS upscaling (material samplers)
uint32_t vk_sampler_gen(void); // changes whenever a sampler slot is registered
void vk_texture_gpu_written(const tex_t *t); // rendered or copied into by the GPU: no re-upload from storage
void vk_texture_storage_bound_locked(const VkTexImage *img); // bound as a storage image in the recording submission
+12 -1
View File
@@ -298,9 +298,20 @@ static int create_pipeline(VkCtx *c) {
}
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
// Unset: on when the largest device-local heap is under 5 GiB. There RGBA8 ASTC, four times the size of BC3, fills
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87); BC3 loses some precision
// (normal maps), so GPUs with more memory keep RGBA8.
static int create_bc_pipeline(VkCtx *c) {
const char *e = cfg_get("TOTK_ASTC_BC");
if (!e || !*e || *e == '0') return 0;
VkDeviceSize vram = 0;
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
vram = c->mem_props.memoryHeaps[h].size;
int on = e && *e ? *e != '0' : vram < (5ull << 30);
if (!(e && *e))
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB)", on ? "on" : "off",
(unsigned long long)(vram >> 20));
if (!on) return 0;
VkFormatProperties p;
vkGetPhysicalDeviceFormatProperties(c->phys, VK_FORMAT_BC3_SRGB_BLOCK, &p);
VkFormatFeatureFlags need = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_TRANSFER_DST_BIT;
+7 -1
View File
@@ -102,6 +102,8 @@ static void present_image(const tex_t *t, const int crop[4], uint64_t at_ns, uin
static volatile int32_t n_skipped;
vk_exec_frame_log(t, frame);
vk_dlaa_frame();
vk_mvsurvey_frame();
vk_mvprev_frame();
vk_mem_census(frame);
if (vk_dump_armed) vk_dump_present(frame);
__atomic_store_n(&gfx_counters.validation_errors, (uint64_t)vk_validation_errors_total(), __ATOMIC_RELAXED);
@@ -166,7 +168,10 @@ static uint64_t ck_reads, ck_diff, ck_diff_unexplained, ck_diff_idle;
int vk_replay_async(void) {
static int on = -1;
if (on < 0) {
on = cfg_bool("TOTK_REPLAY_ASYNC"); // off by default: no measured gain yet, one more frame of latency
// On by default: where the game waits for the GPU it overlaps the replay with the game threads (a GTX 1650 in
// the Depths went from 450-686 frames over 50 ms a minute to 10-13, issue #88); where the replay or the game
// threads bound the frame it neither gains nor loses. It adds up to a frame of latency.
on = cfg_bool("TOTK_REPLAY_ASYNC");
rt_log("vk: replay %s (TOTK_REPLAY_ASYNC=%d)",
on ? "on its own thread, at most one frame behind the game" : "on the submitting thread", on);
}
@@ -315,6 +320,7 @@ static void replay_log(uint64_t frame, int total) {
// vk_backend_shutdown.
void gfx_exit_log(void) {
static int32_t done;
rec_check_log();
if (gfx_replay_async() && r_started && !plat_xchg32(&done, 1)) replay_log(game_frames, 1);
}
+10 -10
View File
@@ -6,8 +6,7 @@
// - minStorageBufferOffsetAlignment above 4 (TOTK_VK_SSBO_ALIGN=<power of two> raises it): the translator computes
// storage offsets from the guest address rounded down to that alignment (Ryujinx's unaligned storage path), and
// vk_exec_draw.c ssbo_info binds the rounded-down address, so shader writes reach guest memory instead of a copy.
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it; the default on AMD's RDNA 1/2 driver):
// vk_caps.gpl = 0, the extension stays disabled, and pipelines are built whole.
// - no graphics pipeline library with fast linking (TOTK_GPL=0 forces it): vk_caps.gpl = 0, the extension stays disabled, and pipelines are built whole.
// - no 32-lane pin (no subgroupSizeControl, 32 outside min..max, or TOTK_VK_SUBGROUP_PIN=0): stages run at the
// device's subgroupSize (SPIR-V 1.3 without a required size) and the translator is told that size.
// TOTK_VK_SUBGROUP=<n> pins n instead of 32 where the driver allows it.
@@ -46,15 +45,18 @@ void vk_caps_driver(VkPhysicalDevice p) {
vk_caps.amd_sc = drv.driverID == VK_DRIVER_ID_AMD_PROPRIETARY && strstr(drv.driverInfo, "shader compiler");
}
// TOTK_GPL=0/1 forces graphics pipeline libraries off or on. Default off on AMD's RDNA 1/2 driver branch: every boot
// that lost the device there had a cold pipeline cache and draws waiting on fast-linked pipelines, and a run with GPL
// off booted (issue #5); Savage Wind Recomp turns GPL off on RDNA 4 under RADV for a GPU hang at the title screen.
// TOTK_GPL=0 turns graphics pipeline libraries off; they are on wherever the driver has fast linking. AMD's RDNA 1/2
// driver branch had them off for a while: its device losses at boot (issue #5) came with a cold pipeline cache, but
// their cause was GPU writes past 3D BC images (vk_exec.c, "3D BC render targets"). With that fixed, the RDNA 1/2
// driver boots with GPL from a cold cache and fast-links without a failure, while with GPL off every new pipeline is
// built whole on the draw's path (frames of 0.5-1.2 s on an RX 6600, issue #381). Savage Wind Recomp turns GPL off on
// RDNA 4 under RADV for a GPU hang at the title screen.
int vk_caps_gpl(const VkPhysicalDeviceGraphicsPipelineLibraryFeaturesEXT *f,
const VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT *p) {
gpl_lib = f->graphicsPipelineLibrary;
gpl_fast = p->graphicsPipelineLibraryFastLinking;
const char *e = cfg_get("TOTK_GPL"); // no fixed default: it depends on the driver, see above
vk_caps.gpl = gpl_lib && gpl_fast && (e && *e ? strcmp(e, "0") != 0 : !vk_caps.amd_sc);
const char *e = cfg_get("TOTK_GPL");
vk_caps.gpl = gpl_lib && gpl_fast && !(e && !strcmp(e, "0"));
return vk_caps.gpl;
}
@@ -137,10 +139,8 @@ void vk_caps_init(VkCtx *c) {
has_ext(ep, ne, "VK_EXT_graphics_pipeline_library"), depth_ok(p, VK_FORMAT_D16_UNORM),
depth_ok(p, VK_FORMAT_D24_UNORM_S8_UINT), depth_ok(p, VK_FORMAT_X8_D24_UNORM_PACK32), depth_ok(p, VK_FORMAT_D32_SFLOAT),
depth_ok(p, VK_FORMAT_D32_SFLOAT_S8_UINT));
const char *ge = cfg_get("TOTK_GPL");
rt_log("vk caps: gpl %s fast-link %s (graphicsPipelineLibrary %u%s)", vk_caps.gpl ? "yes" : "no",
gpl_fast ? "yes" : "no", (unsigned)gpl_lib,
!gpl_lib || !gpl_fast || vk_caps.gpl ? "" : ge && *ge ? ", off by TOTK_GPL=0" : ", off on AMD's RDNA 1/2 driver");
gpl_fast ? "yes" : "no", (unsigned)gpl_lib, !gpl_lib || !gpl_fast || vk_caps.gpl ? "" : ", off by TOTK_GPL=0");
rt_log("vk caps: fallbacks: float64 %s, storage buffers %s %u, subgroup pin %u on stages %#x, translator subgroup vs/fs/cs %u/%u/%u",
vk_caps.float64 ? "native" : "softfloat64", vk_caps.sbuf_align ? "rounded down to" : "exact, alignment",
(unsigned)l->minStorageBufferOffsetAlignment, want, (unsigned)vk_caps.sg_pin_stages, told[0], told[1], told[2]);
+202 -4
View File
@@ -4,19 +4,31 @@
// device loss word 0 names the newest point the GPU reached and word 1 the newest point it had finished: the hang lies
// between them. A code is the crumb kind in the top 4 bits and its number in the low 28. The markers cost two small
// writes per crumb, and crumbs mark submissions, upload batches, ASTC chunks and presents, not draws.
//
// Pass marks (vk.h vk_pass_mark_begin) use words 2 and 3 of the same buffer: the replay marks the start and the end of
// every render pass and dispatch, and a ring of records says what each mark was. The design follows Savage Wind
// Recomp's GPU checkpoints (refs/SavageWindRecomp runtime/nvn/vulkan/exec.c ck_mark and vk_checkpoints_report: a mark
// per pass and dispatch, numbers in a ring of 4096, text only at the device loss); the code is ours.
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "vk.h"
static PFN_vkCmdWriteBufferMarkerAMD write_marker;
static VkBuffer buf;
static VmaAllocation alloc;
static volatile uint32_t *words;
static volatile uint32_t *words; // 0, 1: crumbs; 2, 3: pass marks
void vk_crumbs_init(VkCtx *c) {
if (!c->has_buffer_marker) return;
// Pass marks (below)
int vk_pass_marks_on;
static int pm_amd; // marks into words 2 and 3
static PFN_vkCmdSetCheckpointNV set_checkpoint;
static void pass_marks_init(VkCtx *c);
static void buffer_init(VkCtx *c) {
write_marker = (PFN_vkCmdWriteBufferMarkerAMD)vkGetDeviceProcAddr(c->device, "vkCmdWriteBufferMarkerAMD");
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 16,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
@@ -30,14 +42,20 @@ void vk_crumbs_init(VkCtx *c) {
return;
}
words = info.pMappedData;
words[0] = words[1] = 0;
words[0] = words[1] = words[2] = words[3] = 0;
rt_log("vk: GPU breadcrumbs on (VK_AMD_buffer_marker)");
}
void vk_crumbs_init(VkCtx *c) {
if (c->has_buffer_marker) buffer_init(c);
pass_marks_init(c);
}
void vk_crumbs_release(void) {
VkCtx *c = vk_ctx();
if (buf && c) vmaDestroyBuffer(c->vma, buf, alloc);
buf = VK_NULL_HANDLE, alloc = NULL, words = NULL, write_marker = NULL;
vk_pass_marks_on = pm_amd = 0, set_checkpoint = NULL;
}
void vk_crumb(VkCommandBuffer cb, int kind, uint64_t n) {
@@ -65,3 +83,183 @@ void vk_crumbs_report(void) {
describe(words[1], finished, sizeof finished);
rt_log("vk: GPU breadcrumbs: the GPU last reached %s; everything before %s had finished", reached, finished);
}
// ---- pass marks
// Cost per pass or dispatch: about twenty stores into the ring and two commands, one at the start and one at the end,
// each a marker write or a checkpoint. Per draw nothing, except that the branch of vk_exec_draw.c prepare_draw that
// records changed pipeline state also notes the shaders of a pass's first draw. A full barrier already precedes every
// pass and dispatch (vk_exec.c vk_exec_pass_begin, vk_exec_draw.c op_dispatch), so the end-of-pipe write adds no wait.
static VkPassMark ring[VK_PASS_MARKS];
static uint32_t seq; // marks made; only the replay thread writes it
// TOTK_PASS_CRUMBS: unset, buffer marks where VK_AMD_buffer_marker exists; 1, also NVIDIA checkpoints; 0, neither.
int vk_pass_marks_want_nv(void) {
const char *e = cfg_get("TOTK_PASS_CRUMBS");
return e && *e && strcmp(e, "0") != 0;
}
static void pass_marks_init(VkCtx *c) {
const char *e = cfg_get("TOTK_PASS_CRUMBS");
int off = e && !strcmp(e, "0");
set_checkpoint = c->has_nv_checkpoints
? (PFN_vkCmdSetCheckpointNV)vkGetDeviceProcAddr(c->device, "vkCmdSetCheckpointNV") : NULL;
pm_amd = !off && write_marker && words;
vk_pass_marks_on = pm_amd || set_checkpoint;
if (vk_pass_marks_on)
rt_log("vk: pass marks on (%s%s%s; TOTK_PASS_CRUMBS=0 turns them off%s)", pm_amd ? "VK_AMD_buffer_marker" : "",
pm_amd && set_checkpoint ? " and " : "", set_checkpoint ? "VK_NV_device_diagnostic_checkpoints" : "",
set_checkpoint ? "" : ", =1 adds NVIDIA checkpoints where the driver has them");
else
rt_log("vk: pass marks off (%s)", off ? "TOTK_PASS_CRUMBS=0"
: "no VK_AMD_buffer_marker; TOTK_PASS_CRUMBS=1 asks for NVIDIA checkpoints");
}
void vk_pass_mark_cmd(VkCommandBuffer cb, uint32_t id, int end) {
if (pm_amd)
write_marker(cb, end ? VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT : VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, buf,
end ? 12 : 8, id);
if (set_checkpoint) set_checkpoint(cb, (const void *)(uintptr_t)((uint64_t)id << 1 | (end != 0)));
}
VkPassMark *vk_pass_mark_begin(VkCommandBuffer cb, int kind, uint64_t fence) {
if (!vk_pass_marks_on || !cb) return NULL;
uint32_t id = ++seq;
if (!id) id = ++seq; // 0 means no mark
VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
m->id = id, m->kind = (uint8_t)kind, m->fence = fence, m->draws = UINT32_MAX;
rec_pass_mark(cb, id, 0);
return m;
}
void vk_pass_mark_end(VkCommandBuffer cb, const VkPassMark *m) {
if (m && cb) rec_pass_mark(cb, m->id, 1);
}
int vk_pass_marks_read(uint32_t *started, uint32_t *finished) {
if (!pm_amd) return 0;
*started = words[2], *finished = words[3];
return 1;
}
const VkPassMark *vk_pass_mark_record(uint32_t id) {
const VkPassMark *m = &ring[id & (VK_PASS_MARKS - 1)];
return id && m->id == id ? m : NULL;
}
// The first 8 bytes of the shader's input id (gfx_shader.c), the start of its stored input's name <cache>/in/<id>.in.
static unsigned long long shader_id(const GfxShader *s) {
if (!s) return 0;
const uint8_t *iid = gfx_shader_input_id(s);
unsigned long long v = 0;
for (int i = 0; i < 8; i++) v = v << 8 | iid[i];
return v;
}
static void describe_mark(uint32_t id, char *o, size_t size) {
const VkPassMark *m = vk_pass_mark_record(id);
if (!id) {
snprintf(o, size, "nothing yet");
return;
}
if (!m) {
snprintf(o, size, "no record (a later mark took its slot)");
return;
}
if (m->kind == VK_MARK_DISPATCH) {
snprintf(o, size, "dispatch %u of submission %llu: %ux%ux%u groups%s, cs %016llx", m->step,
(unsigned long long)m->fence, m->w, m->h, m->d, m->w ? "" : " (indirect)", shader_id(m->first[0]));
return;
}
size_t n = (size_t)snprintf(o, size, "pass %u of submission %llu: %ux%u, %u layer%s, colour formats", m->step,
(unsigned long long)m->fence, m->w, m->h, m->d, m->d == 1 ? "" : "s");
for (int i = 0; i < m->ncolor && i < 8 && n < size; i++)
n += (size_t)snprintf(o + n, size - n, "%s%d", i ? "," : " ", (int)m->color[i]);
if (!m->ncolor && n < size) n += (size_t)snprintf(o + n, size - n, " none");
if (n < size) n += (size_t)snprintf(o + n, size - n, ", depth format %d", (int)m->depth);
if (n >= size) return;
if (m->draws == UINT32_MAX)
snprintf(o + n, size - n, ", still recording; first draw vs %016llx fs %016llx", shader_id(m->first[0]),
shader_id(m->first[1]));
else if (!m->draws)
snprintf(o + n, size - n, ", no draws");
else
snprintf(o + n, size - n, ", %u draws; first vs %016llx fs %016llx, last vs %016llx fs %016llx", m->draws,
shader_id(m->first[0]), shader_id(m->first[1]), shader_id(m->last[0]), shader_id(m->last[1]));
}
void vk_pass_marks_format(const char *source, uint32_t started, uint32_t finished, char *o, size_t size) {
char a[640], b[640];
describe_mark(started, a, sizeof a);
describe_mark(finished, b, sizeof b);
snprintf(o, size,
"vk: pass marks (%s, %u made): the GPU last started mark %u and last finished mark %u\n"
"vk: last started %u: %s\n"
"vk: last finished %u: %s",
source, (unsigned)seq, (unsigned)started, (unsigned)finished, (unsigned)started, a, (unsigned)finished, b);
}
static void log_lines(char *t) {
for (char *line = t, *nl; line && *line; line = nl ? nl + 1 : NULL) {
nl = strchr(line, '\n');
if (nl) *nl = 0;
rt_log("%s", line);
}
}
// vkGetQueueCheckpointData2NV, else vkGetQueueCheckpointDataNV, on the graphics queue (the replay's): one entry per
// stage with the last checkpoint that stage executed. A checkpoint is the mark's id times 2, plus 1 at its end, so the
// top-of-pipe entry names the last start or end the GPU reached, and the bottom-of-pipe entry the last one before which
// everything had finished: at a start, that is the end of the mark before it.
static void nv_report(void) {
VkCtx *c = vk_ctx();
if (!c || !set_checkpoint) return;
PFN_vkGetQueueCheckpointData2NV get2 =
(PFN_vkGetQueueCheckpointData2NV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointData2NV");
PFN_vkGetQueueCheckpointDataNV get1 =
(PFN_vkGetQueueCheckpointDataNV)vkGetDeviceProcAddr(c->device, "vkGetQueueCheckpointDataNV");
enum { MAXN = 32 };
uint64_t stage[MAXN], marker[MAXN];
uint32_t n = 0;
if (get2) {
VkCheckpointData2NV d[MAXN];
get2(c->gfx_queue, &n, NULL);
if (n > MAXN) n = MAXN;
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointData2NV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_2_NV};
get2(c->gfx_queue, &n, d);
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
} else if (get1) {
VkCheckpointDataNV d[MAXN];
get1(c->gfx_queue, &n, NULL);
if (n > MAXN) n = MAXN;
for (uint32_t i = 0; i < n; i++) d[i] = (VkCheckpointDataNV){.sType = VK_STRUCTURE_TYPE_CHECKPOINT_DATA_NV};
get1(c->gfx_queue, &n, d);
for (uint32_t i = 0; i < n; i++) stage[i] = d[i].stage, marker[i] = (uint64_t)(uintptr_t)d[i].pCheckpointMarker;
} else {
rt_log("vk: pass marks: no vkGetQueueCheckpointDataNV entry point");
return;
}
uint32_t started = 0, finished = 0;
for (uint32_t i = 0; i < n; i++) {
uint32_t id = (uint32_t)(marker[i] >> 1);
int end = (int)(marker[i] & 1);
rt_log("vk: checkpoint of stage %#llx: the %s of mark %u", (unsigned long long)stage[i], end ? "end" : "start", id);
if (stage[i] == VK_PIPELINE_STAGE_2_TOP_OF_PIPE_BIT) started = id;
if (stage[i] == VK_PIPELINE_STAGE_2_BOTTOM_OF_PIPE_BIT) finished = end ? id : id ? id - 1 : 0;
}
if (!n) rt_log("vk: pass marks: the driver reported no checkpoints");
char t[2048];
vk_pass_marks_format("VK_NV_device_diagnostic_checkpoints", started, finished, t, sizeof t);
log_lines(t);
}
void vk_pass_marks_report(void) {
if (!vk_pass_marks_on) return;
uint32_t started, finished;
char t[2048];
if (vk_pass_marks_read(&started, &finished)) {
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, t, sizeof t);
log_lines(t);
}
nv_report();
}
+10 -3
View File
@@ -261,7 +261,7 @@ static int create_device(void) {
CHAIN(fbl, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ATTACHMENT_FEEDBACK_LOOP_LAYOUT_FEATURES_EXT);
CHAIN(lr, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_LOCAL_READ_FEATURES_KHR);
}
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the ones below
vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the seven below
if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; }
// GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not.
if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; }
@@ -272,6 +272,12 @@ static int create_device(void) {
}
// GPU breadcrumbs for a lost device (vk_crumbs.c): AMD's drivers and RADV report it.
if (has_ext(ep, ne, VK_AMD_BUFFER_MARKER_EXTENSION_NAME)) { g_vk.has_buffer_marker = 1; ext[n_ext++] = VK_AMD_BUFFER_MARKER_EXTENSION_NAME; }
// Pass marks on NVIDIA (vk_crumbs.c): checkpoints only with TOTK_PASS_CRUMBS=1, since NVIDIA documents a cost for
// the extension. NVIDIA's drivers report it, AMD's do not.
if (vk_pass_marks_want_nv() && has_ext(ep, ne, VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME)) {
g_vk.has_nv_checkpoints = 1;
ext[n_ext++] = VK_NV_DEVICE_DIAGNOSTIC_CHECKPOINTS_EXTENSION_NAME;
}
// Graphics pipeline library: chained last so it can be dropped again when vk_caps_gpl declines it.
void **gpl_link = NULL;
if (has_ext(ep, ne, VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME) && has_ext(ep, ne, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME)) {
@@ -430,6 +436,7 @@ void vk_device_fault_report(VkResult r) {
if (done) { plat_mutex_unlock(&lock); return; }
done = 1;
vk_crumbs_report();
vk_pass_marks_report();
VkCtx *c = vk_ctx();
PFN_vkGetDeviceFaultInfoEXT get =
c && c->has_device_fault ? (PFN_vkGetDeviceFaultInfoEXT)vkGetDeviceProcAddr(c->device, "vkGetDeviceFaultInfoEXT")
@@ -500,11 +507,11 @@ int probe_vkdevice(void) {
c->device_index, c->props.deviceName, VK_API_VERSION_MAJOR(v), VK_API_VERSION_MINOR(v), VK_API_VERSION_PATCH(v),
c->gfx_family, c->xfer_family, c->xfer_shared ? " (shared queue)" : "", c->min_imported_host_alignment);
printf("probe-vkdevice: optional: custom_border=%d eds=%d eds2=%d eds3=%d vertex_input=%d cond_render=%d xfb=%d provoking=%d "
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d validation=%d\n",
"robustness2=%d fault=%d budget=%d bda=%d interlock=%d buffer_marker=%d nv_checkpoints=%d validation=%d\n",
c->has_custom_border_color, c->has_eds, c->has_eds2, c->has_eds3, c->has_vertex_input_dynamic_state,
c->has_conditional_rendering, c->has_transform_feedback, c->has_provoking_vertex, c->has_robustness2,
c->has_device_fault, c->has_memory_budget, c->has_buffer_device_address, c->has_fragment_shader_interlock,
c->validation);
c->has_buffer_marker, c->has_nv_checkpoints, c->validation);
vk_shutdown();
printf("probe-vkdevice: %s (validation errors %ld)\n", g_errors_total ? "FAILED" : "passed", g_errors_total);
return g_errors_total ? 1 : 0;
+667 -84
View File
File diff suppressed because it is too large. Load diff
+25
View File
@@ -2,12 +2,18 @@
#pragma once
#include "vk.h"
// The narrowest scene the DLSS pass and the motion passes recognise by its D32F depth: Ultra Performance at a 1080p
// output renders 640 wide, smaller windows less.
#define DLAA_SCENE_MIN_W 320u
int vk_dlaa_wanted(void); // the setting is on (the device may still lack DLSS)
// vk_device.c: NGX's instance and device extensions join the lists (room for max names); vk_dlaa_init after the
// device exists brings NGX up and checks that DLSS is available.
void vk_dlaa_instance_exts(const char **ext, uint32_t *n, uint32_t max);
void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, const char **ext, uint32_t *n, uint32_t max);
void vk_dlaa_init(void);
// 1 when dlaa is on and NGX came up with DLSS (after vk_dlaa_init): a DLSS mode's smaller scene is scaled up by DLSS.
int vk_dlaa_available(void);
// vk_exec.c vk_exec_pass_begin, once the pass's targets are resolved and before its barrier: arms the frame on the
// scene's light target or, at the first quarter-width pass after it, runs DLAA on that target. Each present ends the
// frame (vk_dlaa_frame).
@@ -18,6 +24,25 @@ void vk_dlaa_frame(void);
// the viewport of the scene's draws by the frame's jitter (vk_dlaa_viewport, when the viewport is recorded).
void vk_dlaa_draw(VkExec *ex);
void vk_dlaa_viewport(VkExec *ex, VkViewport *v);
// vk_mvsurvey.c (TOTK_MV_SURVEY): notes the uniform blocks of each scene draw, beside vk_dlaa_draw; each present
// compares them with the previous frames' and logs a summary now and then.
void vk_mvsurvey_draw(VkExec *ex);
void vk_mvsurvey_frame(void);
void vk_mvprev_draw(VkExec *ex); // vk_mvprev.c: notes the scene draw's object and bone blocks
// Per-object motion (vk_exec_draw.c): the moving scene draws are listed again during the replay with
// their vertex shader's last-frame variant; vk_dlaa.c mv_pass records them over the camera motion into ex->cb (and
// leaves every binding of the replay to be set again; draws whose camera block is not cam are left out).
// vk_dlaa_prev_camera: last frame's camera block and this frame's jitter, 0 when there is no history.
int vk_mvobj_execute(VkExec *ex, uint64_t cam, VkImageView mv, VkImageView depth, VkFormat depth_fmt, uint32_t w,
uint32_t h);
int vk_dlaa_prev_camera(VkDescriptorBufferInfo *out, float jitter[2]);
// vk_mvcheck.c (TOTK_MV_CHECK, test): mv_pass hands it the motion image after the camera pass and, after the object
// pass, the motion image again and the scene light target it ran on; it measures how well the vectors fit the picture.
void vk_mvcheck_camera(VkCommandBuffer cb, VkImage mv, uint32_t w, uint32_t h, float jx, float jy);
void vk_mvcheck_final(VkCommandBuffer cb, uint64_t fence, VkImage mv, VkImage cimg, VkFormat cfmt, uint32_t level,
uint32_t layer, uint32_t w, uint32_t h, float jx, float jy);
const char *vk_mvobj_frame_masks(void); // vk_exec_draw.c: the last motion list, per position and changed banks
void vk_mvcheck_output(VkCommandBuffer cb, VkImage out, uint32_t ow, uint32_t oh); // after DLSS on a check frame
// vk_exec_draw.c, once a draw's fragment shader is known: 1 skips the draw (the game's FXAA while TOTK_FXAA is 0, its
// scaling draw when DLSS wrote the scaled picture).
int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs);
+24
View File
@@ -612,6 +612,20 @@ void vk_exec_pass_begin(VkExec *ex) {
if (d->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) da = a;
if (d->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) sa = a;
}
// The start mark goes between the barrier and vkCmdBeginRendering, its end right after vkCmdEndRendering
// (vk_exec_pass_end): outside the pass, and without breaking it.
VkPassMark *m = ex->mark = vk_pass_mark_begin(ex->cb, VK_MARK_PASS, ex->fence);
if (m) {
m->step = ex->mark_passes++;
m->w = ex->pass_w, m->h = ex->pass_h, m->d = ex->pass_layers;
m->ncolor = (uint8_t)(ex->pass_ncolor < 8 ? ex->pass_ncolor : 8);
for (int i = 0; i < m->ncolor; i++) m->color[i] = ex->pass_color[i];
m->depth = ex->pass_depth ? ex->pass_depth : ex->pass_stencil;
// The shaders bound now are the first draw's when it repeats the pipeline state; when it does not,
// vk_exec_draw.c prepare_draw replaces them as it records the new state.
vk_exec_draw_shaders(&m->first[0], &m->first[1]);
ex->mark_draws0 = vk_exec_frame.draws;
}
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {ex->pass_w, ex->pass_h}},
.layerCount = ex->pass_layers, .colorAttachmentCount = ex->pass_ncolor,
.pColorAttachments = ca,
@@ -629,6 +643,14 @@ void vk_exec_pass_end(VkExec *ex) {
vkCmdEndRendering(ex->cb);
ex->recorded++;
ex->in_pass = 0;
if (ex->mark) {
// vk_exec_frame_log resets the frame counters at a present (TOTK_TRACE=gfx): then only the draws since it count
uint64_t d = vk_exec_frame.draws, d0 = ex->mark_draws0;
ex->mark->draws = (uint32_t)(d >= d0 ? d - d0 : d);
if (ex->mark->draws) vk_exec_draw_shaders(&ex->mark->last[0], &ex->mark->last[1]);
vk_pass_mark_end(ex->cb, ex->mark);
ex->mark = NULL;
}
for (int i = 0; i < ex->pass_ncolor && i < 8; i++) // stand-ins of 3D BC targets go to their textures
if (ex->color[i].image && ex->color[i].img && ex->color[i].img->image != ex->color[i].image)
bc_copy_back(ex, &ex->color[i]);
@@ -901,6 +923,8 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t
ex->fence = fence;
ex->recorded = 0;
ex->in_pass = 0;
ex->mark = NULL;
ex->mark_passes = ex->mark_dispatches = 0;
ex->dirty = VK_EXEC_DIRTY_ALL;
ex->rt_resolved = 0; // images may have changed since the last command buffer
ex->nviews = 0;
+11
View File
@@ -47,6 +47,11 @@ typedef struct VkExec {
int recorded; // Vulkan commands recorded so far; every handler adds the ones it records
int in_pass; // inside vkCmdBeginRendering
uint32_t dirty; // VK_EXEC_DIRTY_*
// Pass marks (vk.h vk_pass_mark_begin): the open pass's record (NULL: none or marks off), vk_exec_frame.draws when
// it began, and the passes and dispatches this submission marked so far.
VkPassMark *mark;
uint64_t mark_draws0;
uint32_t mark_passes, mark_dispatches;
// ---- render targets (OP_SET_RT) and the current pass
p_set_rt rt; // last OP_SET_RT payload (a change ends the open pass); rt.n 0, no depth: none
@@ -126,6 +131,8 @@ void vk_exec_inline_end(int reopen);
// vk_exec_draw.c: forget what the command buffer being recorded has bound (pipeline, sets, dynamic state, vertex
// buffers): the replay continues in a new one (vk_sync_split).
void vk_exec_draw_new_cb(void);
// vk_exec_draw.c: the vertex and fragment shader of the last draw recorded into this command buffer (NULL: none yet).
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs);
VkBuffer vk_exec_inline_buffer(VkDeviceSize size);
// vk_exec.c: the stand-in a 3D BC render target is drawn through goes with its image (vk_exec_image_gone).
void vk_exec_bc_gone(VkImage image);
@@ -181,6 +188,10 @@ VkImageView vk_exec_target_view(const VkTexImage *img, VkFormat format, VkImageV
// vk_exec_xfer.c: OP_COPY_B2B, OP_COPY_B2T, OP_COPY_T2B, OP_COPY_T2T, OP_CLEAR_TEX, OP_SIGNAL_EVENT, OP_REPORT_COUNTER,
// OP_RESET_COUNTER, OP_RENDER_ENABLE, OP_RENDER_COND, OP_CLEAR_BUF.
void vk_exec_op_xfer(VkExec *ex, uint32_t op, const void *payload);
// vk_dlaa.c: once DLSS has scaled the scene this frame, passes larger than the scene's light target vk_dlaa_swap_img
// sample DLSS's output vk_dlaa_swap_view in its place, so the game's scaling draw scales DLSS's picture.
extern const VkTexImage *vk_dlaa_swap_img;
extern VkImageView vk_dlaa_swap_view;
// vk_contact.c: a pass about to begin (its targets resolved, no pass open), and a dispatch or copy about to be recorded
void vk_contact_pass_begin(VkExec *ex);
void vk_contact_flush(VkExec *ex);
+820 -13
View File
@@ -198,6 +198,21 @@ static void cb_done(void *arg) { // completion thread
void vk_exec_draw_new_cb(void) { memset(&B, 0, sizeof B); }
void vk_exec_draw_shaders(const GfxShader **vs, const GfxShader **fs) {
*vs = B.st_valid ? B.st.vs : NULL;
*fs = B.st_valid ? B.st.fs : NULL;
}
// Marks a dispatch about to be recorded (vk.h vk_pass_mark_begin); the caller ends it after the dispatch.
static VkPassMark *dispatch_mark(VkExec *ex, const GfxShader *cs, uint32_t x, uint32_t y, uint32_t z) {
VkPassMark *m = vk_pass_mark_begin(ex->cb, VK_MARK_DISPATCH, ex->fence);
if (!m) return NULL;
m->step = ex->mark_dispatches++;
m->w = x, m->h = y, m->d = z, m->draws = 0, m->ncolor = 0;
m->first[0] = cs, m->first[1] = m->last[0] = m->last[1] = NULL;
return m;
}
static CbRes *cb_res(VkExec *ex) {
if (cur && cur_fence == ex->fence) return cur;
cur = calloc(1, sizeof *cur);
@@ -234,8 +249,13 @@ static uint8_t *scratch(VkExec *ex, uint64_t size, VkBuffer *buf, VkDeviceSize *
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
// HOST_COHERENT: the CPU fills these chunks and never flushes them. Intel's integrated GPUs (ANV, Meteor Lake)
// offer host-cached memory only without coherence, which VMA picks for random host access: the GPU then read
// what an earlier submission left in the chunk (an UpdateUniformBuffer payload of another draw: a lighting
// shader's step count of 0x3f800000 instead of 128, and a loop of a billion steps).
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT};
VmaAllocationInfo info;
VkResult res = vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &c->buf, &c->alloc, &info);
if (res != VK_SUCCESS) rt_fatal("vk: transient buffer of %llu bytes failed (%d)", (unsigned long long)n, (int)res);
@@ -351,7 +371,8 @@ static void dummies_init(void) {
VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT |
VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT,
.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT}; // the memset below is not flushed
VmaAllocationInfo info;
if (vmaCreateBuffer(c->vma, &bi, &ai, &Z.zbuf, &Z.zalloc, &info) != VK_SUCCESS) rt_fatal("vk: zero buffer failed");
memset(info.pMappedData, 0, ZERO_BYTES);
@@ -710,6 +731,8 @@ static void answers_for(VkExec *ex, int st, const uint8_t *ctl, uint32_t mask, G
// *present: the program has this stage. Returns NULL when it has not or the translation failed. pending (graphics
// stages, vk_pipeline_async_on): a shader not translated yet is queued and NULL returned with *pending = 1. await: a
// shader queued earlier is waited for (gfx_shader_await) instead.
static int sf_mvprev; // shader_for asks for the vertex shader's last-frame variant (per-object motion)
static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pending, int await) {
const uint8_t *ctl, *code;
uint32_t len;
@@ -720,6 +743,7 @@ static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pendin
GfxShaderAnswers a;
uint64_t serial = ex->prog->serial;
answers_for(ex, st, ctl, slot_mask_of(serial, st), &a);
a.mvprev = (uint8_t)(sf_mvprev && st == NVN_STAGE_VS);
ShaderEnt *set = NULL;
if (serial) {
uint64_t h = (serial * 0x9E3779B97F4A7C15ull ^ (uint64_t)st) ^ nvn_hm_hash(&a, sizeof a); // a is zero-padded
@@ -737,7 +761,7 @@ static const GfxShader *shader_for(VkExec *ex, int st, int *present, int *pendin
}
int gs = st == NVN_STAGE_VS ? GFX_STAGE_VS : st == NVN_STAGE_FS ? GFX_STAGE_FS : GFX_STAGE_CS;
const GfxShader *s = await ? gfx_shader_await(gs, ctl, code, len, &a)
: pending && vk_pipeline_async_on() ? gfx_shader_get_async(gs, ctl, code, len, &a, pending)
: pending && (vk_pipeline_async_on() || a.mvprev) ? gfx_shader_get_async(gs, ctl, code, len, &a, pending)
: gfx_shader_get(gs, ctl, code, len, NULL, &a);
if (pending && *pending) return NULL;
if (s) slot_mask_learn(serial, st, s);
@@ -874,7 +898,14 @@ static void limits_init(void) {
__atomic_store_n(&LIM.ubo_max, l->maxUniformBufferRange, __ATOMIC_RELEASE);
}
// Per-object motion (section at the end): while it records a draw again, these replace the uniform bindings of the
// vertex stage's slots 18-20 (last frame's camera, bones and place) and the fragment stage's slot 3 (its parameters).
static int mo_ovr_on;
static VkDescriptorBufferInfo mo_ovr[2][32]; // [vertex, fragment][slot]; buffer 0: not replaced
static VkDescriptorBufferInfo ubo_info(VkExec *ex, int st, int slot) {
if (mo_ovr_on && (st == NVN_STAGE_VS || st == NVN_STAGE_FS) && slot >= 0 && slot < 32 && mo_ovr[st][slot].buffer)
return mo_ovr[st][slot];
if (!__atomic_load_n(&LIM.ubo_max, __ATOMIC_ACQUIRE)) limits_init();
uint64_t max = LIM.ubo_max, align = LIM.ubo_align;
VkDescriptorBufferInfo bi;
@@ -1377,9 +1408,13 @@ static Twin *twin_update(VkExec *ex, const VkTexImage *img) {
}
// The image descriptor of slot h; in a dispatch, a compare-sampled colour image the driver cannot compare is swapped
// for its depth twin.
// for its depth twin; in a pass larger than it, the scene's light target, once DLSS scaled it this frame, for DLSS's
// output (vk_dlaa.c).
static void tex_info(VkExec *ex, const GfxShaderRes *r, uint64_t h, int storage, int separate, VkDescriptorImageInfo *out) {
tex_info_lookup(ex, r, h, storage, separate, out);
if (h && !storage && vk_dlaa_swap_img && ex->in_pass && ex->pass_w > vk_dlaa_swap_img->width + 1 &&
vk_texture_by_id_locked((int)(h & 0xfffff), NULL, NULL) == vk_dlaa_swap_img)
out->imageView = vk_dlaa_swap_view, out->imageLayout = VK_IMAGE_LAYOUT_GENERAL;
if (!twin_dispatch || storage || separate || !h || !(r->type & 0x400)) return;
VkImageViewType vt = res_view_type(r->type);
if (vt != VK_IMAGE_VIEW_TYPE_2D && vt != VK_IMAGE_VIEW_TYPE_2D_ARRAY) return;
@@ -1891,18 +1926,23 @@ static void bind_pipeline(VkExec *ex, VkPipelineBindPoint bp, VkPipeline p) {
if (!k) B.st_valid = 0;
}
static VkViewport draw_viewport(VkExec *ex) {
float x = 0, y = 0, w = (float)ex->pass_w, h = (float)ex->pass_h;
if (ex->has_vp) {
float k = ex->pass_scale > 1 ? (float)ex->pass_scale : 1.0f;
x = ex->vp[0][0] * k, y = ex->vp[0][1] * k, w = ex->vp[0][2] * k, h = ex->vp[0][3] * k;
}
if (w < 1) w = 1;
if (h < 1) h = 1;
VkViewport v = {x, y + h, w, -h, ex->depth_range[0], ex->depth_range[1]}; // the y flip: negative height
vk_dlaa_viewport(ex, &v); // the scene's draws move by the DLAA jitter
return v;
}
static void record_fixed(VkExec *ex) {
VkCommandBuffer cb = ex->cb;
if (ex->dirty & VK_EXEC_DIRTY_VIEWPORT) {
float x = 0, y = 0, w = (float)ex->pass_w, h = (float)ex->pass_h;
if (ex->has_vp) {
float k = ex->pass_scale > 1 ? (float)ex->pass_scale : 1.0f;
x = ex->vp[0][0] * k, y = ex->vp[0][1] * k, w = ex->vp[0][2] * k, h = ex->vp[0][3] * k;
}
if (w < 1) w = 1;
if (h < 1) h = 1;
VkViewport v = {x, y + h, w, -h, ex->depth_range[0], ex->depth_range[1]}; // the y flip: negative height
vk_dlaa_viewport(ex, &v); // the scene's draws move by the DLAA jitter
VkViewport v = draw_viewport(ex);
vkCmdSetViewportWithCount(cb, 1, &v);
ex->recorded++;
}
@@ -2161,8 +2201,11 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
vk_pipeline_set_dynamic(ex->cb, &s);
ex->recorded++;
B.st = s, B.st_valid = 1;
if (ex->mark && vk_exec_frame.draws == ex->mark_draws0) ex->mark->first[0] = vs, ex->mark->first[1] = fs;
}
vk_dlaa_draw(ex);
vk_mvsurvey_draw(ex);
vk_mvprev_draw(ex);
record_fixed(ex);
repeat_stats(ex, info);
bind_resources(ex, info);
@@ -2175,6 +2218,762 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
return 1;
}
// ---- per-object motion
// The camera pass (vk_dlaa.c mv_pass) gives everything the camera's motion, so a character that moves with the camera,
// or on its own, gets the wrong one and DLSS throws its history away there. Each solid scene draw whose position reads
// an object's own uniform blocks (bones c5, place c7, c6; mvobj_draw_ says which are left out and why) is therefore
// drawn a second time (listed during the replay, MQ, and recorded by mv_pass) with its vertex shader's last-frame
// variant (translator patch 0015: the position again from last frame's blocks, bound at slot bank + 15, and last
// frame's camera, written to a spare output) and the fragment shader of vk_mvobj_fs.h, which writes last frame's window
// position minus this frame's. The draw keeps everything else of the original: vertex and index buffers, the other
// uniform blocks, the jittered viewport and the scene depth, tested with the game's own test and its equal case
// (LEQUAL, GEQUAL) without writes, so only the visible surface writes its vector. Last frame's blocks:
// vk_mvprev_find and mo_prev_block; the camera: vk_dlaa_prev_camera. A draw whose variant or pipeline is still
// building, or whose last-frame blocks are not in the snapshot, keeps the camera's motion. TOTK_DLAA_OBJMV=0 turns it
// off (vk_mvprev.c).
#include "vk_mvobj_fs.h"
static const char MO_FS_RES[] = "{\"key\":\"mvobj\",\"stage\":\"ps\",\"notes\":[],\"cbufs\":[{\"binding\":1,\"slot\":3}],"
"\"sbufs\":[],\"textures\":[],\"images\":[]}";
static struct {
const GfxShader *fs[16]; // per input Location
int failed;
uint64_t cand, still, draws, nodata, building, ran, dropped;
uint64_t ns_check, ns_rec, n_rec, ns_emit, ns_still;
uint64_t ns_part[3], n_part[3], misses, cyc[3], ncyc, big; // checks: filter, data and stillness (memo misses), variant and pipeline // time spent: checks and lookups of every candidate; recording the draws again // cand: draws whose vertex shader reads c5 or c7
uint64_t why[5]; // nodata by cause: camera, bones, place, bones copy, place copy
uint64_t no_out, storage, no_fs; // variant without the extra output, with storage resources; no fs
uint64_t other_cam; // listed draws left out: their camera block is not the scene camera
uint64_t lk[14][5]; // last-frame lookups per binding index: none, self, side, nearer, refused (another object)
uint64_t e_draws, e_pipe, e_dyn, e_push, e_pushw, e_sets, e_vb, e_setw, e_setreuse; // emitted commands (summary)
} MO;
// The motion draws of the frame, kept as a list while the scene replays and recorded into the replay's own command
// buffer by vk_mvobj_execute, through the recorder thread (vk_rec.h) like every other draw: recording them straight
// into a secondary command buffer made the driver's work for each one on the replay thread. What a draw shares with
// the one before it (pipeline, dynamic state, viewport, buffers, descriptors) is recorded once; a push descriptor set
// is pushed again only where a binding changed (pushes update the set in place while its layout stays bound).
typedef struct {
VkPipeline pipe;
const VkPipeInfo *info;
uint32_t st, desc, vb; // indices into MQ.st, MQ.desc (the draw's packed descriptors) and MQ.vb
VkDescriptorSet set[2]; // allocated sets; VK_NULL_HANDLE for a pushed one
VkViewport v;
VkRect2D sc;
float bias[3];
VkBuffer ib;
VkDeviceSize ib_off;
VkIndexType it;
uint32_t count, instances, base_instance;
int32_t base_vertex;
uint64_t cam; // the draw's camera block (c4), this frame's
} MoDraw;
typedef struct { VkBuffer b[17]; VkDeviceSize o[17], sz[17], str[17]; } MoVb;
static struct {
uint64_t frame, fence; // the frame and submission the list belongs to
VkFormat depth_fmt;
MoDraw *d;
VkPipeState *st;
VkDescSlot *desc;
MoVb *vb;
int n, cap, nst, cst, nvb, cvb;
uint32_t ndesc, cdesc;
NVNbufferRange vin[17]; // the vertex ranges MQ.vb[nvb - 1] was resolved from, under vk_mem_gen vgen
uint64_t vgen;
float sup_in[3]; // the support buffer last filled for the list and its inputs
VkDescriptorBufferInfo sup;
float par_in[4]; // the fragment shader's parameters last written and their inputs
VkDescriptorBufferInfo par;
VkDescriptorSetLayout aset_layout[2]; // the last allocated set per set number, with its descriptors
VkDescriptorSet aset[2];
uint32_t aset_n[2];
VkDescSlot aset_d[2][MAX_DESC];
} MQ;
static void *mo_grow(void *p, int *cap, int need, size_t elem) {
if (need <= *cap) return p;
int c = *cap ? *cap : 256;
while (c < need) c *= 2;
if (!(p = realloc(p, elem * (size_t)c))) rt_fatal("vk_exec: out of memory");
*cap = c;
return p;
}
// The position bank masks of the draws considered (gfx_shader_mvprev_banks), with counts, for the summary.
static struct { uint32_t mask; uint64_t n; int ubo, ssbo, tex; } MM[32]; // ubo, ssbo, tex: the first vertex shader's
static void mo_mask_note(uint32_t banks, const GfxShader *vs) {
for (int i = 0; i < 32; i++)
if (MM[i].mask == banks || !MM[i].n) {
if (!MM[i].n) {
const GfxShaderRes *r;
MM[i].ubo = gfx_shader_resources(vs, GFX_RES_UBO, &r), MM[i].ssbo = gfx_shader_resources(vs, GFX_RES_SSBO, &r);
MM[i].tex = gfx_shader_resources(vs, GFX_RES_TEX, &r);
}
MM[i].mask = banks, MM[i].n++;
return;
}
}
static void mo_drop(void) {
if (MQ.n) MO.dropped++;
MQ.n = MQ.nst = MQ.nvb = 0, MQ.ndesc = 0, MQ.frame = 0;
}
static uint64_t mo_gen = 1; // the list's snapshot (mo_prev_cached), bumped by mo_begin
static struct { uint32_t mask, chg, n; } MF[16]; // this list's draws per position banks and changed banks
static char fm_last[400]; // the last executed list's, formatted (vk_mvobj_frame_masks)
// Starts this frame's list (the old one, never executed, is dropped).
static void mo_begin(VkExec *ex, uint64_t frame) {
mo_drop();
mo_gen++;
memset(MF, 0, sizeof MF);
MQ.frame = frame, MQ.fence = ex->fence, MQ.depth_fmt = ex->depth.format;
memset(MQ.vin, 0, sizeof MQ.vin);
MQ.vgen = 0, MQ.sup.buffer = MQ.par.buffer = VK_NULL_HANDLE;
memset(MQ.aset, 0, sizeof MQ.aset);
}
static const GfxShader *mo_fs(int loc) {
if (loc < 0 || loc >= 16) return NULL;
if (!MO.fs[loc]) {
uint32_t w[sizeof mvobj_fs_spv / 4];
memcpy(w, mvobj_fs_spv, sizeof w);
w[MVOBJ_FS_LOCATION_WORD] = (uint32_t)loc;
MO.fs[loc] = gfx_shader_own(w, sizeof w / 4, MO_FS_RES);
if (!MO.fs[loc]) MO.failed = 1, rt_log("vk_exec: the per-object motion fragment shader is malformed: off");
}
return MO.fs[loc];
}
// The write of binding b from its packed descriptors.
static VkWriteDescriptorSet mo_write(const VkPipeBinding *b, const VkDescSlot *d) {
VkWriteDescriptorSet w = {.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = b->binding,
.descriptorCount = b->count ? b->count : 1, .descriptorType = b->type};
if (b->type == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER || b->type == VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE ||
b->type == VK_DESCRIPTOR_TYPE_SAMPLER)
w.pImageInfo = &d->image;
else
w.pBufferInfo = &d->buffer; // VkDescSlot is 24 bytes, as VkDescriptorBufferInfo: arrays line up
return w;
}
// The descriptors of a motion draw, packed into MQ.desc at q->desc without the binding caches of the command buffer
// being replayed (B, DS, RS), which then stay true for its next draw: uniform buffers (through ubo_info, so with
// mo_ovr), the support buffer and sampled textures. An allocated set is written here (or the last one reused when it
// would hold the same); a pushed set is pushed by mo_emit. 0 for a pipeline with storage buffers, images or texel
// buffers (not drawn again).
static int mo_desc(VkExec *ex, const VkPipeInfo *info, MoDraw *q) {
if (info->n_bindings > MAX_BINDINGS) return 0;
uint32_t total = 0;
for (uint32_t i = 0; i < info->n_bindings; i++) total += info->bindings[i].count ? info->bindings[i].count : 1;
if (total > MAX_DESC) return 0;
int cdesc = (int)MQ.cdesc;
MQ.desc = mo_grow(MQ.desc, &cdesc, (int)(MQ.ndesc + total), sizeof *MQ.desc);
MQ.cdesc = (uint32_t)cdesc;
VkDescSlot *d = MQ.desc + MQ.ndesc;
memset(d, 0, total * sizeof *d); // unused bytes zero: equal descriptors compare equal
for (uint32_t i = 0, nd = 0; i < info->n_bindings; i++) {
const VkPipeBinding *b = &info->bindings[i];
const GfxShaderRes *r = b->res;
uint32_t cnt = b->count ? b->count : 1;
VkDescSlot *o = &d[nd];
int st = nvn_stage_of(b->stage);
if (b->kind == VK_PIPE_SUPPORT) {
float in[3];
support_in(ex, in);
if (!MQ.sup.buffer || memcmp(in, MQ.sup_in, sizeof in)) {
VkBuffer sb;
VkDeviceSize so;
support_fill(scratch(ex, SUPPORT_BYTES, &sb, &so), in[0], in[1], in[2]);
MQ.sup = (VkDescriptorBufferInfo){sb, so, SUPPORT_BYTES};
memcpy(MQ.sup_in, in, sizeof in);
}
o->buffer = MQ.sup;
} else if (b->kind == GFX_RES_UBO) {
for (uint32_t k = 0; k < cnt; k++) o[k].buffer = ubo_info(ex, st, r->slot + (int)k);
} else if (b->kind == GFX_RES_SSBO && !(r->flags & 1)) { // read only: instance data of foliage and such
for (uint32_t k = 0; k < cnt; k++) o[k].buffer = ssbo_info(ex, st, r, 0);
} else if (b->kind == GFX_RES_TEX && b->type != VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER) {
vk_texture_lock();
for (uint32_t k = 0; k < cnt; k++) {
int slot = r->slot + (int)k;
tex_info(ex, r, slot >= 0 && slot < 128 ? ex->tex[st][slot] : 0, 0, r->separate, &o[k].image);
}
vk_texture_unlock();
} else {
return 0;
}
nd += cnt;
}
uint32_t n_sets = info->n_sets ? info->n_sets : 1;
q->set[0] = q->set[1] = VK_NULL_HANDLE;
for (uint32_t s = 0, i0 = 0, k0 = 0; s < n_sets && s < 2; s++) {
uint32_t i1 = i0, k1 = k0;
while (i1 < info->n_bindings && (n_sets == 1 || info->bindings[i1].set == s))
k1 += info->bindings[i1].count ? info->bindings[i1].count : 1, i1++;
if (i1 > i0 && !(s ? info->push1 : info->push)) {
VkDescriptorSetLayout sl = s ? info->set_layout1 : info->set_layout;
if (MQ.aset[s] && MQ.aset_layout[s] == sl && MQ.aset_n[s] == k1 - k0 &&
!memcmp(MQ.aset_d[s], d + k0, (k1 - k0) * sizeof *d)) {
q->set[s] = MQ.aset[s];
MO.e_setreuse++;
} else {
static VkWriteDescriptorSet w[MAX_BINDINGS];
VkDescriptorSet set = alloc_set(ex, sl);
for (uint32_t i = i0, k = k0; i < i1; k += info->bindings[i].count ? info->bindings[i].count : 1, i++)
w[i - i0] = mo_write(&info->bindings[i], d + k), w[i - i0].dstSet = set;
vkUpdateDescriptorSets(vk_ctx()->device, i1 - i0, w, 0, NULL);
q->set[s] = MQ.aset[s] = set, MQ.aset_layout[s] = sl, MQ.aset_n[s] = k1 - k0;
memcpy(MQ.aset_d[s], d + k0, (k1 - k0) * sizeof *d);
MO.e_setw++;
}
}
i0 = i1, k0 = k1;
}
q->desc = MQ.ndesc;
MQ.ndesc += total;
return 1;
}
// The draw's vertex buffers as bind_vertex_buffers resolves them, into MQ.vb; the previous entry again while the
// ranges and vk_mem_gen are the same (most motion draws in a row are one model's parts).
static uint32_t mo_vertex_buffers(VkExec *ex) {
uint64_t gen = vk_mem_gen();
if (MQ.nvb && MQ.vgen == gen && !memcmp(MQ.vin, ex->vbuf, sizeof MQ.vin)) {
const MoVb *l = &MQ.vb[MQ.nvb - 1];
int same = 1;
if (!vk_pipeline_vertex_input_dynamic())
for (int b = 0; b < 16 && same; b++) same = l->str[b] == ex->pipe.vstream[b].stride;
if (same) return (uint32_t)MQ.nvb - 1;
}
MQ.vb = mo_grow(MQ.vb, &MQ.cvb, MQ.nvb + 1, sizeof *MQ.vb);
MoVb *v = &MQ.vb[MQ.nvb];
for (int b = 0; b < 17; b++) {
v->b[b] = Z.zbuf, v->o[b] = 0, v->sz[b] = ZERO_BYTES, v->str[b] = b < 16 ? ex->pipe.vstream[b].stride : 0;
if (b == VK_PIPE_ZERO_BINDING) continue;
const NVNbufferRange *r = &ex->vbuf[b];
VkMemSpan sp;
if (!r->address || !vk_mem_resolve(r->address, 1, &sp)) continue;
VK_MEM_USE(sp);
v->b[b] = sp.buffer, v->o[b] = sp.offset, v->sz[b] = r->size && r->size < sp.avail ? r->size : sp.avail;
}
memcpy(MQ.vin, ex->vbuf, sizeof MQ.vin);
MQ.vgen = gen;
return (uint32_t)MQ.nvb++;
}
// Records the list into cb (the replay's command buffer, inside the motion pass's rendering), skipping what the draw
// before already set.
static void mo_emit(VkCommandBuffer cb) {
int dyn_vi = vk_pipeline_vertex_input_dynamic();
VkPipeline pipe = VK_NULL_HANDLE;
uint32_t st = UINT32_MAX, vb = UINT32_MAX;
VkPipelineLayout lay[2] = {0};
const VkPipeInfo *pinfo[2] = {0};
int pdesc[2] = {-1, -1}; // the pushed set's descriptors (MQ.desc index) under lay/pinfo, -1 none
VkDescriptorSet bound[2] = {0};
const MoDraw *p = NULL;
for (int n = 0; n < MQ.n; p = &MQ.d[n++]) {
const MoDraw *q = &MQ.d[n];
const VkPipeInfo *info = q->info;
if (q->pipe != pipe) vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, q->pipe), pipe = q->pipe, MO.e_pipe++;
if (q->st != st) vk_pipeline_set_dynamic(cb, &MQ.st[q->st]), st = q->st, MO.e_dyn++;
if (!p || memcmp(&q->v, &p->v, sizeof q->v)) vkCmdSetViewportWithCount(cb, 1, &q->v);
if (!p || memcmp(&q->sc, &p->sc, sizeof q->sc)) vkCmdSetScissorWithCount(cb, 1, &q->sc);
if (!p || memcmp(q->bias, p->bias, sizeof q->bias)) vkCmdSetDepthBias(cb, q->bias[0], q->bias[1], q->bias[2]);
uint32_t n_sets = info->n_sets ? info->n_sets : 1;
const VkDescSlot *d = MQ.desc + q->desc;
for (uint32_t s = 0, i0 = 0, k0 = 0; s < n_sets && s < 2; s++) {
uint32_t i1 = i0, k1 = k0;
while (i1 < info->n_bindings && (n_sets == 1 || info->bindings[i1].set == s))
k1 += info->bindings[i1].count ? info->bindings[i1].count : 1, i1++;
if (i1 > i0) {
if (s ? info->push1 : info->push) {
// the bindings that differ from the last push of this layout and pipeline (all after another)
int same = lay[s] == info->layout && pinfo[s] == info && pdesc[s] >= 0;
const VkDescSlot *pd = same ? MQ.desc + pdesc[s] : NULL;
VkWriteDescriptorSet w[MAX_BINDINGS];
uint32_t nw = 0;
for (uint32_t i = i0, k = k0; i < i1; i++) {
uint32_t cnt = info->bindings[i].count ? info->bindings[i].count : 1;
if (!same || memcmp(d + k, pd + k, cnt * sizeof *d)) w[nw++] = mo_write(&info->bindings[i], d + k);
k += cnt;
}
if (nw) rec_push(vk_pipeline_push_fn(), cb, VK_PIPELINE_BIND_POINT_GRAPHICS, info->layout, s, nw, w);
MO.e_push += nw != 0, MO.e_pushw += nw;
lay[s] = info->layout, pinfo[s] = info, pdesc[s] = (int)q->desc, bound[s] = VK_NULL_HANDLE;
} else if (q->set[s] != bound[s] || lay[s] != info->layout) {
vkCmdBindDescriptorSets(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, info->layout, s, 1, &q->set[s], 0, NULL);
bound[s] = q->set[s], lay[s] = info->layout, pinfo[s] = info, pdesc[s] = -1, MO.e_sets++;
}
}
i0 = i1, k0 = k1;
}
if (q->vb != vb) {
const MoVb *v = &MQ.vb[q->vb];
vkCmdBindVertexBuffers2(cb, 0, 17, v->b, v->o, v->sz, dyn_vi ? NULL : v->str);
vb = q->vb, MO.e_vb++;
}
if (!p || q->ib != p->ib || q->ib_off != p->ib_off || q->it != p->it) vkCmdBindIndexBuffer(cb, q->ib, q->ib_off, q->it);
vkCmdDrawIndexed(cb, q->count, q->instances, 0, q->base_vertex, q->base_instance);
MO.e_draws++;
}
}
static void mvobj_draw_(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off, uint64_t t0);
static void mvobj_draw(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off) {
if (MO.failed || !vk_mvprev_on() || !B.st_valid) return;
static int timed = -1;
if (timed < 0) timed = rt_knob("MVOBJ_TIMES", "0"); // TOTK_MVOBJ_TIMES=1: the summary's times (the clock reads cost)
if (!timed) return mvobj_draw_(ex, d, ib, ib_off, 0);
uint64_t t0 = kern_now_ns();
mvobj_draw_(ex, d, ib, ib_off, t0);
MO.ns_check += kern_now_ns() - t0;
}
// TOTK_MVOBJ_BANKS=1 (test): for scene draws, the words of each other uniform bank (c3, c6, c8-c16) that changed
// since the same shader's draw last frame, logged at frame 3000: where the time that moves wind and water lives
static void mvobj_banks(VkExec *ex) {
static int on = -1;
if (on < 0) on = rt_knob("MVOBJ_BANKS", "0");
if (!on || !B.st_valid) return;
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (frame < 2999 || frame > 3000) return;
static struct { const GfxShader *vs; int bank; uint64_t frame; float w[64]; int n; } tab[4096];
static int logged;
const GfxShaderRes *ur;
int nu = gfx_shader_resources(B.st.vs, GFX_RES_UBO, &ur);
for (int i = 0; i < nu; i++) {
int bank = ur[i].slot;
if (bank < 3 || bank == 4 || bank == 5 || bank == 7 || bank > 16) continue;
const p_bind_buf *u = &ex->ubo[NVN_STAGE_VS][bank - 3];
uint32_t n = u->size < 256 ? (uint32_t)u->size / 4 : 64;
const float *now = n ? (const float *)vk_usnap_bytes(u->addr, n * 4) : NULL;
if (!now) continue;
uint32_t k = (uint32_t)(((uintptr_t)B.st.vs ^ (uint64_t)bank * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull >> 52);
if (frame == 2999) {
tab[k].vs = B.st.vs, tab[k].bank = bank, tab[k].frame = frame, tab[k].n = (int)n;
memcpy(tab[k].w, now, n * 4);
} else if (tab[k].vs == B.st.vs && tab[k].bank == bank && tab[k].frame == 2999 && logged < 80) {
char line[700];
int o = 0, nd = 0;
for (int j = 0; j < tab[k].n && j < (int)n && o < 600; j++)
if (memcmp(&tab[k].w[j], &now[j], 4)) nd++, o += snprintf(line + o, sizeof line - o, " [%d] %g>%g", j, tab[k].w[j], now[j]);
if (nd) logged++, rt_log("mvbanks: vs %p c%d size %#llx: %d words%s", (const void *)B.st.vs, bank,
(unsigned long long)u->size, nd, line);
tab[k].frame = 0;
}
}
}
// 1: old is cur a little changed or the same (every float within 10 %, at least 1.0 and at most 4.0); *changed: not
// the same. The cap: TotK's world positions are in the thousands, and 10 % of them let another object's block a few
// units away pass as this one moved; nothing moves 4 units in a frame.
static int mo_similar(const uint8_t *cur, const uint8_t *old, uint64_t n, int *changed) {
*changed = 0;
if (!memcmp(cur, old, n)) return 1;
// word by word as integers; float arithmetic only on two normal floats (the blocks hold integers too, and
// denormal arithmetic took this loop 1.9 million cycles a block)
const uint32_t *x = (const uint32_t *)cur, *y = (const uint32_t *)old;
for (uint64_t w = 0; w < n / 4; w++) {
if (x[w] == y[w]) continue;
uint32_t ex = x[w] >> 23 & 0xff, ey = y[w] >> 23 & 0xff;
if (!ex || ex == 0xff || !ey || ey == 0xff) continue; // not both normal floats: an integer or a tiny value
float fx, fy;
memcpy(&fx, &x[w], 4), memcpy(&fy, &y[w], 4);
float dd = fabsf(fx - fy), tol = 0.1f * fabsf(fy);
if (dd > (tol < 1.0f ? 1.0f : tol > 4.0f ? 4.0f : tol)) return 0;
}
*changed = 1;
return 1;
}
// Last frame's copy of a block, accepted when it is this frame's block a little changed (mo_similar,
// which see): a block of another object the address rule found would make motion up. 1 and *out: found (the block itself
// when it is unchanged, *changed 0); 0: none, the draw keeps this frame's block there.
static int mo_prev_block(int index, const p_bind_buf *u, VkDescriptorBufferInfo *out, int *changed) {
uint64_t max = LIM.ubo_max, n = u->size < max ? u->size : max, pa;
int how;
*changed = 0;
// c9 and c11 keep this frame's block. With the copies the address rule finds for c11 and last frame's c9, the
// c4 c7 c9 c11 draws (walls, cliffs, arches) got several pixels of motion under a still camera (TOTK_MV_CHECK,
// those draws alone: 34 % of the picture moving; with c11 kept, 0.8 %). With c11 kept, their c9 still changes every
// frame while nothing moves (the check's per-draw line: about 120 such draws a frame changed in c9 alone), the
// object scale the picture shows is 0.6-0.8 of theirs, and with Link stopped against a cliff 40-77 % of the picture
// got 1-3 px of motion that flipped direction, three times the warp error of the camera's vectors. With c9 kept too,
// the turning run is as good (object warp error 0.46 of the camera's). Why their copies are wrong is not known.
// TOTK_MVPREV_SKIP=<hex bank mask> (test) replaces the default a00 (bit n: cn).
static long skip = -2;
if (skip == -2) {
const char *e = cfg_get("TOTK_MVPREV_SKIP");
skip = e && *e ? strtol(e, NULL, 16) : 1L << 9 | 1L << 11;
}
if (skip >> (index + 3) & 1) return 0;
uint64_t c0 = __builtin_ia32_rdtsc();
int found = vk_mvprev_find(index, u->addr, u->size, &pa, &how);
uint64_t c1 = __builtin_ia32_rdtsc();
MO.cyc[0] += c1 - c0, MO.ncyc++, MO.big += n;
if (index >= 0 && index < 14) MO.lk[index][found ? how : MVPREV_NONE]++;
if (!found || !vk_usnap_find(pa, n, LIM.ubo_align, out)) return 0;
if (how == MVPREV_SELF) return 1; // bound at the same address last frame: not written again
const uint8_t *map = vk_usnap_map(), *cur = vk_usnap_bytes(u->addr, n);
uint64_t c2 = __builtin_ia32_rdtsc();
MO.cyc[1] += c2 - c1;
if (!cur) return 0;
const uint8_t *old = map + out->offset;
static long trust = -2; // TOTK_MVPREV_TRUST=<hex bank mask> (test): the copy found is taken without the check
if (trust == -2) {
const char *e = cfg_get("TOTK_MVPREV_TRUST");
trust = e && *e ? strtol(e, NULL, 16) : 0;
}
if (trust >> (index + 3) & 1) {
*changed = memcmp(cur, old, n) != 0;
return 1;
}
int r = mo_similar(cur, old, n, changed);
MO.cyc[2] += __builtin_ia32_rdtsc() - c2;
if (!r && index >= 0 && index < 14) MO.lk[index][4]++;
return r;
}
// mo_prev_block once per block and list: many draws share their blocks (the scene's c14, a model's c7 and c9 across
// its parts), and the lookup with its comparison was most of the checks' time.
static int mo_prev_cached(int index, const p_bind_buf *u, VkDescriptorBufferInfo *out, int *changed) {
static struct { uint64_t gen, addr, size; int index, r, changed; VkDescriptorBufferInfo out; } bc[4096];
uint32_t i = (uint32_t)((u->addr ^ (uint64_t)index << 56 ^ u->size << 40) * 0x9E3779B97F4A7C15ull >> 52);
if (bc[i].gen == mo_gen && bc[i].addr == u->addr && bc[i].size == u->size && bc[i].index == index) {
*out = bc[i].out, *changed = bc[i].changed;
return bc[i].r;
}
int r = mo_prev_block(index, u, out, changed);
bc[i].gen = mo_gen, bc[i].addr = u->addr, bc[i].size = u->size, bc[i].index = index;
bc[i].r = r, bc[i].changed = *changed, bc[i].out = *out;
return r;
}
static void mvobj_draw_(VkExec *ex, const p_draw_idx *d, VkBuffer ib, VkDeviceSize ib_off, uint64_t t0) {
mvobj_banks(ex);
const VkExecTarget *z = &ex->depth;
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT || B.st.samples > 1 || B.st.raster_discard)
return;
// solid geometry only, which tests and writes the scene depth: effects, particles and screen quads write none, and
// drawn again they would put their motion on the surfaces behind them (DLSS takes those from the surface)
if (!B.st.ds.dt || !B.st.ds.dw) return;
// a vertex shader that reads a user bank besides the camera's (c4) may move on its own
const GfxShaderRes *ur;
int nu = gfx_shader_resources(B.st.vs, GFX_RES_UBO, &ur), reads = 0;
for (int i = 0; i < nu; i++) reads |= ur[i].slot >= 3 && ur[i].slot <= 16 && ur[i].slot != 4;
if (!reads) return;
MO.cand++;
uint64_t tp = t0 ? kern_now_ns() : 0;
if (t0) MO.ns_part[0] += tp - t0, MO.n_part[0]++;
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (MQ.frame != frame || MQ.fence != ex->fence || MQ.depth_fmt != z->format) mo_begin(ex, frame);
if (!__atomic_load_n(&LIM.ubo_max, __ATOMIC_ACQUIRE)) limits_init();
// the variant: the translation of the same code and answers, kept per shader once found; it names the banks the
// position depends on (0: none, the shader is skipped from then on)
static struct { const GfxShader *of, *vs; uint32_t banks; } vmemo[1024];
uint32_t vi = (uint32_t)((uintptr_t)B.st.vs * 0x9E3779B97F4A7C15ull >> 54);
if (vmemo[vi].of != B.st.vs) {
int has, pending = 0;
sf_mvprev = 1;
const GfxShader *v = shader_for(ex, NVN_STAGE_VS, &has, &pending, 0);
sf_mvprev = 0;
if (!v || pending) {
MO.building++;
return;
}
GfxShaderIo io[32];
uint32_t extra = gfx_shader_outputs(v, io) & ~gfx_shader_outputs(B.st.vs, io);
uint32_t banks = extra ? gfx_shader_mvprev_banks(v) & 0x1fff8u : 0;
vmemo[vi].of = B.st.vs, vmemo[vi].vs = v, vmemo[vi].banks = banks;
vk_mvprev_want((banks & ~(1u << 4)) >> 3); // binding index = bank - 3
}
const GfxShader *vs = vmemo[vi].vs;
uint32_t banks = vmemo[vi].banks;
if (!banks || !(banks & ~(1u << 4))) {
MO.no_out++;
return;
}
// a position without the scene camera (c4) has a view of its own (c8 c9 c10 [c13]: its c8 is at a new address every
// frame, last frame's is never found): drawn again it would get this frame's view and no camera motion
// (TOTK_MV_CHECK while turning: 6.2 px end-point error against the camera vectors' 1.7)
static int noc4 = -1;
if (noc4 < 0) noc4 = rt_knob("MVOBJ_NOC4", "0"); // TOTK_MVOBJ_NOC4=1 (test): drawn again all the same
if (!(banks >> 4 & 1) && !noc4) {
MO.no_out++;
return;
}
// only positions that read an object's own blocks: bones (c5), place (c7) or c6. The rest (terrain and grass: c14
// c15 c16, with or without c9) measured no better than the camera's motion (TOTK_MV_CHECK, turning, those draws
// alone: object warp error 0.112 against the camera's 0.113, the object part 0.4 of what the picture shows) and
// were half the draws checked and listed. TOTK_MVOBJ_ALL=1 (test) takes them again.
static int all = -1;
if (all < 0) all = rt_knob("MVOBJ_ALL", "0");
if (!all && !(banks & (1u << 5 | 1u << 6 | 1u << 7))) {
MO.no_out++;
return;
}
// TOTK_MVOBJ_ONLY=<hex bank mask> (test): only draws whose position reads no bank outside the mask (bit n: cn)
static long only = -2;
if (only == -2) {
const char *e = cfg_get("TOTK_MVOBJ_ONLY");
only = e && *e ? strtol(e, NULL, 16) : -1;
}
if (only >= 0 && banks & ~(uint32_t)only & ~(1u << 4)) return;
static long exact = -2; // TOTK_MVOBJ_EXACT=<hex bank mask> (test): only draws with exactly these position banks
if (exact == -2) {
const char *e = cfg_get("TOTK_MVOBJ_EXACT");
exact = e && *e ? strtol(e, NULL, 16) : -1;
}
if (exact >= 0 && banks != (uint32_t)exact) return;
mo_mask_note(banks, B.st.vs);
// last frame's camera and blocks; held still when none of the position's blocks changed (most of the scenery)
VkDescriptorBufferInfo cam, prevb[17];
float jit[2];
if (!vk_dlaa_prev_camera(&cam, jit)) {
MO.nodata++, MO.why[0]++;
return;
}
static struct { uint64_t frame, key; int moving; VkDescriptorBufferInfo b[17]; uint32_t have; } memo[8192];
uint64_t key = banks;
for (int b = 3; b <= 16; b++)
if (banks >> b & 1 && b != 4) key = (key ^ ex->ubo[NVN_STAGE_VS][b - 3].addr) * 0x100000001b3ull;
uint32_t mi = (uint32_t)(key * 0xff51afd7ed558ccdull >> 51);
uint32_t have = 0;
if (memo[mi].frame == frame && memo[mi].key == key) {
if (!memo[mi].moving) {
if (t0) MO.ns_still += kern_now_ns() - tp;
return (void)MO.still++;
}
memcpy(prevb, memo[mi].b, sizeof prevb), have = memo[mi].have;
} else {
MO.misses++;
int moving = 0;
for (int b = 3; b <= 16; b++) {
if (!(banks >> b & 1) || b == 4) continue;
const p_bind_buf *u = &ex->ubo[NVN_STAGE_VS][b - 3];
int changed;
if (u->addr && u->size && mo_prev_cached(b - 3, u, &prevb[b], &changed))
have |= 1u << b, moving |= changed << b; // the banks whose block changed
}
memo[mi].frame = frame, memo[mi].key = key, memo[mi].moving = moving;
memcpy(memo[mi].b, prevb, sizeof prevb), memo[mi].have = have;
if (!moving) {
if (t0) MO.ns_still += kern_now_ns() - tp;
return (void)MO.still++;
}
}
if (t0) MO.ns_part[1] += kern_now_ns() - tp, MO.n_part[1]++, tp = kern_now_ns();
GfxShaderIo io[32];
uint32_t extra = gfx_shader_outputs(vs, io) & ~gfx_shader_outputs(B.st.vs, io);
const GfxShaderRes *r;
if (gfx_shader_resources(vs, GFX_RES_IMG, &r)) {
MO.storage++;
return;
}
const GfxShader *fs = mo_fs(31 - __builtin_clz(extra));
if (!fs) {
MO.no_fs++;
return;
}
VkPipeState ms = B.st;
ms.vs = vs, ms.fs = fs, ms.feedback = 0;
memset(ms.color_fmt, 0, sizeof ms.color_fmt);
ms.color_fmt[0] = NVN_FORMAT_RG16F;
// blending off (blend_en below); the factors stay the game's, valid NVN values
ms.color.blend_en = 0, ms.color.logic_op = 0;
ms.chmask.mask = 0x3; // r and g of target 0
ms.ms.a2c = 0;
// the game's own test with its equal case (LESS -> LEQUAL, GREATER -> GEQUAL): the surface the draw left in the
// scene depth passes, anything nearer hides it (TOTK_MVOBJ_EQUAL=1: EQUAL, for comparison)
static int eq = -1;
if (eq < 0) eq = rt_knob("MVOBJ_EQUAL", "0");
int df = B.st.ds.df;
df = eq ? NVN_DEPTH_FUNC_EQUAL : df == NVN_DEPTH_FUNC_LESS ? NVN_DEPTH_FUNC_LEQUAL
: df == NVN_DEPTH_FUNC_GREATER ? NVN_DEPTH_FUNC_GEQUAL : df == NVN_DEPTH_FUNC_LEQUAL || df == NVN_DEPTH_FUNC_GEQUAL ? df
: NVN_DEPTH_FUNC_EQUAL;
ms.ds.dt = 1, ms.ds.dw = 0, ms.ds.df = (uint64_t)df, ms.ds.st = 0;
// the pipeline per state, kept: the lookup hashes and locks the whole state
static struct { VkPipeState st; VkPipeline p; const VkPipeInfo *info; int used; } pmemo[64];
static int plast = -1; // the entry of the last draw: a model's parts in a row share the state, no hash then
uint32_t pi;
if (plast >= 0 && pmemo[plast].used && !memcmp(&pmemo[plast].st, &ms, sizeof ms)) {
pi = (uint32_t)plast;
} else {
uint64_t ph = 0xcbf29ce484222325ull;
for (size_t i = 0; i < sizeof ms; i += 8) {
uint64_t w8;
memcpy(&w8, (const uint8_t *)&ms + i, sizeof ms - i < 8 ? sizeof ms - i : 8);
ph = (ph ^ w8) * 0x100000001b3ull;
}
pi = (uint32_t)(ph >> 58);
}
const VkPipeInfo *info = NULL;
VkPipeline pipe;
int pending = 0;
if (pmemo[pi].used && !memcmp(&pmemo[pi].st, &ms, sizeof ms)) {
info = pmemo[pi].info;
pipe = __atomic_load_n(&info->pipeline, __ATOMIC_ACQUIRE); // its optimised build once swapped in
if (!pipe) return;
} else {
pipe = vk_pipeline_graphics_async(&ms, &info, &pending, 0);
if (pending || !pipe || !info) {
MO.building += pending != 0;
return;
}
pmemo[pi].st = ms, pmemo[pi].p = pipe, pmemo[pi].info = info, pmemo[pi].used = 1;
}
plast = (int)pi;
uint64_t t1 = kern_now_ns();
if (t0) MO.ns_part[2] += t1 - tp, MO.n_part[2]++;
if (t0) MO.ns_check -= t1 - t0; // counted as recording below
// into the frame's list, past the primary's binding caches (mo_desc); mo_emit records it
MQ.d = mo_grow(MQ.d, &MQ.cap, MQ.n + 1, sizeof *MQ.d);
MoDraw *q = &MQ.d[MQ.n];
float par_in[4] = {(float)z->width, (float)z->height, jit[0], jit[1]};
if (!MQ.par.buffer || memcmp(par_in, MQ.par_in, sizeof par_in)) {
VkBuffer pbuf;
VkDeviceSize poff;
memcpy(scratch(ex, 16, &pbuf, &poff), par_in, sizeof par_in);
MQ.par = (VkDescriptorBufferInfo){pbuf, poff, 16};
memcpy(MQ.par_in, par_in, sizeof par_in);
}
memset(mo_ovr, 0, sizeof mo_ovr);
for (int bk = 3; bk <= 16; bk++) // last frame's block, or this frame's where none was found
mo_ovr[NVN_STAGE_VS][bk + 15] = bk == 4 ? cam : have >> bk & 1 ? prevb[bk] : ubo_info(ex, NVN_STAGE_VS, bk);
mo_ovr[NVN_STAGE_FS][3] = MQ.par;
mo_ovr_on = 1;
int bound = mo_desc(ex, info, q);
mo_ovr_on = 0;
if (!bound) {
MO.storage++;
return;
}
q->pipe = pipe, q->info = info;
if (!MQ.nst || memcmp(&MQ.st[MQ.nst - 1], &ms, sizeof ms)) {
MQ.st = mo_grow(MQ.st, &MQ.cst, MQ.nst + 1, sizeof *MQ.st);
MQ.st[MQ.nst++] = ms;
}
q->st = (uint32_t)MQ.nst - 1;
q->v = draw_viewport(ex), q->sc = vk_exec_scissor(ex);
q->bias[0] = ex->poly_offset.units, q->bias[1] = ex->poly_offset.clamp, q->bias[2] = ex->poly_offset.factor;
q->vb = mo_vertex_buffers(ex);
q->ib = ib, q->ib_off = ib_off;
q->it = d->itype == NVN_INDEX_TYPE_UNSIGNED_SHORT ? VK_INDEX_TYPE_UINT16 : VK_INDEX_TYPE_UINT32;
q->count = (uint32_t)d->count, q->instances = d->instances > 0 ? (uint32_t)d->instances : 1;
q->base_vertex = d->base_vertex, q->base_instance = (uint32_t)d->base_instance;
q->cam = ex->ubo[NVN_STAGE_VS][1].addr;
MQ.n++;
MO.draws++;
for (int i = 0; i < 16; i++) // the frame's draws per position banks and changed banks (vk_mvobj_frame_masks)
if (!MF[i].n || (MF[i].mask == banks && MF[i].chg == (uint32_t)memo[mi].moving)) {
MF[i].mask = banks, MF[i].chg = (uint32_t)memo[mi].moving, MF[i].n++;
break;
}
if (t0) MO.ns_rec += kern_now_ns() - t1, MO.n_rec++;
}
int vk_mvobj_execute(VkExec *ex, uint64_t cam, VkImageView mv, VkImageView depth, VkFormat depth_fmt, uint32_t w,
uint32_t h) {
static uint64_t frames;
if (++frames % 60 == 0)
rt_log("vk_exec: per-object motion: %.2f ms a frame checking, %.2f ms listing (%.1f us a draw), %.2f ms recording",
MO.ns_check / 1e6 / (frames ? frames : 1), MO.ns_rec / 1e6 / (frames ? frames : 1),
MO.n_rec ? MO.ns_rec / 1e3 / MO.n_rec : 0.0, MO.ns_emit / 1e6 / (frames ? frames : 1));
if (frames % 600 == 0) {
char l[1400];
int o = 0;
for (int i = 0; i < 32 && MM[i].n && o < 1300; i++)
o += snprintf(l + o, sizeof l - o, " %#x:%llu (u%d s%d t%d)", MM[i].mask, (unsigned long long)MM[i].n, MM[i].ubo,
MM[i].ssbo, MM[i].tex);
if (o) rt_log("vk_exec: per-object motion position banks (bit n: cn) and draws:%s", l);
o = 0;
for (int i = 0; i < 14 && o < 1300; i++)
if (MO.lk[i][0] + MO.lk[i][1] + MO.lk[i][2] + MO.lk[i][3])
o += snprintf(l + o, sizeof l - o, " c%d %llu/%llu/%llu/%llu/%llu", i + 3, (unsigned long long)MO.lk[i][0],
(unsigned long long)MO.lk[i][1], (unsigned long long)MO.lk[i][2],
(unsigned long long)MO.lk[i][3], (unsigned long long)MO.lk[i][4]);
if (o) rt_log("vk_exec: per-object motion last-frame lookups (none/self/side/nearer/refused):%s", l);
}
if (frames % 60 == 0 && MO.ran)
rt_log("vk_exec: per-object motion recorded a frame: %.0f draws, %.0f pipeline binds, %.0f dynamic states, %.0f "
"pushes of %.1f bindings, %.0f set binds, %.0f vertex buffer binds; sets written %.0f, reused %.0f",
(double)MO.e_draws / MO.ran, (double)MO.e_pipe / MO.ran, (double)MO.e_dyn / MO.ran, (double)MO.e_push / MO.ran,
MO.e_push ? (double)MO.e_pushw / MO.e_push : 0.0, (double)MO.e_sets / MO.ran, (double)MO.e_vb / MO.ran,
(double)MO.e_setw / MO.ran, (double)MO.e_setreuse / MO.ran);
if (frames % 60 == 0)
rt_log("vk_exec: per-object motion checks: filter %.2f ms (%.0f a frame), data %.2f ms (%.0f), variant and "
"pipeline %.2f ms (%.0f); %.0f objects looked up; still draws %.2f ms", MO.ns_part[0] / 1e6 / frames,
(double)MO.n_part[0] / frames,
MO.ns_part[1] / 1e6 / frames, (double)MO.n_part[1] / frames, MO.ns_part[2] / 1e6 / frames,
(double)MO.n_part[2] / frames, (double)MO.misses / frames, MO.ns_still / 1e6 / frames);
if (frames % 60 == 0 && MO.ncyc)
rt_log("vk_exec: per-object motion lookups: %llu cycles finding last frame's, %llu the copies, %llu comparing, "
"a lookup; %llu bytes a lookup", (unsigned long long)(MO.cyc[0] / MO.ncyc),
(unsigned long long)(MO.cyc[1] / MO.ncyc), (unsigned long long)(MO.cyc[2] / MO.ncyc),
(unsigned long long)(MO.big / MO.ncyc));
if (frames % 60 == 0)
rt_log("vk_exec: per-object motion: %llu draws read place or bones, %llu of them still, %llu drawn again, %llu without last frame's data, %llu "
"waiting for a shader or pipeline; %llu frames ran, %llu dropped (no data: camera %llu, bones %llu, place %llu, "
"bones copy %llu, place copy %llu; uniform alignment %llu; variant without its output %llu, with storage %llu, "
"no fragment shader %llu; another camera %llu)",
(unsigned long long)MO.cand, (unsigned long long)MO.still, (unsigned long long)MO.draws, (unsigned long long)MO.nodata,
(unsigned long long)MO.building,
(unsigned long long)MO.ran, (unsigned long long)MO.dropped, (unsigned long long)MO.why[0],
(unsigned long long)MO.why[1], (unsigned long long)MO.why[2], (unsigned long long)MO.why[3],
(unsigned long long)MO.why[4], (unsigned long long)LIM.ubo_align, (unsigned long long)MO.no_out,
(unsigned long long)MO.storage, (unsigned long long)MO.no_fs, (unsigned long long)MO.other_cam);
if (!MQ.n) return 0;
static int exec = -1;
if (exec < 0) exec = rt_knob("MVOBJ_EXEC", "1"); // TOTK_MVOBJ_EXEC=0: listed, never recorded (test)
if (!exec) {
mo_drop();
return 0;
}
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (MQ.frame != frame || MQ.fence != ex->fence || MQ.depth_fmt != depth_fmt) {
mo_drop();
return 0;
}
int fo = 0;
for (int i = 0; i < 16 && MF[i].n && fo < (int)sizeof fm_last - 40; i++)
fo += snprintf(fm_last + fo, sizeof fm_last - fo, " %#x/%#x:%u", MF[i].mask, MF[i].chg, MF[i].n);
if (!fo) fm_last[0] = 0;
// only draws with the scene camera block (vk_dlaa.c mv_pass's): last frame's camera they get is that block's, and
// a draw of another view would move by the difference between the two views
int kept = 0;
for (int i = 0; i < MQ.n; i++)
if (MQ.d[i].cam == cam) MQ.d[kept++] = MQ.d[i];
else MO.other_cam++;
MQ.n = kept;
if (!MQ.n) {
mo_drop();
return 0;
}
uint64_t t0 = kern_now_ns();
VkCommandBuffer cb = ex->cb;
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = mv,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
VkRenderingAttachmentInfo da = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO, .imageView = depth,
.imageLayout = VK_IMAGE_LAYOUT_GENERAL, .loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
VkRenderingInfo ri = {.sType = VK_STRUCTURE_TYPE_RENDERING_INFO, .renderArea = {{0, 0}, {w, h}}, .layerCount = 1,
.colorAttachmentCount = 1, .pColorAttachments = &ca, .pDepthAttachment = &da};
vkCmdBeginRendering(cb, &ri); // through the recorder (vk_rec.h), in order with the draws and the end
mo_emit(cb);
vkCmdEndRendering(cb);
// the draws left their pipeline, descriptors, buffers and dynamic state bound: the next game draw sets everything
vk_exec_draw_new_cb();
ex->dirty = VK_EXEC_DIRTY_ALL;
ex->recorded += MQ.n;
MQ.n = MQ.nst = MQ.nvb = 0, MQ.ndesc = 0, MQ.frame = 0;
MO.ran++;
MO.ns_emit += kern_now_ns() - t0;
return 1;
}
static void bind_index(VkExec *ex, const void *data, uint64_t bytes, VkIndexType type) {
VkBuffer b;
VkDeviceSize o;
@@ -2266,6 +3065,7 @@ static void op_draw_idx(VkExec *ex, const p_draw_idx *d) {
vkCmdDrawIndexed(ex->cb, (uint32_t)d->count, d->instances > 0 ? (uint32_t)d->instances : 1, 0, d->base_vertex,
(uint32_t)d->base_instance);
ex->recorded++;
if (d->itype != NVN_INDEX_TYPE_UNSIGNED_BYTE) mvobj_draw(ex, d, sp.buffer, sp.offset);
}
// DrawElementsIndirect: the arguments are read by the GPU from the pool buffer (they are often written by an earlier
@@ -2312,7 +3112,9 @@ static void op_dispatch(VkExec *ex, const p_dispatch *d) {
twin_dispatch = 1;
bind_resources(ex, info);
twin_dispatch = 0;
VkPassMark *m = dispatch_mark(ex, cs, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
vkCmdDispatch(ex->cb, (uint32_t)d->x, (uint32_t)d->y, (uint32_t)d->z);
vk_pass_mark_end(ex->cb, m);
ex->recorded++;
if (vk_dump_armed) vk_dump_dispatch(ex, cs, p, info, d);
}
@@ -2347,7 +3149,9 @@ static void op_dispatch_ind(VkExec *ex, const p_dispatch_ind *d) {
vk_exec_barrier(ex);
B.c0_valid[2] = 0; // the range no longer holds B.c0[2]
}
VkPassMark *m = dispatch_mark(ex, cs, 0, 0, 0);
vkCmdDispatchIndirect(ex->cb, ip.buffer, ip.offset);
vk_pass_mark_end(ex->cb, m);
ex->recorded++;
vk_mem_count(ip.cls, VK_MUSE_IND, sizeof(VkDispatchIndirectCommand));
VK_MEM_BOUND(ip);
@@ -2469,3 +3273,6 @@ void vk_exec_op_draw(VkExec *ex, uint32_t op, const void *payload) {
default: rt_fatal("vk: op %s is not a draw op", nvn_op_name(op));
}
}
// The last executed list's draws as " <position banks>/<changed banks>:<draws>" (bit n: cn), for TOTK_MV_CHECK's lines.
const char *vk_mvobj_frame_masks(void) { return fm_last; }
+107 -2
View File
@@ -3,7 +3,8 @@
// the completion thread; a guest-memory word a callback writes is visible once fence_wait returns; finish drains every
// submission; a present of a cleared vk_texture.c image (layout GENERAL) reads back exactly through the swapchain
// capture. Clear part: a hand-built op stream (SET_RT to an RGBA8 and a D32 texture, whole-target clears,
// then scissored clears) through submit reads back exactly with read_texels.
// then scissored clears) through submit reads back exactly with read_texels. Pass marks part (test_marks): the GPU's
// buffer marks and the ring records name the last pass of a submission, and the device-lost report text says so.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -175,7 +176,7 @@ static void init_tex(tex_t *t, pool_t *pool, int64_t off, int format) {
t->d.target = NVN_TEXTURE_TARGET_2D, t->d.format = format, t->d.width = TEX_W, t->d.height = TEX_H;
t->d.depth = 1, t->d.levels = 1;
nvn_tex_layout(&t->d, &t->L);
t->pool = pool, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
t->pool = pool, t->pool_flags = (uint16_t)pool->flags, t->off = off, t->mem = pool->mem + off, t->serial = 100 + (uint64_t)off;
}
// Pixel of the RGBA8 target after the stream: the scissored second clear inside the rectangle, the first elsewhere.
@@ -386,6 +387,74 @@ static uint8_t *draw_prefix(uint8_t *q, prog_t *prog, const pool_t *pool, tex_t
return q;
}
// ---- pass marks part (vk_crumbs.c): one submission with a pass of 2 draws into the draw part's target, a dispatch and
// a pass of 3 draws into a second target. Once its fence is done, the buffer marks must name the last pass as both
// started and finished, the ring records must hold what the three were, and vk_pass_marks_format (the text
// vk_device_fault_report logs) must name it. Without VK_AMD_buffer_marker or with TOTK_PASS_CRUMBS=0 it is skipped:
// NVIDIA's checkpoints can only be read once the device is lost.
static char marks_text[2048];
static int marks_skipped;
static int test_marks(const GfxBackend *b, pool_t *pool, tex_t *color, prog_t *gfx, prog_t *cs) {
uint32_t s0, f0;
if (!vk_pass_marks_read(&s0, &f0)) {
marks_skipped = 1;
return 0;
}
static tex_t second;
init_tex(&second, pool, 0x40000, NVN_FORMAT_RGBA8);
CHECK(0x40000 + second.L.size <= 0x80000, "pass-marks target exceeds its pool slot");
static _Alignas(8) uint8_t stream[8192];
uint8_t *q = draw_prefix(stream, gfx, pool, color);
const p_draw d = {NVN_DRAW_PRIMITIVE_TRIANGLES, 0, 3, 0, 0};
for (int i = 0; i < 2; i++) q = put(q, OP_DRAW, &d, sizeof d);
const p_bind_program bc = {cs, 1 << 5};
q = put(q, OP_BIND_PROGRAM, &bc, sizeof bc);
const p_dispatch dp = {2, 1, 1, 0};
q = put(q, OP_DISPATCH, &dp, sizeof dp);
const p_bind_program bg = {gfx, 3};
q = put(q, OP_BIND_PROGRAM, &bg, sizeof bg);
p_set_rt rt;
memset(&rt, 0, sizeof rt);
rt.n = 1, rt.color[0] = &second;
q = put(q, OP_SET_RT, &rt, sizeof rt);
for (int i = 0; i < 3; i++) q = put(q, OP_DRAW, &d, sizeof d);
q = put(q, OP_END, NULL, 0);
CHECK(q <= stream + sizeof stream, "pass-marks op stream overflow");
uint64_t handle = (uint64_t)(uintptr_t)stream;
uint64_t f = b->submit(&handle, 1);
CHECK(b->fence_wait(f, 20000000000ull), "fence_wait of the pass-marks submission timed out");
uint32_t started = 0, finished = 0;
vk_pass_marks_read(&started, &finished);
CHECK(started && started == finished, "after the fence the GPU last started mark %u and finished mark %u (before "
"the submission: %u and %u)", started, finished, s0, f0);
const VkPassMark *p1 = vk_pass_mark_record(finished), *dm = vk_pass_mark_record(finished - 1),
*p0 = vk_pass_mark_record(finished - 2);
CHECK(p1 && p1->kind == VK_MARK_PASS && p1->fence == f && p1->step == 1,
"the last finished mark %u is not pass 1 of submission %llu", finished, (unsigned long long)f);
CHECK(p1->w == TEX_W && p1->h == TEX_H && p1->d == 1 && p1->ncolor == 1 &&
p1->color[0] == VK_FORMAT_R8G8B8A8_UNORM && p1->depth == VK_FORMAT_UNDEFINED,
"pass 1 has %ux%u, %u layers, %u colour targets, format %d, depth %d", p1->w, p1->h, p1->d, p1->ncolor,
(int)p1->color[0], (int)p1->depth);
CHECK(p1->draws == 3 && p1->first[0] && p1->first[1] && p1->first[0] == p1->last[0] && p1->first[1] == p1->last[1],
"pass 1 has %u draws (3 expected) and shaders %p %p ... %p %p", p1->draws, (const void *)p1->first[0],
(const void *)p1->first[1], (const void *)p1->last[0], (const void *)p1->last[1]);
CHECK(dm && dm->kind == VK_MARK_DISPATCH && dm->fence == f && dm->step == 0 && dm->w == 2 && dm->h == 1 &&
dm->d == 1 && dm->first[0],
"the mark before pass 1 is not dispatch 0 of 2x1x1 groups of submission %llu", (unsigned long long)f);
CHECK(p0 && p0->kind == VK_MARK_PASS && p0->fence == f && p0->step == 0 && p0->draws == 2 &&
p0->first[0] == p1->first[0],
"the mark before the dispatch is not pass 0 of submission %llu with 2 draws", (unsigned long long)f);
vk_pass_marks_format("VK_AMD_buffer_marker", started, finished, marks_text, sizeof marks_text);
char want[128];
snprintf(want, sizeof want, "last finished %u: pass 1 of submission %llu: %ux%u, 1 layer, colour formats %d, ",
finished, (unsigned long long)f, TEX_W, TEX_H, (int)VK_FORMAT_R8G8B8A8_UNORM);
CHECK(strstr(marks_text, want) && strstr(marks_text, ", 3 draws; first vs "),
"the report text lacks \"%s\" or the draw count:\n%s", want, marks_text);
b->texture_release(&second);
return 0;
}
static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_tries, int *n_cs) {
char root[PLAT_PATH_MAX * 2], path[PLAT_PATH_MAX * 2 + 64];
CHECK(probe_census_dir(root, sizeof root), "no build/v%d/shaders/jobs.json above the exe (run tools/shader_census.py)",
@@ -503,10 +572,12 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
// Compute: a few census compute shaders, one dispatch each, resources unbound (zero buffers and stand-in images).
int dispatched = 0;
prog_t *cs_prog = NULL;
for (int t = 0; t < 4 && t < nc; t++) {
const DrawEnt *one[1] = {&e[cs[(long long)t * nc / (nc < 4 ? nc : 4)]]};
prog_t *prog = dr_prog(root, one, 1);
CHECK(prog, "census code file of %.16s is missing", one[0]->key);
cs_prog = prog;
uint8_t *q = stream;
q = put(q, OP_BEGIN, NULL, 0);
const p_bind_program bp = {prog, 1 << 5};
@@ -530,6 +601,7 @@ static int test_draw(const GfxBackend *b, uint8_t *guest, int *n_drawn, int *n_t
dispatched++;
ind_dispatches++;
}
if (test_marks(b, &pool, &color, hit, cs_prog)) return 1;
b->texture_release(&color);
b->pool_fini(&pool);
free(pairs), free(cs), free(e), free(jobs);
@@ -845,6 +917,31 @@ static int test_mirror(const GfxBackend *b, uint8_t *guest, int *bytes) {
return 0;
}
// A pool of zero bytes (TotK makes some now and then; MacNativeGames hit one on 1.2.1) initialises as an empty pool: no
// Vulkan buffer or memory (a buffer or import of 0 bytes is invalid usage) and no pool index entry, so a lookup at its
// address still finds the pool that starts there, before and after the empty pool's Finalize.
static int test_empty_pool(const GfxBackend *b, uint8_t *guest) {
pool_t pool = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0x100000,
.game_mem = guest + 0xE00000};
pool_t empty = {.magic = NVN_POOL_MAGIC, .flags = 0x22, .mem = guest + 0xE00000, .size = 0,
.game_mem = guest + 0xE00000};
CHECK(b->pool_init(&pool), "pool_init of a CPU-visible pool for the empty-pool part");
long errors = vk_validation_errors_total();
CHECK(b->pool_init(&empty), "pool_init of a zero-byte pool");
uint64_t a = (uint64_t)(uintptr_t)pool.mem;
VkMemSpan whole, at, after;
CHECK(vk_mem_resolve(a + 16, 16, &whole) && whole.buffer, "the pool beside an empty pool is not resolved");
CHECK(vk_mem_resolve(a, 0, &at) && at.buffer == whole.buffer && at.offset == whole.offset - 16,
"a lookup at the start of a pool finds the empty pool there");
CHECK(vk_validation_errors_total() == errors, "the zero-byte pool raised %ld validation errors",
vk_validation_errors_total() - errors);
b->pool_fini(&empty);
CHECK(vk_mem_resolve(a + 16, 16, &after) && after.buffer == whole.buffer,
"the empty pool's Finalize took the pool beside it out of the index");
b->pool_fini(&pool);
return 0;
}
int probe_vkexec(void) {
vk_hdr_off(); // the present checks read 8-bit swapchain pixels
layouts_equal = 0;
@@ -866,6 +963,7 @@ int probe_vkexec(void) {
int nvnx_bytes = 0, mirror_bytes = 0;
if (!bad) bad = test_nvnx(b, guest, &nvnx_bytes);
if (!bad) bad = test_mirror(b, guest, &mirror_bytes);
if (!bad) bad = test_empty_pool(b, guest);
b->finish();
vk_pipeline_shutdown(); // the pipelines and the draw part's stand-ins go before the device
vk_exec_draw_shutdown();
@@ -910,5 +1008,12 @@ int probe_vkexec(void) {
"between submissions mirrored, CPU bytes beside a fill kept, %d bytes compared)\n", mirror_bytes);
else
printf("probe-vkexec: mirror part skipped (memory mode import)\n");
if (marks_skipped)
printf("probe-vkexec: pass marks part skipped (no VK_AMD_buffer_marker, or TOTK_PASS_CRUMBS=0)\n");
else
printf("probe-vkexec: pass marks part passed (pass, dispatch, pass in one submission: the GPU's last started and "
"finished mark is the last pass, ring records exact); the report reads:\n%s\n", marks_text);
printf("probe-vkexec: empty pool part passed (a zero-byte pool at the start of another: no Vulkan objects, lookups "
"there find the other pool before and after its Finalize)\n");
return 0;
}
+78 -3
View File
@@ -8,7 +8,8 @@
// 8-bit sRGB window texture it presents. So:
// 1. vk_hdr_shader_swap: the tonemap's fragment shader becomes shaders/hdr_tonemap.frag, which keeps values above 1
// (relative to the HUD's white) and rolls off at the peak. It is recognised by its constants and resources, not by
// a hash, so another game version or translator still finds it; its SPIR-V is replaced in memory only.
// a hash, so another game version or translator still finds it; its SPIR-V is replaced in memory only. The game's
// FSR 1 upscale after it would clamp the picture to 1 again; fsr_unclamp drops that clamp in its SPIR-V.
// 2. vk_hdr_draw / vk_hdr_present_source: a draw into a presented window texture that samples one float texture of
// its size is that copy; the present then reads the float texture instead of the window texture. Any other last
// draw into it leaves the window texture, decoded from sRGB: menus and movies stay SDR at the HUD's white.
@@ -50,7 +51,7 @@ void vk_hdr_off(void) { g_wanted = 0; }
// The three nits, read again when the settings change. The replay (every draw's support buffer) and the present
// thread read them: an update fills the snapshot not in use and then publishes it.
typedef struct { float peak, game, ui; } Nits;
typedef struct { float peak, game, ui, hue; } Nits;
static struct { unsigned seen; int cur; Nits snap[2]; PlatMutex lock; } N = {.seen = ~0u, .lock = PLAT_MUTEX_INIT};
static Nits nits(void) {
@@ -62,6 +63,7 @@ static Nits nits(void) {
n->peak = fmaxf((float)cfg_double("TOTK_HDR_PEAK_NITS"), 80.0f);
n->game = fmaxf((float)cfg_double("TOTK_HDR_GAME_NITS"), 80.0f);
n->ui = fmaxf((float)cfg_double("TOTK_HDR_UI_NITS"), 80.0f);
n->hue = fminf(fmaxf((float)cfg_double("TOTK_HDR_HUE"), 0.0f), 100.0f) / 100.0f;
__atomic_store_n(&N.cur, !N.cur, __ATOMIC_RELEASE);
__atomic_store_n(&N.seen, v, __ATOMIC_RELEASE);
}
@@ -74,7 +76,8 @@ void vk_hdr_support(float out[4]) {
Nits n = nits();
out[0] = n.game / n.ui; // the picture's white against the HUD's
out[1] = n.peak > n.game ? n.peak / n.game : 1.0f; // the peak against the picture's white
out[2] = out[3] = 0;
out[2] = n.hue; // hue correction of highlights (hdr_tonemap.frag)
out[3] = 0;
}
// ---- 1. the tonemap shader
@@ -90,8 +93,80 @@ static int name_is(const uint32_t *a, uint32_t len, const char *s) { // OpName
return (len - 2) * 4 >= n + 1 && !memcmp(a + 1, s, n + 1);
}
// The game's FSR 1 pass after the tonemap (some frames take it in place of the plain pass, at 1:1 as well as when it
// scales up; what picks it is not known yet) ends with clamp(colour, 0.0, 1.0) per channel, which cuts the HDR
// highlights back to the HUD's white for as long as the game takes it. Its own 0..1 clamps of the filter weights each
// feed a float bit trick (OpBitcast), so only the clamps whose result is stored to the shader's output change, to
// max(x, 0.0): one word shorter, so the words after it move down and the module shrinks. Recognised by FSR's two
// reciprocal constants (0x7ef07ebb, 0x7ef19fff) and its one texture fp_t_tcb_8. Returns the clamps changed, -1 for
// that shader with no output clamp found (a translator that stores the colour another way), 0 for any other shader.
enum { SPV_OP_EXT_INST_IMPORT = 11, SPV_OP_EXT_INST = 12, SPV_OP_VARIABLE = 59, SPV_OP_STORE = 62,
SPV_OP_ACCESS_CHAIN = 65, SPV_SC_OUTPUT = 3, GLSL_FMAX = 40, GLSL_FCLAMP = 43 };
static int fsr_unclamp(uint32_t *w, uint64_t *words) {
uint64_t n = *words;
uint32_t bound = w[3], glsl = 0, float_type = 0, zero = 0, one = 0;
int magic = 0, tex = 0;
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
if (!len || i + len > n) return 0;
const uint32_t *a = w + i + 1;
if (op == SPV_OP_EXT_INST_IMPORT && len >= 3 && name_is(a, len, "GLSL.std.450")) glsl = a[0];
else if (op == SPV_OP_TYPE_FLOAT && len == 3 && a[1] == 32) float_type = a[0];
else if (op == SPV_OP_CONSTANT && len == 4) {
magic |= (a[2] == 0x7ef07ebbu) | (a[2] == 0x7ef19fffu) << 1;
if (a[0] == float_type && float_type && a[2] == 0) zero = a[1];
if (a[0] == float_type && float_type && a[2] == 0x3f800000u) one = a[1];
} else if (op == SPV_OP_NAME && len >= 3 && name_is(a, len, "fp_t_tcb_8")) tex = 1;
i += len;
}
if (magic != 3 || !tex) return 0;
if (!glsl || !zero || !one || !bound) return -1;
uint8_t *out = calloc(bound, 1); // id -> 1: a pointer into an Output variable, 2: a value stored through one
if (!out) return 0;
for (uint64_t i = 5; i < n; i += w[i] >> 16) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1;
if (op == SPV_OP_VARIABLE && len >= 4 && a[2] == SPV_SC_OUTPUT && a[1] < bound) out[a[1]] = 1;
else if (op == SPV_OP_ACCESS_CHAIN && len >= 4 && a[2] < bound && out[a[2]] == 1 && a[1] < bound) out[a[1]] = 1;
else if (op == SPV_OP_STORE && len >= 3 && a[0] < bound && out[a[0]] == 1 && a[1] < bound) out[a[1]] = 2;
}
int changed = 0;
uint64_t k = 5; // the write position: never past the read position, as instructions only shrink
for (uint64_t i = 5; i < n;) {
uint32_t op = w[i] & 0xffff, len = w[i] >> 16;
const uint32_t *a = w + i + 1; // result type, result, set, instruction, x, min, max
if (op == SPV_OP_EXT_INST && len == 8 && a[2] == glsl && a[3] == GLSL_FCLAMP && a[1] < bound && out[a[1]] == 2 &&
a[5] == zero && a[6] == one) {
uint32_t ins[7] = {7u << 16 | SPV_OP_EXT_INST, a[0], a[1], a[2], GLSL_FMAX, a[4], zero};
memmove(w + k, ins, sizeof ins);
k += 7, changed++;
} else {
memmove(w + k, w + i, len * sizeof *w);
k += len;
}
i += len;
}
free(out);
*words = k;
return changed ? changed : -1;
}
void vk_hdr_shader_swap(uint32_t **spirv, uint64_t *words) {
if (!vk_hdr_wanted() || !*spirv || *words < 5) return;
int unclamped = fsr_unclamp(*spirv, words);
if (unclamped > 0) {
static volatile int32_t fsrs;
rt_log("hdr: the game's FSR pass keeps values above 1 (%d output clamps; %d so far)", unclamped,
plat_inc32(&fsrs));
return;
}
if (unclamped < 0) {
static volatile int32_t missed;
if (plat_inc32(&missed) == 1)
rt_log("hdr: the game's FSR pass was found but no clamp of its output; it still clips HDR highlights");
return;
}
const uint32_t *w = *spirv;
uint64_t n = *words;
// the game's constants: the BT.601 red weight, -1/ln 2 (e^-x as exp2), 2/3 (the grade's red weight), the bloom clamp
+28 -2
View File
@@ -20,6 +20,7 @@
#include "nvn_api.h"
#include "nvn_pool_index.h"
#include "vk.h"
#include "vk_exec.h"
#include "platform/platform.h"
// Timestamps for the tracking cost, in ns; qpc_ms converts.
@@ -245,6 +246,11 @@ int vk_pool_init(pool_t *p) {
// virtual pools that map it. Either gets device-local memory: 1.4.3's CPU never writes its 0x18c pool's storage
// (unchanged over 3600 frames of the intro), so the guest storage and the GPU copy need no sync.
if ((p->flags & NVN_MEMORY_POOL_FLAGS_GPU_NO_ACCESS) && !(p->flags & NVN_MEMORY_POOL_FLAGS_PHYSICAL)) return 1;
// A pool of zero bytes (TotK makes some now and then) holds no address the GPU can use: it gets no record. A record
// would import 0 bytes (invalid usage; the driver refuses the memory) and put an empty range into the index at the
// pool's address, where a lookup of the pool that starts there found the empty one first. vk_pool_fini finds no
// record for it and does nothing.
if (!p->size) return 1;
PoolRec *r = calloc(1, sizeof *r);
if (!r) rt_fatal("vk: out of memory for a pool record");
r->key = p;
@@ -1195,6 +1201,11 @@ static void mirror_mode(int discrete) {
sh && *sh ? " (TOTK_SHADOW)" : "");
rt_log("mem: mode %s (%s): %s", mode == 2 ? "verify" : mode ? "mirror" : "import", why,
mode ? "pools used as buffers get a device-local mirror" : "every CPU-visible pool is imported host memory");
// Import is the integrated-GPU mode. On a discrete card every draw reads the pools over PCIe (the comment above
// this section has the measurements), which is the difference between playable and 5 fps when it is not intended.
if (!mode && discrete)
rt_log("mem: warning: import on a discrete GPU: the GPU reads the game's pools over PCIe on every draw, far "
"slower than mirror (the auto default on a discrete GPU); use memory = auto unless testing");
}
static void range_add(MRange **a, int *n, int *cap, PoolRec *r, uint64_t off, uint64_t size) {
@@ -1801,6 +1812,8 @@ static void collect_locked(VkCtx *c) {
for (int i = 0; i < ndead; i++) {
Dead *d = &dead[i];
if (d->seq > done_seq || d->tl > tl) { dead[k++] = *d; continue; }
rec_check_handle(d->kind == DEAD_VIEW ? REC_VIEW : d->kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, d->handle, 1,
d->tl, tl);
switch (d->kind) {
case DEAD_IMAGE: vkDestroyImage(c->device, (VkImage)(uintptr_t)d->handle, NULL); break;
case DEAD_IMAGE_VMA: vmaDestroyImage(c->vma, (VkImage)(uintptr_t)d->handle, d->alloc); break;
@@ -1932,8 +1945,21 @@ static void defer(int kind, uint64_t handle, VmaAllocation alloc) {
plat_once(&cmd_once, cmd_init, NULL);
plat_rmutex_lock(&cmd_lock);
if (ndead == cap_dead) grow((void **)&dead, &cap_dead, sizeof *dead);
// The open batch may record into the object: it counts as the newest user.
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl_noted};
// The open batch may record into the object: it counts as the newest user. The GPU must also be past every
// submission that names it. tl_noted covers the submitted ones; an object the replay lets go of while it records
// a submission (vk_exec_recording_fence) may be named by that one too, not yet submitted: 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. Stamped with the last submitted value, the object went as soon as the GPU reached it,
// before the submission that names it ran, and with the recorder thread (vk_rec.c) behind, before the recorder
// made the call that names it. LAS Recomp and Savage Wind Recomp stamp every deferral with the submission being
// recorded (runtime/nvn/vulkan/textures.c defer: vk_pending_seq). TOTK_DEFER_STAMP=0 brings back the old stamp.
static int stamp = -1;
if (stamp < 0) stamp = rt_knob("DEFER_STAMP", "1") != 0;
uint64_t tl = tl_noted, rf = stamp ? vk_exec_recording_fence() : 0;
if (rf > tl) tl = rf;
dead[ndead++] = (Dead){kind, handle, alloc, open_active ? open_b.seq : next_seq, tl};
rec_check_handle(kind == DEAD_VIEW ? REC_VIEW : kind == DEAD_BUFFER_VMA ? REC_BUFFER : REC_IMAGE, handle, 0, tl,
vk_sync_fence_done());
plat_rmutex_unlock(&cmd_lock);
}
+474
View File
@@ -0,0 +1,474 @@
// TOTK_MV_CHECK=<frames> (test): measures how well DLSS's motion vectors explain the picture, without screenshots or
// judging by eye. Every <frames> DLAA frames, vk_dlaa.c mv_pass copies to host memory the scene light target of that
// frame and of the frame before (before DLSS, so jittered), and the motion image twice: after the camera pass (camera
// only) and after the per-object pass (what DLSS gets). Once the GPU is done, the completion thread compares them:
// warp error: the frame before, sampled bilinearly at p + mv + d, against this frame at p, as a relative luminance
// difference |a - b| / (a + b) averaged over pixels; d = last frame's jitter minus this frame's (the vectors are
// between unjittered positions, the pictures are jittered: content at x is drawn at x + jitter). Lower is better,
// and the correct vectors give the lowest. Over every pixel: zero motion, the camera's and the final vectors, and
// the final ones with d subtracted or left out (the jitter convention). Over the object pixels (final and camera
// differ by more than a quarter pixel): the camera's, the final ones and variants of the object part (final -
// camera) scaled by k = -1, 0.5, 1.5, 2 or with one axis negated. The variant with the lowest error names the
// correction the object vectors need, if any ("best").
// flow: on up to FLOW_MAX object pixels with some texture, the offset whose 5x5 luminance patch of the frame before
// matches best, searched over a window around both the camera and the final vector and refined to a sub-pixel by
// a parabola: the measured motion. The end-point error of the camera and final vectors (+ d) against it, median in
// render pixels, and how often the final vector is the nearer.
// output (DLSS's picture, which the vectors are for): on about OUT_SAMPLES pixels picked independently of the vectors,
// the picture's own motion is measured with a wide search (12 px around the camera vector); where it differs from
// the camera's by over 1.5 px the pixel moves on its own. At each sample, DLSS's output (at the same unjittered
// place) against this frame's input, as a relative luminance difference: on the moving samples it grows where
// DLSS drags old colour along wrong vectors (ghosting, smearing), on the static ones it is the plain difference
// anti-aliasing and upscaling make. Their ratio compares runs with the per-object pass on and off (it does not
// depend on the pass's own vectors) and modes.
// One "mv_check:" line per check. Disocclusion and changing light add error to every candidate alike, so the comparison
// between candidates counts, not the absolute values. Copies and analysis cost a few ms on check frames only.
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "nvn_api.h"
#include "vk.h"
#include "vk_exec.h"
#include "vk_dlaa.h"
#define FLOW_MAX 20000
#define OBJ_MIN 0.25f // final and camera vectors differ by more than this (render pixels): an object pixel
#define OUT_SAMPLES 4000
enum { PREV, CUR, MV_CAM, MV_FIN, OUT, NBUF };
static struct {
int period; // 0 off, -1 not read yet
VkBuffer buf[NBUF];
VmaAllocation mem[NBUF];
uint8_t *map[NBUF];
uint64_t cap[NBUF];
uint32_t w, h, ow, oh; // ow, oh: DLSS's output in OUT, 0 none this check
int want_out; // the check frame's output is to be copied (vk_mvcheck_output)
char masks[400]; // the check frame's motion draws (vk_mvobj_frame_masks)
VkFormat cfmt;
uint64_t n; // camera-stage calls (DLAA frames with motion)
uint64_t prev_frame; // gfx frame the PREV colour is of, 0 none
float prev_u[2], u[2], uprev[2];
int check; // this frame is a check frame (camera stage copied MV_CAM)
int busy; // analysis pending: the buffers are not touched
uint64_t done;
} K = {.period = -1};
static int on(void) {
if (K.period < 0) {
const char *v = cfg_get("TOTK_MV_CHECK");
K.period = v && *v ? atoi(v) : 0;
if (K.period < 0) K.period = 0;
if (K.period && K.period < 4) K.period = 4;
if (K.period) rt_log("mv_check: motion vectors measured every %d frames", K.period);
}
return K.period > 0;
}
static int cbytes(VkFormat f) {
return f == VK_FORMAT_B10G11R11_UFLOAT_PACK32 ? 4 : f == VK_FORMAT_R16G16B16A16_SFLOAT ? 8 : 0;
}
// Host buffers of at least the size asked for; a larger size gets new ones and leaves the old (test only).
static int ensure(int i, uint64_t bytes) {
if (K.cap[i] >= bytes) return 1;
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = bytes,
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO,
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
VmaAllocationInfo info;
if (vmaCreateBuffer(vk_ctx()->vma, &bi, &ai, &K.buf[i], &K.mem[i], &info) != VK_SUCCESS || !info.pMappedData) {
rt_log("mv_check: host buffer of %llu bytes failed: off", (unsigned long long)bytes);
K.period = 0;
return 0;
}
K.map[i] = info.pMappedData, K.cap[i] = bytes;
return 1;
}
static void barrier(VkCommandBuffer cb) {
VkMemoryBarrier2 m = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT,
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_2_HOST_BIT,
.dstAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT | VK_ACCESS_2_MEMORY_READ_BIT |
VK_ACCESS_2_HOST_READ_BIT};
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &m};
vkCmdPipelineBarrier2(cb, &di);
}
static void copy(VkCommandBuffer cb, int i, VkImage img, uint32_t level, uint32_t layer, uint32_t w, uint32_t h) {
barrier(cb);
VkBufferImageCopy r = {.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, .imageExtent = {w, h, 1}};
vkCmdCopyImageToBuffer(cb, img, VK_IMAGE_LAYOUT_GENERAL, K.buf[i], 1, &r);
barrier(cb);
}
// ---- decoding
static float half(uint16_t v) {
uint32_t s = (uint32_t)(v >> 15) << 31, e = v >> 10 & 31, m = v & 1023, f;
if (!e) return (s ? -1.0f : 1.0f) * ldexpf((float)m, -24);
f = e == 31 ? s | 0x7f800000u | m << 13 : s | (e + 112) << 23 | m << 13;
float r;
memcpy(&r, &f, 4);
return r;
}
static float ufloat(uint32_t v, int mbits) { // unsigned 5-bit exponent mini-float
uint32_t e = v >> mbits, m = v & ((1u << mbits) - 1);
if (e == 31) return 65504.0f;
return e ? ldexpf(1.0f + (float)m / (float)(1u << mbits), (int)e - 15) : ldexpf((float)m / (float)(1u << mbits), -14);
}
static void luminance(const uint8_t *src, VkFormat f, uint32_t n, float *out) {
for (uint32_t i = 0; i < n; i++) {
float r, g, b;
if (f == VK_FORMAT_B10G11R11_UFLOAT_PACK32) {
uint32_t v;
memcpy(&v, src + 4 * (size_t)i, 4);
r = ufloat(v & 0x7ff, 6), g = ufloat(v >> 11 & 0x7ff, 6), b = ufloat(v >> 22, 5);
} else {
uint16_t c[4];
memcpy(c, src + 8 * (size_t)i, 8);
r = half(c[0]), g = half(c[1]), b = half(c[2]);
}
float y = 0.2126f * r + 0.7152f * g + 0.0722f * b;
out[i] = isfinite(y) && y > 0 ? y : 0;
}
}
// ---- analysis
typedef struct { const float *cur, *prev; uint32_t w, h; } Pic;
static int sample(const Pic *p, float x, float y, float *out) { // bilinear, pixel index coordinates
if (!(x >= 0 && y >= 0 && x <= (float)(p->w - 1) && y <= (float)(p->h - 1))) return 0;
uint32_t x0 = (uint32_t)x, y0 = (uint32_t)y, x1 = x0 + 1 < p->w ? x0 + 1 : x0, y1 = y0 + 1 < p->h ? y0 + 1 : y0;
float fx = x - (float)x0, fy = y - (float)y0;
const float *a = p->prev + (size_t)y0 * p->w, *b = p->prev + (size_t)y1 * p->w;
*out = (a[x0] * (1 - fx) + a[x1] * fx) * (1 - fy) + (b[x0] * (1 - fx) + b[x1] * fx) * fy;
return 1;
}
typedef struct { double sum; uint64_t n; } Acc;
static void acc(Acc *a, const Pic *p, uint32_t x, uint32_t y, float mx, float my) {
float b;
if (!sample(p, (float)x + mx, (float)y + my, &b)) return;
float c = p->cur[(size_t)y * p->w + x];
a->sum += fabsf(c - b) / (c + b + 1e-4f), a->n++;
}
static double mean(const Acc *a) { return a->n ? a->sum / (double)a->n : NAN; }
static float sad5(const Pic *p, uint32_t x, uint32_t y, int dx, int dy) {
float s = 0;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++) {
int px = (int)x + i + dx, py = (int)y + j + dy;
if (px < 0 || py < 0 || px >= (int)p->w || py >= (int)p->h) return INFINITY;
float c = p->cur[(size_t)(y + j) * p->w + x + i], b = p->prev[(size_t)py * p->w + px];
s += fabsf(c - b) / (c + b + 1e-4f);
}
return s;
}
static float parabola(float l, float c, float r) { // offset of the minimum of three samples at -1, 0, 1
float den = l - 2 * c + r;
if (!(den > 1e-9f) || !isfinite(l) || !isfinite(r)) return 0;
float o = 0.5f * (l - r) / den;
return o < -0.5f ? -0.5f : o > 0.5f ? 0.5f : o; // a flat curve says nothing past the neighbours
}
static int by_float(const void *a, const void *b) {
float x = *(const float *)a, y = *(const float *)b;
return x < y ? -1 : x > y;
}
// The object pixels' measured motion (sub-pixel), searched over the box around both vectors with a margin of 3;
// 0 when the patch is flat or the match is not clearly better than the box's average.
static int flow(const Pic *p, uint32_t x, uint32_t y, float ax, float ay, float bx, float by, float out[2]) {
float mu = 0, var = 0;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++) mu += p->cur[(size_t)(y + j) * p->w + x + i];
mu /= 25;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++) {
float d = p->cur[(size_t)(y + j) * p->w + x + i] - mu;
var += d * d;
}
if (mu <= 0 || sqrtf(var / 25) < 0.05f * mu) return 0;
int x0 = (int)floorf(fminf(ax, bx)) - 3, x1 = (int)ceilf(fmaxf(ax, bx)) + 3;
int y0 = (int)floorf(fminf(ay, by)) - 3, y1 = (int)ceilf(fmaxf(ay, by)) + 3;
if (x1 - x0 > 40 || y1 - y0 > 40) return 0;
float best = INFINITY, total = 0;
int bdx = 0, bdy = 0, n = 0;
for (int dy = y0; dy <= y1; dy++)
for (int dx = x0; dx <= x1; dx++) {
float s = sad5(p, x, y, dx, dy);
if (!isfinite(s)) continue;
total += s, n++;
if (s < best) best = s, bdx = dx, bdy = dy;
}
if (!n || !isfinite(best) || best > 0.5f * total / (float)n) return 0;
out[0] = (float)bdx + parabola(sad5(p, x, y, bdx - 1, bdy), best, sad5(p, x, y, bdx + 1, bdy));
out[1] = (float)bdy + parabola(sad5(p, x, y, bdx, bdy - 1), best, sad5(p, x, y, bdx, bdy + 1));
return 1;
}
// Totals over every check (logged every TOTAL_EVERY checks): end-point error histograms in HBIN px bins of the object
// samples (camera, final) and of the static samples (the measurement's own noise floor), and the object warp errors.
#define HBIN 0.05f
#define HN 640
#define TOTAL_EVERY 20
#define KN 120 // histogram of per-sample scale ratios, -2 to 4 in steps of 0.05
enum { H_CAM, H_FIN, H_STATIC, NH };
static struct { uint64_t h[NH][HN], n[NH], checks, wrong, kh[KN], kn; double obj[2], objn, knum, kden, out[2], outn[2]; uint64_t cal[3]; } T; // cal: checks where k=0.5, k=1 fit the static pixels best; checks
static void hist_add(int k, float e) {
int b = e >= 0 && e < HBIN * HN ? (int)(e / HBIN) : HN - 1; // NaN and huge errors in the last bin
T.h[k][b < HN ? b : HN - 1]++, T.n[k]++;
}
static float hist_q(int k, double q) { // quantile q of histogram k, bin centre
uint64_t want = (uint64_t)(q * (double)T.n[k]), s = 0;
for (int b = 0; b < HN; b++)
if ((s += T.h[k][b]) > want) return ((float)b + 0.5f) * HBIN;
return NAN;
}
static float kmedian(void) {
uint64_t s = 0;
for (int b = 0; b < KN; b++)
if ((s += T.kh[b]) * 2 > T.kn) return -2 + ((float)b + 0.5f) * 0.05f;
return NAN;
}
static uint32_t mix(uint32_t x, uint32_t y) { // a fixed pseudo-random pick of pixels, spread over the picture
uint32_t v = x * 0x9E3779B1u ^ y * 0x85EBCA77u;
v ^= v >> 15, v *= 0x2C1B3C6Du, v ^= v >> 12;
return v;
}
static void analyse(void *arg) {
(void)arg;
VmaAllocator vma = vk_ctx()->vma;
for (int i = 0; i < NBUF; i++) vmaInvalidateAllocation(vma, K.mem[i], 0, VK_WHOLE_SIZE);
uint32_t w = K.w, h = K.h, n = w * h;
float *cur = malloc(sizeof(float) * n), *prev = malloc(sizeof(float) * n), *e_cam = NULL, *e_fin = NULL,
*e_st = NULL;
if (!cur || !prev) goto out;
luminance(K.map[CUR], K.cfmt, n, cur);
luminance(K.map[PREV], K.cfmt, n, prev);
Pic p = {cur, prev, w, h};
const uint16_t *A = (const uint16_t *)K.map[MV_CAM], *B = (const uint16_t *)K.map[MV_FIN];
float dx = K.uprev[0] - K.u[0], dy = K.uprev[1] - K.u[1];
enum { ZERO, CAM, FIN, FIN_NEG_D, FIN_NO_D, NALL };
enum { O_CAM, O_FIN, O_KM1, O_K05, O_K15, O_K2, O_NX, O_NY, NOBJ };
static const char *const oname[NOBJ] = {"camera", "final", "k=-1", "k=0.5", "k=1.5", "k=2", "-x", "-y"};
Acc all[NALL] = {0}, obj[NOBJ] = {0}, cal[3] = {0};
// the object pixels first, for the sampling rates: about FLOW_MAX object and FLOW_MAX / 4 static samples
uint64_t nobj = 0, huge = 0; // huge: camera vectors over 256 px or not finite
double cam_len = 0;
for (uint32_t y = 2; y + 2 < h; y++)
for (uint32_t x = 2; x + 2 < w; x++) {
size_t i = (size_t)y * w + x;
float ax = half(A[2 * i]), ay = half(A[2 * i + 1]), bx = half(B[2 * i]), by = half(B[2 * i + 1]);
if (isfinite(ax) && isfinite(ay)) cam_len += hypotf(ax, ay);
huge += !isfinite(ax) || !isfinite(ay) || hypotf(ax, ay) > 256;
nobj += fabsf(bx - ax) > OBJ_MIN || fabsf(by - ay) > OBJ_MIN;
}
uint32_t rate_obj = nobj > FLOW_MAX ? (uint32_t)(0xffffffffull * FLOW_MAX / nobj) : 0xffffffffu;
uint64_t nst = (uint64_t)n - nobj;
uint32_t rate_st = nst > FLOW_MAX / 4 ? (uint32_t)(0xffffffffull * (FLOW_MAX / 4) / nst) : 0xffffffffu;
e_cam = malloc(sizeof(float) * FLOW_MAX * 2), e_fin = malloc(sizeof(float) * FLOW_MAX * 2),
e_st = malloc(sizeof(float) * FLOW_MAX);
if (!e_cam || !e_fin || !e_st) goto out;
int nflow = 0, nearer = 0, tried = 0, nst_f = 0;
double knum = 0, kden = 0;
// object samples the final vector misses by more than 1.5 px where the camera's fits better: where and how
struct { int n; uint32_t x0, y0, x1, y1; double ox, oy, mx, my; } bad = {0, UINT32_MAX, UINT32_MAX, 0, 0, 0, 0, 0, 0};
for (uint32_t y = 2; y + 2 < h; y++)
for (uint32_t x = 2; x + 2 < w; x++) {
size_t i = (size_t)y * w + x;
float ax = half(A[2 * i]), ay = half(A[2 * i + 1]), bx = half(B[2 * i]), by = half(B[2 * i + 1]);
if (!isfinite(ax) || !isfinite(ay) || !isfinite(bx) || !isfinite(by)) continue;
acc(&all[ZERO], &p, x, y, dx, dy);
acc(&all[CAM], &p, x, y, ax + dx, ay + dy);
acc(&all[FIN], &p, x, y, bx + dx, by + dy);
acc(&all[FIN_NEG_D], &p, x, y, bx - dx, by - dy);
acc(&all[FIN_NO_D], &p, x, y, bx, by);
float ox = bx - ax, oy = by - ay, f[2];
if (fabsf(ox) <= OBJ_MIN && fabsf(oy) <= OBJ_MIN) {
// calibration: static pixels with a small camera vector (known right), at k = 0.5, 1, 1.5 of it
float al = hypotf(ax, ay);
if (al > 0.25f && al < 2) {
acc(&cal[0], &p, x, y, 0.5f * ax + dx, 0.5f * ay + dy), acc(&cal[1], &p, x, y, ax + dx, ay + dy);
acc(&cal[2], &p, x, y, 1.5f * ax + dx, 1.5f * ay + dy);
}
if (mix(x, y) > rate_st || nst_f >= FLOW_MAX) continue;
if (flow(&p, x, y, ax + dx, ay + dy, ax + dx, ay + dy, f)) e_st[nst_f++] = hypotf(ax + dx - f[0], ay + dy - f[1]);
continue;
}
acc(&obj[O_CAM], &p, x, y, ax + dx, ay + dy);
acc(&obj[O_FIN], &p, x, y, bx + dx, by + dy);
acc(&obj[O_KM1], &p, x, y, ax - ox + dx, ay - oy + dy);
acc(&obj[O_K05], &p, x, y, ax + 0.5f * ox + dx, ay + 0.5f * oy + dy);
acc(&obj[O_K15], &p, x, y, ax + 1.5f * ox + dx, ay + 1.5f * oy + dy);
acc(&obj[O_K2], &p, x, y, ax + 2 * ox + dx, ay + 2 * oy + dy);
acc(&obj[O_NX], &p, x, y, ax - ox + dx, by + dy);
acc(&obj[O_NY], &p, x, y, bx + dx, ay - oy + dy);
if (mix(x, y) > rate_obj || nflow >= 2 * FLOW_MAX) continue;
tried++;
if (!flow(&p, x, y, ax + dx, ay + dy, bx + dx, by + dy, f)) continue;
float ec = hypotf(ax + dx - f[0], ay + dy - f[1]), ef = hypotf(bx + dx - f[0], by + dy - f[1]);
e_cam[nflow] = ec, e_fin[nflow] = ef, nflow++;
nearer += ef < ec;
if (fabsf(ox) > 0.5f || fabsf(oy) > 0.5f) { // the scale of the object part the measured motion shows
float mx = f[0] - ax - dx, my = f[1] - ay - dy, oo = ox * ox + oy * oy, kr = (mx * ox + my * oy) / oo;
knum += mx * ox + my * oy, kden += oo;
int kb = (int)floorf((kr + 2) / 0.05f);
T.kh[kb < 0 ? 0 : kb >= KN ? KN - 1 : kb]++, T.kn++;
}
if (ef > 1.5f && ef > ec) {
bad.n++, bad.ox += ox, bad.oy += oy, bad.mx += f[0] - ax - dx, bad.my += f[1] - ay - dy;
if (x < bad.x0) bad.x0 = x;
if (y < bad.y0) bad.y0 = y;
if (x > bad.x1) bad.x1 = x;
if (y > bad.y1) bad.y1 = y;
}
}
int best = 0;
for (int k = 1; k < NOBJ; k++)
if (obj[k].n && (!obj[best].n || mean(&obj[k]) < mean(&obj[best]))) best = k;
char line[512];
int o = snprintf(line, sizeof line, "object pixels %.1f%% (%llu):", 100.0 * (double)nobj / (double)n,
(unsigned long long)nobj);
for (int k = 0; k < NOBJ && o < (int)sizeof line; k++) o += snprintf(line + o, sizeof line - o, " %s %.4f", oname[k], mean(&obj[k]));
if (o < (int)sizeof line) snprintf(line + o, sizeof line - o, "; best %s", nobj ? oname[best] : "-");
// DLSS's output against this frame's input, on samples moving on their own and static ones (the picture's motion)
char outl[200] = "";
if (K.ow && K.oh) {
float *ol = malloc(sizeof(float) * (size_t)K.ow * K.oh);
if (ol) {
luminance(K.map[OUT], VK_FORMAT_R16G16B16A16_SFLOAT, K.ow * K.oh, ol);
Pic op = {NULL, ol, K.ow, K.oh}; // sample() reads prev
float sx = (float)K.ow / (float)w, sy = (float)K.oh / (float)h;
uint32_t rate = (uint32_t)(0xffffffffull * OUT_SAMPLES / n);
Acc mov = {0}, sta = {0};
for (uint32_t y = 2; y + 2 < h; y++)
for (uint32_t x = 2; x + 2 < w; x++) {
if ((mix(x, y) ^ 0x5bd1e995u) * 0x2C1B3C6Du > rate) continue;
size_t i = (size_t)y * w + x;
float ax = half(A[2 * i]) + dx, ay = half(A[2 * i + 1]) + dy, f[2], ov;
if (!isfinite(ax) || !isfinite(ay) || !flow(&p, x, y, ax - 12, ay - 12, ax + 12, ay + 12, f)) continue;
if (!sample(&op, ((float)x + 0.5f - K.u[0]) * sx - 0.5f, ((float)y + 0.5f - K.u[1]) * sy - 0.5f, &ov))
continue;
float c = cur[i], e = fabsf(ov - c) / (ov + c + 1e-4f);
Acc *a = hypotf(f[0] - ax, f[1] - ay) > 1.5f ? &mov : &sta;
a->sum += e, a->n++;
}
snprintf(outl, sizeof outl, "; output against input: moving on their own %.4f (%llu), static %.4f (%llu), "
"ratio %.3f", mean(&mov), (unsigned long long)mov.n, mean(&sta), (unsigned long long)sta.n,
mean(&mov) / mean(&sta));
if (mov.n) T.out[0] += mov.sum, T.outn[0] += (double)mov.n;
if (sta.n) T.out[1] += sta.sum, T.outn[1] += (double)sta.n;
free(ol);
}
}
float mc = NAN, mf = NAN, ms = NAN;
for (int k = 0; k < nflow; k++) hist_add(H_CAM, e_cam[k]), hist_add(H_FIN, e_fin[k]);
for (int k = 0; k < nst_f; k++) hist_add(H_STATIC, e_st[k]);
if (nflow) {
qsort(e_cam, (size_t)nflow, sizeof *e_cam, by_float), qsort(e_fin, (size_t)nflow, sizeof *e_fin, by_float);
mc = e_cam[nflow / 2], mf = e_fin[nflow / 2];
}
if (nst_f) qsort(e_st, (size_t)nst_f, sizeof *e_st, by_float), ms = e_st[nst_f / 2];
char badl[200] = "";
if (bad.n)
snprintf(badl, sizeof badl, "; %d missed (over 1.5 px, camera nearer) in x %u-%u y %u-%u, object part (%.2f %.2f), "
"measured (%.2f %.2f)", bad.n, bad.x0, bad.x1, bad.y0, bad.y1, bad.ox / bad.n, bad.oy / bad.n,
bad.mx / bad.n, bad.my / bad.n);
rt_log("mv_check: #%llu %ux%u, jitter (%.2f %.2f) after (%.2f %.2f), camera motion %.2f px; warp error, every pixel: "
"zero %.4f camera %.4f final %.4f (jitter subtracted %.4f, left out %.4f); camera vectors over 256 px %llu; %s; "
"flow on %d of %d object "
"samples: median error camera %.2f px, final %.2f px, final nearer %.0f%%; static %.2f px (%d); scale %.2f; calibration, static pixels under 2 px of camera motion (%llu): k=0.5 %.4f k=1 %.4f "
"k=1.5 %.4f%s%s; motion draws (position banks/changed banks:draws)%s",
(unsigned long long)++K.done, w, h, K.u[0], K.u[1], K.uprev[0], K.uprev[1], cam_len / (double)n,
mean(&all[ZERO]), mean(&all[CAM]), mean(&all[FIN]), mean(&all[FIN_NEG_D]), mean(&all[FIN_NO_D]), (unsigned long long)huge, line,
nflow, tried, mc, mf, nflow ? 100.0 * nearer / nflow : 0.0, ms, nst_f, kden > 0 ? knum / kden : NAN, (unsigned long long)cal[1].n, mean(&cal[0]), mean(&cal[1]), mean(&cal[2]), badl, outl, K.masks);
if (cal[1].n > 1000) T.cal[0] += mean(&cal[0]) < mean(&cal[1]) && mean(&cal[0]) < mean(&cal[2]), T.cal[1] += mean(&cal[1]) <= mean(&cal[0]) && mean(&cal[1]) <= mean(&cal[2]), T.cal[2]++;
T.knum += knum, T.kden += kden;
T.checks++;
if (obj[O_FIN].n) {
T.obj[0] += obj[O_CAM].sum, T.obj[1] += obj[O_FIN].sum, T.objn += (double)obj[O_FIN].n;
T.wrong += nobj * 200 > n && best != O_FIN; // over 0.5 % of the picture and the vectors are not the best fit
}
if (T.checks % TOTAL_EVERY == 0)
rt_log("mv_check total: %llu checks, %llu with over 0.5%% object pixels fit better by a variant; object warp error "
"camera %.4f final %.4f; end-point error (median / 90%% / 99%%) object samples camera %.2f / %.2f / %.2f "
"px, final %.2f / %.2f / %.2f px (%llu); static samples %.2f / %.2f / %.2f px (%llu); object scale %.3f (median %.2f of %llu); calibration: k=0.5 best in %llu, k=1 in %llu of %llu checks; "
"output against input: moving on their own %.4f (%.0f), static %.4f (%.0f), ratio %.3f",
(unsigned long long)T.checks, (unsigned long long)T.wrong, T.objn ? T.obj[0] / T.objn : NAN,
T.objn ? T.obj[1] / T.objn : NAN, hist_q(H_CAM, 0.5), hist_q(H_CAM, 0.9), hist_q(H_CAM, 0.99),
hist_q(H_FIN, 0.5), hist_q(H_FIN, 0.9), hist_q(H_FIN, 0.99), (unsigned long long)T.n[H_FIN],
hist_q(H_STATIC, 0.5), hist_q(H_STATIC, 0.9), hist_q(H_STATIC, 0.99), (unsigned long long)T.n[H_STATIC],
T.kden > 0 ? T.knum / T.kden : NAN, kmedian(), (unsigned long long)T.kn, (unsigned long long)T.cal[0], (unsigned long long)T.cal[1],
(unsigned long long)T.cal[2], T.outn[0] ? T.out[0] / T.outn[0] : NAN, T.outn[0],
T.outn[1] ? T.out[1] / T.outn[1] : NAN, T.outn[1],
T.outn[0] && T.outn[1] ? (T.out[0] / T.outn[0]) / (T.out[1] / T.outn[1]) : NAN);
out:
free(cur), free(prev), free(e_cam), free(e_fin), free(e_st);
__atomic_store_n(&K.busy, 0, __ATOMIC_RELEASE);
}
// ---- capture (vk_dlaa.c mv_pass)
void vk_mvcheck_camera(VkCommandBuffer cb, VkImage mv, uint32_t w, uint32_t h, float jx, float jy) {
K.check = 0;
if (!on()) return;
K.n++;
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
if (K.n % (uint64_t)K.period || __atomic_load_n(&K.busy, __ATOMIC_ACQUIRE) || K.prev_frame + 1 != frame ||
K.w != w || K.h != h)
return;
if (!ensure(MV_CAM, (uint64_t)w * h * 4) || !ensure(MV_FIN, (uint64_t)w * h * 4)) return;
copy(cb, MV_CAM, mv, 0, 0, w, h);
K.check = 1, K.u[0] = jx, K.u[1] = jy, K.uprev[0] = K.prev_u[0], K.uprev[1] = K.prev_u[1];
}
void vk_mvcheck_final(VkCommandBuffer cb, uint64_t fence, VkImage mv, VkImage cimg, VkFormat cfmt, uint32_t level,
uint32_t layer, uint32_t w, uint32_t h, float jx, float jy) {
if (!on()) return;
uint64_t frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
int b = cbytes(cfmt);
int want_prev = (K.n + 1) % (uint64_t)K.period == 0 && !__atomic_load_n(&K.busy, __ATOMIC_ACQUIRE);
if (!b) return (void)(K.check = 0, K.prev_frame = 0);
if (K.check) {
if (!ensure(CUR, (uint64_t)w * h * (uint64_t)b)) return;
copy(cb, MV_FIN, mv, 0, 0, w, h);
copy(cb, CUR, cimg, level, layer, w, h);
K.check = 0, K.prev_frame = 0, K.ow = K.oh = 0, K.want_out = 1;
snprintf(K.masks, sizeof K.masks, "%s", vk_mvobj_frame_masks());
__atomic_store_n(&K.busy, 1, __ATOMIC_RELEASE);
vk_completion_add(fence, analyse, NULL);
return;
}
if (!want_prev) return;
if (!ensure(PREV, (uint64_t)w * h * (uint64_t)b)) return;
copy(cb, PREV, cimg, level, layer, w, h);
K.prev_frame = frame, K.prev_u[0] = jx, K.prev_u[1] = jy, K.w = w, K.h = h, K.cfmt = cfmt;
}
// After DLSS ran on a check frame (vk_dlaa.c evaluate): its output (RGBA16F, ow x oh) for the analysis, which runs once
// the submission is done.
void vk_mvcheck_output(VkCommandBuffer cb, VkImage out, uint32_t ow, uint32_t oh) {
if (!K.want_out) return;
K.want_out = 0;
if (!ensure(OUT, (uint64_t)ow * oh * 8)) return;
copy(cb, OUT, out, 0, 0, ow, oh);
K.ow = ow, K.oh = oh;
}
+39
View File
@@ -0,0 +1,39 @@
// Generated by tools/make_mvobj_fs.py: the per-object motion fragment shader (vk_mvobj.c). Do not edit.
#define MVOBJ_FS_LOCATION_WORD 24 // the input Location literal, patched to the variant's
static const uint32_t mvobj_fs_spv[] = {
0x07230203, 0x00010000, 0x00000000, 0x00000034, 0x00000000, 0x00020011, 0x00000001, 0x0003000e,
0x00000000, 0x00000001, 0x0008000f, 0x00000004, 0x00000001, 0x6e69616d, 0x00000000, 0x0000000e,
0x0000000f, 0x00000010, 0x00030010, 0x00000001, 0x00000007, 0x00040047, 0x0000000e, 0x0000001e,
0x00000000, 0x00040047, 0x0000000f, 0x0000000b, 0x0000000f, 0x00040047, 0x00000010, 0x0000001e,
0x00000000, 0x00030047, 0x0000000b, 0x00000002, 0x00050048, 0x0000000b, 0x00000000, 0x00000023,
0x00000000, 0x00040047, 0x00000011, 0x00000022, 0x00000000, 0x00040047, 0x00000011, 0x00000021,
0x00000001, 0x00020013, 0x00000002, 0x00030021, 0x00000003, 0x00000002, 0x00030016, 0x00000004,
0x00000020, 0x00040017, 0x00000005, 0x00000004, 0x00000002, 0x00040017, 0x00000006, 0x00000004,
0x00000004, 0x00040015, 0x00000007, 0x00000020, 0x00000000, 0x00020014, 0x00000008, 0x00040020,
0x00000009, 0x00000001, 0x00000006, 0x00040020, 0x0000000a, 0x00000003, 0x00000006, 0x0003001e,
0x0000000b, 0x00000006, 0x00040020, 0x0000000c, 0x00000002, 0x0000000b, 0x00040020, 0x00000017,
0x00000002, 0x00000006, 0x0004002b, 0x00000004, 0x00000012, 0x3f000000, 0x0004002b, 0x00000004,
0x00000013, 0x00000000, 0x0004002b, 0x00000004, 0x00000014, 0x358637bd, 0x0004002b, 0x00000007,
0x00000015, 0x00000000, 0x0007002c, 0x00000006, 0x00000016, 0x00000013, 0x00000013, 0x00000013,
0x00000013, 0x0004003b, 0x00000009, 0x0000000e, 0x00000001, 0x0004003b, 0x00000009, 0x0000000f,
0x00000001, 0x0004003b, 0x0000000a, 0x00000010, 0x00000003, 0x0004003b, 0x0000000c, 0x00000011,
0x00000002, 0x00050036, 0x00000002, 0x00000001, 0x00000000, 0x00000003, 0x000200f8, 0x00000018,
0x00050041, 0x00000017, 0x00000019, 0x00000011, 0x00000015, 0x0004003d, 0x00000006, 0x0000001a,
0x00000019, 0x0004003d, 0x00000006, 0x0000001b, 0x0000000e, 0x0004003d, 0x00000006, 0x0000001c,
0x0000000f, 0x0007004f, 0x00000005, 0x0000001d, 0x0000001c, 0x0000001c, 0x00000000, 0x00000001,
0x0007004f, 0x00000005, 0x0000001e, 0x0000001a, 0x0000001a, 0x00000002, 0x00000003, 0x00050083,
0x00000005, 0x0000001f, 0x0000001d, 0x0000001e, 0x00050051, 0x00000004, 0x00000020, 0x0000001b,
0x00000000, 0x00050051, 0x00000004, 0x00000021, 0x0000001b, 0x00000001, 0x00050051, 0x00000004,
0x00000022, 0x0000001b, 0x00000003, 0x00050088, 0x00000004, 0x00000023, 0x00000020, 0x00000022,
0x00050088, 0x00000004, 0x00000024, 0x00000021, 0x00000022, 0x00050051, 0x00000004, 0x00000025,
0x0000001a, 0x00000000, 0x00050051, 0x00000004, 0x00000026, 0x0000001a, 0x00000001, 0x00050085,
0x00000004, 0x00000027, 0x00000023, 0x00000012, 0x00050081, 0x00000004, 0x00000028, 0x00000027,
0x00000012, 0x00050085, 0x00000004, 0x00000029, 0x00000028, 0x00000025, 0x00050085, 0x00000004,
0x0000002a, 0x00000024, 0x00000012, 0x00050083, 0x00000004, 0x0000002b, 0x00000012, 0x0000002a,
0x00050085, 0x00000004, 0x0000002c, 0x0000002b, 0x00000026, 0x00050051, 0x00000004, 0x0000002d,
0x0000001f, 0x00000000, 0x00050051, 0x00000004, 0x0000002e, 0x0000001f, 0x00000001, 0x00050083,
0x00000004, 0x0000002f, 0x00000029, 0x0000002d, 0x00050083, 0x00000004, 0x00000030, 0x0000002c,
0x0000002e, 0x00070050, 0x00000006, 0x00000031, 0x0000002f, 0x00000030, 0x00000013, 0x00000013,
0x000500ba, 0x00000008, 0x00000032, 0x00000022, 0x00000014, 0x000600a9, 0x00000006, 0x00000033,
0x00000032, 0x00000031, 0x00000016, 0x0003003e, 0x00000010, 0x00000033, 0x000100fd, 0x00010038
};
+219
View File
@@ -0,0 +1,219 @@
// Last frame's object and bone blocks, for per-object motion vectors (TOTK_DLAA_OBJMV, on with DLAA). The banks kept
// start as c5, c7 and c14 and grow with what the vertex shader variants read (vk_mvprev_want); c7 and c5 are described
// here, the others follow the same two-copy rule.
//
// The scene's vertex shaders read an object's place from bank c7 (gsys_shape: one 3x4 matrix at the start of a
// 0x100 block) and a skinned object's bones from c5 (gsys_skeleton: a run of 3x4 matrices), of this frame only. The
// game writes both into two copies that take turns, the second right after the first: an object's block is at addr in
// one frame and at addr +- step in the next, step its size rounded up to 256 bytes (vk_mvsurvey.c measured it: every
// c7 pair 0x100 apart, c5 pairs their rounded size apart, the pair's contents the same object moved a little). The
// copy this frame does not write still holds last frame's when the frame is submitted, so the uniform snapshot
// (vk_usnap.c) also copies both neighbours of these blocks, and vk_mvprev_find picks last frame's:
// self the block was bound at the same address last frame too (an object the game did not write again): itself
// side one neighbour was bound in that bank last frame: that one
// nearer both were (the copies of adjacent objects interleave): the one with more of its first 16 words equal to
// this frame's
// none neither: a new object, no motion of its own
// The pick is checked against what was snapshotted of that block last frame (intact: the same first CHECK bytes), and
// a summary goes to the log every SUMMARY frames:
// mv_prev: c<n>: <blocks> blocks: self a %, side b %, nearer c %, none d %; intact e %; moved: still f %, under
// one unit g %, more h %
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "vk.h"
#include "vk_exec.h"
#include "vk_dlaa.h"
#define CHECK 128u
#define TABLE (1u << 14)
#define SUMMARY 600
enum { BONES = 2, PLACE = 4, CLOCK = 11, BANKS = 14 }; // NVN binding index: banks c5, c7 and c14; c3..c16
// 16 bytes, so a table of TABLE entries stays in the cache: every scene draw probes it (the bytes of TOTK_MVPREV_CHECK
// live in a table of their own, by entry index, allocated only for the check)
typedef struct {
uint64_t addr; // 0: empty
int index;
uint32_t have;
} Seen;
typedef struct { uint64_t blocks, self, side, nearer, none, checked, intact, still, small, big; } Stat;
static struct {
int on; // 1 knob on and DLAA on this frame
int knob; // -1 not read
Seen *t[2]; // this frame's bound blocks, last frame's
uint8_t (*bytes[2])[CHECK]; // TOTK_MVPREV_CHECK: the first CHECK bytes of each entry of t[k], NULL without the check
int timed; // TOTK_MVOBJ_TIMES: the summary's times
uint32_t frames;
Stat st[BANKS]; // per binding index
uint64_t last[BANKS]; // the block the last scene draw bound per binding index
uint32_t mask; // binding indices kept (vk_mvprev_want): bones, place and clock from the start
uint64_t ns_draw, ns_frame; // time in vk_mvprev_draw and vk_mvprev_frame since the last summary
} P = {.knob = -1, .mask = 1u << BONES | 1u << PLACE | 1u << CLOCK};
static uint32_t slot_of(uint64_t addr, int index) {
uint64_t h = (addr ^ (uint64_t)index * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull;
return (uint32_t)(h >> 40) & (TABLE - 1);
}
static Seen *find(Seen *t, uint64_t addr, int index, int add) {
for (uint32_t i = slot_of(addr, index), k = 0; k < TABLE; i = (i + 1) & (TABLE - 1), k++) {
if (t[i].addr == addr && t[i].index == index) return &t[i];
if (!t[i].addr) {
if (!add) return NULL;
t[i].addr = addr, t[i].index = index;
return &t[i];
}
}
return NULL;
}
int vk_mvprev_on(void) { return P.on > 0; }
void vk_mvprev_want(uint32_t index_mask) {
static int fixed = -1;
if (fixed < 0) fixed = !rt_knob("MVPREV_GROW", "1"); // TOTK_MVPREV_GROW=0: c5, c7 and c14 only (test)
if (!fixed) P.mask |= index_mask & ((1u << BANKS) - 1);
}
uint64_t vk_mvprev_step(int stage, int index, uint64_t size) {
if (P.on <= 0 || stage != 0 || index < 0 || index >= BANKS || !(P.mask >> index & 1)) return 0;
return (size + 255) & ~(uint64_t)255;
}
// Translation distance between the first matrices of two blocks (rows' fourth floats).
static float moved(const uint8_t *a, const uint8_t *b) {
const float *x = (const float *)a, *y = (const float *)b;
float dx = x[3] - y[3], dy = x[7] - y[7], dz = x[11] - y[11];
return sqrtf(dx * dx + dy * dy + dz * dz);
}
int vk_mvprev_find(int index, uint64_t addr, uint64_t size, uint64_t *prev, int *how) {
*how = MVPREV_NONE;
if (P.on <= 0 || !P.t[1]) return 0;
if (find(P.t[1], addr, index, 0)) {
*prev = addr, *how = MVPREV_SELF;
return 1;
}
uint64_t step = (size + 255) & ~(uint64_t)255, cand[2];
int n = 0;
if (find(P.t[1], addr + step, index, 0)) cand[n++] = addr + step;
if (addr >= step && find(P.t[1], addr - step, index, 0)) cand[n++] = addr - step;
if (!n) return 0;
if (n == 2) { // the copies of adjacent objects interleave: the one with more words equal to this frame's
const uint32_t *c = (const uint32_t *)vk_usnap_bytes(addr, 64), *a = (const uint32_t *)vk_usnap_bytes(cand[0], 64),
*b = (const uint32_t *)vk_usnap_bytes(cand[1], 64);
if (!c || !a || !b) return 0;
int na = 0, nb = 0;
for (int w = 0; w < 16; w++) na += c[w] == a[w], nb += c[w] == b[w];
if (nb > na) cand[0] = cand[1];
*how = MVPREV_NEARER;
} else {
*how = MVPREV_SIDE;
}
*prev = cand[0];
return 1;
}
static void mvprev_draw_(VkExec *ex);
void vk_mvprev_draw(VkExec *ex) {
if (P.on <= 0) return;
if (!P.timed) return mvprev_draw_(ex); // the clock reads were 1 % of the render thread
uint64_t t0 = kern_now_ns();
mvprev_draw_(ex);
P.ns_draw += kern_now_ns() - t0;
}
static void mvprev_draw_(VkExec *ex) {
const VkExecTarget *z = &ex->depth;
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT) return;
int check = P.bytes[0] != NULL; // TOTK_MVPREV_CHECK=1: the summary's checks (3.6 ms a frame)
for (int index = 0; index < BANKS; index++) {
if (!(P.mask >> index & 1)) continue;
const p_bind_buf *u = &ex->ubo[0][index];
if (!u->addr || !u->size || u->addr == P.last[index]) continue; // the next draws keep the block: noted already
P.last[index] = u->addr;
Seen *k = find(P.t[0], u->addr, index, 1);
if (!k || k->have || !check) continue; // else only the address is noted
int w = index;
uint32_t want = u->size < CHECK ? (uint32_t)u->size : CHECK;
const uint8_t *cur = vk_usnap_bytes(u->addr, want);
if (!cur) continue;
memcpy(P.bytes[0][k - P.t[0]], cur, want), k->have = want;
Stat *s = &P.st[w];
s->blocks++;
uint64_t prev;
int how;
if (!vk_mvprev_find(index, u->addr, u->size, &prev, &how)) {
s->none++;
continue;
}
if (how == MVPREV_SELF) s->self++;
else if (how == MVPREV_SIDE) s->side++;
else s->nearer++;
const Seen *last = find(P.t[1], prev, index, 0);
const uint8_t *now = vk_usnap_bytes(prev, want);
if (last && now && last->have >= want && want >= 48) {
s->checked++;
if (!memcmp(now, P.bytes[1][last - P.t[1]], want)) s->intact++;
float d = moved(cur, now);
if (d < 0.01f) s->still++;
else if (d < 1.0f) s->small++;
else s->big++;
}
}
}
void vk_mvprev_frame(void) {
// without DLSS (no NGX, no RTX GPU, dlaa = 0) nothing reads last frame's blocks: P.on stays 0, no tables
if (!vk_dlaa_available()) return;
if (P.knob < 0) {
P.knob = rt_knob("DLAA_OBJMV", "1");
if (P.knob) {
P.t[0] = calloc(TABLE, sizeof(Seen)), P.t[1] = calloc(TABLE, sizeof(Seen));
if (!P.t[0] || !P.t[1]) P.knob = 0;
if (P.knob && rt_knob("MVPREV_CHECK", "0")) {
P.bytes[0] = calloc(TABLE, CHECK), P.bytes[1] = calloc(TABLE, CHECK);
if (!P.bytes[0] || !P.bytes[1]) free(P.bytes[0]), free(P.bytes[1]), P.bytes[0] = P.bytes[1] = NULL;
}
P.timed = rt_knob("MVOBJ_TIMES", "0");
}
}
int on = P.knob && cfg_bool("TOTK_DLAA");
if (on != (P.on > 0)) {
P.on = on ? 1 : -1;
if (P.t[0]) memset(P.t[0], 0, TABLE * sizeof(Seen)), memset(P.t[1], 0, TABLE * sizeof(Seen));
return;
}
if (P.on <= 0) return;
uint64_t t0 = P.timed ? kern_now_ns() : 0;
Seen *t = P.t[1];
P.t[1] = P.t[0], P.t[0] = t;
uint8_t (*b)[CHECK] = P.bytes[1];
P.bytes[1] = P.bytes[0], P.bytes[0] = b;
memset(P.t[0], 0, TABLE * sizeof(Seen));
memset(P.last, 0, sizeof P.last);
if (P.timed) P.ns_frame += kern_now_ns() - t0;
if (++P.frames % SUMMARY) return;
if (P.timed) rt_log("mv_prev: %.3f ms a frame in the draws, %.3f ms at the present", P.ns_draw / 1e6 / SUMMARY,
P.ns_frame / 1e6 / SUMMARY);
P.ns_draw = P.ns_frame = 0;
for (int w = 0; w < BANKS; w++) {
Stat *s = &P.st[w];
if (!s->blocks) continue;
double b = (double)s->blocks, c = s->checked ? (double)s->checked : 1;
rt_log("mv_prev: c%d: %llu blocks: self %.1f %%, side %.1f %%, nearer %.1f %%, none %.1f %%; intact %.1f %% of "
"%llu; moved: still %.1f %%, under one unit %.1f %%, more %.1f %%",
w + 3, (unsigned long long)s->blocks, 100 * s->self / b, 100 * s->side / b, 100 * s->nearer / b,
100 * s->none / b, 100 * s->intact / c, (unsigned long long)s->checked, 100 * s->still / c,
100 * s->small / c, 100 * s->big / c);
}
memset(P.st, 0, sizeof P.st);
}
+258
View File
@@ -0,0 +1,258 @@
// Motion-vector survey (TOTK_MV_SURVEY=1, off by default): measures whether per-object motion vectors can be made from
// the uniform blocks the scene's draws bind. Per-object vectors need each object's matrices of the previous frame; the
// game keeps none (its shaders read gsys_shape, gsys_skeleton and gsys_projection of this frame only), so they would
// have to be kept from the previous frame's uniform snapshot (vk_usnap.c). That works when a block's guest address
// stands for the same object from one frame to the next, which this survey measures.
//
// For every scene draw (vertex stage, scene depth bound: D32 at least DLAA_SCENE_MIN_W wide, as vk_dlaa_draw), each user bank
// (c3 + index) with a binding is noted: address, size and its first SNAP bytes as of the submit (the snapshot's copy).
// At each frame the bank's blocks are compared with the previous frame's and the one before:
// repeat1 / repeat2 blocks whose address was bound in that bank one / two frames ago
// same / near / far of the blocks repeated from last frame: bytes unchanged / every float within 5 % or 1.0 of
// last frame's (the same object, moved a little) / some float further (another object, likely)
// The game writes its per-object blocks into two copies that take turns, so a moving object's block is at one address
// in even frames and another in odd ones. To find the rule that pairs them, a bank's blocks bound two frames ago but
// not last frame (this frame's copy) are sorted by address, and so are last frame's blocks not bound this frame (the
// other copy); with equal counts they are paired in order:
// pairs, delta the paired blocks and the commonest address differences (this frame's minus last frame's)
// psame / pnear / pfar the pairs' contents, as same / near / far above (near: the same object, moved)
// unequal frames whose two lists had different lengths (no pairing that frame)
// What the bank holds: each block's leading 3x4 matrices (rows of a rotation times a scale: three rows of the same
// length, at right angles, finite), counted over the whole block as of the submit:
// mats 0 / 1 / 2-7 / 8+ blocks by that count (one: an object's place, gsys_shape; many: bones, gsys_skeleton)
// Every SURVEY_FRAMES frames the log gets two lines per bank with the window's totals:
// mv_survey: c<n>: <draws> draws, <blocks> blocks (<per frame>), sizes <top three>; repeat1 a %, repeat2 b %;
// same c %, near d %, far e %
// mv_survey: c<n>: pairs <n> (<unequal> frames unequal), delta <top three>; psame a %, pnear b %, pfar c %;
// mats 0 x<n> 1 x<n> 2-7 x<n> 8+ x<n>, at most <m>
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "host.h"
#include "vk.h"
#include "vk_exec.h"
#include "vk_dlaa.h"
#define BANKS 18
#define SNAP 256u
#define SURVEY_FRAMES 300
#define TABLE (1u << 15) // blocks per frame, all banks; a town frame binds about 10,000 uniform ranges
typedef struct {
uint64_t addr; // 0: empty slot
uint32_t size, n;
int bank;
uint8_t bytes[SNAP];
uint32_t have; // bytes of `bytes` that were in the snapshot
uint32_t mats; // leading 3x4 matrices of the whole block, 0 when it was not in the snapshot
} Block;
typedef struct {
uint64_t draws, blocks, rep1, rep2, same, near, far, unread;
uint32_t size[4];
uint64_t size_n[4];
uint64_t pairs, psame, pnear, pfar, unequal, mats[4];
uint32_t mats_max;
int64_t delta[4];
uint64_t delta_n[4];
} Stat;
static struct {
int on;
Block *t[3]; // this frame, last frame, the one before
const Block **x, **y; // pairing: this frame's copy, last frame's
uint32_t frames;
Stat st[BANKS];
} S;
static uint32_t slot_of(uint64_t addr, int bank) {
uint64_t h = (addr ^ (uint64_t)bank * 0x9E3779B97F4A7C15ull) * 0xff51afd7ed558ccdull;
return (uint32_t)(h >> 40) & (TABLE - 1);
}
static Block *find(Block *t, uint64_t addr, int bank, int add) {
for (uint32_t i = slot_of(addr, bank), k = 0; k < TABLE; i = (i + 1) & (TABLE - 1), k++) {
if (t[i].addr == addr && t[i].bank == bank) return &t[i];
if (!t[i].addr) {
if (!add) return NULL;
t[i].addr = addr, t[i].bank = bank;
return &t[i];
}
}
return NULL; // full: the block goes uncounted
}
static void note_size(Stat *s, uint32_t size) {
for (int i = 0; i < 4; i++)
if (s->size[i] == size || !s->size_n[i]) {
s->size[i] = size, s->size_n[i]++;
return;
}
s->size_n[3]++; // a fifth size: counted with the last
}
static void note_delta(Stat *s, int64_t d) {
for (int i = 0; i < 4; i++)
if (s->delta[i] == d || !s->delta_n[i]) {
s->delta[i] = d, s->delta_n[i]++;
return;
}
s->delta_n[3]++;
}
// Leading 3x4 matrices: three rows of about the same length (a uniform scale), at right angles, all finite.
static uint32_t lead_mats(const float *f, uint32_t nf) {
uint32_t k = 0;
for (; (k + 1) * 12 <= nf && k < 4096; k++) {
const float *m = f + k * 12;
float l[3];
for (int r = 0; r < 3; r++) {
const float *v = m + r * 4;
if (!isfinite(v[0]) || !isfinite(v[1]) || !isfinite(v[2]) || !isfinite(v[3])) return k;
l[r] = sqrtf(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
if (l[r] < 1e-3f || l[r] > 1e3f) return k;
}
if (fabsf(l[1] - l[0]) > 0.02f * l[0] || fabsf(l[2] - l[0]) > 0.02f * l[0]) return k;
for (int a = 0; a < 3; a++)
for (int b = a + 1; b < 3; b++) {
const float *u = m + a * 4, *v = m + b * 4;
if (fabsf(u[0] * v[0] + u[1] * v[1] + u[2] * v[2]) > 0.02f * l[a] * l[b]) return k;
}
}
return k;
}
static int by_bank_addr(const void *a, const void *b) {
const Block *x = *(const Block *const *)a, *y = *(const Block *const *)b;
if (x->bank != y->bank) return x->bank < y->bank ? -1 : 1;
return x->addr < y->addr ? -1 : x->addr > y->addr;
}
static void survey_init(void) {
S.on = rt_knob("MV_SURVEY", "0") ? 1 : -1;
if (S.on < 0) return;
for (int i = 0; i < 3; i++) S.t[i] = calloc(TABLE, sizeof(Block));
S.x = malloc(TABLE * sizeof *S.x), S.y = malloc(TABLE * sizeof *S.y);
if (!S.t[0] || !S.t[1] || !S.t[2] || !S.x || !S.y) {
S.on = -1;
return;
}
rt_log("mv_survey: on (scene draws' uniform blocks, a summary every %d frames)", SURVEY_FRAMES);
}
void vk_mvsurvey_draw(VkExec *ex) {
if (!S.on) survey_init();
if (S.on < 0) return;
const VkExecTarget *z = &ex->depth;
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT) return;
for (int b = 0; b < BANKS; b++) {
const p_bind_buf *u = &ex->ubo[0][b];
if (!u->addr || !u->size) continue;
S.st[b].draws++;
Block *k = find(S.t[0], u->addr, b, 1);
if (!k) continue;
if (k->n++) continue;
k->size = (uint32_t)u->size;
uint32_t want = u->size < SNAP ? (uint32_t)u->size : SNAP;
VkDescriptorBufferInfo bi;
const uint8_t *src = vk_usnap_find(u->addr, want, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
k->have = src ? want : 0;
if (src) memcpy(k->bytes, src, want);
const uint8_t *all = vk_usnap_find(u->addr, u->size, 1, &bi) ? vk_usnap_map() + bi.offset : NULL;
k->mats = all ? lead_mats((const float *)all, (uint32_t)(u->size / 4)) : 0;
if (all) {
Stat *s = &S.st[b];
s->mats[k->mats == 0 ? 0 : k->mats == 1 ? 1 : k->mats < 8 ? 2 : 3]++;
if (k->mats > s->mats_max) s->mats_max = k->mats;
}
}
}
// Same object moved a little: every float within 5 % or 1.0 of last frame's.
static int compare(const Block *a, const Block *b) {
uint32_t n = a->have < b->have ? a->have : b->have;
if (!n) return -1;
if (!memcmp(a->bytes, b->bytes, n)) return 0;
const float *x = (const float *)a->bytes, *y = (const float *)b->bytes;
for (uint32_t i = 0; i < n / 4; i++) {
float d = fabsf(x[i] - y[i]);
if (isnan(d) || (d > 1.0f && d > 0.05f * fabsf(y[i]))) return 2;
}
return 1;
}
void vk_mvsurvey_frame(void) {
if (S.on <= 0) return;
for (uint32_t i = 0; i < TABLE; i++) {
const Block *k = &S.t[0][i];
if (!k->addr) continue;
Stat *s = &S.st[k->bank];
s->blocks++;
note_size(s, k->size);
const Block *p = find(S.t[1], k->addr, k->bank, 0);
if (p) {
s->rep1++;
int c = compare(k, p);
if (c == 0) s->same++;
else if (c == 1) s->near++;
else if (c == 2) s->far++;
else s->unread++;
}
if (find(S.t[2], k->addr, k->bank, 0)) s->rep2++;
}
// pairing of the two copies, bank by bank
uint32_t nx = 0, ny = 0;
for (uint32_t i = 0; i < TABLE; i++) {
const Block *k = &S.t[0][i];
if (k->addr && !find(S.t[1], k->addr, k->bank, 0) && find(S.t[2], k->addr, k->bank, 0)) S.x[nx++] = k;
k = &S.t[1][i];
if (k->addr && !find(S.t[0], k->addr, k->bank, 0)) S.y[ny++] = k;
}
qsort(S.x, nx, sizeof *S.x, by_bank_addr);
qsort(S.y, ny, sizeof *S.y, by_bank_addr);
for (uint32_t i = 0, j = 0; i < nx;) {
int b = S.x[i]->bank;
uint32_t i1 = i, j0, j1;
while (i1 < nx && S.x[i1]->bank == b) i1++;
while (j < ny && S.y[j]->bank < b) j++;
for (j0 = j1 = j; j1 < ny && S.y[j1]->bank == b;) j1++;
Stat *s = &S.st[b];
if (i1 - i != j1 - j0) s->unequal++;
else
for (uint32_t a = i, c = j0; a < i1; a++, c++) {
s->pairs++;
note_delta(s, (int64_t)(S.x[a]->addr - S.y[c]->addr));
int r = compare(S.x[a], S.y[c]);
if (r == 0) s->psame++;
else if (r == 1) s->pnear++;
else if (r == 2) s->pfar++;
}
i = i1, j = j1;
}
Block *old = S.t[2];
S.t[2] = S.t[1], S.t[1] = S.t[0], S.t[0] = old;
memset(S.t[0], 0, TABLE * sizeof(Block));
if (++S.frames % SURVEY_FRAMES) return;
for (int b = 0; b < BANKS; b++) {
Stat *s = &S.st[b];
if (!s->blocks) continue;
double bl = (double)s->blocks, r1 = s->rep1 ? (double)s->rep1 : 1;
rt_log("mv_survey: c%d: %llu draws, %llu blocks (%.0f a frame), sizes %#x x%llu %#x x%llu %#x x%llu; repeat1 %.1f %%, "
"repeat2 %.1f %%; same %.1f %%, near %.1f %%, far %.1f %%, unread %.1f %%",
b + 3, (unsigned long long)s->draws, (unsigned long long)s->blocks, bl / SURVEY_FRAMES, s->size[0],
(unsigned long long)s->size_n[0], s->size[1], (unsigned long long)s->size_n[1], s->size[2],
(unsigned long long)s->size_n[2], 100 * s->rep1 / bl, 100 * s->rep2 / bl, 100 * s->same / r1,
100 * s->near / r1, 100 * s->far / r1, 100 * s->unread / r1);
double pr = s->pairs ? (double)s->pairs : 1;
rt_log("mv_survey: c%d: pairs %llu (%llu frames unequal), delta %#llx x%llu %#llx x%llu %#llx x%llu; psame %.1f %%, "
"pnear %.1f %%, pfar %.1f %%; mats 0 x%llu 1 x%llu 2-7 x%llu 8+ x%llu, at most %u",
b + 3, (unsigned long long)s->pairs, (unsigned long long)s->unequal, (unsigned long long)s->delta[0],
(unsigned long long)s->delta_n[0], (unsigned long long)s->delta[1], (unsigned long long)s->delta_n[1],
(unsigned long long)s->delta[2], (unsigned long long)s->delta_n[2], 100 * s->psame / pr,
100 * s->pnear / pr, 100 * s->pfar / pr, (unsigned long long)s->mats[0], (unsigned long long)s->mats[1],
(unsigned long long)s->mats[2], (unsigned long long)s->mats[3], s->mats_max);
}
memset(S.st, 0, sizeof S.st);
}
+2
View File
@@ -1913,6 +1913,8 @@ static void rec_fill(KeyRec *r, const VkPipeState *s) {
// A pipeline was built for a draw: queue its record for the file. Caller holds P.lock.
static void keys_note(const VkPipeState *s) {
if (!A.on || !K.path[0]) return;
static const uint8_t none[32];
if (s->fs && !memcmp(gfx_shader_input_id(s->fs), none, 32)) return; // the runtime's own shader (gfx_shader_own)
KeyRec r;
rec_fill(&r, s);
if (!seen_add(rec_hash(&r))) return;
+98 -2
View File
@@ -26,6 +26,10 @@ static VkCommandBuffer rec_cb; // the part being recorded (set while the
static int rec_live; // producer: rec_begin started the recorder for rec_cb
static PlatThread *rec_thread;
static int rec_prio; // the submitting thread's priority, which the recorder takes
static int rec_chk; // TOTK_REC_CHECK (rec_check_handle)
static uint64_t rec_delay_ns; // TOTK_REC_DELAY_US (rec_main)
static volatile uint64_t part_h0; // put_h at rec_begin: where the packets of the part being recorded start
static volatile uint64_t part_fence; // the value of the part being recorded (rec_begin)
// Producer statistics, logged by rec_stats_log.
static struct { uint64_t packets, words, drains, fallbacks, drain_ns, full_waits, parts, parts_on; } RS_;
@@ -34,12 +38,13 @@ enum {
R_PAD, R_BeginQuery, R_BeginRendering, R_BindDescriptorSets, R_BindIndexBuffer, R_BindPipeline, R_BindVertexBuffers2,
R_BlitImage, R_ClearAttachments, R_ClearColorImage, R_ClearDepthStencilImage, R_CopyBuffer, R_CopyBufferToImage,
R_CopyImage, R_CopyImageToBuffer, R_Dispatch, R_DispatchIndirect, R_Draw, R_DrawIndexed, R_DrawIndexedIndirect,
R_EndQuery, R_EndRendering, R_FillBuffer, R_PipelineBarrier2, R_PushConstants, R_ResetQueryPool, R_ResolveImage,
R_EndQuery, R_EndRendering, R_ExecuteCommands, R_FillBuffer, R_PipelineBarrier2, R_PushConstants, R_ResetQueryPool, R_ResolveImage,
R_SetBlendConstants, R_SetCullMode, R_SetDepthBias, R_SetDepthBiasEnable, R_SetDepthCompareOp, R_SetDepthTestEnable,
R_SetDepthWriteEnable, R_SetFrontFace, R_SetPrimitiveRestartEnable, R_SetPrimitiveTopology,
R_SetRasterizerDiscardEnable, R_SetScissorWithCount, R_SetStencilCompareMask, R_SetStencilOp, R_SetStencilReference,
R_SetStencilTestEnable, R_SetStencilWriteMask, R_SetViewportWithCount, R_WriteTimestamp, R_WriteTimestamp2,
R_PushTpl, R_Push, R_VertexInput, R_Bool, R_PolygonMode, R_BlendEnable, R_BlendEquation, R_WriteMask, R_EndCb,
R_PassMark,
};
static uint64_t now_ns(void) { return plat_now_ns(); }
@@ -64,6 +69,9 @@ static void rec_main(void *arg) {
continue;
}
}
// TOTK_REC_DELAY_US (debug): the recorder falls this far behind each batch it finds, as on a slow machine,
// so objects the replay lets go of meet packets it has not made yet (TOTK_REC_CHECK).
if (rec_delay_ns) plat_sleep_ns(rec_delay_ns);
for (int k = 0; t != h; k++) {
uint64_t w = ring[t % RING_WORDS];
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
@@ -103,6 +111,9 @@ static void *put(uint32_t op, size_t bytes) {
}
wait_space(n);
ring[pos] = (uint64_t)op | n << 32;
// Packers reserve room for optional arrays they may leave unwritten (BeginRendering's depth and stencil); with
// TOTK_REC_CHECK those words would still hold handles from the ring's previous lap.
if (rec_chk) memset(&ring[pos + 1], 0, (n - 1) * 8);
put_h += n;
RS_.packets++, RS_.words += n;
return &ring[pos + 1];
@@ -124,7 +135,7 @@ void rec_drain(void) {
RS_.drains++, RS_.drain_ns += now_ns() - t0;
}
void rec_begin(VkCommandBuffer cb) {
void rec_begin(VkCommandBuffer cb, uint64_t fence) {
static int mode = -1;
if (mode < 0) {
// Default off on AMD GPUs. With the recorder those drivers lost the device or hung the GPU at startup on
@@ -140,6 +151,9 @@ void rec_begin(VkCommandBuffer cb) {
if (mode) {
plat_event_init(&wake_ev, 0, 0);
rec_prio = plat_thread_priority_get();
rec_chk = rt_knob("REC_CHECK", "0"); // debug switches, read before the recorder starts
const char *d = cfg_get("TOTK_REC_DELAY_US");
rec_delay_ns = d && *d && atoi(d) > 0 ? (uint64_t)atoi(d) * 1000 : 0;
rec_thread = plat_thread_new(rec_main, NULL, "vk recorder");
if (!rec_thread) {
rt_log("vk_rec: no recorder thread: the replay makes its Vulkan calls itself");
@@ -147,6 +161,9 @@ void rec_begin(VkCommandBuffer cb) {
} else {
rt_log("vk_rec: the replay's Vulkan commands are made by a recorder thread%s",
mode == 2 ? " in the B blocks (TOTK_AB)" : "");
if (rec_chk) rt_log("vk_rec: TOTK_REC_CHECK: deferred and destroyed objects are searched in the ring");
if (rec_delay_ns) rt_log("vk_rec: TOTK_REC_DELAY_US: the recorder waits %llu us before each batch",
(unsigned long long)(rec_delay_ns / 1000));
}
}
}
@@ -154,6 +171,8 @@ void rec_begin(VkCommandBuffer cb) {
if (!RT_KNOB_ON(mode)) return;
RS_.parts_on++;
rec_cb = cb; // the recorder is idle: rec_end drained it
__atomic_store_n(&part_h0, put_h, __ATOMIC_RELEASE);
__atomic_store_n(&part_fence, fence, __ATOMIC_RELEASE);
rec_live = 1;
}
@@ -202,6 +221,64 @@ void rec_stats_log(void) {
memset(&RS_, 0, sizeof RS_);
}
// TOTK_REC_CHECK=1 (debug, recorder on): the packets of the part being recorded are searched word by word for an object
// vk_memory.c lets go of. put() zeroes each packet's room, so only words a packer wrote can match. A hit is a hazard
// - at a deferral (destroy 0) when the stamp tl is below the part's value: vk_memory.c collect_locked may destroy the
// object once the GPU reaches tl, before the part ran and, with the recorder behind, before it made the call;
// - at a destruction (destroy 1) always: a packet not made yet hands the driver a dead handle, a made one leaves it
// in a command buffer not yet submitted.
// "Made" means before the tail the recorder published, which trails it by up to 64 packets. Hazards are logged (the
// first 64) and counted by kind, packet and case for rec_check_log; named deferrals that wait long enough are only
// counted. Reads race the producer only in words below head, which stay put until the ring
// wraps (16 MiB later).
static const char *const kind_name[REC_KINDS] = {"image", "view", "buffer"};
static volatile int32_t chk_seen[2], chk_named, chk_logged, chk_hz[2][2][REC_KINDS][64]; // [destroy][made][kind][op]
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu) {
if (!rec_chk || !handle || kind < 0 || kind >= REC_KINDS || !__atomic_load_n(&rec_live, __ATOMIC_RELAXED)) return;
uint64_t h = __atomic_load_n(&head, __ATOMIC_ACQUIRE), p = __atomic_load_n(&part_h0, __ATOMIC_ACQUIRE);
uint64_t fence = __atomic_load_n(&part_fence, __ATOMIC_ACQUIRE);
if (p > h || h - p > RING_WORDS / 2) return; // a part this large is not searched
plat_inc32(&chk_seen[destroy != 0]);
while (p < h) {
uint64_t w = ring[p % RING_WORDS];
uint32_t op = (uint32_t)(w & 0xffff), n = (uint32_t)(w >> 32);
if (!n) return; // overwritten meanwhile
if (op != R_PAD)
for (uint32_t i = 1; i < n; i++) {
if (ring[(p + i) % RING_WORDS] != handle) continue;
int made = p < __atomic_load_n(&tail, __ATOMIC_ACQUIRE);
if (!destroy && tl >= fence) { plat_inc32(&chk_named); return; }
plat_inc32(&chk_hz[destroy][made][kind][op & 63]);
if (plat_inc32(&chk_logged) <= 64)
rt_log("vk_rec: check: HAZARD: %s %#llx %s while packet op %u (byte %u of its arguments) of the "
"part being recorded names it, %s; deferral stamp %llu, part value %llu, GPU at %llu",
kind_name[kind], (unsigned long long)handle, destroy ? "destroyed" : "deferred", op,
8 * (i - 1), made ? "made" : "not known made", (unsigned long long)tl,
(unsigned long long)fence, (unsigned long long)gpu);
return;
}
p += n;
}
}
void rec_check_log(void) {
static volatile int32_t said;
if (!rec_chk || plat_xchg32(&said, 1)) return;
int any = 0;
for (int d = 0; d < 2; d++)
for (int m = 0; m < 2; m++)
for (int k = 0; k < REC_KINDS; k++)
for (int op = 0; op < 64; op++)
if (chk_hz[d][m][k][op]) {
any = 1;
rt_log("vk_rec: check summary: %d hazards: %s %s, packet op %d %s", (int)chk_hz[d][m][k][op],
kind_name[k], d ? "destroyed" : "deferred", op, m ? "made" : "not known made");
}
rt_log("vk_rec: check summary: %s; searched for %d objects deferred and %d destroyed while a part was recorded; %d "
"deferred ones the part named waited for its value", any ? "hazards above" : "no hazards", (int)chk_seen[0],
(int)chk_seen[1], (int)chk_named);
}
#define OWNED(cb) (rec_live && (cb) == rec_cb)
// A call the ring takes no packet for: everything queued first, then the call here.
#define FALLBACK() (rec_drain(), RS_.fallbacks++)
@@ -414,6 +491,15 @@ void rc_vkCmdClearDepthStencilImage(VkCommandBuffer cb, VkImage i, VkImageLayout
pub();
}
typedef struct { uint32_t n; const VkCommandBuffer *c; } A_ExecuteCommands;
void rc_vkCmdExecuteCommands(VkCommandBuffer cb, uint32_t n, const VkCommandBuffer *c) {
if (!OWNED(cb)) { vkCmdExecuteCommands(cb, n, c); return; }
A_ExecuteCommands *a = put(R_ExecuteCommands, sizeof *a + sizeof *c * n);
uint8_t *at = (uint8_t *)(a + 1);
a->n = n, a->c = tail_copy(&at, c, sizeof *c * n);
pub();
}
typedef struct { VkPipelineLayout l; VkShaderStageFlags st; uint32_t o, n; const void *v; } A_PushConstants;
void rc_vkCmdPushConstants(VkCommandBuffer cb, VkPipelineLayout l, VkShaderStageFlags st, uint32_t o, uint32_t n,
const void *v) {
@@ -531,6 +617,14 @@ void rec_polygon_mode(PFN_vkCmdSetPolygonModeEXT fn, VkCommandBuffer cb, VkPolyg
pub();
}
typedef struct { uint32_t id; int end; } A_PassMark;
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end) {
if (!OWNED(cb)) { vk_pass_mark_cmd(cb, id, end); return; }
A_PassMark *a = put(R_PassMark, sizeof *a);
*a = (A_PassMark){id, end};
pub();
}
typedef struct { void *fn; uint32_t first, n; const void *v; } A_Targets;
#define TARGET_CMD(fname, R, PFN, T) \
void fname(PFN fn, VkCommandBuffer cb, uint32_t first, uint32_t n, const T *v) { \
@@ -592,6 +686,7 @@ static void exec_packet(uint32_t op, void *p) {
break;
}
F(PushConstants, a->l, a->st, a->o, a->n, a->v)
F(ExecuteCommands, a->n, a->c)
#undef F
case R_EndRendering: vkCmdEndRendering(cb); break;
case R_BlitImage: { A_Img *a = p; vkCmdBlitImage(cb, a->s, a->sl, a->d, a->dl, a->n, a->r, a->f); break; }
@@ -618,6 +713,7 @@ static void exec_packet(uint32_t op, void *p) {
case R_BlendEnable: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEnableEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_BlendEquation: { A_Targets *a = p; ((PFN_vkCmdSetColorBlendEquationEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_WriteMask: { A_Targets *a = p; ((PFN_vkCmdSetColorWriteMaskEXT)a->fn)(cb, a->first, a->n, a->v); break; }
case R_PassMark: { A_PassMark *a = p; vk_pass_mark_cmd(cb, a->id, a->end); break; }
case R_EndCb: {
A_EndCb *a = p;
if (a->kind) vk_crumb(cb, a->kind, a->n);
+11 -1
View File
@@ -15,13 +15,19 @@
// MIT, Copyright (c) 2026 Peiikai): the driver's work behind the commands depends on nothing the replay decides next.
#pragma once
void rec_begin(VkCommandBuffer cb); // the submitting thread, sub_lock held
void rec_begin(VkCommandBuffer cb, uint64_t fence); // the submitting thread, sub_lock held; fence: the part's value
void rec_end(void); // waits for the recorder; no-op when rec_begin did not start it
// The end of part cb: the breadcrumb crumb (0: none, vk_crumbs.c), then vkEndCommandBuffer, both made by the recorder
// when it records cb, and the wait for it (1); otherwise rec_end and the breadcrumb, and the caller ends cb (0).
int rec_end_cb(VkCommandBuffer cb, int crumb, uint64_t n);
void rec_drain(void); // waits until the recorder made every call queued so far
void rec_stats_log(void); // TOTK_TRACE=perf: packets, bytes, drains and waits since the last call
// TOTK_REC_CHECK=1 (debug, recorder on): vk_memory.c reports each object it defers (destroy 0, stamped tl) or destroys
// (destroy 1) with the value the GPU reached (gpu); a packet of the part being recorded that still names it is logged
// as a hazard when the object can go before the part ran. rec_check_log: the counts by kind and packet, at exit.
enum { REC_IMAGE, REC_VIEW, REC_BUFFER, REC_KINDS };
void rec_check_handle(int kind, uint64_t handle, int destroy, uint64_t tl, uint64_t gpu);
void rec_check_log(void);
// Extension commands called through device function pointers.
void rec_push_tpl(PFN_vkCmdPushDescriptorSetWithTemplateKHR fn, VkCommandBuffer cb, VkDescriptorUpdateTemplate tpl,
@@ -38,6 +44,8 @@ void rec_blend_equation(PFN_vkCmdSetColorBlendEquationEXT fn, VkCommandBuffer cb
const VkColorBlendEquationEXT *v);
void rec_write_mask(PFN_vkCmdSetColorWriteMaskEXT fn, VkCommandBuffer cb, uint32_t first, uint32_t n,
const VkColorComponentFlags *v);
// A pass mark (vk_crumbs.c vk_pass_mark_cmd: a buffer marker and a checkpoint), in order with the commands around it.
void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end);
#define REC_CMDS(X) \
X(BeginQuery, (VkCommandBuffer cb, VkQueryPool p, uint32_t q, VkQueryControlFlags f), (cb, p, q, f)) \
@@ -72,6 +80,7 @@ void rec_write_mask(PFN_vkCmdSetColorWriteMaskEXT fn, VkCommandBuffer cb, uint32
X(EndRendering, (VkCommandBuffer cb), (cb)) \
X(FillBuffer, (VkCommandBuffer cb, VkBuffer b, VkDeviceSize o, VkDeviceSize n, uint32_t v), (cb, b, o, n, v)) \
X(PipelineBarrier2, (VkCommandBuffer cb, const VkDependencyInfo *di), (cb, di)) \
X(ExecuteCommands, (VkCommandBuffer cb, uint32_t n, const VkCommandBuffer *c), (cb, n, c)) \
X(PushConstants, (VkCommandBuffer cb, VkPipelineLayout l, VkShaderStageFlags st, uint32_t o, uint32_t n, \
const void *v), (cb, l, st, o, n, v)) \
X(ResetQueryPool, (VkCommandBuffer cb, VkQueryPool p, uint32_t f, uint32_t n), (cb, p, f, n)) \
@@ -111,6 +120,7 @@ REC_CMDS(REC_DECL)
#define vkCmdBindPipeline rc_vkCmdBindPipeline
#define vkCmdBindVertexBuffers2 rc_vkCmdBindVertexBuffers2
#define vkCmdBlitImage rc_vkCmdBlitImage
#define vkCmdExecuteCommands rc_vkCmdExecuteCommands
#define vkCmdClearAttachments rc_vkCmdClearAttachments
#define vkCmdClearColorImage rc_vkCmdClearColorImage
#define vkCmdClearDepthStencilImage rc_vkCmdClearDepthStencilImage
+2 -2
View File
@@ -389,7 +389,7 @@ VkCommandBuffer vk_sync_split(void) {
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, cur_fence);
ts_begin(s->cb);
rec_begin(s->cb);
rec_begin(s->cb, cur_fence);
return s->cb;
}
@@ -418,7 +418,7 @@ uint64_t vk_sync_submit(const uint64_t *handles, int n) {
vkBeginCommandBuffer(s->cb, &bi);
vk_crumb(s->cb, VK_CRUMB_SUBMIT, fence);
ts_begin(s->cb);
rec_begin(s->cb);
rec_begin(s->cb, fence);
int recorded = vk_exec_record(s->cb, handles, n, fence);
s = &slots[cur_slot]; // the last part (vk_sync_split)
if (recorded) ts_end(s->cb, fence);
+25 -10
View File
@@ -26,6 +26,7 @@
#include "nvn_tex.h"
#include "tex.h"
#include "vk.h"
#include "vk_dlaa.h"
#include "vk_exec.h"
#include "platform/platform.h"
@@ -166,8 +167,7 @@ static int same_but_swizzle(const TexKey *a, const TexKey *b) {
static int tex_mirrored(const tex_t *t) {
if (!t->mem || nvn_gpu_only((uintptr_t)t->mem)) return 0;
if (t->has_layout) return 1;
const pool_t *p = t->pool;
if (!p || !(p->flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
if (!(t->pool_flags & (NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED | NVN_MEMORY_POOL_FLAGS_CPU_CACHED))) return 0;
if (t->d.flags & (NVN_TEXTURE_FLAGS_DISPLAY | NVN_TEXTURE_FLAGS_COMPRESSIBLE | NVN_TEXTURE_FLAGS_ADAPTIVE_ZCULL |
NVN_TEXTURE_FLAGS_DEPTH16_PREFER_FAST_CLEAR | NVN_TEXTURE_FLAGS_ZCULL_SUPPORT_STENCIL))
return 0;
@@ -1679,14 +1679,8 @@ static VkSampler make_sampler(VkCtx *c, const samp_par *d) {
si.maxLod = d->lod_max > 1000 ? VK_LOD_CLAMP_NONE : d->lod_max;
if (mf <= NVN_MIN_FILTER_LINEAR) si.maxLod = si.minLod + 0.25f; // not mipmapped: stay on the base level
if (si.maxLod < si.minLod) si.maxLod = si.minLod;
float lim = c->props.limits.maxSamplerLodBias, bias = d->lod_bias;
// DLSS upscaling (resolution.c): the scene's material textures, the filtered mipmapped ones that repeat, are
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias.
if (!d->cmp_mode && mf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR &&
(si.addressModeU == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeU == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT ||
si.addressModeV == VK_SAMPLER_ADDRESS_MODE_REPEAT || si.addressModeV == VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT))
bias += res_dlss_lod_bias();
si.mipLodBias = fmaxf(-lim, fminf(lim, bias));
float lim = c->props.limits.maxSamplerLodBias;
si.mipLodBias = fmaxf(-lim, fminf(lim, d->lod_bias));
// TOTK_ANISO raises the level for filtered, mipmapped colour samplers that repeat on U and V: the surface textures,
// which tile, so ground and walls seen at a shallow angle stay sharp into the distance. Depth-compare (shadow)
// samplers, nearest or unmipmapped ones (data maps, LUTs) and clamped ones keep the game's level: clamped samplers
@@ -1754,11 +1748,32 @@ void vk_sampler_register(int id, const samp_par *s) {
unlock();
}
// DLSS upscaling (vk_sampler_dlss_bias): the scene's material textures, the filtered mipmapped ones that repeat, are
// sampled for the output's resolution; clamped ones (post effects, LUTs, the UI) keep the game's bias.
static float dlss_bias;
static int dlss_biased(const samp_par *k) {
int rep_s = k->ws == NVN_WRAP_MODE_REPEAT || k->ws == NVN_WRAP_MODE_MIRRORED_REPEAT;
int rep_t = k->wt == NVN_WRAP_MODE_REPEAT || k->wt == NVN_WRAP_MODE_MIRRORED_REPEAT;
return !k->cmp_mode && k->minf == NVN_MIN_FILTER_LINEAR_MIPMAP_LINEAR && (rep_s || rep_t);
}
void vk_sampler_dlss_bias(float b) {
lock();
if (b != dlss_bias) {
dlss_bias = b;
for (int i = 0; i < nsslots; i++) sslots[i].s = sslots[i].sp = VK_NULL_HANDLE; // looked up again (the key moves)
samp_gen++;
}
unlock();
}
// Caller holds the lock.
static VkSampler sampler_for(VkCtx *c, const samp_par *s) {
samp_par k;
memset(&k, 0, sizeof k);
k = *s;
if (dlss_bias != 0 && dlss_biased(&k)) k.lod_bias += dlss_bias; // part of the key: samplers live until shutdown
VkSampler v = (VkSampler)nvn_hm_get(&samplers, &k);
if (!v) {
v = make_sampler(c, &k);
+50 -4
View File
@@ -22,6 +22,7 @@
#include "host.h"
#include "nvn_api.h"
#include "vk.h"
#include "vk_dlaa.h"
#include "platform/platform.h"
#define ALIGN 256u
@@ -34,6 +35,9 @@ typedef struct Arena {
VkBuffer buf;
VmaAllocation alloc;
uint8_t *map;
uint8_t *cpu; // with DLSS (vk_dlaa_available), the same bytes in host memory for the CPU's reads (vk_usnap_bytes):
// map is often device memory through the BAR, written fast but read back at about a microsecond a
// cache line. Without DLSS nothing reads them back, and the bytes go into map in one copy.
uint64_t size;
} Arena;
@@ -121,9 +125,35 @@ static void collect(const op_hdr *h, int depth) {
if (h->op == OP_BIND_UBO) {
const p_bind_buf *b = a;
if (b->addr && b->size) {
if (U.ncol == U.ccol) U.col = grow(U.col, &U.ccol, sizeof *U.col);
uint64_t n = b->size < MAX_RANGE ? b->size : MAX_RANGE;
U.col[U.ncol++] = (Rng){b->addr & ~(uint64_t)(ALIGN - 1), b->addr + n};
// the vertex stage's object and bone blocks: also the game's other copy on either side, which still
// holds last frame's (vk_mvprev.c)
uint64_t step = vk_mvprev_step(b->stage, b->index, b->size);
// which neighbours lie in an imported pool, per block and vk_mem_gen: the same blocks are bound by
// many draws, and two resolves for each binding were a quarter of the snapshot's time
static struct { uint64_t addr, size, gen; int ok; } nc[1024];
uint32_t ci = (uint32_t)((b->addr ^ n << 48) * 0x9E3779B97F4A7C15ull >> 54);
uint64_t gen = vk_mem_gen();
int known = step && nc[ci].addr == b->addr && nc[ci].size == n && nc[ci].gen == gen, ok = 0;
for (int k = step ? -1 : 0; k <= (step ? 1 : 0); k++) {
uint64_t at = b->addr + (uint64_t)((int64_t)k * (int64_t)step);
if (k < 0 && b->addr < step) continue;
// a neighbour past its pool would make the whole merged region fail the pool check below, and
// the bindings in it read live memory again
VkMemSpan sp;
uint64_t lo = at & ~(uint64_t)(ALIGN - 1);
if (k && known) {
if (!(nc[ci].ok >> (k + 1) & 1)) continue;
} else if (k) {
if (!vk_mem_resolve(lo, at + n - lo, &sp) || sp.alloc || !sp.pool || sp.avail < at + n - lo ||
(sp.cls != VK_MCLS_HOST_CACHED && sp.cls != VK_MCLS_HOST_UNC))
continue;
ok |= 1 << (k + 1);
}
if (U.ncol == U.ccol) U.col = grow(U.col, &U.ccol, sizeof *U.col);
U.col[U.ncol++] = (Rng){at & ~(uint64_t)(ALIGN - 1), at + n};
}
if (step && !known) nc[ci].addr = b->addr, nc[ci].size = n, nc[ci].gen = gen, nc[ci].ok = ok;
}
}
h = nvn_op_next(h);
@@ -254,8 +284,14 @@ void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence) {
}
if (!U.nreg) return;
if (!(U.arena = arena_get(total))) return;
uint8_t *dst = U.arena->map;
if (vk_dlaa_available()) {
if (!U.arena->cpu && !(U.arena->cpu = malloc(U.arena->size))) rt_fatal("vk_usnap: out of memory");
dst = U.arena->cpu;
}
for (int i = 0; i < U.nreg; i++)
memcpy(U.arena->map + U.reg[i].off, (const void *)(uintptr_t)U.reg[i].lo, U.reg[i].hi - U.reg[i].lo);
memcpy(dst + U.reg[i].off, (const void *)(uintptr_t)U.reg[i].lo, U.reg[i].hi - U.reg[i].lo);
if (dst != U.arena->map) memcpy(U.arena->map, dst, total); // one sequential write into the device copy
vmaFlushAllocation(vk_ctx()->vma, U.arena->alloc, 0, total); // a no-op on coherent memory
U.on = 1;
U.subs++, U.regions += (uint64_t)U.nreg, U.bytes += total, U.copy_ns += kern_now_ns() - t0;
@@ -301,10 +337,20 @@ void vk_usnap_drop(uint64_t addr, uint64_t n) {
uint64_t vk_usnap_gen(void) { return U.gen; }
// the host copy where there is one (DLSS), else the device copy
static uint8_t *host_copy(void) { return U.arena->cpu ? U.arena->cpu : U.arena->map; }
const uint8_t *vk_usnap_map(void) { return U.arena ? host_copy() : NULL; }
const uint8_t *vk_usnap_bytes(uint64_t addr, uint64_t n) {
VkDescriptorBufferInfo bi;
return vk_usnap_find(addr, n, 1, &bi) ? host_copy() + bi.offset : NULL;
}
// The arenas back on the free list (vk_exec_draw_shutdown: the device is idle, every completion ran).
void vk_usnap_shutdown(void) {
plat_mutex_lock(&free_lock);
for (Arena *a = free_arenas, *next; a; a = next) next = a->next, vmaDestroyBuffer(vk_ctx()->vma, a->buf, a->alloc), free(a);
for (Arena *a = free_arenas, *next; a; a = next) next = a->next, vmaDestroyBuffer(vk_ctx()->vma, a->buf, a->alloc), free(a->cpu), free(a);
free_arenas = NULL;
plat_mutex_unlock(&free_lock);
}
+129 -4
View File
@@ -3,6 +3,7 @@ with a newer release, totk.ini and the event stream. No game files, no tools; st
Run: python tests/installer_test.py
"""
import http.client
import io
import json
import re
@@ -11,6 +12,7 @@ import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
@@ -209,6 +211,41 @@ class ReleaseFilesTest(unittest.TestCase):
self.assertEqual(used - set(make_release.TOOLS_FILES), set())
class RenamedVersionTest(unittest.TestCase):
"""An install the setup before 0.3.1 recorded as 1.1.1 is the 1.1.0 update (#332)."""
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = Path(__file__).resolve().parents[1]
layout = Layout(root=root, dev=True, release='9.9', python=Path('python'), tools=root / 'tools',
recomp=root / 'recomp', llvm=Path('llvm'), deps=Path('deps'), runtime=Path('rt'),
runtime_lib=Path('lib'), ninja=Path('ninja'), hactool=Path('hactool'), xlate=Path('xlate'))
state = {'format': 1, 'release': '0.2.8', 'version': '1.1.1', 'inputs': {},
'steps': {'extract': {'key': 'recorded by 0.2.8', 'release': '0.2.8'}}}
out = io.StringIO()
self.ctx = runner.Context(layout, Path(tmp.name), Path(tmp.name), Reporter(out=out), SimpleNamespace(),
state=state)
def test_old_name_is_read_as_1_1_0(self):
steps.migrate_state(self.ctx)
self.assertEqual(steps.game_version(self.ctx), '1.1.0')
self.assertEqual(steps.version_tag(steps.game_version(self.ctx)), 'v110')
self.assertEqual(steps.expected_update(self.ctx)['display_version'], '1.1.0')
self.assertTrue(self.ctx.layout.patches(steps.game_version(self.ctx)).is_file())
def test_extraction_is_kept_under_the_new_name(self):
steps.migrate_state(self.ctx)
self.assertEqual(self.ctx.state['steps']['extract']['key'], steps.Extract().key(self.ctx))
self.assertEqual(self.ctx.state['steps']['extract']['release'], '0.2.8') # full check of kept output runs
def test_other_versions_are_untouched(self):
self.ctx.state['version'] = '1.4.3'
steps.migrate_state(self.ctx)
self.assertEqual(self.ctx.state['version'], '1.4.3')
self.assertEqual(self.ctx.state['steps']['extract']['key'], 'recorded by 0.2.8')
class LiftKeyTest(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
@@ -254,6 +291,19 @@ class LiftKeyTest(unittest.TestCase):
self.add_cheats('main.npdm', 'not a patch', mod='Fps', where='exefs')
self.assertEqual(self.key(), both)
def test_renaming_a_mod_folder_changes_the_key(self):
# tools/cheats.py and tools/exefs_patches.py name a patch set after "<mod folder>/<cheat or file>": a renamed
# folder needs a lift even though its files are the same
self.add_cheats('277178B7DBA1B6D4.txt', '[a]', mod='MyNXCheats')
self.add_cheats('dynamic-fps.pchtxt', '@nsobid-277178B7DBA1B6D4', mod='Fps', where='exefs')
before = self.key()
mods = self.dest / 'mods'
(mods / 'MyNXCheats').rename(mods / 'MyNXCheats 1.4.3')
cheats_renamed = self.key()
self.assertNotEqual(cheats_renamed, before)
(mods / 'Fps').rename(mods / 'Fps 2')
self.assertNotEqual(self.key(), cheats_renamed)
def test_status_and_other_files_keep_the_key(self):
self.add_cheats('277178B7DBA1B6D4.txt', '[a]')
before = self.key()
@@ -433,7 +483,7 @@ class FontsTest(unittest.TestCase):
def test_all_steps_order(self):
self.assertEqual([s.id for s in steps.all_steps()],
['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'cleanup'])
['check', 'fonts', 'extract', 'lift', 'compile', 'prepare', 'dlss', 'cleanup'])
def test_extracts_into_the_install_folder(self):
self.fake_tool()
@@ -520,11 +570,86 @@ class EventsTest(unittest.TestCase):
catalog = json.loads((ROOT / 'installer' / 'text' / 'en.json').read_text(encoding='utf-8'))
used = set()
for path in (ROOT / 'installer').glob('*.py'):
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", path.read_text()))
used |= set(re.findall(r"warning\('([a-z_.]+)'", path.read_text()))
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", path.read_text())}
text = path.read_text(encoding='utf-8')
used |= set(re.findall(r"InstallError\('([a-z_.]+)'", text))
used |= set(re.findall(r"warning\('([a-z_.]+)'", text))
used |= {f'phase.{s}.{p}' for s, p in re.findall(r"progress\('(\w+)', '(\w+)'[,)]", text)}
self.assertEqual(sorted(i for i in used if i not in catalog), [])
@unittest.skipUnless(steps.os.name == 'nt', 'DLSS is Windows-only')
class DlssTest(unittest.TestCase):
"""The DLSS step: off without --dlss, needs an RTX GPU, keeps a player's library, verifies the download."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
self.dest, temp = base / 'dest', base / 'temp'
self.dest.mkdir()
temp.mkdir()
self.src = base / 'nvngx_dlss.dll'
self.src.write_bytes(b'dlss' * 1000)
self.saved = steps.DLSS_URL, steps.DLSS_SIZE, steps.DLSS_SHA256
steps.DLSS_URL = self.src.as_uri() # urllib reads file: URLs as it reads https: ones
steps.DLSS_SIZE, steps.DLSS_SHA256 = 4000, steps.file_sha256(self.src)
rtx = {'devices': [{'name': 'NVIDIA GeForce RTX 4080 SUPER', 'api': [1, 4, 0], 'type': 2}]}
self.ctx = SimpleNamespace(dest=self.dest, temp=temp, host=rtx, reporter=Reporter(out=io.StringIO()),
options=SimpleNamespace(dlss=True))
def tearDown(self):
steps.DLSS_URL, steps.DLSS_SIZE, steps.DLSS_SHA256 = self.saved
self.ctx.reporter.close()
self.tmp.cleanup()
def warnings(self):
return [json.loads(line)['id'] for line in self.ctx.reporter.out.getvalue().splitlines()
if json.loads(line)['type'] == 'warning']
def test_off_without_the_option(self):
self.ctx.options.dlss = False
self.assertEqual(steps.Dlss().run(self.ctx), {})
self.assertFalse((self.dest / 'nvngx_dlss.dll').exists())
self.assertNotEqual(steps.Dlss().key(self.ctx), steps.Dlss().key(SimpleNamespace(options=SimpleNamespace(dlss=True))))
def test_downloads_and_checks_the_library(self):
rec = steps.Dlss().run(self.ctx)
self.assertEqual(rec, {'installed': steps.DLSS_VERSION})
self.assertEqual((self.dest / 'nvngx_dlss.dll').read_bytes(), self.src.read_bytes())
self.assertFalse(list(self.ctx.temp.iterdir()))
self.assertTrue(steps.Dlss().current(self.ctx, rec))
(self.dest / 'nvngx_dlss.dll').unlink()
self.assertFalse(steps.Dlss().current(self.ctx, rec))
def test_a_wrong_file_is_not_installed_and_tried_again(self):
steps.DLSS_SHA256 = '0' * 64
rec = steps.Dlss().run(self.ctx)
self.assertEqual(rec, {'failed': True})
self.assertFalse((self.dest / 'nvngx_dlss.dll').exists())
self.assertEqual(self.warnings(), ['warn.dlss_download_failed'])
self.assertFalse(steps.Dlss().current(self.ctx, rec))
def test_a_cut_connection_warns(self):
class Cut(io.BytesIO): # a response whose connection drops mid-file
def read(self, n=-1):
raise http.client.IncompleteRead(b'dl', 3998)
with mock.patch.object(steps.urllib.request, 'urlopen', return_value=Cut()):
self.assertEqual(steps.Dlss().run(self.ctx), {'failed': True})
self.assertFalse((self.dest / 'nvngx_dlss.dll').exists())
self.assertFalse(list(self.ctx.temp.iterdir()))
self.assertEqual(self.warnings(), ['warn.dlss_download_failed'])
def test_keeps_the_players_library(self):
(self.dest / 'nvngx_dlss.dll').write_bytes(b'newer')
self.assertEqual(steps.Dlss().run(self.ctx), {'kept': True})
self.assertEqual((self.dest / 'nvngx_dlss.dll').read_bytes(), b'newer')
self.assertEqual(self.warnings(), ['warn.dlss_kept'])
def test_skipped_without_an_rtx_gpu(self):
self.ctx.host = {'devices': [{'name': 'AMD Radeon RX 7800 XT', 'api': [1, 3, 0], 'type': 2}]}
self.assertEqual(steps.Dlss().run(self.ctx), {'skipped': 'no_rtx'})
self.assertEqual(self.warnings(), [])
if __name__ == '__main__':
unittest.main()
+173
View File
@@ -0,0 +1,173 @@
"""tools/ncz.py and the installer's unpacking of NSZ input (installer/steps.py, Check.unpack) on small made-up files:
an NCA with an AES-CTR section and a plain one, stored as an NCZ with one zstd stream or as NCZBLOCK blocks.
Run: .venv-py314\\Scripts\\python tests/ncz_test.py
"""
import hashlib
import io
import os
import struct
import sys
import tempfile
import unittest
from compression import zstd
from pathlib import Path
from types import SimpleNamespace
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / 'tools'))
import ncz # noqa: E402
from installer import steps # noqa: E402
from installer.events import Reporter # noqa: E402
KEY, COUNTER = bytes(range(16)), bytes(range(16, 32))
# (offset, size, crypto type): an AES-CTR section, then a plain one
SECTIONS = ((0x4000, 0x9000, 3), (0xD000, 0x3200, 1))
def pfs0(files):
"""An NSP of (name, data) pairs."""
strings = b''.join(name.encode() + b'\0' for name, _ in files)
strings += bytes(-(16 + 0x18 * len(files) + len(strings)) % 0x20)
head = b'PFS0' + struct.pack('<III', len(files), len(strings), 0)
offset = name_at = 0
for name, data in files:
head += struct.pack('<QQII', offset, len(data), name_at, 0)
offset += len(data)
name_at += len(name) + 1
return head + strings + b''.join(data for _, data in files)
def made_up_nca():
"""(the NCA as stored on the console, its body after the header with the sections decrypted)."""
rand = os.urandom
plain = rand(0x2000) + bytes(0x7000) # half random, half compressible
rest = bytes(0x1200) + rand(0x2000)
counter = COUNTER[:8] + struct.pack('>Q', 0x4000 >> 4)
secret = Cipher(algorithms.AES(KEY), modes.CTR(counter)).encryptor().update(plain)
header = rand(0x4000)
return header + secret + rest, plain + rest
def ncz_file(nca, body, blocks=False):
out = nca[:0x4000] + b'NCZSECTN' + struct.pack('<Q', len(SECTIONS))
for offset, size, crypto in SECTIONS:
out += struct.pack('<QQQQ', offset, size, crypto, 0) + KEY + COUNTER
if not blocks:
return out + zstd.compress(body)
exponent = 14
pieces = [body[i:i + (1 << exponent)] for i in range(0, len(body), 1 << exponent)]
stored = [zstd.compress(p) if p.count(0) > len(p) // 2 else p for p in pieces] # random blocks stay as they are
out += b'NCZBLOCK' + bytes([2, 1, 0, exponent]) + struct.pack('<IQ', len(pieces), len(body))
out += struct.pack(f'<{len(stored)}I', *map(len, stored))
return out + b''.join(stored)
class UnpackTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.dir = Path(self.tmp.name)
self.nca, self.body = made_up_nca()
self.name = hashlib.sha256(self.nca).hexdigest()[:32]
self.tik = b'ticket' * 40
def tearDown(self):
self.tmp.cleanup()
def nsz(self, blocks=False, damage=None):
data = ncz_file(self.nca, self.body, blocks)
if damage:
data = damage(bytearray(data))
path = self.dir / 'game.nsz'
path.write_bytes(pfs0([(self.name + '.ncz', bytes(data)), ('a.tik', self.tik)]))
return path
def expected(self):
return pfs0([(self.name + '.nca', self.nca), ('a.tik', self.tik)])
def test_stream_comes_back_byte_for_byte(self):
src = self.nsz()
self.assertEqual(ncz.compressed(src)[0], 'nsz')
self.assertEqual(ncz.unpacked_size(src), len(self.expected()))
seen = []
self.assertEqual(ncz.unpack(src, self.dir / 'out.nsp', lambda d, t: seen.append((d, t))), 'nsz')
self.assertEqual((self.dir / 'out.nsp').read_bytes(), self.expected())
self.assertEqual(seen[-1], (len(self.expected()), len(self.expected())))
self.assertFalse((self.dir / 'out.nsp.part').exists())
def test_blocks_come_back_byte_for_byte(self):
ncz.unpack(self.nsz(blocks=True), self.dir / 'out.nsp')
self.assertEqual((self.dir / 'out.nsp').read_bytes(), self.expected())
def test_plain_nsp_is_not_compressed(self):
path = self.dir / 'plain.nsp'
path.write_bytes(self.expected())
self.assertIsNone(ncz.compressed(path))
def test_changed_byte_fails_the_hash(self):
def flip(data):
data[0x4000 + 16 + 0x40 * len(SECTIONS) + 0x100] ^= 1 # inside a stored (random) block
return data
with self.assertRaises(ncz.DamagedFile) as e:
ncz.unpack(self.nsz(blocks=True, damage=flip), self.dir / 'out.nsp')
self.assertIn('hash mismatch', e.exception.detail)
def test_cut_stream_is_damaged(self):
with self.assertRaises(ValueError):
ncz.unpack(self.nsz(damage=lambda d: d[:-40]), self.dir / 'out.nsp')
self.assertFalse((self.dir / 'out.nsp').exists())
class CheckUnpackTest(unittest.TestCase):
"""Check.unpack: an NSZ base becomes <temp>/unpacked/base.nsp, a plain NSP update is read in place, a second run
reuses the NSP."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
nca, body = made_up_nca()
name = hashlib.sha256(nca).hexdigest()[:32]
self.base, self.update = base / 'base.nsz', base / 'update.nsp'
self.base.write_bytes(pfs0([(name + '.ncz', ncz_file(nca, body))]))
self.update.write_bytes(pfs0([('a.tik', b'x' * 64)]))
self.expected = pfs0([(name + '.nca', nca)])
self.out = io.StringIO()
self.ctx = SimpleNamespace(options=SimpleNamespace(base=str(self.base), update=str(self.update)),
temp=base / 'temp', reporter=Reporter(out=self.out, log_path=base / 'x.log'))
def tearDown(self):
self.ctx.reporter.close()
self.tmp.cleanup()
def test_unpacks_the_base_and_reuses_it(self):
steps.Check().unpack(self.ctx)
target = self.ctx.temp / 'unpacked' / 'base.nsp'
self.assertEqual(target.read_bytes(), self.expected)
self.assertEqual(steps.game_file(self.ctx, 'base'), target)
self.assertEqual(steps.game_file(self.ctx, 'update'), self.update)
self.assertIn('"phase": "unpack_base"', self.out.getvalue())
mtime = target.stat().st_mtime_ns
steps.Check().unpack(self.ctx)
self.assertEqual(target.stat().st_mtime_ns, mtime)
def test_content_decides_not_the_name(self):
renamed = self.base.with_name('base.nsp')
self.base.rename(renamed)
self.ctx.options.base = str(renamed)
steps.Check().unpack(self.ctx)
self.assertEqual((self.ctx.temp / 'unpacked' / 'base.nsp').read_bytes(), self.expected)
def test_damaged_base_names_the_chosen_file(self):
data = bytearray(self.base.read_bytes())
data[-100] ^= 0xFF
self.base.write_bytes(bytes(data))
with self.assertRaises(steps.InstallError) as e:
steps.Check().unpack(self.ctx)
self.assertIn(e.exception.id, ('check.input_damaged', 'check.unpack_failed'))
if __name__ == '__main__':
unittest.main()
+19 -2
View File
@@ -68,6 +68,7 @@ import time
import urllib.request
from pathlib import Path
import check_isa
import romfs_index
from build_shaders import stale as stale_shaders
from check_content import GENERATED_MARKER
@@ -114,7 +115,7 @@ if WIN:
RT_FLAGS.append('-D_WIN32_WINNT=0x0A00')
# -Xlinker passes the map path as one argument; -Wl, would split it at any comma in the build dir path
LINK_FLAGS = ['-fuse-ld=lld', '-Wl,--stack,16777216', '-Xlinker', '"--Map=$builddir/totk.map"'] # the map feeds tools/prof_report.py
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
LINK_LIBS = ['-lpsapi', '-lbcrypt', '-lvulkan-1', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lSDL3', '-lopus', '-l:libvpx.a', '-l:libzstd.a', '-l:libstdc++.a', '-l:libwinpthread.a'] # Vulkan back end; libvpx static (movies); libzstd static (resolution.c)
VMA_INCLUDE = UCRT.parent / 'include' / 'vma' # vk_mem_alloc.h; MSYS2 package vulkan-memory-allocator
else:
# -no-pie: the lifted modules' function tables (hundreds of thousands of pointers) need no load-time relocation.
@@ -150,7 +151,7 @@ def vma_include():
# --toolchain: an llvm-mingw tree. The libraries, headers and DLLs MSYS2 supplies come from the deps directory instead.
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lvulkan-1', '-lSDL3', '-lopus',
LLVM_LINK_LIBS = ['-lpsapi', '-lbcrypt', '-luser32', '-lgdi32', '-ldwmapi', '-limm32', '-lwinmm', '-lole32', '-lshell32', '-luuid', '-lvulkan-1', '-lSDL3', '-lopus',
'-l:libvpx.a', '-l:libzstd.a', '-l:libc++.a', '-l:libc++abi.a', '-l:libunwind.a', '-l:libwinpthread.a']
DEFAULT_DEPS = ROOT / 'toolchain' / 'deps-mingw'
# Runtime sources compiled per install, not in the library: romfs.c includes romfs_config.h, patches.c and ov_zstd.c
@@ -715,6 +716,16 @@ def elf_sections(path):
return out, hi - (lo or 0)
def objdump_tool():
"""The disassembler for tools/check_isa.py: llvm-objdump of --toolchain, else MSYS2's or the one on PATH."""
suffix = '.exe' if WIN else ''
for d in (TOOLCHAIN_BIN, UCRT if WIN else None):
for name in ('llvm-objdump', 'objdump'):
if d and (d / (name + suffix)).is_file():
return str(d / (name + suffix))
return check_isa.find_objdump()
def mib(n):
return f'{n / (1 << 20):,.1f} MiB'
@@ -886,6 +897,12 @@ def main():
size = sum(o.stat().st_size for o in files)
print(f' {mod:8} {len(files):5} objects {mib(size):>14}')
verify_objects(out, objs, pairs is not None, main_out, pairs, base, env)
if args.cpu == 'v2':
# Inline assembly, target attributes and intrinsics pass -march=x86-64-v2 by; one vmovdqa of a YMM register
# stops a CPU without AVX (tools/check_isa.py)
fail = check_isa.check_objects(objs['runtime'], objdump_tool(), out / 'obj' / 'runtime' / 'isa-v2.json')
if fail:
sys.exit(f'build_game: {fail}')
if args.compile_only:
return
if args.make_runtime_lib:
+3 -3
View File
@@ -1,7 +1,7 @@
"""Build the shader translator: build/xlate/totk_xlate.exe (Ryujinx 1.3.2 + patches, NativeAOT, SPIR-V for Vulkan).
Copies tools/vendor/ryujinx to build/xlate/ryujinx, applies tools/xlate/patches/*.patch there (tools/vendor stays
byte-identical; 0001-0004 are MNG's, 0005-0011 are ours), then runs `dotnet publish`. Needs the .NET 10 SDK, the MSVC build
byte-identical; 0001-0004 are MNG's, 0005-0014 are ours), then runs `dotnet publish`. Needs the .NET 10 SDK, the MSVC build
tools and vswhere.exe (docs/building.md). Run with .venv. On Linux it builds build/xlate/totk_xlate for linux-x64, which
NativeAOT links with the system's clang (no MSVC); NativeAOT builds for the OS it runs on only.
@@ -99,8 +99,8 @@ def prepare_sources() -> Path:
for junk in list(src.glob("src/*/obj")) + list(src.glob("src/*/bin")):
shutil.rmtree(junk)
patches = sorted((XLATE / "patches").glob("*.patch"))
if len(patches) != 11:
sys.exit(f"expected 11 patches, found {len(patches)}")
if len(patches) != 15:
sys.exit(f"expected 15 patches, found {len(patches)}")
for patch in patches:
# git apply works outside a repository when given a directory; --check first so a failure names the patch.
for extra in (["--check"], []):
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more