72 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
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
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
80 changed files with 6375 additions and 725 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.
+9 -2
View File
@@ -17,14 +17,19 @@ 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
@@ -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.4
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`).
+20 -5
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`).
@@ -213,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.
+3
View File
@@ -86,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
+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',
+187 -18
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')
@@ -432,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:
@@ -520,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
@@ -641,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'):
@@ -653,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
@@ -664,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
@@ -686,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}
@@ -707,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-'
@@ -907,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):
@@ -1022,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'
@@ -1040,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)
@@ -1090,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()]
+15 -6
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",
@@ -58,14 +62,15 @@
"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}.",
@@ -101,6 +106,8 @@
"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",
@@ -109,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",
@@ -122,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",
+15 -6
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",
@@ -58,14 +62,15 @@
"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}.",
@@ -101,6 +106,8 @@
"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",
@@ -109,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",
@@ -122,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",
+15 -6
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": "Удаление временных файлов",
@@ -58,14 +62,15 @@
"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}.",
@@ -101,6 +106,8 @@
"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": "Пересборка игры под ваши моды",
@@ -109,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": "Язык игры",
@@ -122,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": "Начать",
+15 -6
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": "删除临时文件",
@@ -58,14 +62,15 @@
"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}。",
@@ -101,6 +106,8 @@
"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": "正在为你的模组重新构建游戏",
@@ -109,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": "游戏语言",
@@ -122,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": "开始",
+22 -17
View File
@@ -56,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) {
@@ -69,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];
}
@@ -321,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;
@@ -334,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);
}
@@ -351,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;
@@ -369,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";
+84 -42
View File
@@ -5,8 +5,9 @@
//
// 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, 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
@@ -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;
@@ -105,8 +108,32 @@ static int prefill_blocks_for(const char *e) {
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;
@@ -121,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;
@@ -131,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;
@@ -147,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;
@@ -164,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);
}
}
@@ -186,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());
@@ -199,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;
@@ -208,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) {
@@ -224,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);
@@ -232,8 +265,7 @@ 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
@@ -268,16 +300,16 @@ static void sink_dump(const float *raw, int frames) {
}
// 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;
@@ -328,6 +360,7 @@ void audio_mixer_open(AudioRenderer *r) {
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);
@@ -846,33 +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;
raw[2 * k] = l; raw[2 * k + 1] = rr;
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) {
@@ -911,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) {
+23 -13
View File
@@ -30,20 +30,22 @@ 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",
"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",
@@ -55,7 +57,7 @@ static const char *const known[] = {
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UPSCALER", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VS_OUTS",
"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"},
@@ -97,6 +100,7 @@ static const struct { const char *key, *section; } sections[] = {
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
{"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,7 +126,7 @@ 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|256|512|768|1024|1536|2048", RESTART},
{"SHADOW_SCALE", "1", "1..4", 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},
+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,
};
+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.
+38 -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) {
@@ -826,6 +846,7 @@ static void reflect(GfxShader *s) {
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]);
@@ -1037,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);
}
@@ -1069,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.
@@ -1148,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);
@@ -1350,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);
@@ -1706,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;
@@ -1795,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.
+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;
}
+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();
+1
View File
@@ -699,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);
+35 -3
View File
@@ -405,8 +405,22 @@ static void probe_applet(void) {
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);
@@ -875,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];
+70 -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);
@@ -1656,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
+25 -8
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
@@ -413,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.
+121
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);
}
@@ -1553,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,
+125 -30
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>
@@ -53,12 +56,15 @@ 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 (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. Off Windows vk_dlaa.c is built as no-op stubs, so there is no DLSS at all.
// 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];
@@ -68,10 +74,52 @@ static int dlss_dll(void) {
snprintf(path, sizeof path, "%.*s/nvngx_dlss.dll", (int)(last - exe), exe);
return plat_file_exists(path);
#else
return 0;
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;
@@ -88,26 +136,7 @@ static void decide(void) {
}
const char *s = cfg_value("TOTK_RENDER_SCALE");
double k = 5.0 / 6.0;
// a DLSS mode other than dlaa sets the scene size, only while DLSS/DLAA is on: switched off, the mode the menu
// still holds would shrink the scene for the plain upscaler
const char *m = cfg_value("TOTK_DLSS");
static const struct { const char *name; double k; } dlss[] = {
{"quality", 2.0 / 3.0}, {"balanced", 0.58}, {"performance", 0.5}};
for (int i = 0; cfg_bool("TOTK_DLAA") && i < 3; i++)
if (!strcmp(m, dlss[i].name)) R.dlss_k = dlss[i].k;
if (R.dlss_k && !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 runs on Windows only: the scene follows render_scale", m);
#endif
R.dlss_k = 0;
}
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) {
@@ -117,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);
@@ -144,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
}
@@ -309,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).
@@ -379,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) {
+64 -31
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];
@@ -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();
}
@@ -851,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();
}
@@ -936,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);
@@ -1373,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);
@@ -1400,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;
+145 -29
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 "
@@ -109,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.",
@@ -123,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.",
@@ -191,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",
@@ -322,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",
@@ -359,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.",
@@ -404,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] = "-",
@@ -483,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 игры доводит её до этого размера. "
@@ -538,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 "
"самой игры: края теней и тени листвы чётче. Действует после перезапуска.",
@@ -552,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 оно не нужно.",
@@ -617,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] = "Сила вибрации",
@@ -752,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] = "Шейдеры и конвейеры",
@@ -789,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 (размеры хранилищ, размер запуска, размер точки, базовый экземпляр) вместо чтения нулей.",
@@ -836,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] = "-",
@@ -945,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 "
@@ -1100,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ê.",
@@ -1140,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.",
@@ -1186,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] = "-",
@@ -1280,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 "
@@ -1300,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.",
@@ -1419,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 会将其缩放到该尺寸。"
@@ -1657,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] = "着色器和管线",
@@ -1715,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 读取,"
"避免物体错位闪现,或远景有一帧变平。",
@@ -1775,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] = "内存",
@@ -1850,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] = "阴影级联范围",
@@ -1862,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] = "金手指",
@@ -1921,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];
+16 -3
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,
@@ -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,
@@ -203,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,
@@ -246,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.
+12
View File
@@ -255,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);
@@ -390,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;
+6 -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);
}
+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]);
+644 -88
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -2,6 +2,10 @@
#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.
@@ -20,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);
+4
View File
@@ -188,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);
+792 -11
View File
@@ -731,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;
@@ -741,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
@@ -758,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);
@@ -895,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;
@@ -1398,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;
@@ -1912,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++;
}
@@ -2185,6 +2204,8 @@ static int prepare_draw(VkExec *ex, int prim, int32_t count, int restart) {
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);
@@ -2197,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;
@@ -2288,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
@@ -2495,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; }
+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
+5
View File
@@ -1201,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) {
+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;
+11 -1
View File
@@ -38,7 +38,7 @@ 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,
@@ -491,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) {
@@ -677,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; }
+2
View File
@@ -80,6 +80,7 @@ void rec_pass_mark(VkCommandBuffer cb, uint32_t id, int end);
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)) \
@@ -119,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
+23 -9
View File
@@ -1679,15 +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. Without DLSS
// running (dlaa = 0, or NGX without DLSS) nothing accumulates the extra detail, and the sharper mips only shimmer.
if (vk_dlaa_available() && !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
@@ -1755,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);
}
+112 -1
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()
@@ -446,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()
@@ -540,5 +577,79 @@ class EventsTest(unittest.TestCase):
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()
+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"], []):
+203
View File
@@ -0,0 +1,203 @@
"""Writes runtime/dlaa_mask_spv.h: the SPIR-V of DLSS's reactive mask pass (vk_dlaa.c, mask_pass), assembled here like
tools/make_mvobj_fs.py (no GLSL compiler on the machine this was written on).
The shader, as GLSL:
#version 450
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D before; // the light target when the transparents start
layout(binding = 1) uniform sampler2D after; // the light target when DLSS runs
layout(binding = 2, r8) uniform writeonly image2D mask;
layout(push_constant) uniform P { vec4 p; }; // render size xy, threshold z, gain w
void main() {
vec2 g = vec2(gl_GlobalInvocationID.xy);
if (g.x < p.x && g.y < p.y) {
ivec2 c = ivec2(gl_GlobalInvocationID.xy);
vec3 b = texelFetch(before, c, 0).rgb, a = texelFetch(after, c, 0).rgb;
vec3 d = abs(a - b);
float dm = max(max(d.r, d.g), d.b);
float m = max(max(max(a.r, a.g), a.b), max(max(b.r, b.g), b.b));
float r = dm / max(m, 1e-4);
imageStore(mask, c, vec4(clamp((r - p.z) * p.w, 0.0, 1.0)));
}
}
The mask is the share of a pixel's colour the transparents changed (particles, smoke, sparks: what has no motion
vectors of its own), 0 where nothing was drawn over the solid scene.
python tools/make_dlaa_mask.py
"""
import struct
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / 'runtime' / 'dlaa_mask_spv.h'
SECTIONS = ('cap', 'ext', 'mem', 'entry', 'exec', 'dec', 'types', 'func')
class Asm:
def __init__(self):
self.next = 1
self.sections = {k: [] for k in SECTIONS}
def id(self):
self.next += 1
return self.next - 1
def op(self, sec, code, *words):
self.sections[sec].append([(len(words) + 1) << 16 | code, *words])
def words(self):
out = [0x07230203, 0x00010000, 0, self.next, 0]
for sec in SECTIONS:
for ins in self.sections[sec]:
out.extend(ins)
return out
def string(s):
b = s.encode() + b'\0'
b += b'\0' * (-len(b) % 4)
return list(struct.unpack('<%dI' % (len(b) // 4), b))
def f32(x):
return struct.unpack('<I', struct.pack('<f', x))[0]
OpCapability, OpExtInstImport, OpExtInst, OpMemoryModel, OpEntryPoint, OpExecutionMode = 17, 11, 12, 14, 15, 16
OpDecorate, OpMemberDecorate = 71, 72
OpTypeVoid, OpTypeBool, OpTypeInt, OpTypeFloat, OpTypeVector, OpTypeImage, OpTypeSampledImage = 19, 20, 21, 22, 23, 25, 27
OpTypeStruct, OpTypePointer, OpTypeFunction = 30, 32, 33
OpConstant, OpConstantComposite, OpVariable, OpFunction, OpFunctionEnd, OpLabel, OpReturn = 43, 44, 59, 54, 56, 248, 253
OpLoad, OpAccessChain, OpVectorShuffle, OpCompositeExtract, OpCompositeConstruct = 61, 65, 79, 81, 80
OpImage, OpImageFetch, OpImageWrite, OpConvertUToF, OpBitcast = 100, 95, 99, 112, 124
OpFSub, OpFMul, OpFDiv, OpFOrdLessThan, OpLogicalAnd = 131, 133, 136, 184, 167
OpSelectionMerge, OpBranch, OpBranchConditional = 247, 249, 250
FAbs, FMax, FClamp = 4, 40, 43 # GLSL.std.450
UniformConstant, Input, PushConstant = 0, 1, 9
Binding, DescriptorSet, Block, Offset, BuiltIn, NonReadable = 33, 34, 2, 35, 11, 25
GlobalInvocationId = 28
a = Asm()
a.op('cap', OpCapability, 1) # Shader
a.op('cap', OpCapability, 49) # StorageImageExtendedFormats (r8)
glsl = a.id()
a.op('ext', OpExtInstImport, glsl, *string('GLSL.std.450'))
a.op('mem', OpMemoryModel, 0, 1) # Logical GLSL450
main = a.id()
t_void, t_fn, t_bool, t_f, t_i, t_u = a.id(), a.id(), a.id(), a.id(), a.id(), a.id()
t_v2, t_v3, t_v4, t_v2i, t_v2u, t_v3u = a.id(), a.id(), a.id(), a.id(), a.id(), a.id()
t_img, t_simg, t_wimg = a.id(), a.id(), a.id()
t_psimg, t_pwimg, t_pgid, t_pc, t_ppc, t_ppv4 = a.id(), a.id(), a.id(), a.id(), a.id(), a.id()
v_before, v_after, v_mask, v_gid, v_p = a.id(), a.id(), a.id(), a.id(), a.id()
c_i0, c_zero, c_one, c_eps = a.id(), a.id(), a.id(), a.id()
a.op('entry', OpEntryPoint, 5, main, *string('main'), v_gid) # GLCompute
a.op('exec', OpExecutionMode, main, 17, 8, 8, 1) # LocalSize
a.op('dec', OpDecorate, v_gid, BuiltIn, GlobalInvocationId)
for var, binding in ((v_before, 0), (v_after, 1), (v_mask, 2)):
a.op('dec', OpDecorate, var, DescriptorSet, 0)
a.op('dec', OpDecorate, var, Binding, binding)
a.op('dec', OpDecorate, v_mask, NonReadable)
a.op('dec', OpDecorate, t_pc, Block)
a.op('dec', OpMemberDecorate, t_pc, 0, Offset, 0)
T = 'types'
a.op(T, OpTypeVoid, t_void)
a.op(T, OpTypeFunction, t_fn, t_void)
a.op(T, OpTypeBool, t_bool)
a.op(T, OpTypeFloat, t_f, 32)
a.op(T, OpTypeInt, t_i, 32, 1)
a.op(T, OpTypeInt, t_u, 32, 0)
a.op(T, OpTypeVector, t_v2, t_f, 2)
a.op(T, OpTypeVector, t_v3, t_f, 3)
a.op(T, OpTypeVector, t_v4, t_f, 4)
a.op(T, OpTypeVector, t_v2i, t_i, 2)
a.op(T, OpTypeVector, t_v2u, t_u, 2)
a.op(T, OpTypeVector, t_v3u, t_u, 3)
a.op(T, OpTypeImage, t_img, t_f, 1, 0, 0, 0, 1, 0) # 2D, sampled, format unknown
a.op(T, OpTypeSampledImage, t_simg, t_img)
a.op(T, OpTypeImage, t_wimg, t_f, 1, 0, 0, 0, 2, 15) # 2D, storage, R8
a.op(T, OpTypePointer, t_psimg, UniformConstant, t_simg)
a.op(T, OpTypePointer, t_pwimg, UniformConstant, t_wimg)
a.op(T, OpTypePointer, t_pgid, Input, t_v3u)
a.op(T, OpTypeStruct, t_pc, t_v4)
a.op(T, OpTypePointer, t_ppc, PushConstant, t_pc)
a.op(T, OpTypePointer, t_ppv4, PushConstant, t_v4)
a.op(T, OpConstant, t_i, c_i0, 0)
a.op(T, OpConstant, t_f, c_zero, f32(0.0))
a.op(T, OpConstant, t_f, c_one, f32(1.0))
a.op(T, OpConstant, t_f, c_eps, f32(1e-4))
a.op(T, OpVariable, t_psimg, v_before, UniformConstant)
a.op(T, OpVariable, t_psimg, v_after, UniformConstant)
a.op(T, OpVariable, t_pwimg, v_mask, UniformConstant)
a.op(T, OpVariable, t_pgid, v_gid, Input)
a.op(T, OpVariable, t_ppc, v_p, PushConstant)
F = 'func'
def ins(code, rtype, *args):
r = a.id()
a.op(F, code, rtype, r, *args)
return r
def ext(rtype, inst, *args):
return ins(OpExtInst, rtype, glsl, inst, *args)
l_entry, l_body, l_merge = a.id(), a.id(), a.id()
a.op(F, OpFunction, t_void, main, 0, t_fn)
a.op(F, OpLabel, l_entry)
pp = ins(OpAccessChain, t_ppv4, v_p, c_i0)
p = ins(OpLoad, t_v4, pp)
gid = ins(OpLoad, t_v3u, v_gid)
gu = ins(OpVectorShuffle, t_v2u, gid, gid, 0, 1)
gx = ins(OpConvertUToF, t_f, ins(OpCompositeExtract, t_u, gid, 0))
gy = ins(OpConvertUToF, t_f, ins(OpCompositeExtract, t_u, gid, 1))
px = ins(OpCompositeExtract, t_f, p, 0)
py = ins(OpCompositeExtract, t_f, p, 1)
inside = ins(OpLogicalAnd, t_bool, ins(OpFOrdLessThan, t_bool, gx, px), ins(OpFOrdLessThan, t_bool, gy, py))
a.op(F, OpSelectionMerge, l_merge, 0)
a.op(F, OpBranchConditional, inside, l_body, l_merge)
a.op(F, OpLabel, l_body)
c = ins(OpBitcast, t_v2i, gu)
def fetch(var):
img = ins(OpImage, t_img, ins(OpLoad, t_simg, var))
texel = ins(OpImageFetch, t_v4, img, c, 2, c_i0) # Lod 0
return ins(OpVectorShuffle, t_v3, texel, texel, 0, 1, 2)
def maxc(v):
x, y, z = (ins(OpCompositeExtract, t_f, v, k) for k in range(3))
return ext(t_f, FMax, ext(t_f, FMax, x, y), z)
b = fetch(v_before)
af = fetch(v_after)
d = ext(t_v3, FAbs, ins(OpFSub, t_v3, af, b))
dm = maxc(d)
m = ext(t_f, FMax, ext(t_f, FMax, maxc(af), maxc(b)), c_eps)
r = ins(OpFDiv, t_f, dm, m)
thr = ins(OpCompositeExtract, t_f, p, 2)
gain = ins(OpCompositeExtract, t_f, p, 3)
k = ext(t_f, FClamp, ins(OpFMul, t_f, ins(OpFSub, t_f, r, thr), gain), c_zero, c_one)
out = ins(OpCompositeConstruct, t_v4, k, k, k, k)
a.op(F, OpImageWrite, ins(OpLoad, t_wimg, v_mask), c, out)
a.op(F, OpBranch, l_merge)
a.op(F, OpLabel, l_merge)
a.op(F, OpReturn)
a.op(F, OpFunctionEnd)
w = a.words()
lines = [', '.join(f'0x{x:08x}' for x in w[i:i + 8]) for i in range(0, len(w), 8)]
OUT.write_text(
'// Generated by tools/make_dlaa_mask.py: DLSS\'s reactive mask pass (vk_dlaa.c mask_pass). Do not edit.\n'
'#pragma once\n#include <stdint.h>\n'
'static const uint32_t dlaa_mask_spv[] = {\n ' + ',\n '.join(lines) + '\n};\n', newline='\n')
print(f'{OUT}: {len(w)} words')
+163
View File
@@ -0,0 +1,163 @@
"""Writes runtime/vk_mvobj_fs.h: the SPIR-V of the per-object motion fragment shader (vk_mvobj.c), assembled here
because it must be patched at run time (the Location of its input follows the vertex shader variant) and is small.
The shader, as GLSL:
#version 450
layout(location = 0) in vec4 prev; // last frame's clip position (patch 0015); Location patched
layout(set = 0, binding = 1, std140) uniform P { vec4 p; }; // render size xy, this frame's jitter zw (pixels)
layout(location = 0) out vec4 o;
void main() {
vec2 win = gl_FragCoord.xy - p.zw; // this frame's window position, without the jitter
vec2 pn = prev.xy / prev.w;
vec2 pwin = vec2((pn.x * 0.5 + 0.5) * p.x, (0.5 - pn.y * 0.5) * p.y);
o = prev.w > 1e-6 ? vec4(pwin - win, 0.0, 0.0) : vec4(0.0);
}
The vector is last frame's window position minus this frame's, in render pixels, as runtime/shaders/dlaa_mv.comp
writes it. Its descriptor set and binding are renumbered by vk_pipeline.c like a translated shader's (the .res.json
text in vk_mvobj.c names binding 1 as slot 3).
python tools/make_mvobj_fs.py
"""
import struct
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / 'runtime' / 'vk_mvobj_fs.h'
class Asm:
def __init__(self):
self.next = 1
self.sections = {k: [] for k in ('cap', 'mem', 'entry', 'exec', 'dec', 'types', 'func')}
self.loc_word = None # index of the patched Location literal in the final words
def id(self):
self.next += 1
return self.next - 1
def op(self, sec, code, *words):
self.sections[sec].append([(len(words) + 1) << 16 | code, *words])
return self.sections[sec][-1]
def words(self):
out = [0x07230203, 0x00010000, 0, self.next, 0]
for sec in ('cap', 'mem', 'entry', 'exec', 'dec', 'types', 'func'):
for ins in self.sections[sec]:
if ins is self.loc_ins:
self.loc_word = len(out) + 3
out.extend(ins)
return out
def string(s):
b = s.encode() + b'\0'
b += b'\0' * (-len(b) % 4)
return list(struct.unpack('<%dI' % (len(b) // 4), b))
def f32(x):
return struct.unpack('<I', struct.pack('<f', x))[0]
a = Asm()
OpCapability, OpMemoryModel, OpEntryPoint, OpExecutionMode, OpDecorate, OpMemberDecorate = 17, 14, 15, 16, 71, 72
OpTypeVoid, OpTypeFunction, OpTypeFloat, OpTypeVector, OpTypePointer, OpTypeStruct, OpTypeInt, OpTypeBool = 19, 33, 22, 23, 32, 30, 21, 20
OpConstant, OpConstantComposite, OpVariable, OpFunction, OpLabel, OpLoad, OpStore, OpReturn, OpFunctionEnd = 43, 44, 59, 54, 248, 61, 62, 253, 56
OpAccessChain, OpVectorShuffle, OpCompositeExtract, OpCompositeConstruct, OpFSub, OpFMul, OpFAdd, OpFDiv = 65, 79, 81, 80, 131, 133, 129, 136
OpFOrdGreaterThan, OpSelect = 186, 169
Input, Output, Uniform = 1, 3, 2
Location, Binding, DescriptorSet, Block, Offset, BuiltIn = 30, 33, 34, 2, 35, 11
FragCoord = 15
a.op('cap', OpCapability, 1) # Shader
a.op('mem', OpMemoryModel, 0, 1) # Logical GLSL450
main = a.id()
t_void, t_fn, t_f, t_v2, t_v4, t_u = a.id(), a.id(), a.id(), a.id(), a.id(), a.id()
t_bool = a.id()
t_pin, t_pout, t_struct, t_pu, t_pf_u = a.id(), a.id(), a.id(), a.id(), a.id()
v_prev, v_frag, v_out, v_p = a.id(), a.id(), a.id(), a.id()
c_half, c_zero, c_eps, c_u0, c_vzero = a.id(), a.id(), a.id(), a.id(), a.id()
t_pv4u = a.id()
a.op('entry', OpEntryPoint, 4, main, *string('main'), v_prev, v_frag, v_out) # Fragment
a.op('exec', OpExecutionMode, main, 7) # OriginUpperLeft
a.loc_ins = a.op('dec', OpDecorate, v_prev, Location, 0)
a.op('dec', OpDecorate, v_frag, BuiltIn, FragCoord)
a.op('dec', OpDecorate, v_out, Location, 0)
a.op('dec', OpDecorate, t_struct, Block)
a.op('dec', OpMemberDecorate, t_struct, 0, Offset, 0)
a.op('dec', OpDecorate, v_p, DescriptorSet, 0)
a.op('dec', OpDecorate, v_p, Binding, 1)
a.op('types', OpTypeVoid, t_void)
a.op('types', OpTypeFunction, t_fn, t_void)
a.op('types', OpTypeFloat, t_f, 32)
a.op('types', OpTypeVector, t_v2, t_f, 2)
a.op('types', OpTypeVector, t_v4, t_f, 4)
a.op('types', OpTypeInt, t_u, 32, 0)
a.op('types', OpTypeBool, t_bool)
a.op('types', OpTypePointer, t_pin, Input, t_v4)
a.op('types', OpTypePointer, t_pout, Output, t_v4)
a.op('types', OpTypeStruct, t_struct, t_v4)
a.op('types', OpTypePointer, t_pu, Uniform, t_struct)
a.op('types', OpTypePointer, t_pv4u, Uniform, t_v4)
a.op('types', OpConstant, t_f, c_half, f32(0.5))
a.op('types', OpConstant, t_f, c_zero, f32(0.0))
a.op('types', OpConstant, t_f, c_eps, f32(1e-6))
a.op('types', OpConstant, t_u, c_u0, 0)
a.op('types', OpConstantComposite, t_v4, c_vzero, c_zero, c_zero, c_zero, c_zero)
a.op('types', OpVariable, t_pin, v_prev, Input)
a.op('types', OpVariable, t_pin, v_frag, Input)
a.op('types', OpVariable, t_pout, v_out, Output)
a.op('types', OpVariable, t_pu, v_p, Uniform)
F = 'func'
a.op(F, OpFunction, t_void, main, 0, t_fn)
a.op(F, OpLabel, a.id())
pp = a.id(); a.op(F, OpAccessChain, t_pv4u, pp, v_p, c_u0)
p = a.id(); a.op(F, OpLoad, t_v4, p, pp)
prev = a.id(); a.op(F, OpLoad, t_v4, prev, v_prev)
frag = a.id(); a.op(F, OpLoad, t_v4, frag, v_frag)
fxy = a.id(); a.op(F, OpVectorShuffle, t_v2, fxy, frag, frag, 0, 1)
jit = a.id(); a.op(F, OpVectorShuffle, t_v2, jit, p, p, 2, 3)
win = a.id(); a.op(F, OpFSub, t_v2, win, fxy, jit)
px, py, pw = a.id(), a.id(), a.id()
a.op(F, OpCompositeExtract, t_f, px, prev, 0)
a.op(F, OpCompositeExtract, t_f, py, prev, 1)
a.op(F, OpCompositeExtract, t_f, pw, prev, 3)
nx, ny = a.id(), a.id()
a.op(F, OpFDiv, t_f, nx, px, pw)
a.op(F, OpFDiv, t_f, ny, py, pw)
sx, sy = a.id(), a.id()
a.op(F, OpCompositeExtract, t_f, sx, p, 0)
a.op(F, OpCompositeExtract, t_f, sy, p, 1)
# x: (nx * 0.5 + 0.5) * sx
x1, x2, x3 = a.id(), a.id(), a.id()
a.op(F, OpFMul, t_f, x1, nx, c_half)
a.op(F, OpFAdd, t_f, x2, x1, c_half)
a.op(F, OpFMul, t_f, x3, x2, sx)
# y: (0.5 - ny * 0.5) * sy
y1, y2, y3 = a.id(), a.id(), a.id()
a.op(F, OpFMul, t_f, y1, ny, c_half)
a.op(F, OpFSub, t_f, y2, c_half, y1)
a.op(F, OpFMul, t_f, y3, y2, sy)
wx, wy = a.id(), a.id()
a.op(F, OpCompositeExtract, t_f, wx, win, 0)
a.op(F, OpCompositeExtract, t_f, wy, win, 1)
mx, my = a.id(), a.id()
a.op(F, OpFSub, t_f, mx, x3, wx)
a.op(F, OpFSub, t_f, my, y3, wy)
mv = a.id(); a.op(F, OpCompositeConstruct, t_v4, mv, mx, my, c_zero, c_zero)
ok = a.id(); a.op(F, OpFOrdGreaterThan, t_bool, ok, pw, c_eps)
res = a.id(); a.op(F, OpSelect, t_v4, res, ok, mv, c_vzero)
a.op(F, OpStore, v_out, res)
a.op(F, OpReturn)
a.op(F, OpFunctionEnd)
w = a.words()
lines = [', '.join(f'0x{x:08x}' for x in w[i:i + 8]) for i in range(0, len(w), 8)]
OUT.write_text(
'// Generated by tools/make_mvobj_fs.py: the per-object motion fragment shader (vk_mvobj.c). Do not edit.\n'
f'#define MVOBJ_FS_LOCATION_WORD {a.loc_word} // the input Location literal, patched to the variant\'s\n'
'static const uint32_t mvobj_fs_spv[] = {\n ' + ',\n '.join(lines) + '\n};\n', newline='\n')
print(f'{OUT}: {len(w)} words, Location at word {a.loc_word}')
+4 -3
View File
@@ -105,9 +105,10 @@ LLVM_SKIP_DIRS = {'aarch64-w64-mingw32', 'armv7-w64-mingw32', 'i686-w64-mingw32'
LLVM_SKIP_SUFFIXES = {'.idl', '.tlb'}
# tools/ files the installer steps run (and what they import), copied from the repo.
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'check_isa.py', 'extract.py', 'hostmem.py',
'make_romfs_patches.py', 'pack_shader_cache.py', 'prepare_shaders.py', 'romfs_index.py', 'shader_census.py',
'shader_list.py', 'ultrawide_layout.py', 'cheats.py', 'exefs_patches.py']
TOOLS_FILES = ['build_game.py', 'build_shaders.py', 'button_icons.py', 'check_content.py', 'check_isa.py', 'extract.py',
'hostmem.py', 'make_romfs_patches.py', 'ncz.py', 'pack_shader_cache.py', 'prepare_shaders.py',
'romfs_index.py', 'shader_census.py', 'shader_list.py', 'ultrawide_layout.py', 'cheats.py',
'exefs_patches.py']
TOOLS_DIRS = ['vendor/macnativegames', 'data'] # data/: expected hashes
RUNTIME_SUFFIXES = {'.h', '.inc', '.ico'} # .ico: the installer window icon
+231
View File
@@ -0,0 +1,231 @@
"""Unpack an NSZ or XCZ (a game file compressed by the nsz tool) into a plain NSP.
An NSZ is an NSP whose NCAs are stored as NCZ files: the first 0x4000 bytes of the NCA (its encrypted header) as they
are, then an NCZSECTN table of the NCA's sections with the AES-CTR key and counter of each, then the rest of the NCA
decrypted and compressed with zstd, either as one stream or as an NCZBLOCK table of independently compressed blocks.
An XCZ is an XCI whose secure partition holds NCZ files. Unpacking encrypts the sections again, so every NCA comes back
byte for byte; its file name is the first half of its SHA-256, which is checked. The output is an NSP of the secure
partition (an XCZ) or of the NSZ's files, with .ncz renamed to .nca. Format: nicoboss/nsz, nsz/NszDecompressor.py
(__decompressNcz) and nsz/BlockDecompressorReader.py; this is our own code.
usage: ncz.py <file.nsz|file.xcz> <out.nsp>. Runs under .venv-py314 (compression.zstd).
"""
import hashlib
import io
import struct
import sys
from compression import zstd
from pathlib import Path
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
sys.path.insert(0, str(Path(__file__).resolve().parent / 'vendor' / 'macnativegames'))
from extract_exefs import container_entries, read_at, require # noqa: E402
HEADER = 0x4000 # the NCA header, kept encrypted and uncompressed
CHUNK = 1 << 20
CRYPTO_CTR = (3, 4) # AES-CTR and BKTR sections; the other types are stored as they are
class DamagedFile(ValueError):
"""An NCZ whose unpacked NCA does not match its name's hash, or whose data ends early."""
def __init__(self, path, detail):
self.path, self.detail = Path(path), detail
super().__init__(f'{self.path} is damaged or incomplete ({detail})')
def compressed(path):
"""('nsz' or 'xcz', entries) for a game file holding NCZ files, else None. Raises ValueError for a file that is
neither an NSP nor an XCI."""
with Path(path).open('rb') as f:
kind, entries = container_entries(f, Path(path).stat().st_size)
if not any(name.lower().endswith('.ncz') for name, _, _ in entries):
return None
return ('nsz' if kind == 'nsp' else 'xcz'), entries
def sections(f, start):
"""[(offset, size, crypto type, key, counter)] of the NCZ at start, and the offset of its data after the table."""
head = read_at(f, start + HEADER, 16)
require(head[:8] == b'NCZSECTN', 'NCZ without its NCZSECTN table')
count = struct.unpack_from('<Q', head, 8)[0]
require(0 < count <= 0x10000, 'Invalid NCZ section count')
raw = read_at(f, start + HEADER + 16, count * 0x40)
out = []
for i in range(count):
offset, size, crypto = struct.unpack_from('<QQQ', raw, i * 0x40)
out.append((offset, size, crypto, raw[i * 0x40 + 0x20:i * 0x40 + 0x30], raw[i * 0x40 + 0x30:i * 0x40 + 0x40]))
out.sort()
require(out[0][0] >= HEADER, 'NCZ section inside the NCA header')
for (a, size, *_), (b, *_) in zip(out, out[1:]):
require(a + size <= b, 'Overlapping NCZ sections')
return out, start + HEADER + 16 + count * 0x40
def nca_size(f, start):
table, _ = sections(f, start)
return table[-1][0] + table[-1][1]
def layout(source):
"""(entries, NSP header, NSP size) of the NSP that unpack writes for the NSZ or XCZ source: its files in order,
.ncz renamed to .nca, the PFS0 table padded to 0x20 bytes."""
_, entries = compressed(source)
with Path(source).open('rb') as f:
sizes = [nca_size(f, start) if name.lower().endswith('.ncz') else length for name, start, length in entries]
names = [name[:-4] + '.nca' if name.lower().endswith('.ncz') else name for name, _, _ in entries]
strings = b''.join(n.encode('utf-8') + b'\0' for n in names)
strings += bytes(-(16 + 0x18 * len(names) + len(strings)) % 0x20)
head = b'PFS0' + struct.pack('<III', len(names), len(strings), 0)
offset, name_at = 0, 0
for name, size in zip(names, sizes):
head += struct.pack('<QQII', offset, size, name_at, 0)
offset += size
name_at += len(name.encode('utf-8')) + 1
head += strings
return entries, head, len(head) + offset
def unpacked_size(source):
"""Bytes of the NSP that unpack writes for the NSZ or XCZ source."""
return layout(source)[2]
class Slice(io.RawIOBase):
"""Bytes start..end of a file, for zstd.ZstdFile: a stream may hold several frames and must stop at the entry's
end."""
def __init__(self, f, start, end):
self.f, self.pos, self.end = f, start, end
def readable(self):
return True
def readinto(self, buffer):
n = min(len(buffer), self.end - self.pos)
if n <= 0:
return 0
self.f.seek(self.pos)
data = self.f.read(n)
buffer[:len(data)] = data
self.pos += len(data)
return len(data)
def blocks(f, pos, end, total):
"""The NCZBLOCK data at pos, decompressed, in pieces. A block whose stored size is not smaller than its
decompressed size is stored as it is."""
head = read_at(f, pos, 24)
exponent, count = head[11], struct.unpack_from('<I', head, 12)[0]
size = struct.unpack_from('<Q', head, 16)[0]
require(head[8] == 2 and head[9] == 1 and 14 <= exponent <= 32, 'Unsupported NCZBLOCK header')
require(size == total and count == -(-size // (1 << exponent)), 'NCZBLOCK sizes do not match the sections')
stored = struct.unpack_from(f'<{count}I', read_at(f, pos + 24, 4 * count))
pos += 24 + 4 * count
for i, length in enumerate(stored):
plain = min(1 << exponent, size - (i << exponent))
require(pos + length <= end, 'Truncated NCZ block')
data = read_at(f, pos, length)
pos += length
yield zstd.decompress(data) if length < plain else data
def stream(f, pos, end, total):
"""The decompressed NCA body after the header, in pieces of up to CHUNK bytes."""
if read_at(f, pos, 8) == b'NCZBLOCK':
yield from blocks(f, pos, end, total)
return
with zstd.ZstdFile(io.BufferedReader(Slice(f, pos, end), CHUNK)) as z:
while data := z.read(CHUNK):
yield data
def unpack_ncz(f, start, end, out, name, source, progress):
"""Write the NCA of the NCZ at start..end to out; check it against name's hash. progress(bytes) after each piece."""
table, pos = sections(f, start)
total = table[-1][0] + table[-1][1]
digest = hashlib.sha256()
header = read_at(f, start, HEADER)
out.write(header)
digest.update(header)
progress(HEADER)
at = HEADER
pending = b''
try:
pieces = stream(f, pos, end, total - HEADER)
for piece in pieces:
pending += piece
while pending and at < total:
section = next((s for s in table if s[0] <= at < s[0] + s[1]), None)
stop = section[0] + section[1] if section else next((s[0] for s in table if s[0] > at), total)
n = min(len(pending), stop - at)
crypto = section and section[2] in CRYPTO_CTR
if crypto and at + n < stop:
n -= n % 16 # the counter of the next piece starts at a whole AES block
if not n:
break
data, pending = pending[:n], pending[n:]
if crypto:
require(at % 16 == 0, 'Unaligned NCZ section')
counter = section[4][:8] + struct.pack('>Q', at >> 4)
data = Cipher(algorithms.AES(section[3]), modes.CTR(counter)).encryptor().update(data)
out.write(data)
digest.update(data)
at += n
progress(n)
if at == total:
break
except (zstd.ZstdError, EOFError) as e: # EOFError: the stream ends before its end-of-frame marker
raise DamagedFile(source, f'{name}: {e}') from None
if at != total or pending:
raise DamagedFile(source, f'{name}: {at - HEADER:,} of {total - HEADER:,} bytes')
stem = name[:-4]
if len(stem) == 32 and digest.hexdigest()[:32] != stem.lower():
raise DamagedFile(source, f'{name}: hash mismatch')
def unpack(source, target, progress=lambda done, total: None):
"""Write the NSP of the NSZ or XCZ source to target (through target.part). progress(done, total) in bytes."""
source, target = Path(source), Path(target)
kind = compressed(source)[0]
entries, head, total = layout(source)
part = target.with_name(target.name + '.part')
done = 0
def step(n):
nonlocal done
done += n
progress(done, total)
with source.open('rb') as f, part.open('wb') as out:
out.write(head)
step(len(head))
for name, start, length in entries:
if name.lower().endswith('.ncz'):
unpack_ncz(f, start, start + length, out, name, source, step)
continue
for at in range(start, start + length, CHUNK):
data = read_at(f, at, min(CHUNK, start + length - at))
out.write(data)
step(len(data))
part.replace(target)
return kind
def main():
source, target = sys.argv[1:3]
shown = [-1]
def progress(done, total):
percent = done * 100 // total
if percent != shown[0]:
shown[0] = percent
print(f'\r{percent}%', end='', flush=True)
kind = unpack(source, target, progress)
print(f'\n{kind}: wrote {target}')
if __name__ == '__main__':
main()
+12 -2
View File
@@ -32,9 +32,15 @@ MARKER_ROOT = {
'AttentionBalloon_00': ('C_Capture_00', 'N_InOut_00'), # N_PosRoot_00 moves; N_InOut_00 animates y and alpha
'AttentionLockOn_00': ('N_Offset_00',), # N_PosRoot_00 moves; N_Offset_00 animates y
'EnemyInfo_00': ('Pa_EnemyLife_00', 'Pa_EnemyNotice_00'), # the parts move; N_EnemyLifeArea_* stay screen-sized
'BalloonMessage_00': ('Pa_BalloonMessage_00',),
'BalloonMessage_00': (), # the speech balloon part is pooled: its scale goes inside, see BALLOON_PART
}
# Speech balloons are pooled: the game clones Pa_BalloonMessage_00 into Pa_BalloonMessage_%02d at run time, and the clones
# don't keep a scale set on the template part pane, so they drew at full canvas width. The scale goes on the part
# layout's top panes instead (their offset scaled with them, so they sit where a scaled root would put them); its
# animations only key alpha, visibility and N_Loop_00's y. (pane, x, y) as shipped.
BALLOON_PART = (('C_CaptureBase_00', 226.0, 20.0), ('N_InOut_00', 30.0, -10.0))
def edits(k, hud_corner):
"""(layout, pane, field, value) for the aspect: k = 16/9 over the aspect (the root's scale), w = how far a 1920-wide
layout's edge moves out in its own units (960 * (1 / k - 1)). OFF moves a pane out of sight."""
@@ -67,6 +73,8 @@ def edits(k, hud_corner):
('PaRecipeCardList_00', 'P_BG_00', 'scale_x', wide),
('SaveLoadIcon_00', 'RootPane', 'x', -960 * (1 - k)),
('ShopMenu_00', 'N_BG_00', 'scale_x', wide), ('StaffRoll_00', 'N_BG_00', 'scale_x', wide),
*[e for pane, x, _ in BALLOON_PART for e in (('PaBalloonMessage_00', pane, 'scale_x', k),
('PaBalloonMessage_00', pane, 'x', x * k))],
]
# Menus framed by two side decorations, a header and a background: the frame stays at the screen's edges.
for layout, deco in (('PauseMenuOverlay_00', 'P_Deco{}_00'), ('SystemLoadList_00', 'P_Deco{}_00'),
@@ -117,7 +125,9 @@ def tall_edits(k):
*[('CameraPointer_00', p, 'scale_y', tall) for p in ('N_InOutScope_00', 'Pa_CameraFocus_00', 'S_Scissor_00',
'W_Shadow_01')],
('CameraSystemWindow_00', 'N_InOut_00', 'scale_y', tall), ('MessageLog_01', 'P_Capture_00', 'scale_y', tall),
*[(layout, 'N_SystemBG_00', 'scale_y', tall) for layout in ('SystemActionGuide_00',) + SYSTEM_MENUS]]
*[(layout, 'N_SystemBG_00', 'scale_y', tall) for layout in ('SystemActionGuide_00',) + SYSTEM_MENUS],
*[e for pane, _, y in BALLOON_PART for e in (('PaBalloonMessage_00', pane, 'scale_y', k),
('PaBalloonMessage_00', pane, 'y', y * k))]]
# The HUD at the screen's top and bottom edges instead of the middle 16:9
out += [(name, 'RootPane', 'y', 540 * (1 - k)) for name in (
'AppAlbum_00', 'AppPictureBook_00', 'ChallengeLog_00', 'Challenge_00', 'InstantTipsWithPict_00', 'InstantTips_00',
+7
View File
@@ -117,6 +117,12 @@ static class SwshXlate
public int QueryHostStorageBufferOffsetAlignment() => _job["sbuf_align"] is JsonNode n ? (int)n : 16;
// "subgroup": n: lanes per subgroup when the host cannot pin 32 (AMD GCN/Vega run 64): ballots and votes span n lanes.
public int QueryHostSubgroupSize() => _job["subgroup"] is JsonNode n ? (int)n : 32;
// "mvprev": true: the vertex shader also writes last frame's position, computed from slots bank + 15 in place of
// the user banks c3-c16 (patch 0015, runtime/vk_mvprev.c); res.json's "mvprev" names the output location (-1:
// none) and "mvprev_banks" the banks the position depends on (bit n: cn).
public bool QueryMotionPrevious() => _job["mvprev"] is JsonValue v && v.TryGetValue<bool>(out bool b) && b;
public int MotionPreviousLocation = -1, MotionPreviousBanks;
public void ReportMotionPrevious(int location, int banks) => (MotionPreviousLocation, MotionPreviousBanks) = (location, banks);
}
@@ -140,6 +146,7 @@ static class SwshXlate
["uses_frag_coord"] = p.Info.UsesFragCoord, ["frag_out_map"] = p.Info.FragmentOutputMap,
["tex_queried"] = new JsonArray(acc.TexQueried.Select(x => (JsonNode)x).ToArray()), ["img_queried"] = new JsonArray(acc.ImgQueried.Select(x => (JsonNode)x).ToArray()),
};
if (acc.QueryMotionPrevious()) (res["mvprev"], res["mvprev_banks"]) = (acc.MotionPreviousLocation, acc.MotionPreviousBanks);
foreach (var b in p.Info.CBuffers) res["cbufs"].AsArray().Add((JsonNode)new JsonObject { ["binding"] = b.Binding, ["slot"] = b.Slot });
foreach (var b in p.Info.SBuffers) res["sbufs"].AsArray().Add((JsonNode)new JsonObject { ["binding"] = b.Binding, ["slot"] = b.Slot, ["cb_slot"] = b.SbCbSlot, ["cb_offset"] = b.SbCbOffset, ["flags"] = (int)b.Flags });
foreach (var (list, name) in new[] { (p.Info.Textures, "textures"), (p.Info.Images, "images") })
@@ -0,0 +1,64 @@
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitBitfield.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitBitfield.cs
@@ -142,13 +142,46 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
+ // TOTK: Maxwell's BFE (as PTX bfe) cuts a field that runs past bit 31 there, and a field that starts past it
+ // gives 0, or bit 31 repeated when signed and the size is not 0. SPIR-V leaves an offset or offset + count over
+ // 32 undefined, and AMD's bitfield instructions take offset and width modulo 32. TOTK's Depths terrain vertex
+ // shaders take position and size from a register (BFE.R), so they are clamped into the defined range here;
+ // constant fields inside the word are emitted as before.
+ bool constantInRange = srcB.Type == OperandType.Constant && (srcB.Value & 0xff) + ((srcB.Value >> 8) & 0xff) <= 32;
+ Operand positionIn = position;
+ Operand sizeIn = size;
+
+ if (!constantInRange)
+ {
+ ClampBitfield(context, ref position, ref size);
+ }
+
Operand res = isSigned
? context.BitfieldExtractS32(srcA, position, size)
: context.BitfieldExtractU32(srcA, position, size);
+ if (!constantInRange && isSigned)
+ {
+ // The clamped extract gave 0 for a field that starts past bit 31; a signed one is bit 31 repeated.
+ Operand signBits = context.ConditionalSelect(context.ICompareEqual(sizeIn, Const(0)), Const(0), context.ShiftRightS32(srcA, Const(31)));
+
+ res = context.ConditionalSelect(context.ICompareGreaterOrEqualUnsigned(positionIn, Const(32)), signBits, res);
+ }
+
context.Copy(GetDest(rd), res);
- // TODO: CC, X, corner cases.
+ // TODO: CC, X.
+ }
+
+ // TOTK: position and size of a bitfield cut to the 32-bit word: the size shrinks to what is left of the word
+ // after the position (0 for a position of 32 or more) and the position stays at most 31, so offset and
+ // offset + count stay inside the range SPIR-V defines.
+ private static void ClampBitfield(EmitterContext context, ref Operand position, ref Operand size)
+ {
+ Operand left = context.ISubtract(Const(32), context.IMinimumU32(position, Const(32)));
+
+ size = context.IMinimumU32(size, left);
+ position = context.IMinimumU32(position, Const(31));
}
private static void EmitBfi(EmitterContext context, Operand srcA, Operand srcB, Operand srcC, int rd)
@@ -157,6 +190,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand size = context.BitfieldExtractU32(srcB, Const(8), Const(8));
+ // TOTK: as in EmitBfe, a field past bit 31 is cut there; one that starts past it inserts nothing (size 0
+ // after the clamp), so C comes back unchanged, as on Maxwell.
+ if (!(srcB.Type == OperandType.Constant && (srcB.Value & 0xff) + ((srcB.Value >> 8) & 0xff) <= 32))
+ {
+ ClampBitfield(context, ref position, ref size);
+ }
+
Operand res = context.BitfieldInsert(srcC, srcA, position, size);
context.Copy(GetDest(rd), res);
@@ -0,0 +1,32 @@
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitShift.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitShift.cs
@@ -119,6 +119,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
bool signedShift = maxShift == MaxShift.S64;
int maxShiftConst = isLongShift ? 64 : 32;
+ // TOTK: the shift amount as a constant, when it is one (taken before the mask makes it an operation).
+ bool constantShift = srcB.Type == OperandType.Constant;
+ int shiftValue = constantShift ? (mask ? srcB.Value & (maxShiftConst - 1) : srcB.Value) : 0;
+
if (mask)
{
srcB = context.BitwiseAnd(srcB, Const(maxShiftConst - 1));
@@ -159,6 +163,18 @@ namespace Ryujinx.Graphics.Shader.Instructions
}
}
+ // TOTK: a shift by 0 shifts the other word by 32, which SPIR-V leaves undefined; AMD's shift instructions take
+ // the amount's low 5 bits and give A | C. Maxwell gives the unshifted word: C for SHF.L, A for SHF.R, 32- and
+ // 64-bit alike. TOTK's Depths terrain vertex shaders shift by registers (SHF.L.R, SHF.R.R).
+ if (!constantShift)
+ {
+ res = context.ConditionalSelect(context.ICompareEqual(srcB, Const(0)), left ? srcC : srcA, res);
+ }
+ else if (shiftValue == 0)
+ {
+ res = left ? srcC : srcA;
+ }
+
if (!mask)
{
// Clamped shift value.
@@ -0,0 +1,18 @@
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -33,8 +33,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
foreach (AstOperand local in function.Locals)
{
- SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, context.GetType(local.VarType));
- SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function);
+ SpvInstruction localType = context.GetType(local.VarType);
+ SpvInstruction localPointerType = context.TypePointer(StorageClass.Function, localType);
+
+ // TOTK: a register read on a path that did not write it reads 0. Without an initializer the load is
+ // undefined, and a compiler that folds undefined values (Mesa's, on the Steam Deck) may compute anything
+ // from it.
+ SpvInstruction spvLocal = context.Variable(localPointerType, StorageClass.Function, context.ConstantNull(localType));
context.AddLocalVariable(spvLocal);
context.DeclareLocal(local, spvLocal);
@@ -0,0 +1,376 @@
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -184,6 +184,14 @@ namespace Ryujinx.Graphics.Shader
return false;
}
+ // TOTK: true when a vertex shader should also write last frame's position (Transforms/MotionPrevious.cs),
+ // the user output location it went to (-1: none) and the banks the position depends on (bit n: cn).
+ bool QueryMotionPrevious() => false;
+
+ void ReportMotionPrevious(int location, int banks)
+ {
+ }
+
/// <summary>
/// Queries host about the presence of the FrontFacing built-in variable bug.
/// </summary>
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
@@ -65,7 +65,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_stage = stage;
_stagePrefix = GetShaderStagePrefix(stage);
- _cbSlotToBindingMap = new SetBindingPair[18];
+ _cbSlotToBindingMap = new SetBindingPair[32]; // TOTK: slots 18-31 for Transforms/MotionPrevious.cs
_sbSlotToBindingMap = new SetBindingPair[16];
_cbSlotToBindingMap.AsSpan().Fill(new(-1, -1));
_sbSlotToBindingMap.AsSpan().Fill(new(-1, -1));
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/MotionPrevious.cs
@@ -0,0 +1,330 @@
+using Ryujinx.Graphics.Shader.IntermediateRepresentation;
+using System.Collections.Generic;
+using static Ryujinx.Graphics.Shader.IntermediateRepresentation.OperandHelper;
+
+namespace Ryujinx.Graphics.Shader.Translation.Transforms
+{
+ // TOTK: last frame's position for per-object motion vectors (runtime/vk_exec_draw.c, vk_mvprev.c). A vertex shader's
+ // position depends on uniform banks of this frame: the camera (c4), the object's place (c7), its bones (c5), the
+ // clock that moves wind and water (c14), and so on. Every operation the position depends on that reads one of the
+ // user banks c3..c16, directly or through other values, is computed a second time beside the original, reading slot
+ // bank + 15 (18..31) instead, where the runtime binds that bank's block of last frame; the position this gives is
+ // written to a free user output. Control flow stays the original's. Operations that cannot be repeated (stores,
+ // texture operations) keep their original result. The runtime gets the output location and the banks the position
+ // depends on (ReportMotionPrevious): an object with none of them changed since last frame held still.
+ static class MotionPrevious
+ {
+ public const int SlotOffset = 15;
+
+ private static int Remap(int slot) => slot >= 3 && slot <= 16 ? slot + SlotOffset : -1;
+
+ // The output location written (-1: none free, or no position store), and in banks the mask of the banks (bit
+ // n: cn) the position depends on.
+ public static int Run(BasicBlock[] blocks, ResourceManager resourceManager, AttributeUsage attributeUsage, out int banks)
+ {
+ banks = 0;
+ int location = -1;
+
+ for (int l = Constants.MaxAttributes - 1; l >= 0; l--)
+ {
+ if ((attributeUsage.UsedOutputAttributes & (1 << l)) == 0)
+ {
+ location = l;
+ break;
+ }
+ }
+
+ if (location < 0)
+ {
+ return -1;
+ }
+
+ // 1. What the position depends on: back from the values the position stores write, through every
+ // operation and phi that defines them.
+ HashSet<INode> slice = new();
+ Stack<Operand> work = new();
+ bool stores = false;
+
+ foreach (BasicBlock block in blocks)
+ {
+ for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
+ {
+ if (IsPositionStore(node.Value))
+ {
+ stores = true;
+ work.Push(((Operation)node.Value).GetSource(2));
+ }
+ }
+ }
+
+ if (!stores)
+ {
+ return -1;
+ }
+
+ while (work.Count != 0)
+ {
+ Operand v = work.Pop();
+
+ if (v == null || v.Type != OperandType.LocalVariable || v.AsgOp is not INode def || !slice.Add(def))
+ {
+ continue;
+ }
+
+ for (int s = 0; s < def.SourcesCount; s++)
+ {
+ work.Push(def.GetSource(s));
+ }
+ }
+
+ // 2. The banks the slice reads, and the bindings of those it loads with a computed offset to their
+ // last-frame slot's (only these get one: a binding is declared in the SPIR-V whether used or not, and the
+ // host's layout comes from the used ones). Direct operands (c[slot][offset]) get theirs when the
+ // structured program is made.
+ Dictionary<int, int> bindings = new();
+
+ foreach (INode n in slice)
+ {
+ if (n is Operation op && IsBankLoad(op, resourceManager, out int slot))
+ {
+ banks |= 1 << slot;
+
+ if (!bindings.ContainsKey(op.GetSource(0).Value))
+ {
+ bindings[op.GetSource(0).Value] = resourceManager.GetConstantBufferBinding(Remap(slot));
+ }
+ }
+
+ for (int s = 0; s < n.SourcesCount; s++)
+ {
+ Operand src = n.GetSource(s);
+
+ if (src != null && src.Type == OperandType.ConstantBuffer && Remap(src.GetCbufSlot()) >= 0)
+ {
+ banks |= 1 << src.GetCbufSlot();
+ }
+ }
+ }
+
+ if (banks == 0)
+ {
+ return -1;
+ }
+
+ // 3. Which of the slice's values depend on those banks, to a fixed point (phis carry it around loops).
+ HashSet<Operand> tainted = new();
+ bool changed = true;
+
+ while (changed)
+ {
+ changed = false;
+
+ foreach (BasicBlock block in blocks)
+ {
+ for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
+ {
+ INode n = node.Value;
+
+ if (!slice.Contains(n) || n.DestsCount == 0 || !Reads(n, tainted, bindings))
+ {
+ continue;
+ }
+
+ for (int d = 0; d < n.DestsCount; d++)
+ {
+ Operand dest = n.GetDest(d);
+
+ if (dest != null && dest.Type == OperandType.LocalVariable && tainted.Add(dest))
+ {
+ changed = true;
+ }
+ }
+ }
+ }
+ }
+
+ // 4. A copy of each such operation beside it, with new destinations.
+ Dictionary<Operand, Operand> prev = new();
+ List<(INode Original, INode Copy)> copies = new();
+
+ foreach (BasicBlock block in blocks)
+ {
+ for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
+ {
+ INode n = node.Value;
+
+ if (!slice.Contains(n) || n.DestsCount == 0 || !IsTainted(n, tainted))
+ {
+ continue;
+ }
+
+ INode copy = null;
+
+ if (n is PhiNode phi)
+ {
+ Operand dest = Local();
+ prev[phi.Dest] = dest;
+ copy = new PhiNode(dest);
+ }
+ else if (n is Operation op && op.GetType() == typeof(Operation) && op.Inst != Instruction.Store)
+ {
+ Operand[] dests = new Operand[op.DestsCount];
+
+ for (int d = 0; d < dests.Length; d++)
+ {
+ Operand dest = op.GetDest(d);
+ dests[d] = dest != null && dest.Type == OperandType.LocalVariable ? Local() : dest;
+
+ if (dest != null && dest.Type == OperandType.LocalVariable)
+ {
+ prev[dest] = dests[d];
+ }
+ }
+
+ Operand[] sources = new Operand[op.SourcesCount];
+
+ for (int s = 0; s < sources.Length; s++)
+ {
+ sources[s] = op.GetSource(s);
+ }
+
+ Operation c = op.StorageKind != StorageKind.None && dests.Length == 1
+ ? new Operation(op.Inst, op.StorageKind, dests[0], sources)
+ : new Operation(op.Inst, op.Index, dests, sources);
+ c.ForcePrecise = op.ForcePrecise;
+ copy = c;
+ }
+
+ if (copy != null)
+ {
+ node = block.Operations.AddAfter(node, copy);
+ copies.Add((n, copy));
+ }
+ }
+ }
+
+ // 5. The copies' sources: last frame's values, the remapped banks.
+ foreach ((INode original, INode copy) in copies)
+ {
+ if (copy is PhiNode phiCopy)
+ {
+ PhiNode phi = (PhiNode)original;
+
+ for (int s = 0; s < phi.SourcesCount; s++)
+ {
+ phiCopy.AddSource(phi.GetBlock(s), Prev(phi.GetSource(s), prev));
+ }
+
+ continue;
+ }
+
+ Operation op = (Operation)original, c = (Operation)copy;
+
+ for (int s = 0; s < op.SourcesCount; s++)
+ {
+ Operand src = op.GetSource(s);
+
+ if (s == 0 && op.Inst == Instruction.Load && op.StorageKind == StorageKind.ConstantBuffer &&
+ src.Type == OperandType.Constant && bindings.TryGetValue(src.Value, out int binding))
+ {
+ c.SetSource(s, Const(binding));
+ }
+ else if (src.Type == OperandType.ConstantBuffer && Remap(src.GetCbufSlot()) >= 0)
+ {
+ c.SetSource(s, Cbuf(Remap(src.GetCbufSlot()), src.GetCbufOffset()));
+ }
+ else
+ {
+ c.SetSource(s, Prev(src, prev));
+ }
+ }
+ }
+
+ // 6. Each position store also writes last frame's position to the output.
+ foreach (BasicBlock block in blocks)
+ {
+ for (LinkedListNode<INode> node = block.Operations.First; node != null; node = node.Next)
+ {
+ if (!IsPositionStore(node.Value))
+ {
+ continue;
+ }
+
+ Operation op = (Operation)node.Value;
+ Operation store = new(Instruction.Store, StorageKind.Output, null,
+ Const((int)IoVariable.UserDefined), Const(location), op.GetSource(1), Prev(op.GetSource(2), prev));
+ node = block.Operations.AddAfter(node, store);
+ }
+ }
+
+ attributeUsage.SetOutputUserAttribute(location);
+
+ return location;
+ }
+
+ private static bool IsPositionStore(INode n)
+ {
+ return n is Operation op && op.Inst == Instruction.Store && op.StorageKind == StorageKind.Output &&
+ op.SourcesCount == 3 && op.GetSource(0).Type == OperandType.Constant &&
+ (IoVariable)op.GetSource(0).Value == IoVariable.Position;
+ }
+
+ private static bool IsBankLoad(Operation op, ResourceManager resourceManager, out int slot)
+ {
+ slot = -1;
+
+ return op.Inst == Instruction.Load && op.StorageKind == StorageKind.ConstantBuffer &&
+ op.GetSource(0).Type == OperandType.Constant &&
+ resourceManager.TryGetConstantBufferSlot(op.GetSource(0).Value, out slot) && Remap(slot) >= 0;
+ }
+
+ private static Operand Prev(Operand src, Dictionary<Operand, Operand> prev)
+ {
+ return src != null && prev.TryGetValue(src, out Operand p) ? p : src;
+ }
+
+ private static bool IsTainted(INode n, HashSet<Operand> tainted)
+ {
+ for (int d = 0; d < n.DestsCount; d++)
+ {
+ if (n.GetDest(d) is Operand dest && tainted.Contains(dest))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static bool Reads(INode n, HashSet<Operand> tainted, Dictionary<int, int> bindings)
+ {
+ if (n is Operation op && op.Inst == Instruction.Load && op.StorageKind == StorageKind.ConstantBuffer &&
+ op.GetSource(0).Type == OperandType.Constant && bindings.ContainsKey(op.GetSource(0).Value))
+ {
+ return true;
+ }
+
+ for (int s = 0; s < n.SourcesCount; s++)
+ {
+ Operand src = n.GetSource(s);
+
+ if (src == null)
+ {
+ continue;
+ }
+
+ if (src.Type == OperandType.ConstantBuffer && Remap(src.GetCbufSlot()) >= 0)
+ {
+ return true;
+ }
+
+ if (tainted.Contains(src))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
--- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -301,6 +301,12 @@ namespace Ryujinx.Graphics.Shader.Translation
Optimizer.RunPass(context);
TransformPasses.RunPass(context);
+
+ if (i == 0 && Definitions.Stage == ShaderStage.Vertex && GpuAccessor.QueryMotionPrevious())
+ {
+ int location = MotionPrevious.Run(cfg.Blocks, resourceManager, AttributeUsage, out int banks);
+ GpuAccessor.ReportMotionPrevious(location, banks);
+ }
}
funcs[i] = new Function(cfg.Blocks, $"fun{i}", false, inArgumentsCount, outArgumentsCount);
+50 -11
View File
@@ -32,8 +32,9 @@
# A thread waiting for a virtual core interrupts the owner when it runs game code: 1 = on, 2 = priority inheritance
# without the interrupt, 0 = off (the old scheduling).
# async_preempt = 1
# Replay NVN submissions on their own thread, at most one frame behind the game (1 = on). Adds up to a frame of latency.
# replay_async = 0
# Replay NVN submissions on their own thread, at most one frame behind the game, 0 or 1. Default: on. Adds up to a
# frame of latency; 0 replays them on the game thread.
# replay_async = 1
# EXPERIMENTAL, THE GAME CRASHES WITH 1. 1 lifts the virtual core gate: guest threads run without the console's limit
# of one thread per core, and the game keeps per-core data without locks. With 1, 1.2.1 and 1.4.3 crash or hang,
# usually at start (#212). Keep 0 to play.
@@ -55,13 +56,16 @@
# pipe_prebuild = 1
# Threads that translate known shaders at start (1 to 64; default: half the CPU threads, at most 16).
# prewarm_xlate = 8
# Vulkan graphics pipeline library, 0 or 1. Default: on, except on AMD's driver for RDNA 1 and 2 (RX 5000 and 6000).
# Vulkan graphics pipeline library, 0 or 1. Default: on.
# gpl = 1
# Textures and memory.
# ASTC texture decoding: native (the GPU's own ASTC support), gpu (a compute shader), cpu, or auto (native when the GPU
# has it, gpu otherwise).
# astc = auto
# With astc = gpu: re-encode the decoded ASTC textures to BC3, a quarter of the video memory, textures slightly blockier,
# 0 or 1. Default: on when the GPU has less than 5 GB of video memory, off otherwise.
# astc_bc = 1
# Game memory shared with the GPU: auto (mirror on a discrete GPU, import on an integrated one), import (host memory
# imported into Vulkan), mirror (pools used as buffers get a copy in video memory) or verify (mirror plus a check of
# every copy; slow). On a discrete GPU import can be far slower than mirror, not slightly slower: the GPU reads the
@@ -136,8 +140,9 @@
# Game resolution: the size the game draws its picture and interface in (1280x720 to 7680x4320). Above 1920x1080 the
# game's texture heap and memory pool grow with it. Another shape than 16:9, such as 3440x1440 on a 21:9 monitor or
# the Steam Deck's 1280x800 (16:10), gives the camera's view that shape and lays the HUD out for it. The installer
# prepares the UI layout for ten sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9), 3840x1080, 5120x1440 (32:9),
# 1280x800, 1680x1050, 1920x1200 and 2560x1600 (16:10). Any other size
# prepares the UI layout for fifteen sizes: 2560x1080, 3440x1440, 3840x1600, 5120x2160 (21:9), 3840x1080, 5120x1440
# (32:9), 1280x800, 1680x1050, 1920x1200, 2560x1600 (16:10), 1920x1280, 2160x1440, 2256x1504, 2880x1920 and 3000x2000
# (3:2). Any other size
# gets the camera's view but a 16:9 HUD, stretched over the picture, unless you build its layout with
# tools/make_romfs_patches.py --aspect WxH. Read at start.
# output_res = 1920x1080
@@ -191,14 +196,30 @@
# patch 0010), so the shader cache is rebuilt once after an update. Read at start.
# pcss = 0
# NVIDIA DLSS / DLAA for the 3D scene, on NVIDIA RTX GPUs with nvngx_dlss.dll (from NVIDIA's DLSS SDK) next to
# totk.exe; elsewhere the setting does nothing. The HUD is drawn after it and is not touched. Read at start.
# NVIDIA DLSS / DLAA for the 3D scene, on NVIDIA RTX GPUs with the DLSS model from NVIDIA's DLSS SDK next to the
# executable (Windows: nvngx_dlss.dll next to totk.exe; Linux: libnvidia-ngx-dlss.so.<version> next to totk, with the
# NVIDIA driver's libnvidia-ngx.so.1 installed); elsewhere the setting does nothing. The HUD is drawn after it and is not touched.
# Read at start.
# dlaa = 0
# What DLSS works from: dlaa (the 3D scene at full size), quality, balanced or performance (the scene at 67, 58 or
# 50 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering to match). They apply
# only with dlaa = 1 and nvngx_dlss.dll next to totk.exe, on Windows; otherwise the scene follows render_scale. Read at
# start.
# What DLSS works from: dlaa (the 3D scene at full size), quality, balanced, performance or ultra_performance (the
# scene at 67, 58, 50 or 33 % of the output, scaled up by DLSS; these replace render_scale and sharpen texture filtering
# to match). They apply only with dlaa = 1 and the DLSS model next to the executable; otherwise the scene follows
# render_scale. Changes while the game runs take effect at once (1.4.3; 1.2.1 at the next start).
# dlss = dlaa
# The DLSS model: default (NVIDIA's advice per mode: K for dlaa, quality and balanced, M for performance, L for
# ultra_performance), k (K in every mode) or j (the other transformer model: slightly less ghosting behind moving
# characters, a little more flicker). Any other model by its letter, a to m: on slower RTX cards the older model e is
# cheaper. Changes while the game runs take effect at once (DLSS starts a new history).
# dlss_preset = default
# Per-object motion vectors for DLSS: characters, animals and moving objects get their own motion instead of the
# camera's, so DLSS keeps their history instead of smearing or rejecting it. Costs some CPU time each frame (about 1-2 ms
# on a laptop CPU); 0 leaves camera-only motion. Read at start.
# dlaa_objmv = 1
# Characters' trails: where a character, animal or held item moves without motion vectors of its own (all of them with
# dlaa_objmv = 0), DLSS can drag a faint copy of it. The G-buffer marks those objects; on them and where they were the
# frame before, DLSS is told to favour the current frame by this percentage (0, 35, 65 or 100; 0 off). Higher removes
# more of the trails and leaves those edges a little less smooth. Changes while the game runs take effect at once.
# dlss_char_mask = 65
# The game's own FXAA after the post effects; with DLAA on it is not needed. Changes while the game runs take effect at
# once.
# fxaa = 1
@@ -211,6 +232,9 @@
# hdr_peak_nits = 1000
# hdr_game_nits = 203
# hdr_ui_nits = 203
# Highlight hue correction in percent (0 to 100): bright colours such as fire turn towards white as in the game's SDR
# picture, matching the light they cast; 0 keeps them fully saturated. Changes take effect at once.
# hdr_hue = 80
[language]
# Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale.
@@ -222,6 +246,18 @@
# it is none of them.
# menu_lang = en
[discord]
# Rich Presence: "Playing TotK Recomp" with the play time in your Discord status while the game runs, through the
# local IPC of a running Discord client. 1 = on; the Menu tab of the settings menu (F1) switches it too, and the
# change reaches the presence thread at once.
# discord_rpc = 0
# The application whose name and artwork the status shows. The default is the project's own application,
# registered as "TotK Recomp" with the port's green ring icon as its art. Register your own at
# discord.com/developers (name and art are yours to choose) and put its id here.
# discord_app_id = 1567232599846293504
# The key of the application's uploaded art asset shown as the status's large image.
# discord_image = totk_512x512
[audio]
# none mixes without opening an audio device.
# audio = default
@@ -229,6 +265,9 @@
# audio_prefill_ms = 20
# Volume in percent (0 to 100), on top of the game's own. Changes while the game runs take effect at once.
# volume = 100
# Speakers: stereo, 5.1 or auto (5.1 when the Windows speaker setup has six or more channels, stereo otherwise). The
# game is told the same channel count and mixes for it. Takes effect at the next start.
# audio_channels = stereo
[input]
# Gamepad motion controls (0 = off) and vibration (0 = off).